9 ting dit Docker Compose-setup med Postgres skal kunne (før det er værd at bruge)

9 ting dit Docker Compose-setup med Postgres skal kunne (før det er værd at bruge)

Hvad vi bygger: et lille Docker Compose-setup der faktisk kan bruges

Du har en app, du vil køre lokalt sammen med en Postgres database, uden at bruge en halv aften på at fikse porte, volumes og migrations, hver gang du puller ny kode.

Målet her er et lille, reproducérbart setup med Docker Compose + Postgres, hvor:

  • din app kan starte med én kommando
  • Postgres data overlever et docker compose down
  • migrations og seed-data kører på en styrbar måde
  • .env filer og secrets ikke ligger og flyder i repoet

Eksemplerne tager udgangspunkt i en simpel Node app, men mønstrene er de samme for Python, Ruby, hvad som helst. Hvis du arbejder med backend til web, rammer du de samme problemer.

1. Compose-filen forklaret: de felter du rent faktisk behøver

Start med en lille docker-compose.yml (eller compose.yml). Ingen magi, bare to services: app og db.

version: "3.9"

services:
  app:
    build: .
    command: npm run dev
    ports:
      - "3000:3000"
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: app_password
      POSTGRES_DB: app_db
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
      interval: 5s
      timeout: 3s
      retries: 5

De vigtigste felter

  • services: hver service er en container, typisk en app, en db, en worker osv.
  • build: peger på en Dockerfile i samme mappe. Her pakker du din app.
  • ports: host:container. Venstre side er din maskine, højre side inde i containeren.
  • env_file: loader miljøvariabler fra en fil, så du ikke hardcoder dem i YAML.
  • depends_on: sikrer rækkefølge. Med condition: service_healthy venter app på at db svarer på healthcheck.
  • healthcheck: lille kommando der siger “db’en er klar” i stedet for bare “containeren kører”.

Du kan godt lave Compose uden healthchecks, men så rammer du ret hurtigt “connection refused” fordi appen prøver at forbinde til Postgres, før den er klar.

2. Postgres container: bruger, database, init scripts og volumes

Postgres-delen bliver ofte noget rod, fordi den både har credentials, data og init scripts at tage højde for.

Minimal Postgres-opsætning

Vi bygger videre på db servicen fra før:

services:
  db:
    image: postgres:16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: app_password
      POSTGRES_DB: app_db
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./docker/initdb:/docker-entrypoint-initdb.d

volumes:
  db-data:

Hvad sker der her?

  • Bruger og database: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB oprettes automatisk ved første start.
  • Data-volumen: db-data er et navngivet volume. Det gør at data overlever, selvom du smider containeren væk.
  • Init scripts: alt i ./docker/initdb bliver kørt første gang databasen initialiseres. Typisk SQL-filer eller shell scripts.

Et simpelt init script kunne være:

-- ./docker/initdb/001_schema.sql
CREATE TABLE IF NOT EXISTS users (
  id serial PRIMARY KEY,
  email text NOT NULL UNIQUE
);

Det kører kun første gang volumet er tomt. Hvis du sletter containeren men lader volumet leve, kører det ikke igen. Det er både en feature og en kilde til forvirring (vi kommer tilbage til den under fejlsektionen).

3. Migrations: hvem kører dem og hvornår?

Migrations er dit “sandhedslag” for schemaet. Det værste du kan gøre, er at blande init scripts, manuelle ændringer i en SQL-klient og tilfældigt kørte migrations.

Jeg plejer at beslutte én af to modeller:

  • Migrations kører fra app-containere (typisk via et script som npm run migrate).
  • Migrations kører fra en separat “migrations” service i Compose.

Model 1: migrations fra app

Her kører du migrations manuelt når du har brug for dem:

services:
  app:
    build: .
    command: npm run dev
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

Og i din package.json:

{
  "scripts": {
    "dev": "node server.js",
    "migrate": "node_modules/.bin/knex migrate:latest"
  }
}

Når du så starter miljøet første gang:

docker compose up -d
docker compose exec app npm run migrate

