Tilbake til bloggen
Guide11. mars 2026

Docker: Fra utviklingsmiljø til produksjon — slik containeriserer jeg systemer

Første gang jeg kjørte docker build var det ikke en åpenbaring. Det var tregt, imaget ble på over en gigabyte, og jeg forstod ikke helt hva jeg vant over å bare kjøre go run main.go lokalt. Men over tid — etter å ha deployet tjenester i Tuli, satt opp lokale utviklingsmiljøer med Postgres og Redis, og feilsøkt "fungerer på min maskin"-problemer — har Docker blitt et av verktøyene jeg bruker mest.

Dette innlegget handler om hvordan jeg faktisk bruker Docker i praksis. Ikke den teoretiske versjonen med perfekte diagrammer, men den reelle hverdagen med multistage builds, compose-filer og produksjonsklare images.

Hvorfor Docker?

Det korte svaret: konsistente miljøer. Når jeg skriver kode på MacBook-en min, kjører CI på Ubuntu, og produksjon er en Linux-container på Google Cloud Run, trenger jeg en garanti for at det som fungerer lokalt også fungerer i produksjon. Docker gir meg det.

Men la meg være mer spesifikk om hva Docker løser for meg i praksis:

Lokal utvikling med avhengigheter. Tuli sin API trenger PostgreSQL, og noen pipelines trenger Redis. Uten Docker måtte jeg installert begge lokalt, holdt dem oppdatert, og håndtert portkonflikter. Med Docker Compose spinner jeg opp hele stacken med én kommando.

Reproduserbare builds. Når jeg bygger Go-tjenestene i Tuli, kompilerer Docker imaget med nøyaktig de avhengighetene som er spesifisert i go.mod. Ingen overraskelser fra systembiblioteker som er forskjellige mellom utviklermaskiner.

Deployment-artefakter. Istedenfor å sende kildekode til en server og håpe at riktig runtime er installert, sender jeg et ferdig image. Cloud Run, Kubernetes, eller en enkel VPS — alle kjører det samme imaget.

Isolasjon. Forskjellige prosjekter kan bruke forskjellige versjoner av Postgres, forskjellige Go-versjoner, forskjellige systembiblioteker. Docker holder alt adskilt.

Slik bruker jeg Docker i praksis

Multistage builds for Go

Den viktigste teknikken jeg bruker er multistage builds. Konseptet er enkelt: bruk ett steg for å bygge applikasjonen, og et annet steg for å kjøre den. Build-steget har alt du trenger for kompilering — Go-kompilatoren, avhengigheter, kildekode. Runtime-steget har bare den kompilerte binærfilen.

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/api

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

La meg forklare de viktige detaljene. CGO_ENABLED=0 betyr at vi bygger en statisk binærfil uten C-avhengigheter. -ldflags="-s -w" fjerner debug-symboler og reduserer binærfilens størrelse. Vi kopierer go.mod og go.sum før resten av kildekoden, slik at Docker kan cache avhengighetene — dette gjør påfølgende builds mye raskere.

Resultatet? Et image på rundt 15 MB, ned fra over 1 GB med et naivt FROM golang uten multistage. Det er ikke bare en akademisk øvelse — mindre images betyr raskere pull-tider i produksjon, raskere skalering, og lavere lagringskostnader.

Docker Compose for lokal utvikling

For lokal utvikling er Docker Compose uunnværlig. Én fil beskriver hele stacken, og docker compose up starter alt:

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: nextbook
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./migrations:/docker-entrypoint-initdb.d

  api:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      DATABASE_URL: postgres://dev:dev@db:5432/nextbook?sslmode=disable
    ports:
      - "8080:8080"
    depends_on:
      - db

volumes:
  pgdata:

Noen viktige detaljer her. volumes med pgdata betyr at databasen overlever restart — du mister ikke dataene dine hver gang du stopper compose. ./migrations:/docker-entrypoint-initdb.d kjører SQL-migrasjoner automatisk første gang databasen starter. depends_on sørger for at databasen er oppe før API-et prøver å koble til.

Legg merke til at compose-filen bruker dev/dev som brukernavn og passord. Aldri gjør dette i produksjon — men for lokal utvikling handler det om enkelhet og lav friksjon.

Health checks og produksjonsklar konfigurasjon

En ting som skiller utvikler-Docker fra produksjons-Docker er health checks. Uten dem vet ikke orkestratoren (Kubernetes, Cloud Run, eller bare Docker selv) om containeren faktisk fungerer — bare at prosessen kjører.

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

Og endepunktet i Go:

mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
    if err := db.PingContext(r.Context()); err != nil {
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }
    w.WriteHeader(http.StatusOK)
})

Health-endepunktet sjekker ikke bare om applikasjonen responderer — det verifiserer at databaseforbindelsen faktisk fungerer. Hvis Postgres er nede, rapporterer containeren seg som unhealthy, og orkestratoren kan ta handling.

Image-størrelse betyr noe

Jeg nevnte at vi gikk fra over 1 GB til 15 MB. Her er hva som gjør forskjellen:

Bruk alpine-varianter. golang:1.22-alpine istedenfor golang:1.22 sparer hundrevis av megabytes i build-steget. For runtime, bruk alpine:3.19 eller til og med scratch (helt tomt image) hvis du har en statisk binærfil.

Ikke installer unødvendige pakker. Hver apk add eller apt-get install øker image-størrelsen. I runtime-steget trenger du bare det absolutt nødvendige.

Bruk `.dockerignore`. Uten den kopierer Docker hele prosjektmappen inn i build-konteksten — inkludert .git, node_modules, IDE-filer og alt annet. En god .dockerignore ser slik ut:

.git
node_modules
*.md
.env
.vscode

Multi-stage er nøkkelen. Build-verktøyene (kompilatoren, avhengigheter, kildekode) blir igjen i build-steget og havner aldri i det endelige imaget.

Når du IKKE bør bruke Docker

Docker er ikke alltid svaret. Jeg bruker det ikke overalt, og du bør heller ikke.

Serverless-plattformer. Denne nettsiden kjører på Vercel. Tuli sin frontend er også på Vercel. Docker ville vært unødvendig overhead her — Vercel håndterer build og deployment bedre enn en custom Docker-pipeline. Firebase Functions er en annen plattform der Docker typisk er overkill.

Enkle scripts og engangsoppgaver. Hvis du skriver et Python-script som kjører lokalt en gang i uken, trenger du ikke containerisere det. pip install og python script.py er helt greit.

Når læringskurven overstiger prosjektets levetid. Hvis du bygger en prototype som lever i to uker, er tid brukt på Docker-oppsett tid du ikke bruker på å validere ideen. Docker gir mest verdi for prosjekter som lever lenge og deployes til flere miljøer.

Utvikling av frontend-apper. Hot module replacement, filsystem-watching og raske feedback-loops fungerer generelt bedre utenfor Docker. Jeg kjører Next.js-utvikling direkte på maskinen min, ikke i en container.

Oppsummering

Docker er et kraftig verktøy, men som alle verktøy handler det om å bruke det riktig. For backend-tjenester i Go, for lokale utviklingsmiljøer med databaser, og for produksjonsdeployments gir det enorm verdi. For serverless-frontender, enkle scripts og korte prototyper er det unødvendig kompleksitet.

Start med multistage builds, lær Docker Compose for lokal utvikling, og husk at et godt image er et lite image. Resten kommer med erfaring.

#docker#devops#infrastruktur#produksjon

Nyhetsbrev

Få nye innlegg rett i innboksen.