Slik deployer du applikasjoner med Docker Compose på en VPS
En praktisk produksjonsveileder til Docker Compose v2 på Ubuntu: installer motoren, skriv en Compose-fil med nettverk og volumer, håndter hemmeligheter, sett en reversproxy foran, oppdater med rolige utrullinger, og sikkerhetskopier navngitte volumer.

Docker Compose er den vanlige måten å kjøre en liten stakk på én VPS: en webapp, en database, en Redis-cache og en reversproxy, beskrevet i én YAML-fil. Det er ikke Kubernetes. Du får ikke failover over flere noder. Du får et gjentakbart, gjennomgåelig oppsett som du kan gjenskape på en ny Hiddence-VPS på minutter — som er nøyaktig det de fleste sideprosjekter og små produkter trenger.
Denne artikkelen forutsetter at du vil ha noe nærmere produksjon enn docker run en fredagskveld. Vi installerer Docker Engine og Compose-pluginet, oppretter en deploy-bruker uten root, skriver en Compose-fil med healthchecks og omstartspolicyer, holder hemmeligheter utenfor Git, publiserer bare reversproxy-portene mot internett, og definerer en oppdaterings- og sikkerhetskopieringsrutine. Eksempelstakken er en typisk webapp pluss PostgreSQL pluss Caddy, men samme mønster fungerer for Node, PHP, Python eller en haug workers.
Hvorfor Compose på én VPS fortsatt gir mening
Folk hopper til Kubernetes fordi det er på moten, og bruker deretter helgen på YAML for ett nettsted. Compose forblir lesbart. Du versjonerer filen, dokumenterer miljøvariabler, og kan diff:e en endring før du tar den i bruk. Isolasjonen er god nok: en kompromittert appcontainer skal ikke holde databasens UNIX-socket hvis du brukte nettverk og brukere med minst privilegium. Ressursgrenser stopper en minnelekkasje fra å fryse hele VPS-en.
- Én fil beskriver hele stakken
- Navngitte volumer overlever at containere gjenskapes
- Interne Docker-nettverk holder Postgres unna det offentlige internett
- restart: unless-stopped dekker de fleste omstarter
- Lett å kopiere til en andre VPS når du vokser ut av den første
Krav
Bruk en VPS med nok RAM til appen pluss Postgres. En 2 GB-plan er et realistisk minimum for app + database + proxy. Ubuntu 24.04 er forutsatt. Ikke installer Docker fra en tilfeldig Snap uten å lese hva den gjør med cgroups; trinnene under bruker Dockers offisielle apt-arkiv.
- Ubuntu 22.04/24.04, root eller sudo
- 2 GB RAM anbefales (1 GB bare for bittesmå stakker uten Postgres)
- Et domene som peker på VPS-en hvis du vil ha automatisk HTTPS
- Git eller en annen måte å kopiere prosjektet til serveren
Steg 1: Installer Docker Engine og Compose v2
Compose v2 er et plugin som kalles som docker compose (med mellomrom), ikke den gamle docker-compose Python-binæren. Installer begge fra Dockers arkiv slik at du får aktuelle sikkerhetsoppdateringer.
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 versionSteg 2: Deploy-bruker og katalogoppsett
Å kjøre Compose som root fungerer, men en dedikert bruker med medlemskap i docker-gruppen er renere. Legg prosjektet i /opt eller /srv, ikke i /root, slik at sikkerhetskopier og rettigheter er åpenbare. Gjør aldri katalogen som inneholder .env skrivbar for alle.
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/appSteg 3: Skriv en produksjonsrettet Compose-fil
Filen under er en mal. Bytt app-imagen med din. Postgres publiseres ikke til 0.0.0.0:5432 — bare Caddy publiseres på 80 og 443. Healthchecks stopper en reversproxy fra å sende trafikk til en app som fortsatt migrerer databasen. Lås image-tagger; latest er hvordan overraskelsesbrudd skjer.
# /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:Steg 4: Hemmeligheter, .env og Caddyfile
Legg passord i .env på serveren, chmod 600, og commit aldri den filen. Generer passord med openssl rand -base64 32. Caddyfile trenger bare å reversprox til appens tjenestenavn på Docker-nettverket.
# /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=100Steg 5: Brannmur, logger og ressursgrenser
UFW skal bare tillate 22, 80 og 443. Docker omgår noen ganger UFW for publiserte porter; hvis du trenger en streng vertsbrannmur, slå opp den aktuelle Docker + UFW-interaksjonen for Ubuntu-versjonen din, eller publiser porter bare på 127.0.0.1 og sett en verts-Caddy foran. Sett minnegrenser i Compose slik at Postgres ikke kan spise VPS-en.
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 dockerSteg 6: Oppdateringer og tilbakerullinger
Den kjedelige oppdateringen som virker: hent nye images, up -d, følg healthchecks, behold forrige tagg i git slik at du kan låse tilbake. Ikke kjør docker compose down på en produksjonsdatabase med mindre du har tenkt å slutte å ta imot trafikk. down -v sletter volumer — det er en sletting, ikke en omstart.
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 volumesSikkerhetskopier navngitte volumer
Et VPS-øyeblikksbilde er bra. En logisk Postgres-dump er bedre hvis du må gjenopprette på en annen maskin. Planlegg dette med cron. Test en gjenoppretting én gang, ellers har du ikke sikkerhetskopier — du har filer du håper er sikkerhetskopier.
# 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 appFeilsøking
Les docker compose ps og docker compose logs service. Image-hentefeil er vanligvis ghcr/docker hub-hastighetsgrenser eller et privat image uten innlogging. 502 fra Caddy betyr at appen ikke er healthy, eller at proxy-vertsnavnet er feil (bruk Compose-tjenestenavnet, ikke localhost, innenfra Caddy-containeren). Rettighetsfeil på volumer betyr ofte at imagen kjører som uid 1000 mens vertskatalogen er root.
- compose: command not found — du installerte det gamle binærnavnet; bruk docker compose
- port is already allocated — noe annet eier 80/443 (Apache, Nginx, en annen Caddy)
- database connection refused — appen startet før Postgres var klar; bruk healthcheck + depends_on condition
- disk full — docker system df, deretter prune ubrukte images forsiktig
- permission denied on docker.sock — brukeren er ikke i docker-gruppen, eller du trenger en ny innloggingsøkt
Sikkerhet
Ikke publiser databaseporter. Ikke kjør privileged: true. Ikke monter /var/run/docker.sock i en appcontainer med mindre du bevisst skriver en containerbehandler. Hold motoren oppdatert. Skann dine egne images hvis du bygger dem. .env på disk er fortsatt en hemmelig fil — begrens hvem som kan ssh til boksen.
- Ingen offentlig 5432 / 3306 / 6379
- Lås image-digests eller i det minste uforanderlige tagger
- chmod 600 .env
- Unattended-upgrades for verts-OS-et betyr fortsatt noe
- Ett Compose-prosjekt per app holder blast radius mindre
Tips
- Legg compose.yaml i git; behold .env.example uten ekte hemmeligheter
- Bruk profiler for valgfrie workers (docker compose --profile workers up -d)
- Følg med på docker stats når du dimensjonerer VPS-en
- For bind mounts av kildekode er dette ikke produksjonsmønsteret — bak et image
- Hvis stakken vokser til flere VPS, se da på orkestrering — ikke før
Et produksjonslignende Compose-oppsett på én VPS er: offisielle Docker-pakker, en nedlåst .env, en YAML-fil som ikke publiserer databasen, healthchecks, en reversproxy på 80/443, UFW, image-tagger du kan rulle tilbake, og Postgres-dumper du faktisk har gjenopprettet én gang. Start fra malen i denne artikkelen, bytt app-imagen, og behandle docker compose down -v som en destruktiv kommando med samme respekt som rm -rf.