self-hostingvpsdockernginx

Self-hosting from scratch: your first server with SSH, Docker and Nginx

Complete guide to running your own server: securing SSH with Ed25519 keys, deploying with Docker Compose and publishing with Nginx and HTTPS.

August 23, 2026·9 min read

Renting a 5-euro-per-month VPS and running your own services on it is one of the most educational skills in the trade: you learn networking, Linux, security and operations simultaneously. This guide walks the full path —from freshly minted server to published application with HTTPS— using exactly the pieces you would use in production.

Step 0: pick a provider and create the machine

Hetzner, OVH, DigitalOcean, Vultr... any serious provider works. What matters when creating the instance:

  • Ubuntu LTS or Debian: maximum documentation, long update cycles.
  • Region closest to your users: latency matters more than price.
  • SSH key from the start: if the panel allows it, inject your public key during creation and avoid passwords from second zero.

If you don't have a key pair yet, generate an Ed25519 one (the full process is covered in the SSH keys guide):

ssh-keygen -t ed25519 -C "miguel@laptop"

Step 1: the critical first minutes

A newborn VPS receives automated scans within minutes. Before installing anything, close the front door:

ssh root@YOUR_IP

adduser miguel
usermod -aG sudo miguel

Now edit SSH configuration (/etc/ssh/sshd_config) with three changes:

PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes

Restart the service (sudo systemctl restart ssh) without closing your current session and open a second terminal to verify key-based login works before losing your first connection. This order saves you from locking yourself out.

The firewall comes next, with ufw:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Everything else stays blocked: only SSH, HTTP and HTTPS answer from outside.

Step 2: install Docker and Compose

Docker turns every service into something reproducible and isolated. Official installation in two commands:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker miguel

(Log out and back in for the docker group to apply.)

From here on, each application is a directory with its docker-compose.yml. A typical app + database stack:

services:
  app:
    build: .
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://app:cambiar@db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=cambiar
      - POSTGRES_DB=app
    restart: unless-stopped

volumes:
  pgdata:

Notice what's NOT there: port publishing. The app needs no ports: because Nginx will talk to it through Docker's internal network. Less exposed surface, zero risk of port collisions between services. The full pattern is developed in the Docker Compose guide.

To build your application image with layer caching and multistage, the step-by-step Dockerfile covers Node, Next.js, Python, Go and Rust.

Step 3: Nginx as the front door

With the app running inside Docker, Nginx on the host becomes the single entry point: terminates TLS, compresses, applies security headers and routes domains.

server {
    listen 80;
    server_name mydomain.es;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name mydomain.es;

    ssl_certificate /etc/letsencrypt/live/mydomain.es/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.es/privkey.pem;

    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Wait —if we said the app publishes no ports, how does 127.0.0.1:3000 reach it? Two valid options: publish the port on loopback only ("127.0.0.1:3000:3000" in compose) or join Nginx to the project's Docker network and target the service name. Both work; the second is cleaner, the first simpler. All three complete configuration patterns (static, SPA, proxy) are in the Nginx guide.

Certificates, free and automatic:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d mydomain.es

Certbot rewrites your config adding the SSL block and schedules its own renewal.

Step 4: painless deployments

With the base in place, updating your application should be one command:

git pull && docker compose up -d --build

When the project grows, that command migrates to GitHub Actions: push to main → CI runs tests → automatic deploy. The copy-ready GitHub Actions workflow does exactly that.

Minimum viable maintenance

Three routines keep a small server healthy:

  • Updates: sudo apt update && sudo apt upgrade weekly (enable unattended-upgrades for automatic security patches). Docker images update by re-pulling tags.
  • Backups: the pgdata volume is not a backup. A nightly cron with docker compose exec db pg_dump app > backup.sql synced off-server is.
  • Basic watching: docker compose logs -f, disk space (df -h) and failed SSH attempts (journalctl -u ssh) tell you everything needed on small servers. Fail2ban optionally automates brute-force banning.

Rookie mistakes (we all make them)

Opening the app's port 3000 to the world instead of proxying it — duplicating attack surface. Leaving PasswordAuthentication yes after creating users. Not testing the new SSH session before closing the old one. Forgetting docker compose down -v deletes volumes —and the database with them—. Each takes a minute to fix if caught early; none is fun at 3 AM.

Generate the pieces before touching the server

Prepare your keys with the SSH Key Generator, validate resulting TLS state with the SSL Checker, and assemble your stack's compose file with the Docker Compose Generator.

FAQ

How much VPS do I need to start? For 2-3 light services, 2 GB RAM and 1 vCPU are plenty. Docker allows vertical growth by changing plans without rebuilding anything.

Dynamic VPS IP? VPSes come with static IPs; point your domain with an A record and done. Only home services behind dynamic IPs need DDNS.

Kubernetes yet? No. Compose scales further than most expect, and Kubernetes solves problems (multi-node, autoscaling, complex rollouts) a first server doesn't even have. It will come if ever needed.


Start by generating your key pair with the SSH Key Generator, right in your browser.

Try it without code

SSH Key Generator

Ed25519/RSA in your browser.

Open SSH Key Generator

Built by

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

I develop web apps, digital tools and full projects — from design to deployment.

Contact me