Det här sätter du upp
Docker kör program i containrar: varje app levereras som en avbild med allt den behöver, och körs avskild från resten av servern. Docker Compose startar en uppsättning containrar från en enda fil. Många projekt levererar sin app som en Docker-avbild med en compose.yaml, och verktyg som Coolify, Dokploy och Pterodactyl behöver Docker i botten.
Våra egna appguider använder i stället Podman utan root, där varje app körs under en egen användare utan root. Om du bara vill köra containrar är Podman-guiden alternativet. Den här guiden är för när du behöver just Docker.
Docker Engine utvecklas av Docker, Inc., ett företag i Palo Alto i Kalifornien i USA, tillsammans med projektet Moby, som är öppen källkod. Engine, kommandot docker och Compose är öppen källkod under Apache License 2.0. Docker kontaktar ingenting på eget initiativ: på vår testserver slog det inte upp några namn utom när det hämtade en avbild. Avbilder hämtas som standard från Docker Hub, som drivs av Docker, Inc. i USA och ser serverns IP-adress och vilka avbilder den hämtar; själva lagren levereras via Amazon CloudFront. Enligt Dockers integritetspolicy kan data behandlas i USA. Utan konto tillåter Docker Hub 100 hämtningar på sex timmar per IPv4-adress eller IPv6-/64. Vill du undvika Docker Hub använder du avbilder från ett annat register med hela namnet, till exempel ghcr.io/... eller quay.io/....
Varje steg nedan har körts på en nyinstallerad VC-P Alloy (2 vCPU, 8 GB) hos Melonslab med Debian 13:
- Docker 29.8.2 och Compose 5.5.1 installerades från Dockers paketförråd på ungefär 20 sekunder.
- Med ufw påslaget och ingen regel för den svarade en containerport som publicerats på alla adresser från internet över IPv4: 32 av 40 testplatser runt om i världen fick kontakt. En systemd-tjänst som lägger till regler i Dockers egen kedja stängde den över IPv4 och IPv6.
- Som standard hade containrarna ingen IPv6. På ett nätverk med IPv6 påslaget nådde de IPv6-adresser, och en publicerad port såg besökarens riktiga IPv6-adress.
- En första app med Compose, publicerad bara på
127.0.0.1, svarade bakom Caddy med ett certifikat från Let's Encrypt och såg varje besökares riktiga adress, över IPv4 och IPv6. - Loggarna från en container som skrev 213 MB tog 4,6 MB på disken.
- Docker uppdaterades av de automatiska uppdateringarna, och appens container fortsatte att köra.
- Allt startade igen av sig självt efter en omstart av servern.
Docker självt, dockerd och containerd, använde omkring 160 MB minne. Med Caddy och två små containrar använde hela servern omkring 540 MB.
Innan du börjar
Du behöver:
- en server hos Melonslab med Debian 13. Docker stöder även Debian 12;
- SSH-nycklar, automatiska säkerhetsuppdateringar och ufw, uppsatta enligt steg 1 till 4 i säkerhetsguiden, med port 22, 80 och 443 öppna;
- för steg 5, ett namn för appen, till exempel
app.example.com, med en A- och en AAAA-post som pekar på servern.
Exemplen använder 203.0.113.10 och 2001:db8::10 för serverns adresser och 198.51.100.7 för din egen. Byt genomgående ut dem mot dina egna.
1. Installera Docker från Dockers paketförråd
Debian har ett eget paket, docker.io, men det är äldre: version 26.1.5 på Debian 13, mot 29.8.2 från Docker. Det här är stegen från Dockers dokumentation. På en nyinstallerad server finns inget av de paket som dokumentationen anger att du först ska ta bort. Som root:
apt update
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
Lägg till paketförrådet:
tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
apt update
Installera Docker, Buildx och Compose-tillägget:
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Docker startar direkt, och startar vid varje uppstart. Kontrollera versionerna:
docker version --format '{{.Server.Version}}'
docker compose version
29.8.2
Docker Compose version v5.5.1
Kör en testcontainer:
docker run --rm hello-world
Den hämtar en liten avbild och skriver ut Hello from Docker!.
2. Se Docker öppna en port förbi ufw
ufw släpper bara in port 22, 80 och 443. Starta en container som publicerar port 8080:
docker run -d --name test -p 8080:80 traefik/whoami
Från din egen dator:
curl http://203.0.113.10:8080
Den svarar, med din adress på raden RemoteAddr. Porten är öppen mot hela internet, fast ufw saknar regel för den och ufw status inte visar den. Docker vidarebefordrar publicerade portar till containern i sina egna brandväggsregler, innan ufw:s regler nås. Varje port i en ports:-lista, eller efter -p, är publik om du inte begränsar den. Dockers dokumentation varnar också för detta.
Över IPv6 svarade inte den här containerns port: den ligger på Dockers standardnätverk, som saknar IPv6, så Docker skickar IPv6-anslutningar genom ett hjälpprogram på servern, och dem blockerar ufw som för vilket annat program som helst. På ett nätverk med IPv6, som i steg 4, är porten öppen även över IPv6.
3. Stäng publicerade portar
Det finns två sätt, och oftast använder du båda.
Publicera på 127.0.0.1 när en webbserver står framför. En port som skrivs 127.0.0.1:8082:80 lyssnar bara lokalt på servern, över IPv4 och IPv6, så ingen utifrån når den. Caddy, på samma server, gör det. Steg 5 gör så.
Stoppa trafiken till portar som måste publiceras på alla adresser. Vissa appar publicerar en port som du inte kan ändra, till exempel en panel på 8000. Docker lämnar en kedja av sina regler åt dig, DOCKER-USER, som kontrolleras före dess egna. Skapa /etc/systemd/system/docker-ports.service, med portarna som ska stängas i listan efter for p in:
[Unit]
Description=Keep Docker ports off the internet
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c "for t in iptables ip6tables; do for p in 8080; do $t -C DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport $p -j DROP 2>/dev/null || $t -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport $p -j DROP; done; done"
[Install]
WantedBy=multi-user.target
Reglerna matchar porten som besökaren anslöt till, --ctorigdstport, och inte porten inne i containern, som är vad Dockers egna regler annars ser. -i eth0 begränsar dem till trafik från internet, så att servern och andra containrar fortfarande kan ansluta. Starta den:
systemctl daemon-reload
systemctl enable --now docker-ports.service
iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 -j DROP
ip6tables -S DOCKER-USER visar samma regel för IPv6. Prova curl http://203.0.113.10:8080 från din egen dator igen: nu väntar den tills den ger upp. Reglerna ligger kvar när Docker startas om, och tjänsten lägger till dem igen efter en omstart av servern. För att stänga en port till lägger du till den i listan och kör systemctl daemon-reload och systemctl restart docker-ports.service.
Ta bort testcontainern:
docker rm -f test
4. Ge containrar IPv6
Dockers standardnätverk saknar IPv6. Containrar på det når inga IPv6-adresser alls:
docker run --rm alpine wget -qO- -T5 http://ipv6.icanhazip.com
wget: can't connect to remote host: Network unreachable
Nätverk som du skapar kan ha IPv6. Docker ger då nätverket ett eget privat IPv6-intervall, som börjar med fd, och översätter containrarnas adresser till serverns egen när de ansluter utåt, på samma sätt som för IPv4. Docker slår också på vidarebefordran av IPv6 på servern av sig självt. Prova med ett nätverk som du skapar för hand:
docker network create --ipv6 test6
docker run --rm --network test6 alpine wget -qO- -T5 http://ipv6.icanhazip.com
docker network rm test6
Den skriver ut serverns IPv6-adress, 2001:db8::10. I Compose slår du på IPv6 för appens nätverk i compose.yaml:
networks:
default:
enable_ipv6: true
En port som publiceras på alla adresser från ett sådant nätverk är öppen även över IPv6, förbi ufw, och containern ser besökarens riktiga IPv6-adress. Reglerna från steg 3 stänger den även över IPv6, vilket vi kontrollerade med en portskanning över IPv6 utifrån.
Du behöver inte IPv6 på nätverket för en app bakom Caddy: Caddy svarar besökare över både IPv4 och IPv6 och skickar vidare deras adress, som nästa steg visar.
5. Kör en första app med Compose
Appen här är traefik/whoami, en liten webbserver som visar förfrågan den fick. Varje app får en katalog med sin compose.yaml. Skapa /opt/whoami/compose.yaml:
services:
whoami:
image: traefik/whoami:v1.11.0
restart: unless-stopped
ports:
- "127.0.0.1:8082:80"
Lås en version i image:, som här, så att appen bara ändras när du ändrar filen. restart: unless-stopped startar containern igen efter en krasch eller en omstart. Starta den:
cd /opt/whoami
docker compose up -d
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
whoami-whoami-1 traefik/whoami:v1.11.0 "/whoami" whoami 1 second ago Up Less than a second 127.0.0.1:8082->80/tcp
Ställ nu Caddy framför, från Debians eget paket. Den hämtar certifikatet från Let's Encrypt och förnyar det:
apt install -y caddy
Ersätt /etc/caddy/Caddyfile med:
{
email you@example.com
}
app.example.com {
reverse_proxy 127.0.0.1:8082
}
E-postadressen är dit Let's Encrypt skriver om något är fel med certifikatet. Kontrollera filen och läs in den:
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
systemctl reload caddy
Inom en minut svarar https://app.example.com, och http:// omdirigeras dit. Sidan visar vad appen ser:
RemoteAddr: 172.18.0.1:35068
X-Forwarded-For: 198.51.100.7
X-Forwarded-Proto: https
RemoteAddr är Dockers adress för servern, eftersom Caddy ansluter därifrån. Besökarens egen adress finns i X-Forwarded-For, över IPv4 och IPv6 på samma sätt: i vårt test var den första besökaren över IPv6 en skanner, några minuter efter att certifikatet utfärdats. Appar som ska logga besökare eller begränsa inloggningsförsök måste få veta att de kan lita på den headern från proxyn; varje apps dokumentation beskriver hur.
Caddys logg visar varningen no OCSP stapling for [app.example.com] när certifikatet har utfärdats. Den är ofarlig: Let's Encrypt driver inte längre den tjänsten.
Det du ska säkerhetskopiera för en app som den här är dess katalog under /opt och dess data. Data i namngivna volymer ligger under /var/lib/docker/volumes/.
6. Rotera loggar och håll containrar igång genom omstarter
Docker sparar allt som en container skriver ut, som standard i en fil som växer utan gräns. Skapa /etc/docker/daemon.json:
{
"log-driver": "local",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"live-restore": true
}
localsparar loggar komprimerade och behåller högst tre filer om 10 MB per container. I vårt test tog en container som skrev ut 213 MB text 4,6 MB på disken, ochdocker logsvisade den fortfarande.live-restorehåller containrar igång medan Docker självt startas om, till exempel vid en uppdatering. Med den fortsatte appens container att köra genom en uppdatering; porten på127.0.0.1var borta en till två sekunder medan Docker startade igen. Den gäller containrar som du startar själv, inte tjänster i Docker Swarm.
Kontrollera filen och starta om Docker:
dockerd --validate --config-file /etc/docker/daemon.json
systemctl restart docker
docker info | grep -E 'Logging Driver|Live Restore'
Logging Driver: local
Live Restore Enabled: true
Logginställningarna gäller bara containrar som skapas efter ändringen. Skapa om de du har:
cd /opt/whoami
docker compose up -d --force-recreate
7. Bestäm vem som får använda Docker
Den som kan köra docker styr servern. Docker körs som root, och en container kan montera serverns hela filsystem: en användare som vi lade till i gruppen docker läste /etc/shadow, som bara root får läsa, med ett enda docker run. Lägg därför inte till användare i gruppen docker om du inte skulle ge dem root. Använd Docker som root, som i den här guiden, eller använd Podman utan root för användare som bara ska köra sina egna containrar.
8. Uppdatera Docker och dina appar
Docker kommer från ett eget paketförråd, som de automatiska uppdateringarna från säkerhetsguiden inte omfattar. För att de även ska installera Dockers uppdateringar skapar du /etc/apt/apt.conf.d/52unattended-upgrades-docker:
Unattended-Upgrade::Origins-Pattern {
"origin=Docker,label=Docker CE";
};
Det här läggs till Debians källor, det ersätter dem inte. För att kontrollera:
unattended-upgrade --dry-run --debug 2>&1 | grep 'Allowed origins'
Raden slutar med origin=Docker,label=Docker CE. I vårt test uppdaterade en körning av unattended-upgrade Docker från 29.8.1 till 29.8.2, och med live-restore fortsatte appens container att köra. Dockers paketförråd levererar även nya huvudversioner på det här sättet. Vill du hellre välja när, utelämna filen och kör apt update && apt upgrade själv.
Appar uppdateras när du byter deras avbild. Ändra versionen i compose.yaml, hämta sedan den nya avbilden och skapa om containern:
cd /opt/whoami
sed -i 's/whoami:v1.11.0/whoami:v1.12.0/' compose.yaml
docker compose pull
docker compose up -d
Compose skapar bara om containrar vars avbild eller inställningar har ändrats. Appen var borta en till två sekunder. Läs appens versionsinformation före en ny huvudversion: vissa behöver ett eget steg, till exempel en databasmigrering.
9. Frigör diskutrymme
Gamla avbilder ligger kvar på disken efter en uppdatering. Så här ser du hur mycket utrymme Docker använder:
docker system df
docker system prune tar bort stoppade containrar, nätverk som ingen container använder, byggcache och avbilder utan namn. Den frågar först. Kontrollera docker ps -a innan du kör den: en container som du har stoppat med avsikt tas också bort, men dess namngivna volymer behålls. Data i volymer tas bara bort med --volumes, och även då bara volymer utan namn.
Gamla versioner av en avbild har kvar sitt namn, så prune behåller dem. Så här tar du bort alla avbilder som ingen container använder:
docker image prune -a
I vårt test tog det bort traefik/whoami:v1.11.0 och testavbilderna, och behöll versionen som appen kör. En avbild som du har tagit bort hämtas igen nästa gång något behöver den.
10. Kontrollera efter en omstart
reboot
Kontrollera efter omstarten att Docker, reglerna och appen är tillbaka:
systemctl is-active docker docker-ports caddy
docker ps --format '{{.Names}} {{.Status}}'
iptables -S DOCKER-USER
Alla tre tjänsterna är active, appens container är Up och DROP-regeln för port 8080 finns. I vårt test svarade https://app.example.com inom en minut efter omstarten, och port 8080 förblev stängd över IPv4 och IPv6.
Felsökning
En port är öppen fast ufw inte tillåter den. Den publiceras av en container. docker ps visar varje containers portar: 0.0.0.0: och [::]: betyder alla adresser. Publicera den på 127.0.0.1, eller lägg till den i tjänsten från steg 3.
Job for docker.service failed because start of the service was attempted too often. Docker startades om många gånger på kort tid, och systemd slutade försöka. Det hände oss under testet. Kör systemctl reset-failed docker.service docker.socket och sedan systemctl start docker. Tjänsten från steg 3 stoppas tillsammans med Docker: starta den igen med systemctl start docker-ports.service och kontrollera med iptables -S DOCKER-USER.
Containrar är stoppade efter att du stängt av live-restore. I vårt test stoppades containrar som körde när vi ändrade den från true till false när Docker startades om, och de startade inte igen av sig själva. Starta dem med docker compose up -d i varje apps katalog. Att slå på den stoppade dem inte.
En container når inte IPv6-adresser: Network unreachable. Den ligger på ett nätverk utan IPv6, till exempel Dockers standardnätverk. Slå på IPv6 för dess nätverk som i steg 4.
Appen ser 172.18.0.1 som varje besökares adress. Det är adressen som Caddy ansluter från. Besökarens adress finns i X-Forwarded-For: ställ in appen så att den litar på den headern från proxyn.