Docker fyller 13 år i 2026, og verktøyet er fortsatt der de fleste utviklerteam starter når en applikasjon skal pakkes, testes og sendes til produksjon. I Stack Overflows utvikler­undersøkelse for 2024 svarte 59 % av profesjonelle utviklere at de bruker Docker, og verktøyet endte som det mest “beundrede” utviklerverktøyet i samme undersøkelse med 78 % tilfredshet, ifølge Stack Overflow. Markedet for containerhåndtering er ikke mindre travelt: Gartner anslo at det passerte 2,5 milliarder dollar i 2024 med over 20 % vekst året før, og venter at det runder 4,5 milliarder dollar innen 2028.

Denne guiden tar deg gjennom Docker fra bunnen av: installasjon, images, containere, volumer, nettverk, Docker Compose og til slutt en reell utrulling av en Node.js-applikasjon til skyen. Du trenger ingen forkunnskaper om containere, men det hjelper om du har skrevet litt JavaScript eller et annet backend-språk tidligere. Alt kode­eksemplene her er testet mot Docker Engine 29.x og Compose V2, og du sitter igjen med et fungerende prosjekt du kan bygge videre på.

Hva er Docker, og hvorfor bruker så mange det?

Docker er en plattform for å pakke en applikasjon sammen med alt den trenger for å kjøre: kjøretidsmiljø, biblioteker, systemverktøy og konfigurasjon. Pakken kalles et image, og når du starter et image får du en container, altså en isolert prosess som kjører akkurat som om den hadde sin egen maskin. Forskjellen fra en virtuell maskin er at containeren deler kjernen med vertsmaskinen i stedet for å virtualisere hele operativsystemet, noe som gjør oppstart nesten momentan og ressursbruken langt lavere.

Problemet Docker løser er kjent for de fleste som har hørt setningen “det fungerer på min maskin”. Uten containere er en applikasjon avhengig av at utviklings-, test- og produksjonsmiljøet har nøyaktig samme versjon av språk, biblioteker og systempakker. Med Docker blir hele miljøet en del av selve leveransen. Du bygger et image én gang, og det samme imaget kjører identisk uansett om det står på en bærbar Mac, en Linux-server hos en skyleverandør eller i et Kubernetes-cluster.

Containerteknologi har eksistert lenge i Linux-kjernen gjennom cgroups og namespaces, men Docker gjorde teknologien tilgjengelig med et enkelt kommandolinjegrensesnitt og et sentralt bilderegister i Docker Hub. I Datadogs container­rapport for 2023 brukte 53 % av organisasjonene containerd som kjøretidsmotor under Docker og Kubernetes, opp fra 23 % året før og 8 % to år tidligere, ifølge Datadog. Det viser hvor raskt selve infrastrukturlaget under containerne har modnet de siste årene, samtidig som Docker-kommandoene utviklerne bruker daglig har holdt seg stabile.

Forutsetninger og verktøy du trenger

Før du starter trenger du et par verktøy installert. Denne guiden bruker Docker Engine (den delen som faktisk bygger og kjører containere) sammen med Docker Compose V2, som er innebygd i moderne Docker-installasjoner. Compose V1, den frittstående Python-versjonen med bindestrek i kommandoen (docker-compose), regnes i praksis som avviklet, og du bør bruke docker compose uten bindestrek fra nå av.

VerktøyAnbefalt versjonFormål
Docker Engine / Docker DesktopSiste stabile versjon i 29.x-serienBygger og kjører containere lokalt
Docker ComposeV2 (innebygd som docker compose)Orkestrerer flere containere sammen
Node.js24 LTSKjøretidsmiljø for eksempel­applikasjonen
GitSiste versjonVersjonskontroll for prosjektfilene
Terminal / skallbash, zsh eller PowerShellKjøre Docker-kommandoene

Du trenger minst 4 GB minne tilgjengelig for Docker Desktop på Windows og macOS, og på Linux kjører Docker Engine direkte mot kjernen uten en ekstra virtualiseringslayer, så minnebruken er lavere der. Sjekk at alt er riktig installert med denne kommandoen før du går videre:

docker --version
docker compose version
docker run hello-world

Kommandoen docker run hello-world laster ned et lite test-image, starter det som en container, skriver en bekreftelsesmelding og avslutter automatisk. Ser du den meldingen, er installasjonen komplett og du er klar for neste steg.

