Flera containrar som hittar varandra
Hoppa till
Var vi är
I förra delen paketerade Lisa sin applikation i en image med hjälp av en Dockerfile. Den startar
och den svarar — men den gör ingenting användbart, för e-handelssystemet behöver en databas bredvid
sig.
Den här delen handlar om det som faktiskt är svårt med flera containrar: hur de hittar varandra, och varför den första anslutningen lätt kan misslyckas.
Vad Docker Compose är
Docker Compose startar och kopplar ihop flera containrar utifrån en fil som beskriver dem.
Filen heter compose.yaml och ligger i projektets rot. Den beskriver vilka tjänster som ingår,
vilka images de bygger på och hur de ska nå varandra.
Compose är inget eget körsystem. Det använder Docker och gör samma saker du hade kunnat göra för hand — men utifrån en fil i stället för ur minnet.
Compose vs flera docker run
Samma resultat, två helt olika sätt att komma dit.
docker run startar en container i taget. Varje anrop behöver sina egna flaggor för portar,
miljövariabler och nätverk, och du måste komma ihåg dem i rätt ordning.
Compose läser beskrivningen av alla tjänster och startar dem tillsammans, med ett gemensamt nätverk redan uppsatt.
Skillnaden är inte bekvämlighet. Det är att uppsättningen blir en fil som ligger i Git och granskas i en merge request, i stället för ett kommando i någons terminalhistorik.
Felet alla får först
Lisa gör det som känns rimligast. Hon startar databasen i en container, appen i en annan, och låter
appen ansluta till localhost:5432 — precis som när Postgres låg installerat direkt på hennes dator.
Resultatet är ett felmeddelande hon kommer att se många gånger:
Error: connect ECONNREFUSED 127.0.0.1:5432
Anledningen förklarar det mesta om containernätverk, och den är värd en egen sektion.
Vad localhost betyder inuti en container
localhost betyder alltid “den här maskinen”. Frågan är vilken maskin som är “den här”.
En container har sitt eget nätverksutrymme. Inuti API-containern är “den här maskinen” API-containern — inte din laptop, och absolut inte databascontainern.
Lisas app frågar alltså sig själv efter en databas den inte har. Svaret blir nej, direkt.
localhost i din terminal är din dator. localhost inuti API-containern är API-containern.
localhost inuti databascontainern är databascontainern.
Tre olika platser med samma namn — och den enda av dem som aldrig är rätt svar när två containrar ska prata är den första gissningen.
Nätverket Compose skapar åt dig
Compose sätter alla tjänster i projektet på ett gemensamt nätverk, utan att du ber om det.
Compose härleder normalt projektnamnet från mappen. Startar du projektet i shop blir nätverket
därför ofta shop_default, och varje tjänst kopplas in på det.
Namnet kan sättas uttryckligen. Kontrollera därför alltid det faktiska namnet med
docker network ls. Du behöver inget networks-block i filen.
På det nätverket finns en inbyggd namnuppslagning, och det är den som gör nästa avsnitt möjligt.
Tjänstnamnet är ett värdnamn
Nyckeln du ger en tjänst i Compose-filen blir också dess värdnamn på nätverket.
Heter tjänsten db når andra containrar den på db:5432. Ingen IP-adress skrivs någonstans.
db:5432 — namnet, inte en adress.slår upp dbComposes DNSVarje Compose-projekt får ett eget nätverk med inbyggd namnuppslagning.svarar med IP172.18.0.3Adressen kan ändras vid omstart — därför använder vi aldrig den direkt.ansluter på 5432db-containernPostgres lyssnar, anslutningen går igenom.Döper du om tjänsten till postgres måste anslutningssträngen ändras samtidigt. Det är samma namn
på två ställen, och Compose varnar inte när de glider isär.
Filen som binder ihop det
services:
api:
build: ./backend
ports:
- "5000:5000"
environment:
DATABASE_URL: postgres://user:pw@db:5432/shop
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pw
POSTGRES_DB: shop
Lägg märke till att db saknar ports. Den är ändå fullt nåbar för api, eftersom de ligger på
samma nätverk.
Det är inte en glömska utan ett medvetet val, och nästa avsnitt förklarar varför.
Vad .env gör
En fil som heter .env bredvid compose.yaml läses automatiskt, och dess värden fyller i
${...} i compose-filen.
TAGG=22-alpine
services:
api:
image: node:${TAGG}
Kontrollera resultatet utan att starta något:
docker compose config | grep image:
image: node:22-alpine
Substitutionen skedde alltså innan Compose ens började. Det är samma idé som Terraform-seriens variabler, ett lager längre ut.
environment vs env_file
Här sitter förväxlingen, och den kostar tid varje gång.
.env fyller i platshållare i compose-filen. Värdena når inte containern.
env_file: pekar ut en fil vars rader blir miljövariabler inuti containern.
environment: sätter enskilda variabler direkt i compose-filen, och vinner över env_file
när samma namn finns i båda.
Uppmätt med en .env som sätter TAGG och en app.env som sätter HEMLIG:
HEMLIG=fran-envfile EXTRA=bara-har TAGG=
TAGG är tomt inuti containern. Variabeln användes för att bygga compose-filen och skickades
aldrig vidare. Lägger tjänsten dessutom till environment: HEMLIG: fran-environment blir svaret
HEMLIG=fran-environment — den vinner.
Minnesregeln: .env talar med filen, env_file talar med containern.
Vad ports faktiskt gör
ports öppnar en väg från din dator in i containern. Den har ingenting med containrarna emellan
att göra.
ports.Portmappning · 5000 → 5000Docker vidarebefordrar trafiken in i containern.api-containern · 0.0.0.0:5000Applikationen lyssnar här, oavsett om någon mappat porten eller inte.ports: 5000:5000 läses vänster till höger: värdens port först, containerns sist.Att databasen saknar ports betyder alltså att Docker inte publicerar porten på din dator. I den
här Compose-filen är databasen nåbar för API:et på det gemensamma nätverket.
Andra vägar, som en proxy eller en annan nätverkskonfiguration, kan finnas i andra miljöer. Vill du
felsöka med ett databasverktyg lägger du tillfälligt till ports: ["5432:5432"] — och tar bort det
efteråt.
Värdens port vs containerns port
"5000:5000" läses vänster till höger, och de två talen betyder olika saker.
Vänster är porten på din dator. Det är den du skriver i webbläsaren.
Höger är porten inuti containern. Det är den applikationen lyssnar på.
De sätts ofta till samma nummer, vilket döljer skillnaden. Skriver du "8080:5000" surfar du till
localhost:8080 medan appen fortfarande lyssnar på 5000 — och det är helt i sin ordning.
ports vs expose
Två fält som ser ut att göra samma sak.
ports publicerar porten på din dator. Den skapar en verklig väg utifrån och in.
expose publicerar ingenting. Det är i praktiken dokumentation om vilken port containern
lyssnar på.
Containrar på samma nätverk når varandras portar oavsett om expose står där eller inte. Behöver du
komma åt något från din dator är ports det enda som gör skillnad.
Applikationen inuti containern måste lyssna på 0.0.0.0, inte på 127.0.0.1. Många ramverk lyssnar
på 127.0.0.1 som standard i utvecklingsläge, och då släpps bara trafik som redan är inne i
containern fram. Portmappningen ser ut att vara trasig fast felet ligger i appen.
Symtomet är typiskt: containern körs, loggen ser glad ut, och webbläsaren säger tom svarsfil.
depends_on betyder inte “redo”
Nu till fällan som kostar mest tid. depends_on styr startordningen, inte om tjänsten faktiskt
svarar.
Compose väntar bara på att databascontainern ska ha startats.
Postgres behöver därefter några sekunder på sig att initiera. API:et hinner ansluta under tiden, får nej, och kraschar.
Det ser slumpmässigt ut: fungerar på din dator, misslyckas i pipelinen — där maskinen är långsammare.
Databasen får ett eget hälsotest, och API:et väntar tills det testet går igenom.
Nu startar API:et först när Postgres faktiskt svarar på anrop, oavsett hur lång tid det tog.
Beteendet blir likadant på alla maskiner.
Så här ser den andra varianten ut:
services:
api:
build: ./backend
depends_on:
db:
condition: service_healthy
db:
image: postgres:17-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d shop"]
interval: 5s
timeout: 3s
retries: 10
healthcheck kör kommandot inuti databascontainern med jämna mellanrum. Så länge det misslyckas
räknas tjänsten som ohälsosam, och condition: service_healthy håller kvar API:et i startgroparna.
Att läsa loggarna från flera tjänster
När tre containrar startar tillsammans blandas deras utskrifter, och det är avsiktligt.
docker compose logs -f
docker compose logs -f api
Utan tjänstenamn får du allt, med ett prefix per rad som säger vilken tjänst den kom från. Med tjänstenamn får du bara den ena.
Prefixet är det som gör den blandade vyn användbar. Startordningen syns direkt, och ett fel i databasen som får API:et att krascha en sekund senare står som två rader efter varandra i stället för i två fönster.
-f följer loggen medan den skrivs, precis som tail -f i Linux-delen. Utan flaggan får du det som
finns just nu och tillbaka till prompten.
Vad det här betyder i ditt arbete
Du kommer att köra docker compose up och läsa docker compose logs när något inte startar.
Anslutningsfel mellan tjänster kan bero på fel värdnamn, en saknad portmappning när anslutningen
kommer från din dator, eller depends_on utan hälsotest.
Compose-filen har begrepp du känner igen när systemet ska till Kubernetes. En långlivad tjänst blir ofta en Deployment och en Service, och ett hälsotest kan motsvara en probe. Exakt översättning beror på hur trafiken och driften ska fungera.
Egna nätverk, profiler och docker compose watch kan vänta. Standardnätverket räcker för i stort
sett allt du gör i utveckling.
Vanliga nybörjarförväxlingar
localhost inuti en container är containern själv — aldrig din dator och aldrig grannen.
ports behövs inte för att två containrar ska prata med varandra. De ligger redan på samma nätverk.
expose öppnar ingenting. Det är dokumentation, inte en portmappning.
Vänster tal i "8080:5000" är din dator, höger är containern. De behöver inte vara samma.
Att appen lyssnar på 127.0.0.1 inuti containern gör portmappningen verkningslös, fast den ser rätt
ut i filen.
depends_on väntar på start, inte på att tjänsten svarar.
Prova själv
Utgå från Compose-filen ovan, eller ett projekt du har.
docker compose up -d
docker compose ps
-d startar i bakgrunden. ps visar vilka tjänster som körs och vad deras hälsostatus säger. Titta
sedan på nätverket Compose skapade åt dig:
docker network ls
Där ligger nätverket Compose skapade. Namnet är ofta projektets namn plus _default, men
kontrollera utskriften innan du använder det. Försök sedan kontrollera namnuppslagningen inifrån
API-containern:
docker compose exec api getent hosts db
Om imagen innehåller getent får du en IP-adress och namnet db. Det är Composes DNS som svarar,
och det förklarar varför db:5432 fungerar.
Tre saker kan förvirra. Många slimmade images saknar nätverksverktyg helt. Svarar kommandot
executable file not found saknas verktyget i imagen; det säger ingenting om Composes DNS.
Kör i så fall en tillfällig container på samma nätverk i stället. Byt shop_default mot namnet
som docker network ls visade:
docker run --rm --network shop_default \
busybox nslookup db
Dessutom kan IP-adressen du får vara en annan nästa gång du startar projektet, vilket är precis
därför du aldrig ska skriva in den någonstans. Och docker compose up utan -d låser terminalen —
avsluta med Ctrl+C.
localhostinuti en container är containern själv — aldrig din dator, aldrig grannen.- Compose skapar normalt ett nätverk och gör tjänstnamnet till värdnamn.
portsöppnar en dörr från din dator. Containrar emellan behövs det inte.- Vänster tal är värdens port, höger är containerns.
- Applikationen måste lyssna på
0.0.0.0för att portmappningen ska fungera. depends_onväntar på start, inte på att tjänsten är redo. Lägg tillhealthcheckochcondition: service_healthy.
Varför fungerar db:5432 men inte localhost:5432 inifrån API-containern?
localhost pekar på API-containern själv, som inte kör någon databas. db är tjänstnamnet i Compose-filen, och Composes DNS översätter det till databascontainerns IP-adress på det gemensamma nätverket.Databastjänsten saknar ports. Är det ett fel?
ports publicerar Docker inte databasen på din dator, medan API:et når den på det gemensamma nätverket.Vad betyder vänster respektive höger tal i ports: "8080:5000"?
localhost:8080, medan applikationen i containern lyssnar på 5000.Vad är skillnaden mellan ports och expose?
ports publicerar porten på din dator och skapar en verklig väg utifrån och in. expose publicerar ingenting — det är dokumentation om vilken port containern lyssnar på. Containrar på samma nätverk når varandra ändå.Pipelinen misslyckas ibland med anslutningsfel mot databasen, men på din dator fungerar allt. Vad är den troligaste orsaken?
depends_on utan hälsotest. Compose väntar bara på att databascontainern ska ha startat, inte på att Postgres är redo. På en långsammare maskin hinner API:et ansluta för tidigt. Lägg till healthcheck på databasen och condition: service_healthy på beroendet.Portmappningen ser rätt ut men webbläsaren får tom svarsfil. Vad kontrollerar du?
127.0.0.1 släpps bara trafik igenom som redan är inne i containern. Den måste lyssna på 0.0.0.0.Snabb sammanfattning
- Docker Compose startar och kopplar ihop flera containrar utifrån en fil.
- Compose ersätter inte Docker. Det använder Docker, men utifrån en fil i stället för ur minnet.
localhostinuti en container är containern själv. Det är därför den första anslutningen misslyckas.- Compose skapar normalt ett nätverk åt dig, ofta döpt efter projektet plus
_default. - Tjänstnamnet i filen är också värdnamnet på nätverket. Ingen IP-adress skrivs någonstans.
portsöppnar en väg från din dator in i containern — inte mellan containrar.- Vänster tal är värdens port, höger är containerns. De behöver inte vara samma.
exposepublicerar ingenting och är i praktiken dokumentation.- Appen måste lyssna på
0.0.0.0, annars är portmappningen verkningslös. depends_onstyr startordning. Vill du vänta på att tjänsten svarar krävs etthealthcheck.
Nästa del
Nu pratar API:et med databasen och startordningen håller. I nästa del kör Lisa docker compose down
och up igen nästa morgon — och upptäcker att databasen är tom. Det är inte en bugg, och
förklaringen är sista byggstenen innan Docker sitter.