Docker

Skriv din första Dockerfile

Från kommandon till en image som går att lita på

Hoppa till

Var vi är

Förra delen gick igenom fyra begrepp: container, image, registry och Compose. Lisa körde images som någon annan byggt, och lärde sig att imagen är ritningen och containern den körande kopian.

Nu paketerar hon sin egen applikation. Den här delen handlar om filen som beskriver bygget, och om varför ordningen mellan raderna avgör hur lång tid det tar.

Vad en Dockerfile är

En Dockerfile är en textfil med instruktioner för hur en image ska byggas.

Docker läser filen uppifrån och ner och utför en instruktion i taget. Resultatet är en image.

Filen heter Dockerfile utan filändelse och ligger normalt i projektets rot, bredvid koden den paketerar. Den hör hemma i Git och granskas som all annan kod.

Dockerfile vs image vs container

Tre begrepp i rad, och varje steg har sitt eget kommando.

Dockerfilen är receptet. En textfil du skriver för hand.

Imagen är resultatet av bygget. Oföränderlig, taggad, delbar via ett registry.

Containern är en körande kopia av imagen. Kastas och startas om fritt.

DockerfileReceptet. En textfil i ditt repo, granskad som all annan kod.docker buildImageDen färdiga ritningen. Oföränderlig, taggad, delbar via ett registry.docker runContainerEn körande kopia. Kastas och startas om fritt.
Två kommandon, tre tillstånd. Receptet är det enda du skriver för hand.

Skillnaden mot att installera saker för hand är att receptet går att läsa, granska och köra om. Ingen behöver minnas vilka kommandon som kördes en fredagseftermiddag för ett halvår sedan.

Filen, rad för rad

Här är en minimal Dockerfile för en Node-applikation. Sex rader, och varje rad gör en sak:

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

Vi tar dem en i taget nedan. Instruktionerna skrivs versalt av tradition — Docker bryr sig inte, men alla andra Dockerfiles ser ut så.

FROM — utgångsläget

FROM väljer imagen du bygger vidare på.

Du börjar aldrig från ingenting. node:22-alpine är en färdig image med Node 22 installerat ovanpå Alpine Linux, en avskalad distribution som gör resultatet betydligt mindre.

FROM startar första byggsteget och är normalt första instruktionen i filen. Ett globalt ARG kan stå före när taggen hämtas från en byggvariabel.

Du behöver inte använda det nu — poängen är att FROM väljer imagen allt som följer byggs ovanpå.

WORKDIR och COPY

WORKDIR sätter arbetskatalogen inuti imagen. Allt som följer sker där, och katalogen skapas om den inte finns.

COPY flyttar filer från din dator in i imagen. COPY package*.json ./ kopierar alla filer som matchar mönstret till den katalog WORKDIR pekar ut.

Utan WORKDIR hamnar allt i rotkatalogen, vilket fungerar men blir rörigt så fort någon annan ska läsa filen.

COPY vs ADD

Två instruktioner som ser utbytbara ut. De är det inte.

COPY kopierar filer och kataloger. Det är allt den gör, och det är därför den är förutsägbar.

ADD kan dessutom hämta filer från en URL och packa upp lokala tar-arkiv automatiskt.

Just den automatiken är problemet: Docker avgör om ett lokalt tar-arkiv packas upp från arkivets innehåll, inte från filändelsen. Beteendet blir därför svårt att läsa sig till i Dockerfilen.

Använd COPY. Behöver du hämta något från nätet, gör det med ett synligt RUN-kommando i stället, så syns det i filen.

RUN vs CMD

Den vanligaste förväxlingen i hela ämnet, och den enda som får bygget att bete sig obegripligt.

RUN kör ett kommando när imagen byggs. Det sker en gång, och resultatet fryses in i imagen.

CMD anger vad som ska köras när en container startas ur imagen. Det körs inte vid bygget alls.

I filen ovan installerar RUN npm ci --omit=dev beroendena vid bygget, så att de redan ligger där när containern startar. CMD ["node", "server.js"] startar applikationen — varje gång någon kör docker run.

Kontrollfrågan när du är osäker: ska det här ha hänt innan imagen är färdig? Ja → RUN. Nej → CMD.

Det finns en tredje, ENTRYPOINT, som också rör starten men beter sig annorlunda när man skickar med argument. Den behöver du inte nu — CMD räcker för allt i den här serien.

Varje instruktion blir ett lager

Varje instruktion i Dockerfilen skapar ett lager i imagen.

Lagren är skrivskyddade och staplas på varandra. Den färdiga imagen är summan av dem, och två images som delar de nedersta lagren lagrar dem bara en gång.

CMD ["node", "server.js"]Ändrar bara vad som startas. Kostar ingenting att bygga om.COPY . .Din källkod. Ändras varje gång du sparar — här faller cachen, nästan alltid.RUN npm ci --omit=devInstallationen. Dyr att köra om — men behöver bara göras när raden under ändrats.COPY package*.json ./Beroendelistan. Ändras sällan.FROM node:22-alpineBasen. Hämtas en gång och delas mellan alla dina images.
Docker Engine
Cachen faller vid första lagret som ändrats — och allt ovanför det måste byggas om.