Steg 1: Installer Docker riktig for ditt operativsystem

På Windows og macOS installerer du Docker Desktop, som pakker Docker Engine, Compose og et grafisk grensesnitt i én installasjonsfil hentet fra Dockers offisielle nedlastingsside. Windows-brukere bør kjøre Docker gjennom WSL2 (Windows Subsystem for Linux) i stedet for det eldre Hyper-V-baserte oppsettet, fordi WSL2 gir raskere filsystem­ytelse og lavere minnebruk.

På Linux-distribusjoner som Ubuntu installerer du Docker Engine direkte fra Dockers offisielle APT-repository i stedet for distribusjonens standardpakker, som ofte henger flere versjoner bak. Fremgangsmåten er dokumentert i detalj i Dockers offisielle installasjonsguide, men i korte trekk legger du til Dockers GPG-nøkkel og repository, og installerer deretter engine, CLI og containerd-runtime-pakkene sammen.

# Ubuntu: legg til Dockers offisielle repository
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /tmp/docker.gpg
sudo install -m 0755 -d /etc/apt/keyrings
sudo cp /tmp/docker.gpg /etc/apt/keyrings/docker.asc
sudo 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" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

Etter installasjon på Linux må brukeren din legges til i docker-gruppen for å kjøre Docker-kommandoer uten sudo foran hver linje. Kjør sudo usermod -aG docker $USER og logg deretter ut og inn igjen for at gruppemedlemskapet skal tre i kraft. Hopp aldri over dette steget ved å kjøre alt som root i det daglige, siden det gir containerprosessen unødvendig høye rettigheter på verten.

Steg 2: Forstå images og containere med dine første kommandoer

Et image er en skrivebeskyttet mal bygget opp av lag, der hvert lag representerer en endring, som å installere en pakke eller kopiere inn en fil. Når du starter et image med docker run, legger Docker til et skrivbart lag på toppen og gir deg en container. Du kan kjøre flere containere fra samme image samtidig, og de påvirker aldri hverandre eller det opprinnelige imaget.

# Last ned et image uten å kjøre det
docker pull nginx:1.27-alpine

# List alle images du har lokalt
docker images

# Start en container i bakgrunnen med portmapping
docker run -d --name mitt-nginx -p 8080:80 nginx:1.27-alpine

# List kjørende containere
docker ps

# Se loggene fra containeren
docker logs mitt-nginx

# Stopp og fjern containeren igjen
docker stop mitt-nginx
docker rm mitt-nginx

Flagget -d starter containeren “detached”, altså i bakgrunnen, mens -p 8080:80 kobler port 8080 på verten din til port 80 inne i containeren. Åpner du http://localhost:8080 i nettleseren mens containeren kjører, ser du standardsiden til Nginx. Dette mønsteret, med portmapping fra vert til container, er noe du kommer til å bruke i nesten alle Docker-prosjekter fremover.

Legg merke til at vi bruker nginx:1.27-alpine og ikke bare nginx. Alpine-varianter av offisielle images bygger på Alpine Linux i stedet for Debian, og ender typisk på 5-15 MB mot 150-200 MB for standardvarianten. Det gjør nedlasting raskere og reduserer angrepsflaten fordi færre systempakker følger med.

Steg 3: Skriv din første Dockerfile

En Dockerfile er en tekstfil med instruksjoner for hvordan et image skal bygges. Vi lager en enkel Node.js-applikasjon og pakker den i en container. Start med en ny mappe og disse filene:

mkdir docker-tutorial-app && cd docker-tutorial-app
npm init -y
npm install express

Opprett filen server.js med et minimalt Express-API:

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({ status: 'ok', melding: 'Hei fra en Docker-container' });
});

app.get('/health', (req, res) => {
  res.status(200).send('healthy');
});

app.listen(port, () => {
  console.log(`Server kjører på port ${port}`);
});

Opprett deretter Dockerfile (uten filendelse) i samme mappe:

FROM node:24-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

EXPOSE 3000
USER node

CMD ["node", "server.js"]