Fordele: simpelt, du styrer det selv. Ulempe: du skal huske det (derfor er en README ret vigtig, se fx den her artikel om README’er).

Model 2: migrations som separat service

Hvis du vil automatisere det lidt mere:

services:
  migrate:
    build: .
    command: npm run migrate
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

Så kan du køre:

docker compose run --rm migrate

Her laver du en kortlivet container der kun kører migrations og dør bagefter.

Typisk migrations-fejl

  • At lade init scripts og migrations gøre det samme.
  • At køre migrations automatisk på hver app-start (og så fejle når schema allerede er up to date).
  • At committe en halvt kørt migration og lade dine kolleger rode med resterne.

Et simpelt mønster der hjælper: init scripts laver kun “første gang” setup (fx opret schema eller extensions). Alt schema-udvikling bagefter går gennem migrations.

4. Seed-data: hurtigere udvikling uden at ødelægge state

Seed-data er testdata du kan leve med at få igen og igen. Ikke rigtige brugere, ikke rigtige ordrer. Bare ting der gør UI’et brugbart.

Der er to hovedspørgsmål her:

  • Hvor ligger seed-logikken?
  • Hvordan gør du seeding idempotent (så den kan køre flere gange uden kaos)?

Placering af seed

Jeg foretrækker seeding som en separat kommando i appen, lidt som migrations:

"scripts": {
  "seed": "node scripts/seed.js"
}

Og i scripts/seed.js:

import pg from 'pg';

const client = new pg.Client({ connectionString: process.env.DATABASE_URL });

async function main() {
  await client.connect();

  await client.query(
    `INSERT INTO users (email)
     VALUES ($1)
     ON CONFLICT (email) DO NOTHING`,
    ['test@example.com']
  );

  await client.end();
}

main().catch(err => {
  console.error(err);
  process.exit(1);
});

Nøglen er ON CONFLICT ... DO NOTHING. Det gør scriptet idempotent: du kan køre det 10 gange uden duplikerede rækker.

Seed i Compose-flowet

Typisk CLI-sekvens for en ny udvikler:

docker compose up -d
docker compose exec app npm run migrate
docker compose exec app npm run seed

Hvis det her står tydeligt i din README, reducerer du “det virker ikke hos mig” ret markant. Det hænger godt sammen med tankerne i kategorien udviklingsværktøjer, hvor du vil have én vej gennem systemet, ikke fem forskellige.

5. .env og secrets: hvad må bo i Compose, og hvad skal blive lokalt?

Her går det ofte galt: nogen smider API-nøgler, DB passwords og alt andet direkte ind i docker-compose.yml og committer det.

Del ting op i tre niveauer

  • Harmløse defaults: fx NODE_ENV=development, PORT=3000. De må godt ligge i repoet.
  • Lokale miljøvariabler: fx DB connection strings til lokal Postgres, dummy API keys. De må ligge i en .env der ikke committes.
  • Rigtige secrets: produktionstokens, virkelige API-nøgler. De skal slet ikke have en værdi i repoet.

Sådan kan du strukturere det

Lav en .env.example:

NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://app_user:app_password@db:5432/app_db
THIRD_PARTY_API_KEY=changeme

Og en lokal .env (som er i .gitignore):

NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://app_user:app_password@db:5432/app_db
THIRD_PARTY_API_KEY=super-hemmelig-nøgle

I docker-compose.yml refererer du bare til env_file: .env på appen. Postgres-bruger og password i Compose bør være “dummy” nok til at de ikke gør skade, men stadig ikke genbruges andre steder. Artiklen om API-nøgler går mere i dybden med, hvor du gemmer de følsomme ting.

6. De 7 klassiske Docker Compose + Postgres fejl (og hurtige fixes)

Nu til den del de fleste rammer. Jeg gennemgår de fejl, jeg oftest ser i små full stack workflows.

Fejl 1: “port 5432 is already in use”

Symptom: docker compose up fejler, og du ser noget med bind: address already in use på port 5432.

