Contents
Docker is the most practical way to package an application with its dependencies and run several of them side by side on one server. It does not run on shared hosting; because it needs root, a Türkiye VDS or France VDS is the right base. This guide goes from a clean Ubuntu 24.04 LTS install to an application in production.
Which VDS plan?
- One small application (n8n, Uptime Kuma, a small API): 2 cores, 2 GB RAM and 35 GB NVMe are enough.
- Application + database + reverse proxy: 4 cores and 4 GB RAM run comfortably; NVMe storage makes a difference for the database.
- Several projects or a CI runner: 8 cores, 8 GB RAM and up. The France plans with AMD EPYC 4584PX processors and DDR5 memory are faster for build-heavy work.
Take a snapshot in the FiberVM panel before you start; if anything goes wrong you are back in a minute. You can also reinstall the server with Ubuntu 24.04 from the same panel.
1. Prepare the server
apt update && apt upgrade -y
adduser deploy && usermod -aG sudo deploy
timedatectl set-timezone Europe/Istanbul
For SSH keys, disabling password login and UFW, follow our first-setup security checklist. Open only ports 22, 80 and 443 in UFW for now.
2. Install Docker from the official repository
Ubuntu's own docker.io package lags behind; use Docker's repository:
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] 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 -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker deploy
Log out and back in, then verify with docker run --rm hello-world. A user in the docker group effectively has root; add only accounts you trust.
3. Log limits and restart policy
By default container logs grow without limit and fill the disk. Put this in /etc/docker/daemon.json and run systemctl restart docker:
{
"log-driver": "json-file",
"log-opts": { "max-size": "20m", "max-file": "5" }
}
Add restart: unless-stopped to every service in your Compose files so the application comes back on its own after a reboot.
4. Reverse proxy with SSL: a Caddy example
Instead of binding applications to 80/443 directly, put a reverse proxy in front. Caddy obtains and renews Let's Encrypt certificates itself. Example compose.yaml:
services:
caddy:
image: caddy:2
restart: unless-stopped
ports: ["80:80", "443:443"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
app:
image: ghcr.io/example/app:latest
restart: unless-stopped
environment:
- DATABASE_URL=postgres://app:secret@db:5432/app
depends_on: [db]
db:
image: postgres:16
restart: unless-stopped
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
caddy_data:
db_data:
The Caddyfile is two lines: app.example.com { reverse_proxy app:3000 }. Point the domain's A record at the VDS IP, run docker compose up -d, and the certificate is issued on the first request. The database and the application publish no ports, so they are reachable only through Caddy.
5. The UFW trap: Docker bypasses your rules
When Docker sees a published port such as ports: "5432:5432" it writes its own iptables rules and bypasses the UFW block. A database port you believe is closed may be open to the internet. Two safe approaches:
- Do not write a
portsline for services that need no outside access; containers on the same Compose network reach each other by name. - If you need local access, bind the port to loopback only:
"127.0.0.1:5432:5432", then connect through an SSH tunnel.
Verify from another machine with nmap -p 1-65535 VDS_IP; only 22, 80 and 443 should show as open.
6. Updates and backups
- Image updates:
docker compose pull && docker compose up -d; remove old images withdocker image prune -f. - Data backups: dump the database from inside the container:
docker compose exec db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz. Copying volume directories (/var/lib/docker/volumes) while running produces inconsistent backups; prefer dumps. - Off-server copy: send backups to a VDS in another location or to object storage. A snapshot alone is not a backup; see our VDS backup strategy article.
- Monitoring: the live CPU, RAM and disk graphs in the FiberVM panel show the combined load of your containers; use
docker statsper service.
If you are unsure which server fits, open a support ticket with the applications you plan to run and we will pick the plan together. Unlimited traffic and DDoS protection are included on every VDS plan.

Comments0
No Comments Yet
Be the first to share your thoughts.
Please sign in or sign up to leave a comment.