Hver linje i Dockerfilen bygger et nytt lag. FROM setter basisimaget, WORKDIR definerer arbeidsmappen inne i containeren, og de to COPY-linjene er delt bevisst: først kopieres bare package.json og package-lock.json, deretter kjøres npm ci, og først etterpå kopieres resten av kildekoden. Dette utnytter Dockers lagcache. Så lenge du ikke endrer avhengighetene dine, slipper Docker å installere npm-pakker på nytt hver gang du bygger om etter en kodeendring.

USER node bytter fra root til en ikke-privilegert bruker som følger med det offisielle Node-imaget, noe som er en av de mest oversette sikkerhetsforbedringene nye Docker-brukere kan gjøre. Bygg og kjør imaget slik:

# Bygg imaget og gi det et navn og en tag
docker build -t docker-tutorial-app:1.0 .

# Kjør containeren og koble port 3000
docker run -d -p 3000:3000 --name tutorial-app docker-tutorial-app:1.0

# Test at API-et svarer
curl http://localhost:3000/

Forventet output fra curl-kallet er:

{"status":"ok","melding":"Hei fra en Docker-container"}

Steg 4: Reduser imagestørrelsen med multi-stage bygg

For enkle Node.js-apper holder ofte ett byggetrinn, men så snart du bruker TypeScript, en bundler eller andre byggeverktøy, vil du ikke ha kompilatoren og alle utviklingsavhengighetene liggende i produksjonsimaget. Løsningen er multi-stage bygg, der du bruker ett midlertidig steg til å bygge applikasjonen og et annet, slankere steg til å kjøre den.

FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Det første steget, build, installerer alle avhengigheter inkludert TypeScript og kjører byggeskriptet. Det andre steget starter fra et helt nytt, rent node:24-alpine-image og kopierer bare den ferdigbygde dist-mappen over med COPY --from=build. Sluttresultatet er et image uten kompilator, kildekode i TypeScript eller devDependencies, bare det som faktisk trengs for å kjøre applikasjonen. Denne teknikken er den mest effektive enkeltendringen du kan gjøre for å redusere imagestørrelse og angrepsflate samtidig.

Steg 5: Håndter data med volumer og bind mounts

Containere er ment å være forgjengelige. Fjerner du en container, forsvinner alt som ble skrevet til dens skrivbare lag. For data som skal overleve, som en database eller opplastede filer, bruker du volumer i stedet.

# Opprett et navngitt volum
docker volume create app-data

# Kjør en container og monter volumet
docker run -d --name db-test -v app-data:/var/lib/postgresql/data postgres:17-alpine

# List alle volumer
docker volume ls

# Se detaljer om et spesifikt volum
docker volume inspect app-data

Navngitte volumer som app-data forvaltes av Docker selv og lagres et sted Docker Engine bestemmer, uavhengig av vertsmaskinens filstruktur. Bind mounts er alternativet: der kobler du en spesifikk mappe på verten direkte inn i containeren, noe som er nyttig under lokal utvikling fordi kodeendringer på verten dukker opp i containeren umiddelbart.

# Bind mount for lokal utvikling med live reload
docker run -d -p 3000:3000 \
  -v "$(pwd)":/app \
  -v /app/node_modules \
  --name dev-app \
  docker-tutorial-app:1.0

Den andre -v-linjen, -v /app/node_modules, er et lite triks som hindrer at den tomme node_modules-mappen på verten overskriver den installerte i containeren. Bruk navngitte volumer i produksjon og bind mounts kun under lokal utvikling, siden bind mounts kobler containerens oppførsel tett til vertsmaskinens filsystem på en måte som ikke er portabel.

Steg 6: Koble containere sammen med nettverk

Når applikasjonen din trenger en database eller en cache, må containerne kunne snakke med hverandre uten å eksponere alt til vertsmaskinen. Docker løser dette med brukerdefinerte nettverk, der containere kan referere til hverandre ved navn i stedet for IP-adresse.

# Opprett et eget nettverk
docker network create app-network

# Start en database på nettverket
docker run -d --name postgres-db \
  --network app-network \
  -e POSTGRES_PASSWORD=hemmelig \
  -v app-data:/var/lib/postgresql/data \
  postgres:17-alpine

# Start applikasjonen på samme nettverk
docker run -d --name tutorial-app \
  --network app-network \
  -p 3000:3000 \
  -e DATABASE_URL=postgres://postgres:hemmelig@postgres-db:5432/postgres \
  docker-tutorial-app:1.0

