Containrar

Docker

En praktisk introduktion till hur Docker paketerar programvara, kör containrar och kopplar ihop applikationens delar.

Docker paketerar en applikation och dess körtidsberoenden i en portabel, isolerad enhet som kallas container. Till skillnad från virtuella maskiner delar containrar värdoperativsystemets kärna men isoleras med Linux-namnrymder och kontrollgrupper, vilket gör dem lättviktiga, snabba att starta och mycket portabla. Resultatet är en leveransmodell där samma image byggs en gång, testas konsekvent och befordras genom miljöer utan konfigurationsavvikelse eller beroendemismatch.

Lärandemål

Det här ska du kunna efter genomläsning.
  • Förklara skillnaden mellan en image, en container och värden de kör på.
  • Beskriva de viktigaste filerna och arbetsstegen när en Docker-baserad applikation byggs och körs.
  • Känna igen den vanliga livscykeln från build till körning till uppdatering.

I korthet

En snabb mental modell innan du går på djupet.
Byggstenar
  • Images
  • Containrar
  • Dockerfile
Körperspektiv
  • Processer
  • Portar
  • Volymer
Bra arbetssätt
  • Repeterbara builds
  • Tydlig taggning
  • Enkel koppling mellan tjänster

Kärnidén

På operativsystemnivå är en container en process som begränsas av två Linux-primitiver. Namnrymder och kontrollgrupper. Namnrymder isolerar vad processen kan se. Dess filsystemsvy, nätverksgränssnitt, värdnamn och processträd. Kontrollgrupper (cgroups) begränsar hur mycket CPU, minne och I/O processen får använda. Docker samlar dessa kärnfunktioner bakom ett utvecklarvänligt verktyg, men det är dessa primitiver som faktiskt skapar isoleringen.

Den distinktionen är viktig när man resonerar om säkerhet. En container är inte en virtuell maskin. Den delar värdkärnan. Om kärnan har en sårbarhet som en containerprocess kan nå kan isoleringen brytas. Det är därför kärnversion, containerkörningskonfiguration och arbetslaststilägg alla påverkar säkerhetsläget för ett containeriserat system.

Reproducerbarhet är den andra centrala fördelen. En image fångar inte bara applikationskod utan exakta versioner av systembibliotek, konfiguration och verktyg som koden är beroende av. Att flytta den imagen från en utvecklares dator till test och produktion ändrar bara körtidskontexten, inte programvaran. Det gör miljöer lättare att resonera om och problem lättare att återskapa.

Docker-arkitekturen skiljer på imagen (en skrivskyddad, lagerbaserad ritning) och containern (en körande instans med ett skrivbart toppskikt). Flera containrar kan köra från samma image samtidigt utan att störa varandras filtillstånd, och att stoppa en container lämnar imagen intakt för nästa körning.

Arbetsmodell

  • Skriv en Dockerfile som exakt beskriver hur imagen byggs, inklusive basimage, beroenden och konfiguration.
  • Bygg imagen för att producera en versionshanterad, taggad artefakt som kan pushas till ett registry och dras i vilken miljö som helst.
  • Kör imagen som en eller flera containrar och exponera bara de portar och volymer som arbetslasten faktiskt behöver.
  • Befordra samma imageartefakt genom miljöer i stället för att bygga om i varje steg.

Operativ basnivå

  • Tagga images med en specifik version eller commit-referens i stället för att förlita sig på rörliga taggar som 'latest' för automatiserade driftsättningar.
  • Håll varje container till ett enda ansvar så att dess Dockerfile, resursbehov och felbeteende är lätta att förstå.
  • Behandla Dockerfile och Compose-fil som förstaklassig applikationskod. Versionera, granska och testa dem med samma disciplin som applikationen.
  • Baka aldrig in miljöspecifik konfiguration eller hemligheter i en image, injicera dem vid körning via miljövariabler eller en hemlighetshanterare.

Signaler att bevaka

Mönster som är värda att undersöka vidare.
  • En container-image ändras utan att det finns en motsvarande kodändring.
  • Flera tjänster packas ihop i samma container av bekvämlighetsskäl.
  • Portar exponeras utan att det finns ett tydligt syfte eller en tydlig ägare.

