Skriv din första Dockerfile
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.
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.
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.
COPY . .
RUN npm ci --omit=devEn 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.
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.
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.
.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.
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
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.
- En
Dockerfileär ett recept: instruktioner för hur en image byggs. RUNkörs vid bygget,CMDnä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.
.dockerignorehåller imagen liten och hemligheterna kvar på din dator.- Välj en specifik tagg i
FROM—latestgör bygget oförutsägbart. En digest låser exakt image.
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?
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?
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?
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 buildochdocker run. FROMväljer utgångsläget och startar normalt första byggsteget.WORKDIRsätter arbetskatalogen;COPYflyttar in filer från byggkontexten.- Använd
COPY, inteADD.ADDhar extra automatik för lokala tar-arkiv. RUNkörs vid bygget,CMDnä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.jsonför sig. - Byggkontexten är punkten i
docker build .och avgör vadCOPYkan nå. .dockerignorekrymper 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.