Legg merke til at DATABASE_URL peker på postgres-db, altså containernavnet, og ikke på localhost eller en IP-adresse. Dockers innebygde DNS løser containernavn automatisk til riktig intern IP innenfor samme brukerdefinerte nettverk. Dette gjør konfigurasjonen forutsigbar selv om Docker tildeler containere nye IP-adresser hver gang de startes på nytt.

Steg 7: Orkestrer flere containere med Docker Compose

Å starte applikasjon og database manuelt med lange docker run-kommandoer blir raskt uoversiktlig. Docker Compose lar deg definere hele oppsettet i én YAML-fil og starte alt med én kommando. Opprett compose.yaml i prosjektmappen:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:hemmelig@db:5432/postgres
      - NODE_ENV=production
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:17-alpine
    environment:
      - POSTGRES_PASSWORD=hemmelig
    volumes:
      - app-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  app-data:

Compose oppretter automatisk et eget nettverk for prosjektet, så app kan nå databasen på hostnavnet db uten at du trenger å opprette nettverket manuelt slik vi gjorde i forrige steg. Feltet depends_on med condition: service_healthy sørger for at applikasjonscontaineren ikke starter før databasen faktisk svarer på helsesjekken, ikke bare før den er startet.

# Bygg og start alle tjenester
docker compose up -d --build

# Se status på tjenestene
docker compose ps

# Følg loggene fra alle containere samtidig
docker compose logs -f

# Stopp og fjern alt, inkludert nettverk
docker compose down

Merk kommandoen uten bindestrek: docker compose, ikke docker-compose. Den frittstående V1-versjonen med bindestrek regnes som avviklet, og nyere Docker-installasjoner leverer Compose som et plugin til hoved-CLI-et i stedet. Skriver du fortsatt docker-compose i skript og CI-pipeliner, bør du oppdatere dem.

Steg 8: Sett opp .dockerignore og reduser byggetiden

Uten en .dockerignore-fil sender Docker hele prosjektmappen til byggemotoren når du kjører docker build, inkludert node_modules, .git og lokale miljøfiler. Det gjør byggingen tregere og kan i verste fall lekke hemmeligheter inn i imaget. Opprett .dockerignore i prosjektroten:

node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
Dockerfile
.dockerignore
README.md
dist
coverage
*.test.js

Å ekskludere node_modules er spesielt viktig fordi mappen kan inneholde plattformspesifikke binærfiler bygget for din lokale maskin, som ikke nødvendigvis fungerer inne i Linux-containeren. La RUN npm ci gjøre jobben inne i containeren i stedet for å kopiere en lokalt installert versjon.

Steg 9: Skann images for sårbarheter før du sender dem videre

Et image arver alle sårbarhetene i basisimaget og alle pakkene du installerer oppå det. Docker Scout, som er innebygd i moderne Docker Desktop-installasjoner, gir deg en oversikt over kjente sårbarheter (CVE-er) i imaget direkte fra kommandolinjen:

# Skann det lokale imaget for kjente sårbarheter
docker scout cves docker-tutorial-app:1.0

# Sammenlign to versjoner av samme image
docker scout compare docker-tutorial-app:1.0 --to docker-tutorial-app:0.9

Bygg inn et slikt skann som et steg i CI-pipelinen din, ikke bare som noe du kjører manuelt av og til. Et sårbarhetsskann som kjører automatisk på hver pull request fanger opp nye CVE-er før imaget noensinne når produksjon, og det er langt billigere å rette en sårbar pakke i et kodegjennomgangssteg enn etter at imaget er distribuert til hundrevis av noder.

Steg 10: Push imaget til et register

For å kunne kjøre imaget på en server eller i skyen må det først lastes opp til et container-register, enten Docker Hub eller et privat register hos skyleverandøren din. Logg inn og tagg imaget med riktig navnerom før du pusher:

# Logg inn på Docker Hub
docker login

# Tagg imaget med ditt Docker Hub-brukernavn
docker tag docker-tutorial-app:1.0 dittbrukernavn/docker-tutorial-app:1.0

# Push imaget til registeret
docker push dittbrukernavn/docker-tutorial-app:1.0