FÖRDJUPNING

Containergrunder

En container är inte ett paket. Det är en körande process med en begränsad vy av operativsystemet. När Docker startar en container skapar Linux-kärnan en ny namnrymduppsättning för den. Ett privat filsystem (monteringsnamnrymd), en privat nätverksstack (nätverksnamnrymd), ett privat värdnamn (UTS-namnrymd) och ett isolerat processträd (PID-namnrymd). Containerprocessen tror att den är den enda arbetslasten på maskinen, även om den delar kärnan med många andra.

Resursisolering hanteras av kontrollgrupper (cgroups) som sätter hårda gränser för CPU-tid, minnesanvändning, disk-I/O och nätverksbandbredd. Utan cgroups kan en enda felaktig container svälta ut alla andra. Tillsammans definierar namnrymder och cgroups vad en container kan se och hur mycket den kan förbruka.

Containerimages använder ett union-filsystem byggt av lager. Varje instruktion i en Dockerfile som modifierar filsystemet lägger till ett nytt skrivskyddat lager. Dessa lager delas mellan images med gemensam historik, vilket gör lagringen effektiv. När en container startar läggs ett tunt skrivbart lager ovanpå, skrivningar hamnar där och försvinner när containern stoppas.

En vanlig missuppfattning är att behandla containrar som lättviktiga virtuella maskiner. De är det inte. Att köra som root inne i en container innebär att processen har root-behörighet inom sin namnrymd. Och om namnrymdisoleringen bryts, blir det root på värden. Det är därför det är viktigt att undvika root i containrar och att använda minimala capabilities för produktionsarbetslaster.

Images och containrar

En image är en skrivskyddad, innehållsadresserad artefakt. Den innehåller en filsystemögonblicksbild byggd från Dockerfile-instruktioner, plus metadata som standardstartpunkt, exponerade portar och miljövariabler. Images är komposerbara. Varje instruktion skapar ett nytt filsystemslager, och dessa lager återanvänds mellan images med gemensam bas.

En container är resultatet av att starta en image. Docker lägger till ett tunt skrivbart lager ovanpå imagelagerens skrivskyddade lager. Alla filändringar i containern hamnar i detta skrivbara lager. När containern tas bort kasseras det skrivbara lagret. De ursprungliga imagelagren är orörda och tillgängliga för nästa containerinstans.

Denna copy-on-write-modell har praktiska konsekvenser. Att läsa filer är snabbt eftersom data serveras direkt från de delade, oföränderliga lagren. Att skriva filer kopierar dem till det skrivbara lagret, vilket håller olika containerinstanser från att störa varandras filtillstånd. Det är därför volymer finns. För att bevara data som behöver överleva containerns skrivbara lager.

Imagestorleken påverkar starttid, lagring, nedladdningslatens och attackyta. Varje lager i en image finns i varje container som startas från den. Filer som lagts till i ett tidigt lager kan inte tas bort av en senare RUN-instruktion. De finns fortfarande i imagen, bara dolda. Det är den viktigaste motivationen för flerstegsbyggen. Att säkerställa att den slutliga imagen bara innehåller det applikationen behöver för att köra.

Dockerfile

En Dockerfile är en sekvens av instruktioner som Docker-byggmotorn kör en efter en. Varje instruktion som modifierar filsystemet producerar ett nytt oföränderligt lager. Ordningen på instruktioner spelar roll både för korrekthet och bygghastighet, eftersom Docker cachar lager. Om en instruktion och dess indata inte ändrats sedan det senaste bygget återanvänder Docker det cachade resultatet.

Cache-effektiva Dockerfiler placerar de minst förändrade instruktionerna först. Att lägga till och installera beroenden (COPY package.json && RUN npm install) innan källkod kopieras innebär att beroendelagret bara byggs om när paketfilen ändras. I en aktiv utvecklingsmiljö kan detta minska byggtiderna från minuter till sekunder.

