Docker

Flera containrar som hittar varandra

Compose, det automatiska nätverket och varför localhost inte betyder vad du tror

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.

din dator api db localhost localhost localhost
Tre lådor, tre pilar som vänder tillbaka in i sig själva. Ingen av dem pekar på någon annan.
Samma ord, tre olika maskiner

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.

api-containernAnsluter till 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.
Tjänstnamnet i Compose-filen är också värdnamnet på nätverket. Ingen IP-adress behöver skrivas någonstans.

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.

api db db:5432 din dator shop_default
Inuti ringen räcker namnet. Utifrån krävs en öppnad port.

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.

Din dator · localhost:5000Det du skriver i webbläsaren. Finns bara tack vare 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.
Compose-nätverket shop_default — här behövs ingen portmappning alls
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.

Fallgrop

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.

Bara depends_on

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.

depends_on + healthcheck

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.

Skillnaden mellan 'processen har startat' och 'tjänsten svarar'.

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.

Kom ihåg
  • localhost inuti 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.0 för att portmappningen ska fungera.
  • depends_on väntar på start, inte på att tjänsten är redo. Lägg till healthcheck och condition: service_healthy.
Kolla att du hänger med
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?
Nej, det är önskvärt. Containrar på samma Compose-nätverk når varandra utan portmappning. Utan 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"?
Vänster är porten på din dator, höger är porten inuti containern. Du surfar alltså till 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?
Vilken adress applikationen lyssnar på inuti containern. Lyssnar den på 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.
  • localhost inuti 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.
  • expose publicerar ingenting och är i praktiken dokumentation.
  • Appen måste lyssna på 0.0.0.0, annars är portmappningen verkningslös.
  • depends_on styr startordning. Vill du vänta på att tjänsten svarar krävs ett healthcheck.

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.