Vær oppmerksom på nedlastingsgrenser hvis du bruker anonyme eller gratis Docker Hub-kontoer til å hente images ofte, for eksempel i en CI-pipeline. Docker opererer med rate-begrensning per seksjon der uautentiserte kall og gratiskontoer har lavere tak enn betalte abonnement, dokumentert i Dockers offisielle oversikt over nedlastingsgrenser. Løsningen for CI-miljøer med høyt volum er enten å autentisere alle kall eller å bruke et gjennomsiktig cache-register foran Docker Hub.

Steg 11: Deploy applikasjonen til skyen

Med imaget i et register kan du kjøre det på nesten hvilken som helst infrastruktur som støtter containere. Under følger et eksempel med en enkel virtuell maskin, som er den mest direkte veien fra lokal Docker til en offentlig tilgjengelig tjeneste. Logg inn på serveren via SSH, installer Docker slik du gjorde i steg 1, og hent så imaget ditt:

# På produksjonsserveren
docker pull dittbrukernavn/docker-tutorial-app:1.0

docker run -d \
  --name tutorial-app \
  -p 80:3000 \
  --restart unless-stopped \
  -e NODE_ENV=production \
  dittbrukernavn/docker-tutorial-app:1.0

Flagget --restart unless-stopped sørger for at containeren starter automatisk igjen etter en serverreboot eller om prosessen krasjer, uten at du må logge inn manuelt hver gang. For team som allerede kjører managed container-tjenester i skyen, som Google Cloud Run eller AWS Fargate, er selve deploy-flyten enda enklere fordi du slipper å administrere en VM i det hele tatt. Har du allerede et Kubernetes-cluster i produksjon, er imaget du nettopp bygde umiddelbart klart til å puttes inn i en Deployment-manifest.

Håndter miljøvariabler for dev, staging og produksjon

De fleste applikasjoner trenger ulik konfigurasjon avhengig av om de kjører lokalt, i et testmiljø eller i produksjon: forskjellig databaseadresse, ulikt loggnivå, forskjellige API-nøkler. Docker Compose støtter .env-filer som lastes inn automatisk, noe som gjør det enkelt å holde miljøene fra hverandre uten å endre selve compose.yaml-filen.

# .env (leses automatisk av docker compose)
NODE_ENV=development
DATABASE_URL=postgres://postgres:hemmelig@db:5432/postgres
LOG_LEVEL=debug

For å bruke en annen konfigurasjon i staging eller produksjon, lag separate filer som .env.production og pek eksplisitt på riktig fil ved oppstart, i stedet for å stole på at Compose plukker opp riktig fil automatisk:

docker compose --env-file .env.production up -d --build

Legg aldri .env-filer med reelle passord eller API-nøkler i Git. Legg dem til i .gitignore og .dockerignore samtidig, og del i stedet et eksempel som .env.example med tomme eller falske verdier, slik at andre utviklere vet hvilke variabler applikasjonen forventer uten at hemmeligheter havner i versjonskontroll. For produksjonsmiljøer i skyen er det ofte tryggere å hente hemmeligheter fra skyleverandørens eget hemmelighetshåndteringsverktøy ved oppstart, i stedet for å ha dem liggende som filer på disk i det hele tatt.

Steg 12: Sett opp helsesjekker og automatisk restart

En container som kjører betyr ikke nødvendigvis at applikasjonen inni den svarer riktig. Docker støtter innebygde helsesjekker som overvåker applikasjonens faktiske tilstand, ikke bare om prosessen fortsatt eksisterer. Legg til i Dockerfilen:

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

Med denne linjen kaller Docker /health-endepunktet hvert 30. sekund og markerer containeren som “unhealthy” hvis den svarer feil tre ganger på rad. Kombinert med --restart unless-stopped og en healthcheck i Compose-filen (slik vi gjorde med databasen i steg 7) kan orkestreringsverktøy automatisk bytte ut eller starte om containere som har sluttet å fungere riktig, uten at et menneske trenger å oppdage det manuelt først.

Sammenligning: Docker-kommandoer for images og containere

KommandoVirker påHva den gjør
docker buildImageBygger et nytt image fra en Dockerfile
docker runContainerStarter en ny container fra et image
docker psContainerLister kjørende containere
docker imagesImageLister lokale images
docker execContainerKjører en kommando inne i en kjørende container
docker logsContainerViser konsollutdata fra en container
docker system pruneAltRydder bort ubrukte images, containere og nettverk