Årsag: Du har en lokal Postgres installeret der allerede bruger port 5432, eller en anden container kører med samme portbinding.

Løsning:

  • Enten stop lokal Postgres.
  • Eller brug en anden host-port:
services:
  db:
    ports:
      - "55432:5432"  # host:container

Og husk at opdatere din DATABASE_URL til postgres://...@db:5432/... når du forbinder fra en container, og til localhost:55432 hvis du forbinder fra værtsmaskinen.

Fejl 2: Volumes “husker” gamle data

Symptom: Du ændrer på init scripts, men intet sker. Tabellen er stadig gammel. Nye kolonner dukker ikke op.

Årsag: Dit navngivne volume (db-data) har allerede data. docker-entrypoint-initdb.d-scripts kører kun første gang databasen initialiseres.

Løsning:

  • Enten drop volumet, hvis du kan tåle at miste data:
docker compose down -v
# -v sletter navngivne volumes
  • Eller migrer databasen med rigtige migrations i stedet for init scripts.

Hvis du ofte ender i “jeg sletter bare volumet”, er det et signal om, at du mangler solide migrations.

Fejl 3: Appen kan ikke connecte til databasen

Symptom: ECONNREFUSED, connection refused eller lignende, selvom du synes, alt kører.

Typiske årsager:

  • Du bruger localhost som host i DATABASE_URL inde i en container.
  • Postgres er ikke klar endnu.

Fix 1: brug servicens navn som host

Fra en container til en anden hedder databasen db, ikke localhost:

DATABASE_URL=postgres://app_user:app_password@db:5432/app_db

Fix 2: healthcheck + depends_on

Som vist tidligere: sørg for, at app venter på at db er healthy.

Fejl 4: Migrations kører i “forkert rækkefølge” eller på forkert database

Symptom: Din CI kører tests fint, men lokalt fejler det. Eller omvendt. Tabellen er ikke der, hvor du forventer den.

Årsager:

  • Du har forskellige DATABASE_URL lokalt og i CI, men deler migrations.
  • Du kører migrations mod localhost, mens appen kører mod db-servicen.

Løsning:

  • Sørg for at migrations og app læser samme miljøvariabel (DATABASE_URL).
  • Test migrations med samme Compose-setup som du bruger til daglig.

Mange fejl forsvinder bare af at standardisere på én connection string pr miljø.

Fejl 5: File watching virker ikke i containeren

Symptom: Din Node dev-server (eller tilsvarende) genstarter ikke når du ændrer filer på din maskine.

Årsag: Du har ikke mountet din kildekode ind i containeren som volume, så containeren ser ikke ændringerne.

Løsning:

services:
  app:
    build: .
    command: npm run dev
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules

Første linje mount’er hele projektmappen ind i containeren. Anden linje er et lille trick til at beholde node_modules inde i containeren, så de ikke forsvinder i volume-mountet.

Fejl 6: Byg-cache driller, koden opdateres ikke

Symptom: Du ændrer i koden, bygger billedet igen, men appen ligner den gamle version.

Årsag: Docker bruger cache-lag fra tidligere builds, og din Dockerfile er ikke struktureret, så kodeændringer invalidere de rigtige lag.

Typisk Dockerfile til Node:

FROM node:20

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install

COPY . .

CMD ["npm", "run", "dev"]

Hvis du stadig har problemer, kan du tvinge et build uden cache:

docker compose build --no-cache app

Men hvis du skal gøre det ofte, er det et tegn på, at din Dockerfile burde justeres.

Fejl 7: Permissions-problemer i volumes

Symptom: Postgres nægter at starte med noget ala “permission denied” på data-mappen. Eller appen kan ikke skrive til et volume.

Årsag: Filsystemet på værtsmaskinen og brugeren i containeren har ikke matchende rettigheder.

Løsning (hurtig, ikke altid pæn): Brug navngivne volumes i stedet for at mappe direkte til en lokal mappe, når det gælder Postgres:

services:
  db:
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Hvis du absolut vil mappe til en lokal mappe, må du ofte rode lidt med chown eller user:-feltet i Compose. Til almindelig udvikling er navngivne volumes rigeligt.