Valet av basimage i FROM-instruktionen är ett av de viktigaste besluten i en Dockerfile. Officiella slim- eller alpine-varianter innehåller färre paket än fullstora images, vilket minskar både imagestorleken och antalet paket som kan innehålla kända sårbarheter. Distroless- eller scratch-baserade images går längre och innehåller bara körtiden och applikationsbinären.

Vanliga misstag inkluderar att köra apt-get utan --no-install-recommends, att COPY:a hela repositoryt in i imagen, att lämna byggverktyg i en körtidsimage och att lagra hemligheter i ARG- eller ENV-instruktioner (som visas i imagelagerhistoriken och i docker inspect). Vart och ett av dessa förstorar antingen imagen, saktar ner bygget eller lämnar känslig data tillgänglig för alla som kan dra imagen.

Compose

Docker Compose definierar en applikation med flera containrar som en enda deklarativ YAML-fil. En typisk webbapplikation kan behöva en applikationstjänst, en databas och en cache. Utan Compose kräver att starta den stacken tre separata docker run-kommandon med noggrant koordinerade portmappningar och miljövariabler. Compose reducerar detta till docker compose up.

Compose ger automatisk tjänstupptäckt via namn. Tjänster på samma Compose-nätverk når varandra med tjänstenamnet som värdnamn. App-containern ansluter till 'db' utan att känna till dess IP-adress, och det namnet löser sig till vilken container som för tillfället kör databastjänsten. Detta gör konfigurationen portabel.

Compose är utmärkt för lokal utveckling och integrationstester men är inte ett produktionsorkestreringsverktyg. Det körs på en enda Docker-värd, hanterar inte värdfel och kan inte skala tjänster över maskiner. Team som fortsätter använda Compose i produktion för enkla arbetslaster bör förstå detta tydligt. Kubernetes eller liknande orkestrerare existerar för att hantera vad Compose inte gör.

Ett vanligt misstag är att behandla docker-compose.yml som ett engångsskript snarare än en versionshanterad del av applikationen. När Compose-filer avviker från applikationens verkliga körtidskrav slutar de att vara ett tillförlitligt sätt att återskapa miljön. Filen ska finnas i källkodskontroll och granskas som vilken annan konfiguration som helst.

Volymer

Docker tillhandahåller tre typer av beständig lagring. Namngivna volymer hanteras av Docker. Det väljer lagringsplats på värden, skapar den automatiskt och spårar dess livscykel. Bind mounts mappar en specifik värdkatalog eller fil direkt in i containerns filsystem. Tmpfs-monteringar lagrar data i värdminnet och försvinner när containern stoppas.

Namngivna volymer rekommenderas för applikationsdata som måste överleva containeromstarter och uppdateringar. De är inte bundna till en specifik värdsökväg, de är enklare att säkerhetskopiera via Docker-verktyg och undviker filsystemsbehörighetsmissmatchningar som ofta uppstår med bind mounts. När en container ersätts med en ny version kvarstår den namngivna volymen.

Bind mounts är vanliga i utveckling eftersom de gör att värdens källkodskatalog återspeglas live inne i containern. Redigera en fil på värden och den körande containern ser förändringen omedelbart. Men bind mounts exponerar mer av värdens filsystem för containern och kopplar ihop containern tätt med värdens katalogstruktur, vilket gör dem mindre lämpliga för produktion.

Designprincipen bakom volymer är att containrar ska vara tillståndslösa. En container som skriver viktig data till sitt eget filsystem skapar ett dolt beroende mellan containerns livscykel och datas överlevnad. Att hålla containerfilsystemet efemärt och dirigera beständigt tillstånd till volymer eller externa system gör containrar enklare att ersätta, skala och återhämta.

Nätverk

Docker skapar virtuella nätverk och ansluter containrar till dem snarare än direkt till värdens fysiska gränssnitt. Standardbryggnätverket tillåter containrar att kommunicera via IP-adress men inte via namn. Användardefinierade bryggnätverk lägger till automatisk DNS-upplösning via containernamn, vilket gör tjänstupptäckt i miljöer med flera containrar mycket enklare.