5 vanlige feil nybegynnere gjør med Docker

1. Å kjøre alt som root inne i containeren. Uten en eksplisitt USER-instruksjon kjører prosessen som root som standard. Klarer en angriper å bryte seg ut av applikasjonslaget, arver de root-rettigheter inne i containeren. Legg alltid til en ikke-privilegert bruker slik vi gjorde i steg 3.

2. Å bruke latest som tag i produksjon. Tagen latest peker på hva som helst avhengig av når imaget ble pushet sist, og gir deg ingen garanti for hvilken versjon som faktisk kjører. Bruk eksplisitte versjonsnumre eller Git-commit-hasher som tag, slik at et rulle-tilbake alltid er forutsigbart.

3. Å glemme .dockerignore. Uten den havner node_modules, .git-historikk og lokale .env-filer lett inn i byggekonteksten, noe som gjør byggingen treg og i verste fall lekker hemmeligheter inn i et image som blir pushet til et offentlig register.

4. Å ikke rydde opp i lagrede volumer og ubrukte images. Docker sletter aldri volumer automatisk, selv når containerne som brukte dem er fjernet. Over tid fyller dette opp disken. Kjør docker system prune og docker volume prune jevnlig, eller sett opp automatisk rydding i CI-miljøer.

5. Å legge hemmeligheter direkte i Dockerfilen. En verdi satt med ENV eller ARG i en Dockerfile blir en permanent del av imagets historikk, synlig for alle med tilgang til imaget selv om du fjerner den i et senere lag. Bruk miljøvariabler satt ved kjøretid, eller et eget hemmelighetshåndteringsverktøy, aldri hardkodede passord eller API-nøkler i selve filen.

Feilsøking: 8 vanlige problemer og løsninger

“Cannot connect to the Docker daemon”. Docker-tjenesten kjører ikke. På Linux: sudo systemctl start docker. På Windows/macOS: sjekk at Docker Desktop faktisk er startet i bakgrunnen.

“Permission denied” ved kjøring av docker-kommandoer på Linux. Brukeren din er ikke lagt til i docker-gruppen. Kjør sudo usermod -aG docker $USER og logg ut og inn igjen.

“Port is already allocated”. En annen container eller lokal prosess bruker allerede porten du prøver å mappe. Kjør docker ps for å se hva som opptar porten, eller bytt vertsport med -p 3001:3000 i stedet.

Containeren starter og stopper umiddelbart. Hovedprosessen (den definert i CMD) har krasjet. Kjør docker logs [container-navn] for å se feilmeldingen, ofte en manglende avhengighet eller feil filsti.

Endringer i koden vises ikke i containeren. Du kjører sannsynligvis et bygget image, ikke en bind mount. Under utvikling må du enten bygge imaget på nytt med docker build, eller bruke en bind mount slik som vist i steg 5.

“No space left on device”. Docker har brukt opp diskplassen med gamle images, stoppede containere og ubrukte volumer. Kjør docker system df for å se hva som tar plass, og docker system prune -a --volumes for å rydde grundig (vær obs på at dette sletter alt ubrukt).

Containere på samme Compose-nettverk finner ikke hverandre. Sjekk at du refererer til tjenestenavnet definert i compose.yaml, ikke localhost. Inne i en container peker localhost alltid på containeren selv, aldri på andre containere.

Bygging er treg selv på små kodeendringer. Rekkefølgen i Dockerfilen ødelegger sannsynligvis lagcachen. Sørg for at instruksjoner som endrer seg sjeldnere (som COPY package.json og RUN npm ci) står før instruksjoner som endrer seg ofte (som COPY . .), slik vi gjorde i steg 3.

Multi-stage-bygget kopierer ikke filene du forventer. Sjekk at stien i COPY --from=build stemmer med hvor byggeverktøyet faktisk skriver output i det første steget. Kjør docker build --target build -t debug-image . og deretter docker run -it debug-image sh for å utforske filsystemet i mellomsteget direkte.

Avanserte tips for produksjonsklare Docker-oppsett