7. Mini-beslutningsmatrix: hvornår Docker Compose + Postgres giver mening

Inden jeg runder af, vil jeg faktisk kort vende hvornår du ikke behøver Docker. Der er ingen grund til at skyde alt ind i containere bare fordi du kan.

Brug Docker Compose + Postgres når

  • flere personer skal kunne starte projektet ens med én kommando
  • du vil have samme Postgres-version lokalt og i produktion
  • du er træt af “på min maskine har jeg Postgres 14”-samtaler
  • du vil øve dig i et realistisk deployment og drift-setup

Overvej at droppe Docker (i hvert fald i starten) når

  • det er et lille begynderprojekt, hvor du mest vil lære SQL og queries
  • du allerede har en lokal Postgres installeret og styr på versionen
  • du sidder helt alene og vil minimere bevægelige dele

Hvis du fx bare vil lære SELECT, JOINS og indexes, er noget simpelt som lokal Postgres + en SQL-klient ofte nok. Der er masser af læring i data og databaser, før Docker overhovedet kommer i spil.

8. Sådan kan du bygge videre: din egen lille standard

Hvis du vil gøre det her til noget, der faktisk hjælper dig hver gang du starter et nyt projekt, så lav en lille skabelon til dig selv.

Forslag til mini-øvelse

  1. Lav et nyt repo med:
    • docker-compose.yml med app, db og evt. migrate
    • .env.example med alle nødvendige variabler
    • en simpel migrations-mekanisme (det kan være et enkelt SQL-script i starten)
    • et lille seed-script der opretter én testbruger
  2. Skriv en README med en “first successful run”-sektion, der beskriver:
    • hvordan man kopierer .env.example til .env
    • docker compose up -d
    • docker compose exec app npm run migrate
    • docker compose exec app npm run seed
  3. Brug den skabelon til dit næste lille begynderprojekt eller studieprojekt.

Når du har gjort det et par gange, opdager du dine egne “standard-fejl” og kan tilpasse din Compose-fil til din måde at arbejde på. Lidt ligesom at finjustere en klatre-rute: samme greb, men din sekvens bliver bedre hver gang.

Jeg er selv nysgerrig på, om Docker bliver endnu mere usynligt i hverdagen om et par år, eller om vi finder et endnu lettere lag ovenpå til de små projekter – hvad tror du selv, du vil række ud efter næste gang du skal sætte Postgres op?

Sara Vestergaard er selvlært kode-nørd, der stille og roligt er gået fra at rode med en enkelt HTML-side til at bygge små værktøjer, scripts og hjemmesider til sig selv og vennerne. Hun startede med at lave en simpel band-hjemmeside som teenager og opdagede, hvor tilfredsstillende det er, når noget, du har skrevet, pludselig lever på skærmen.

For Sara handler kodning ikke om store ord eller imponerende titler, men om meget konkrete problemer: den kedelige opgave, der tager for lang tid, den ven der mangler en lille porteføljeside, eller den liste, der burde sortere sig selv. Hun elsker at pille ting fra hinanden – også kode – for at se, hvad der egentlig foregår, og hun har brugt utallige aftener på at google fejlbeskeder, teste små eksempler og langsomt bygge sin forståelse op.

På Coding Class deler hun den tilgang videre. Hun skriver til dig, der gerne vil lære at kode ved at gøre det i praksis: små projekter, korte kodebidder og forklaringer, der hænger sammen med det, du faktisk sidder med på skærmen. Hun skærer ind til benet, viser typiske fejl og deres løsninger og giver altid et forslag til, hvordan du kan bygge en tand videre, når grundideen først virker.

Når hun ikke skriver til Coding Class eller nørkler med nye små projekter, hænger Sara på klatrevæggen, vander sine altanplanter eller spiller gamle Nintendo-spil. Men hun ender næsten altid tilbage ved tasterne – for der er altid endnu en lille ting, der kunne være smartere, hurtigere eller bare lidt sjovere at bruge.

Send kommentar

You May Have Missed