Toepassingen uitrollen met Docker Compose op een VPS
Praktische productiegids voor Docker Compose v2 op Ubuntu: de engine installeren, een Compose-bestand met netwerken en volumes schrijven, secrets afhandelen, een reverse proxy ervoor zetten, zonder paniek bijwerken en named volumes back-uppen.

Docker Compose is de gebruikelijke manier om een kleine stack op één VPS te draaien: een webapp, een database, een Redis-cache en een reverse proxy, beschreven in één YAML-bestand. Het is geen Kubernetes. Je krijgt geen multi-node-failover. Je krijgt een herhaalbare, reviewbare setup die je in minuten op een nieuwe Hiddence-VPS kunt nabootsen — precies wat de meeste bijprojecten en kleine producten nodig hebben.
Dit artikel gaat ervan uit dat je dichter bij productie wilt dan docker run op een vrijdagavond. We installeren Docker Engine en de Compose-plugin, maken een deploy-gebruiker zonder root, schrijven een Compose-bestand met healthchecks en herstartbeleid, houden secrets uit Git, publiceren alleen de reverse-proxypoorten naar internet en definiëren een update- en back-uproutine. De voorbeeldstack is een typische webapp plus PostgreSQL plus Caddy, maar hetzelfde patroon werkt voor Node, PHP, Python of een reeks workers.
Waarom Compose op één VPS nog steeds zinvol is
Mensen springen naar Kubernetes omdat het hip is en brengen dan het weekend door met YAML voor één website. Compose blijft leesbaar. Je versiont het bestand, documenteert omgevingsvariabelen en kunt een wijziging diffen voordat je die toepast. Isolatie is goed genoeg: een gecompromitteerde appcontainer mag de UNIX-socket van de database niet vasthouden als je netwerken en least-privilege-gebruikers hebt gebruikt. Resourcelimieten voorkomen dat één memory leak de hele VPS bevriest.
- Eén bestand beschrijft de hele stack
- Named volumes overleven het opnieuw aanmaken van containers
- Interne Docker-netwerken houden Postgres van het openbare internet
- restart: unless-stopped dekt de meeste reboots
- Makkelijk te kopiëren naar een tweede VPS als de eerste te krap wordt
Vereisten
Gebruik een VPS met genoeg RAM voor de app plus Postgres. Een 2 GB-plan is een realistisch minimum voor app + database + proxy. Ubuntu 24.04 wordt aangenomen. Installeer Docker niet vanuit een willekeurige Snap zonder te lezen wat dat met cgroups doet; de stappen hieronder gebruiken de officiële apt-repository van Docker.
- Ubuntu 22.04/24.04, root of sudo
- 2 GB RAM aanbevolen (1 GB alleen voor minuscule stacks zonder Postgres)
- Een domein dat naar de VPS wijst als je automatische HTTPS wilt
- Git of een andere manier om het project naar de server te kopiëren
Stap 1: Docker Engine en Compose v2 installeren
Compose v2 is een plugin die je aanroept als docker compose (met een spatie), niet het oude docker-compose-Pythonbinary. Installeer beide vanuit de repository van Docker zodat je actuele beveiligingspatches krijgt.
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
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" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker version
docker compose versionStap 2: Deploy-gebruiker en mapindeling
Compose als root draaien werkt, maar een eigen gebruiker in de groep docker is schoner. Zet het project in /opt of /srv, niet in /root, zodat back-ups en rechten duidelijk zijn. Maak de map met .env nooit wereldschrijfbaar.
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app
# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/appStap 3: Een productiegericht Compose-bestand schrijven
Het bestand hieronder is een sjabloon. Vervang de app-image door de jouwe. Postgres wordt niet gepubliceerd op 0.0.0.0:5432 — alleen Caddy op 80 en 443. Healthchecks voorkomen dat een reverse proxy verkeer stuurt naar een app die de database nog migreert. Pin imagetags; latest is hoe verrassingsstoringen ontstaan.
# /srv/app/compose.yaml
services:
caddy:
image: caddy:2.8-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
app:
condition: service_healthy
app:
image: ghcr.io/example/webapp:1.4.2
restart: unless-stopped
env_file: .env
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
interval: 10s
timeout: 3s
retries: 10
networks:
- frontend
- backend
db:
image: postgres:16-alpine
restart: unless-stopped
env_file: .env
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- backend
networks:
frontend: {}
backend: {}
volumes:
pgdata:
caddy_data:
caddy_config:Stap 4: Secrets, .env en Caddyfile
Zet wachtwoorden in .env op de server, chmod 600, en commit dat bestand nooit. Genereer wachtwoorden met openssl rand -base64 32. De Caddyfile hoeft alleen te reverse-proxyn naar de appservicenaam op het Docker-netwerk.
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs
# /srv/app/Caddyfile
app.example.com {
encode gzip
reverse_proxy app:8000
}
chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100Stap 5: Firewall, logs en resourcelimieten
UFW mag alleen 22, 80 en 443 toestaan. Docker omzeilt UFW voor gepubliceerde poorten soms; heb je een strikte hostfirewall nodig, zoek dan de huidige Docker + UFW-interactie voor jouw Ubuntu-versie op, of publiceer poorten alleen op 127.0.0.1 en zet een host-Caddy ervoor. Stel geheugenlimieten in Compose in zodat Postgres de VPS niet opeet.
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
# Optional in compose.yaml under app or db:
# deploy:
# resources:
# limits:
# memory: 512M
# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart dockerStap 6: Updates en rollbacks
De saaie update die werkt: nieuwe images pullen, up -d, healthchecks in de gaten houden, de vorige tag in git bewaren zodat je kunt terugpinnen. Voer docker compose down niet uit op een productiedatabase tenzij je verkeer wilt stoppen. down -v verwijdert volumes — dat is een wipe, geen herstart.
cd /srv/app
git pull # if the Compose file lives in git
# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps
# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v # this deletes named volumesNamed volumes back-uppen
Een VPS-snapshot is goed. Een logische Postgres-dump is beter als je op een andere machine moet herstellen. Plan dit met cron. Test één keer een restore, anders heb je geen back-ups — je hebt bestanden waarvan je hoopt dat het back-ups zijn.
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz
# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~
# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app appProblemen oplossen
Lees docker compose ps en docker compose logs service. Mislukte image-pulls zijn meestal ghcr/Docker Hub-ratelimits of een privé-image zonder login. 502 van Caddy betekent dat de app niet healthy is of dat de proxyhostnaam verkeerd is (gebruik de Compose-servicenaam, niet localhost, vanuit de Caddy-container). Rechtenfouten op volumes betekenen vaak dat de image als uid 1000 draait terwijl de hostmap van root is.
- compose: command not found — je hebt de oude binarynaam geïnstalleerd; gebruik docker compose
- port is already allocated — iets anders bezit 80/443 (Apache, Nginx, een andere Caddy)
- database connection refused — app startte voordat Postgres klaar was; gebruik healthcheck + depends_on-voorwaarde
- disk full — docker system df, daarna ongebruikte images voorzichtig prunen
- permission denied on docker.sock — gebruiker niet in de groep docker, of je hebt een nieuwe loginsessie nodig
Beveiliging
Publiceer geen databasepoorten. Gebruik privileged: true niet. Mount /var/run/docker.sock niet in een appcontainer tenzij je expres een containermanager schrijft. Houd de engine bijgewerkt. Scan je eigen images als je ze bouwt. .env op schijf blijft een geheim bestand — beperk wie via SSH op de machine mag.
- Geen openbare 5432 / 3306 / 6379
- Pin imagedigests of minstens onveranderlijke tags
- chmod 600 .env
- Unattended-upgrades voor het host-OS blijven belangrijk
- Eén Compose-project per app houdt de blast radius kleiner
Tips
- Zet compose.yaml in git; houd .env.example zonder echte secrets
- Gebruik profielen voor optionele workers (docker compose --profile workers up -d)
- Bekijk docker stats als je de VPS dimensioneert
- Bind mounts van broncode is niet het productiepatroon — bak een image
- Groeit de stack naar meerdere VPS, kijk dan naar orkestratie — niet eerder
Een productieachtige Compose-setup op één VPS is: officiële Docker-pakketten, een afgeschermde .env, YAML die de database niet publiceert, healthchecks, een reverse proxy op 80/443, UFW, imagetags waarnaar je kunt terugrollen, en Postgres-dumps die je écht één keer hebt hersteld. Begin bij het sjabloon in dit artikel, vervang de app-image en behandel docker compose down -v als een destructief commando, met hetzelfde respect als rm -rf.