Når grunnleggende bygg og kjøring fungerer, er det noen teknikker som skiller et hobbyoppsett fra noe som tåler faktisk produksjonstrafikk. BuildKit, som er standard byggemotor i moderne Docker-versjoner, støtter cache-mounts som lar deg cache pakkeinstallasjoner mellom bygg uten at cachen havner i selve imaget:

# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

Sett også ressursgrenser på containere i produksjon, slik at én feiloppførende container ikke kan sulte resten av verten for CPU eller minne:

docker run -d \
  --name tutorial-app \
  --memory="256m" \
  --cpus="0.5" \
  -p 3000:3000 \
  dittbrukernavn/docker-tutorial-app:1.0

For team som bygger images i CI, bruk gjerne --platform linux/amd64,linux/arm64 sammen med docker buildx for å produsere multi-arkitektur-images i én operasjon, spesielt relevant nå som ARM-baserte servere og lokale Apple Silicon-maskiner begge er vanlige i samme utviklerteam. Følg også med på Dockers offisielle anbefalinger for imagebygging, som oppdateres etter hvert som BuildKit får nye funksjoner.

Ditt komplette prosjekt: filstruktur og full kildekode

Etter å ha fulgt alle stegene over sitter du igjen med et komplett, fungerende prosjekt. Slik ser den ferdige mappestrukturen ut:

docker-tutorial-app/
├── server.js
├── package.json
├── package-lock.json
├── Dockerfile
├── .dockerignore
└── compose.yaml

Med denne strukturen på plass kan hvem som helst klone prosjektet og få det opp og kjøre med to kommandoer:

git clone [din-repo-url]
cd docker-tutorial-app
docker compose up -d --build

Det er selve poenget med Docker: et prosjekt som kjører identisk uansett hvilken maskin det klones til, uten en side med manuell oppsettsdokumentasjon. Bekreft at alt fungerer med curl http://localhost:3000/ og docker compose ps, som skal vise begge tjenestene som “healthy”.

Docker i CI/CD: bygg og test automatisk ved hver commit

Manuell bygging og pushing fungerer fint mens du lærer, men i et reelt team bør Docker-imaget bygges automatisk hver gang noen pusher kode. De fleste CI-plattformer, enten det er GitHub Actions, GitLab CI eller Bitbucket Pipelines, har Docker forhåndsinstallert på sine standard-runnere, så du slipper å installere noe selv. Under er et eksempel på en GitHub Actions-workflow som bygger imaget, kjører et sårbarhetsskann og pusher til registeret, men bare når koden ligger på main-grenen:

name: Bygg og push Docker-image
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Logg inn på Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Bygg og push image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: dittbrukernavn/docker-tutorial-app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Legg merke til at imaget tagges med github.sha, altså commit-hashen, i stedet for latest. Det gir hver bygg et unikt, sporbart versjonsnummer, og du kan alltid finne tilbake til nøyaktig hvilken kode som kjører i produksjon ved å se på taggen på det kjørende imaget. Feltene cache-from og cache-to med type=gha lar GitHub Actions gjenbruke lagcachen mellom kjøringer, noe som kan kutte byggetiden betydelig sammenlignet med å starte helt på nytt hver gang.

Hemmeligheter som Docker Hub-passord skal alltid ligge i CI-plattformens innebygde hemmelighetslagring, som secrets.DOCKERHUB_TOKEN i eksempelet over, aldri direkte i workflow-filen. Bruk et tilgangstoken generert spesifikt for CI fremfor selve Docker Hub-passordet ditt, slik at du kan tilbakekalle det uten å måtte bytte passord på hele kontoen hvis tokenet skulle lekke.

Logging og overvåking av containere i produksjon

Som standard skriver Docker alt en container sender til standard-out og standard-error til en JSON-fil på verten, tilgjengelig via docker logs. Det fungerer greit for en enkelt server, men blir fort uoversiktlig når du har flere containere spredt på flere maskiner. Sett en grense på loggfilstørrelsen fra start, ellers kan en pratsom applikasjon fylle opp disken over tid:

services:
  app:
    build: .
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Med denne konfigurasjonen beholder Docker maksimalt tre loggfiler på 10 MB hver per container, og roterer automatisk bort de eldste. For produksjonsmiljøer med mer enn noen få containere bør du dessuten sende loggene videre til et sentralt system, enten gjennom en logg-driver som syslog eller fluentd, eller ved å la en sidecar-container samle opp og videresende logger fra flere tjenester samtidig. Poenget er at du skal kunne søke på tvers av alle containere fra ett sted når noe går galt, i stedet for å SSH-e inn på hver enkelt server og kjøre docker logs manuelt.

