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
.envfiler 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: hverserviceer 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. Medcondition: service_healthyventerapppå atdbsvarer 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_DBoprettes automatisk ved første start. - Data-volumen:
db-dataer et navngivet volume. Det gør at data overlever, selvom du smider containeren væk. - Init scripts: alt i
./docker/initdbbliver 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
.envder 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
localhostsom host iDATABASE_URLinde 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_URLlokalt og i CI, men deler migrations. - Du kører migrations mod
localhost, mens appen kører moddb-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
- Lav et nyt repo med:
docker-compose.ymlmedapp,dbog evt.migrate.env.examplemed 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
- Skriv en README med en “first successful run”-sektion, der beskriver:
- hvordan man kopierer
.env.exampletil.env docker compose up -ddocker compose exec app npm run migratedocker compose exec app npm run seed- 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?









Send kommentar
Du skal være logget ind for at skrive en kommentar.