A lot of Docker tutorials show you the fast path: curl | bash, done. That's fine for a local machine. On a production VPS, it leaves you with Docker running as root, open ports you didn't intend to expose, and no cleanup routine — meaning your disk fills up silently over time.
This guide does it properly. It takes about 10 extra minutes compared to the lazy install, and it saves you from the kind of problems that show up at 2am.
Before you start
You need:
- Ubuntu 22.04 or 24.04 LTS VPS
- A non-root sudo user (never run this as root)
- SSH access and a basic UFW firewall in place
If your VPS is brand new, do this first:
bashsudo apt update && sudo apt upgrade -y sudo adduser deploy sudo usermod -aG sudo deploy
Then reconnect as deploy before continuing.
Step 1: Install Docker from the official repository
Don't use the Ubuntu snap version or apt install docker.io — both are outdated. Install from Docker's own repo:
bashsudo apt update sudo apt install ca-certificates curl gnupg -y sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
Verify it installed:
bashdocker --version docker compose version
Step 2: Run Docker without root
Running containers as root is a security risk. Add your user to the docker group:
bashsudo usermod -aG docker $USER newgrp docker docker run hello-world
If hello-world runs successfully without sudo, you're set. If you still need sudo, log out and back in — group membership needs a fresh session.
Step 3: Enable Docker on reboot
By default Docker may not start automatically after a server restart:
bashsudo systemctl enable docker sudo systemctl enable containerd sudo systemctl status docker --no-pager
Status should show active (running). Test this properly by actually rebooting your VPS once and confirming Docker comes back up:
bashsudo reboot # after reconnecting: docker ps
Step 4: Firewall rules — keep ports minimal
Here's where most guides skip something important: Docker can bypass UFW rules. By default, Docker modifies iptables directly. If you docker run -p 8080:80, that port is publicly accessible even if UFW says it's blocked.
The safest approach for a production VPS: put Nginx in front and only expose 80 and 443. Don't bind container ports directly to public interfaces unless you specifically need to.
bashsudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable sudo ufw status
For containers that don't need public access (databases, caches), bind only to localhost:
yamlports: - "127.0.0.1:5432:5432" # PostgreSQL — local only
Step 5: A compose file with health checks
Health checks tell Docker whether your container is actually working, not just running. Add this to any compose.yml:
yamlservices: app: image: nginx:alpine ports: - "127.0.0.1:8080:80" restart: unless-stopped healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost"] interval: 30s timeout: 5s retries: 3 start_period: 10s
restart: unless-stopped means the container comes back after a VPS reboot or a crash — unless you explicitly stopped it yourself.
Start it:
bashdocker compose up -d docker ps
Look for (healthy) in the STATUS column. If you see (health: starting), wait 30 seconds and check again.
Step 6: Nginx reverse proxy in front of Docker
Run Nginx on the host (not in Docker) to handle SSL and route traffic:
bashsudo apt install nginx -y
/etc/nginx/sites-available/myapp:
nginxserver { listen 80; server_name yourdomain.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
bashsudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
Then add SSL via Certbot:
bashsudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d yourdomain.com
Step 7: Maintenance routine — keep your VPS clean
Docker is notorious for quietly filling up disks. Old images, stopped containers, unused volumes — they accumulate. Add a weekly cleanup:
bash# See what Docker is using: docker system df # Remove stopped containers, unused images, dangling volumes: docker system prune -f # More aggressive — removes all unused images (not just dangling): docker image prune -a -f
Set a cron job to run this weekly:
bashcrontab -e # Add: 0 3 * * 0 docker system prune -f >> /var/log/docker-prune.log 2>&1
This runs every Sunday at 3am. Adjust to suit your schedule.
Common mistakes to avoid
Running everything as root. If your container gets compromised, root access means the attacker has your whole server.
Exposing database ports publicly. MySQL on port 3306, Redis on 6379 — these should never be publicly accessible. Bind to 127.0.0.1 only.
No restart policy. Without restart: unless-stopped, a container crash or server reboot leaves your app offline until you notice.
Skipping the prune routine. On a 40GB VPS, it takes a few months of Docker use to quietly fill the disk. By the time you notice, it's causing errors.
Quick FAQ
Should I run Nginx inside Docker or on the host?
For most VPS setups, running Nginx on the host and your app(s) in Docker is simpler to manage. It avoids Docker networking complexity and makes SSL via Certbot straightforward. The containerized Nginx approach makes more sense when you're orchestrating many services across multiple servers.
How do I update a running container?
bashdocker compose pull docker compose up -d
This pulls the latest image and recreates containers with zero downtime if your app supports it.
Docker or Docker Desktop?
Docker Desktop is for local development on macOS/Windows. Your VPS runs Docker Engine (what we installed here). They're different products.
You're ready
A properly installed Docker setup on Ubuntu VPS — from the right repository, with non-root access, sensible firewall rules, health checks, and a cleanup routine — is solid production infrastructure that'll run reliably without surprises.
Need a clean Ubuntu VPS to run this on? HostAccent VPS plans come with Ubuntu pre-installed and ready to go.











Discussion
Have a question or tip about this topic? Share it below — your comment will appear after review.