Portpublicering mappar en containerport till en värdport och gör tjänsten nåbar utifrån containern. Syntaxen -p 8080:80 innebär att förfrågningar på värdport 8080 vidarebefordras till port 80 inne i containern. Utan explicit portpublicering finns en containers lyssnande portar bara inom dess nätverksnamnrymd och är onåbara utifrån. Detta standardstängda beteende är en användbar säkerhetsegenskap.

För driftsättningar på flera värdar stöder Docker overlay-nätverk och externa CNI-plugin som gör att containrar på olika maskiner kan kommunicera som om de vore i samma nätverkssegment. Detta är grunden för Swarm och den konceptuella basen för hur Kubernetes-nätverk fungerar.

Säkerhetsvanan att bygga tidigt är att aldrig exponera portar som standard. Interna tjänster, databaser, cachar, interna API:er, ska bara vara tillgängliga på interna Compose- eller Docker-nätverk, inte publicerade till värden. Att behandla nätverksexponering som ett medvetet designbeslut minskar attackytan för varje tjänst.

Registries

Ett registry lagrar och serverar Docker-images. Docker Hub är det offentliga standardregistryt, team hämtar officiella basimages därifrån och kan pusha sina egna publika images. Privata registrys, AWS ECR, Google Artifact Registry, GitHub Container Registry eller självhanterat Harbor, kräver autentisering och används för att lagra proprietära images som inte ska vara offentligt tillgängliga.

Imagetaggar är som standard muterbara. En tagg som myapp:latest eller nginx:1.25 kan uppdateras för att peka på en annan image utan förvarning till konsumenterna. Att hämta via tagg ensam garanterar inte reproducerbarhet. För driftsättningsautomation och säkerhetsgranskningar är det mer tillförlitliga alternativet att pinna via digest. En innehållsadresserad SHA256-hash som unikt identifierar en specifik image.

God registry-hygien inkluderar att hålla basimages uppdaterade (gamla images ackumulerar oupplagade CVE:er), ta bort images som inte längre driftsätts och definiera en tydlig befordringsväg där images rör sig från build till staging till produktionsregistrys när de passerar valideringsportar.

Registrykontroll av åtkomst spelar roll eftersom alla som kan pusha till ett registry kan ersätta images som automatiserade system kommer att hämta och köra. Pipeline-autentiseringsuppgifter som tillåter publicering ska begränsas till specifika repositoryn och användas bara i dedikerade publiceringssteg. Inte i varje jobb som rör repositoryt.

Typiskt arbetsflöde

Det vanliga Docker-arbetsflödet är. Skriv eller uppdatera Dockerfile, bygg imagen med en specifik tagg, testa den körande containern lokalt, pusha imagen till ett registry och kör sedan samma image i nästa miljö. Denna enkelriktade befordringsväg är grunden för 'bygg en gång, driftsätt överallt'. Samma artefakt som testades är den som går till produktion.

Ett välfungerande arbetsflöde producerar deterministiska resultat. Samma Dockerfile och samma indatafiler ska bygga till samma image vid varje körning. I praktiken hotas reproducerbarheten av rörliga taggar i FROM-instruktioner, obundna paketinstallationer och byggargument som injicerar miljöspecifika värden i lager. Var och en av dessa kan orsaka att två byggen från samma commit producerar olika images.

Taggningsstrategi signalerar vad en image är och var den hör hemma i releaseprocessen. Att använda commit-SHA:n (myapp:a3f2c9b) ger spårbarhet. Du kan alltid hitta källkoden som producerade en körande container. Semantiska versioner (myapp:2.4.1) kommunicerar stabilitet för externa releaser. Taggen 'latest' är bekväm för lokal testning men farlig i automatiserade pipelines.

Att uppdatera containrar i en verklig miljö beror inte bara på Docker. Det beror på orkestratörens rullande uppdateringslogik, hälsokontroller och beredskapssignaler. En container som startar men misslyckas med sin hälsokontroll ska inte ta emot trafik. Att bygga in hälsokontroller i Dockerfile och driftsättningskonfigurationen från början är det som gör leveransarbetsflödet tillförlitligt.