Lagercachen

Bygger du om en image och ingenting har ändrats i ett lager återanvänder Docker det i stället för att köra om instruktionen. Det kallas cache, och det syns i byggloggen som CACHED.

Cachen faller vid det första lagret som ändrats — och därefter byggs allt ovanför om, oavsett om de raderna ändrats eller inte.

Det gör att ett bygge kan gå på två sekunder eller två minuter beroende på var i filen din ändring råkade landa.

Varför ordningen spelar roll

Nu blir de fem raderna begripliga. Varför kopieras package.json för sig, innan resten av koden?

Beroendena ändras sällan. Källkoden ändras hela tiden. Genom att kopiera in beroendelistan och installera innan koden kopieras får npm ci behålla sin cache tills du faktiskt lägger till ett paket.

Koden först
COPY . .
RUN npm ci --omit=dev

En enda ändrad bokstav i koden invaliderar COPY-lagret — och därmed allt ovanför.

npm ci körs om varje gång. Bygget går från sekunder till någon minut, hela dagen.

Beroendena först
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

Kodändringen träffar bara det sista lagret.

npm ci återanvänds ur cachen tills du faktiskt lägger till ett paket.

Samma instruktioner. Bara ordningen skiljer — och den avgör om bygget tar sekunder eller minuter.

Tumregel: lägg det som ändras sällan högt upp i filen, och det som ändras ofta längst ner.

Vad byggkontexten är

Byggkontexten är den katalog vars innehåll skickas till Docker när bygget startar.

Det är punkten i docker build -t min-app:1.0 . — det lilla tecknet på slutet som är lätt att missa. Allt i den katalogen packas ihop och skickas iväg innan första instruktionen körs.

Kontexten avgör vad COPY kan komma åt. Ligger en fil utanför kontexten går den inte att kopiera in, hur rätt sökvägen än ser ut.

docker build -t app:1.0 . app/ package.json Docker ../nycklar byggkontexten
Pilen går från punkten till ramen. Filen under ramen är streckad och kommer aldrig in.

.dockerignore

COPY . . kopierar in allt i byggkontexten — inklusive node_modules, .git, lokala loggfiler och den där .env-filen med hemligheter.

Lösningen är en .dockerignore, som fungerar precis som en .gitignore:

node_modules
.git
.env
*.log

Filen krymper både byggkontexten och den färdiga imagen. Att den saknas märks sällan förrän någon frågar varför imagen är 900 MB.

Fallgrop

En .env som kopierats in i ett lager finns kvar i imagen även om ett senare lager raderar den — precis som en committad hemlighet finns kvar i Git-historiken.

Den som hämtar imagen kan gräva fram filen ur lagret. Lägg till .dockerignore innan första bygget, och betrakta hemligheten som läckt om imagen redan delats.

.dockerignore vs .gitignore

Två filer med samma syntax och nästan samma namn, som skyddar mot olika saker.

.gitignore håller filer utanför repot. Den påverkar vad som versionshanteras och delas med teamet.

.dockerignore håller filer utanför byggkontexten. Den påverkar vad COPY kan nå och vad som hamnar i imagen.

De överlappar men är inte utbytbara, och skillnaden går åt båda hållen. node_modules hör hemma i båda. Men .git-katalogen ska med i .dockerignore och kan omöjligt stå i .gitignore. Och en .env som redan är i .gitignore finns ändå kvar på din disk, alltså i byggkontexten, om du inte skriver in den i .dockerignore också.

Har du bara den ena är det nästan alltid .dockerignore som saknas.

Bygg och kör

docker build -t min-app:1.0 .
docker run -p 3000:3000 min-app:1.0

-t sätter namn och tagg, så du kan referera till imagen efteråt. Utan den får imagen bara ett hash-id och dyker upp som <none> i docker images.

-p 3000:3000 kopplar port 3000 på din dator till port 3000 inuti containern. Utan den publicerar Docker inte porten på din dator. Containrar på samma Docker-nätverk kan fortfarande nå applikationen.

Portar tar vi ordentligt i nästa del.

Fallgropen: latest är inget löfte

Fallgrop

Det är frestande att skriva FROM node:latest. Problemet är att latest inte betyder “senaste stabila” — det är bara en tagg som pekar på vad ägaren senast råkade publicera.

Bygger du om samma Dockerfile om tre månader kan du få en helt annan Node-version. Applikationen som fungerade i går slutar fungera i dag utan att du ändrat en rad kod.

Välj en specifik tagg: node:22-alpine. Det gör versionsvalet synligt, men en tagg kan fortfarande flyttas. När ett bygge måste använda exakt samma basimage låser du den med en digest.

Vad det här betyder i ditt arbete

Du kommer att skriva en Dockerfile per projekt och sedan ändra i den ett par gånger om året. Det du gör dagligen är att läsa byggloggar och undra varför bygget tog fyra minuter.

Svaret står nästan alltid i instruktionsordningen, och det är därför lagercachen är den delen av det här avsnittet som betalar sig mest.

