Tilbake til blogg
August 19, 2026Guider

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.

Slik deployer du applikasjoner med Docker Compose på en VPS

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.

bash
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 version

Steg 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.

bash
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/app

Steg 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.

bash
# /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.

bash
# /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=100

Steg 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.

bash
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 docker

Steg 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.

bash
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 volumes

Sikkerhetskopier 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.

bash
# 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 app

Feilsø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.