For ren ressursovervåking uten å sette opp et helt observability-stack kan du bruke docker stats til å se CPU-, minne- og nettverksbruk for alle kjørende containere i sanntid direkte i terminalen:

docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

Dette gir deg en løpende tabell som oppdateres kontinuerlig, nyttig når du feilsøker en container som spiser mer minne enn forventet, eller når du vil bekrefte at ressursgrensene satt med --memory og --cpus faktisk overholdes under reell last.

Docker vs. andre containerverktøy: hva bør du velge?

VerktøyBest egnet forMerknad
DockerLokal utvikling, enkeltmaskin-deploy, CI-byggStørst økosystem og dokumentasjon
PodmanRootless containere, RHEL-baserte miljøerKommandolinje-kompatibel med Docker
containerdKjøretidsmotor under KubernetesBrukes internt av Docker selv, sjelden brukt direkte av utviklere
KubernetesOrkestrering av mange containere på tvers av servereBygger videre på images laget med Docker

Docker og Kubernetes konkurrerer ikke egentlig med hverandre. Docker er verktøyet du bruker for å bygge og teste images lokalt, mens Kubernetes tar over når du trenger å kjøre og skalere de samme imagene på tvers av mange servere samtidig. De aller fleste team starter med Docker Compose for lokal utvikling og mindre deployeringer, og flytter først til Kubernetes når driftskompleksiteten faktisk krever det.

Ofte stilte spørsmål om Docker

Er Docker gratis å bruke?
Docker Engine er åpen kildekode og gratis. Docker Desktop er gratis for personlig bruk, utdanning og små bedrifter, men krever et betalt abonnement for større kommersielle organisasjoner. Sjekk gjeldende vilkår på Dockers offisielle nettsider før bruk i et større selskap.

Hva er forskjellen på et image og en container?
Et image er en statisk, skrivebeskyttet mal. En container er en kjørende instans av det imaget, med et eget skrivbart lag. Du kan starte flere containere fra samme image samtidig uten at de påvirker hverandre.

Trenger jeg Kubernetes hvis jeg allerede bruker Docker?
Nei, ikke nødvendigvis. Docker Compose dekker behovet for de fleste mindre applikasjoner og team. Kubernetes blir relevant først når du trenger automatisk skalering på tvers av mange servere, avansert lastbalansering eller høy grad av selvhelbredende infrastruktur.

Hvorfor er imaget mitt så stort?
Vanlige årsaker er at du bruker et fullt Debian-basert basisimage i stedet for en Alpine-variant, at devDependencies havner i produksjonsimaget, eller at du ikke bruker multi-stage bygg til å luke ut byggeverktøy fra sluttresultatet.

Er det trygt å kjøre Docker i produksjon?
Ja, forutsatt at du følger grunnleggende sikkerhetspraksis: kjør som ikke-root-bruker, skann images for kjente sårbarheter jevnlig, unngå latest-tags, og begrens ressursbruk per container. Docker driver produksjonsmiljøer hos svært mange selskaper globalt.

Kan jeg kjøre Docker på Windows uten WSL2?
Teknisk sett ja, gjennom det eldre Hyper-V-baserte oppsettet, men WSL2 er anbefalt fordi det gir betydelig raskere filsystemytelse og lavere minnebruk sammenlignet med den eldre virtualiseringsmodusen.

Hva er forskjellen på volumer og bind mounts?
Volumer forvaltes av Docker selv og er portable mellom miljøer, mens bind mounts kobler en spesifikk mappe på vertsmaskinen direkte inn i containeren. Bruk volumer i produksjon og bind mounts primært under lokal utvikling.

Hvordan oppdaterer jeg en kjørende container uten nedetid?
Bygg det nye imaget, start en ny container fra det oppdaterte imaget på en annen port eller bak en lastbalanserer, verifiser at den nye versjonen fungerer, og bytt deretter trafikken over før du fjerner den gamle containeren. Denne teknikken kalles gjerne rullerende eller “blue-green”-utrulling.