Multi-stage builds, imagesignering och sårbarhetsskanning kan vänta. De blir aktuella när imagen ska till produktion och någon ställer krav på storlek och innehåll — och då bygger de på exakt samma lagermodell.

Vanliga nybörjarförväxlingar

En Dockerfile är inte en image. Filen är receptet; docker build producerar imagen.

RUN och CMD är inte utbytbara. RUN hör till bygget, CMD till körningen.

CACHED i byggloggen betyder inte att något hoppades över felaktigt. Det betyder att lagret återanvändes.

.dockerignore städar inte en redan byggd image. Filen som kopierats in ligger kvar i sitt lager.

Punkten i docker build . är inte dekoration. Den är byggkontexten och avgör vad COPY kan nå.

latest betyder inte “senaste stabila”. Det är bara en tagg som någon senast satte.

Prova själv

Skapa en mapp med filen ovan och en tom server.js, eller använd ett projekt du har.

docker build -t min-app:1.0 .

Läs utskriften uppifrån och ner — varje instruktion i Dockerfilen syns som ett eget steg. Titta sedan på lagren i den färdiga imagen:

docker history min-app:1.0

Varje rad är ett lager, med storleken det bidrar med. Bygg nu om utan att ändra någonting:

docker build -t min-app:1.0 .

Nästan varje steg står nu som CACHED och bygget tar ett ögonblick. Ändra slutligen en bokstav i server.js och bygg igen — räkna hur många steg som fortfarande är cachade.

Flytta gärna COPY . . högst upp i filen och bygg om, så ser du cachen falla direkt.

Två saker som kan förvirra: docker history visar <missing> i id-kolumnen för lager som kommer från basimagen, vilket är normalt och inte ett fel. Och första bygget laddar ner basimagen, så tidsjämförelsen blir rättvis först från andra bygget.

Kom ihåg
  • En Dockerfile är ett recept: instruktioner för hur en image byggs.
  • RUN körs vid bygget, CMD när containern startar.
  • Varje instruktion blir ett lager som cachas. Lägg det som ändras sällan överst.
  • Cachen faller vid första ändrade lagret, och allt ovanför byggs om.
  • .dockerignore håller imagen liten och hemligheterna kvar på din dator.
  • Välj en specifik tagg i FROMlatest gör bygget oförutsägbart. En digest låser exakt image.
Kolla att du hänger med
Vad är skillnaden mellan RUN och CMD?
RUN körs när imagen byggs och resultatet fryses in i ett lager. CMD körs när en container startas från imagen. Kontrollfrågan: ska det ha hänt innan imagen är färdig? Ja → RUN.
Varför kopieras package.json in före resten av koden?
För att skydda cachen. Beroendena ändras sällan, koden ofta. Installeras beroendena innan koden kopieras kan npm ci återanvändas ur cachen tills du faktiskt lägger till ett paket — annars körs den om vid varje sparning.
Du ändrar en bokstav i server.js och bygger om. Vilka lager byggs om?
Lagret för COPY . . och allt ovanför det. Lagren under — basimagen, beroendelistan och npm ci — återanvänds ur cachen, eftersom ingenting i dem ändrats.
Varför ska du välja COPY framför ADD?
COPY gör bara en sak: kopierar filer. ADD kan dessutom packa upp lokala tar-arkiv och hämta från URL:er. Uppackningen avgörs av arkivets innehåll, vilket gör beteendet svårare att läsa i filen.
Du lägger till .env i .dockerignore efter att ha byggt imagen en gång. Är hemligheten borta?
Inte ur den redan byggda imagen — filen ligger kvar i det lager där den kopierades in, även om ett senare lager raderar den. Bygg om från grunden, och betrakta hemligheten som läckt om imagen delats.
Vad är problemet med FROM node:latest?
latest är bara en tagg som pekar på det senast publicerade, inte på en version du valt. En specifik tagg gör valet synligt, men kan flyttas. När du måste låsa exakt basimage använder du dess digest.

Snabb sammanfattning

  • En Dockerfile är en textfil med instruktioner för hur en image byggs.
  • Dockerfile → image → container, via docker build och docker run.
  • FROM väljer utgångsläget och startar normalt första byggsteget.
  • WORKDIR sätter arbetskatalogen; COPY flyttar in filer från byggkontexten.
  • Använd COPY, inte ADD. ADD har extra automatik för lokala tar-arkiv.
  • RUN körs vid bygget, CMD när containern startar.
  • Varje instruktion blir ett skrivskyddat lager, och lagren cachas.
  • Cachen faller vid första ändrade lagret. Allt ovanför byggs om.
  • Sällanändrat högst upp, ofta ändrat längst ner. Därför kopieras package.json för sig.
  • Byggkontexten är punkten i docker build . och avgör vad COPY kan nå.
  • .dockerignore krymper kontexten och imagen — och den ska finnas före första bygget.
  • Välj en specifik tagg i FROM. latest är en tagg, inte ett löfte.

Nästa del

Nu har Lisa en image som startar och svarar. Men e-handelssystemet behöver en databas bredvid sig, och i nästa del stöter hon på det fel nästan alla får först: varför localhost inte betyder vad hon tror inuti en container.