Docker-netwerken op een VPS: bridge, host en macvlan uitgelegd
Hoe Docker bridge-, host- en macvlan-netwerken werken op een enkele VPS. Custom bridge DNS-resolutie, port publishing, IPv6-configuratie en netwerkisolatie met Docker Compose.
Je hebt één VPS. Meerdere containers moeten met elkaar en met de buitenwereld communiceren. Docker biedt drie relevante netwerkdrivers: bridge, host en macvlan. Elk maakt andere afwegingen tussen isolatie, prestaties en gebruiksgemak.
Dit artikel legt uit wanneer je welke driver kiest, hoe je containers verbindt via aangepaste bridge-netwerken en Docker Compose, en hoe je de veelgemaakte fouten vermijdt die services onbereikbaar of per ongeluk openbaar maken.
Vereisten: basiskennis van de Docker CLI en bekendheid met Docker Compose.
Wat is het verschil tussen Docker bridge en host networking?
Bridge-netwerken geven elke container een eigen network namespace met een virtueel ethernet-paar (veth) dat het verbindt met een softwarebridge op de host. Containers krijgen privé-IP's (doorgaans in het bereik 172.17.0.0/16) en bereiken de buitenwereld via NAT beheerd door iptables. Host-networking verwijdert de network namespace volledig: de container deelt de netwerkstack van de host en bindt rechtstreeks aan hostpoorten zonder adresvertaling.
Snelle vergelijking:
| Criterium | Custom Bridge | Host | Macvlan |
|---|---|---|---|
| Netwerkisolatie | Volledig (eigen namespace) | Geen (deelt host) | Volledig (eigen MAC-adres) |
| DNS-resolutie | Automatisch op containernaam | Gebruikt host-DNS | Geen ingebouwde DNS |
| Poortmapping | Vereist (-p) |
Niet nodig (directe binding) | Niet nodig (echt IP) |
| Prestatie-overhead | Klein (NAT + veth) | Geen | Geen |
| Beveiligingsrisico | Laag (standaard geïsoleerd) | Hoog (geen netwerkgrens) | Gemiddeld (promiscuous mode vereist) |
| VPS-relevantie | Eerste keuze | Monitoring, hoge doorvoer | Zelden bruikbaar |
Waarom een aangepast bridge-netwerk gebruiken in plaats van de standaard bridge?
Het standaard bridge-netwerk (genaamd docker0) biedt geen DNS-resolutie tussen containers. Containers kunnen elkaar alleen bereiken via IP-adres, dat bij elke herstart verandert. Aangepaste bridge-netwerken bieden automatische DNS, betere isolatie en de mogelijkheid om containers on the fly te verbinden of los te koppelen. Gebruik een aangepaste bridge voor alles. Beschouw de standaard bridge als een overblijfsel.
Het DNS-probleem
Als Docker start, maakt het een standaard bridge-netwerk aan. Elke container zonder expliciet --network-flag belandt erop. Maar containers op de standaard bridge kunnen elkaar niet op naam resolven:
# Start two containers on the default bridge
docker run -d --name web nginx:alpine
docker run -d --name client alpine sleep 3600
# Try name resolution - this fails
docker exec client ping -c1 web
# ping: bad address 'web'
Probeer hetzelfde met een aangepaste bridge:
# Create a custom bridge network
docker network create app-net
# Start containers on it
docker run -d --name web2 --network app-net nginx:alpine
docker run -d --name client2 --network app-net alpine sleep 3600
# Name resolution works
docker exec client2 ping -c1 web2
# PING web2 (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.089 ms
Hoe Dockers ingebouwde DNS-server werkt
Elke container op een door de gebruiker gedefinieerd netwerk krijgt 127.0.0.11 als DNS-server. Dit is Dockers ingebouwde DNS-server. Het resolvet containernamen en service-aliassen naar hun huidige IP-adressen. Als de naam geen container is, stuurt het de query door naar de DNS-servers die op de host zijn geconfigureerd.
docker exec client2 cat /etc/resolv.conf
# nameserver 127.0.0.11
De DNS-server draait in de network namespace van de container, maar de daadwerkelijke resolutie vindt plaats in de Docker-daemon op de host. Om conflicten te voorkomen met services die poort 53 in de container gebruiken, luistert Dockers DNS intern op een willekeurige hoge poort en leidt queries om via iptables-regels.
Er is geen IPv6-equivalent adres. Het adres 127.0.0.11 werkt ook in IPv6-only containers.
De ingebouwde DNS-server retourneert een TTL van 600 seconden (10 minuten) voor containernaamrecords. Dit is relevant voor blue-green deployments: als je een container vervangt, kunnen andere containers nog tot 10 minuten het oude IP resolven. Applicaties met agressieve DNS-caching (Java cached standaard onbeperkt) houden verouderde adressen nog langer vast.
Containers op hetzelfde aangepaste bridge-netwerk stellen alle poorten aan elkaar beschikbaar zonder -p-flag. Port publishing regelt alleen de toegang van buiten het netwerk (de host of het internet). Twee containers op app-net communiceren vrij op elke poort.
Standaard bridge vs. aangepaste bridge
| Feature | Standaard bridge (docker0) |
Aangepaste bridge |
|---|---|---|
| DNS op containernaam | Nee | Ja |
| Dynamisch verbinden/loskoppelen | Nee (herstart container vereist) | Ja |
| Configureerbaar per netwerk | Nee (daemon.json-wijziging + herstart nodig) |
Ja |
| Isolatie van andere stacks | Nee (alle niet-toegewezen containers delen het) | Ja |
| Aanbevolen voor productie | Nee | Ja |
Wanneer host-networking gebruiken op een VPS?
Gebruik host-networking als een container maximale netwerkprestaties nodig heeft of dynamisch aan veel poorten moet binden. De container deelt rechtstreeks de netwerkstack van de host, waardoor NAT en de virtuele ethernet-bridge worden omzeild. Dit elimineert de meetbare overhead voor workloads met hoge doorvoer.
Typische toepassingen op een VPS:
- Monitoring-agents zoals Prometheus node-exporter of Netdata die alle host-interfaces en verkeer moeten zien
- DNS-servers die op het werkelijke IP van de host moeten luisteren
- Prestatiegevoelige services waar de NAT-overhead ertoe doet (hoge pakketratio's, UDP-intensieve workloads)
# Run a container with host networking
docker run -d --name node-exporter --network host \
prom/node-exporter:latest
De --publish-flag wordt genegeerd bij host-networking. De container bindt direct aan hostpoorten. Als poort 9100 al in gebruik is op de host, start de container niet.
Beveiligingsafweging
Host-networking biedt dezelfde netwerkisolatie als het proces direct op de host draaien: geen. De container kan alle netwerkinterfaces zien, aan elke poort binden en verkeer afluisteren. Gebruik het alleen als je een specifieke reden hebt.
# Verify which ports a host-networked container opened
ss -tlnp | grep node_exporter
# LISTEN 0 4096 *:9100 *:* users:(("node_exporter",pid=12345,fd=3))
Let goed op: deze output toont dat het proces luistert op *:9100, dus alle interfaces. Met bridge-networking regel je dit met de -p-flag. Met host-networking bepaalt de applicatie zelf waaraan wordt gebonden.
Wat is macvlan-networking en heb je het nodig op een VPS?
Macvlan geeft elke container een eigen MAC-adres, waardoor het als een apart fysiek apparaat op het netwerk verschijnt. De container krijgt een echt IP uit je LAN-subnet en is direct bereikbaar zonder poortmapping.
Op een VPS heb je macvlan vrijwel zeker niet nodig. Dit is waarom:
- De meeste cloudproviders blokkeren het. Macvlan vereist dat de fysieke NIC in promiscuous mode draait. VPS-hypervisors blokkeren dit doorgaans op het niveau van de virtuele switch.
- Geen LAN om mee te verbinden. Je VPS heeft één publieke interface. Macvlan is ontworpen voor omgevingen waar containers eigen adressen op een bestaand LAN moeten krijgen, zoals een thuislab met IoT-apparaten of legacy-applicaties die een vast IP verwachten.
- Host-containercommunicatie werkt niet. Het Linux-kernel voorkomt by design dat een macvlan-container communiceert met de host waarop hij draait. Je hebt workarounds nodig zoals een tweede bridge-netwerk.
Als je VPS-provider een dedicated server biedt met volledige NIC-toegang en meerdere IP's, wordt macvlan haalbaar. Voor standaard VPS-deployments blijf je bij aangepaste bridge-netwerken.
Ter referentie, een macvlan-netwerk aanmaken ziet er zo uit (je hebt dit op een VPS waarschijnlijk niet nodig):
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
macnet
De parent-optie specificeert de host-interface om aan te koppelen. De container krijgt een eigen IP uit het subnet en een eigen MAC-adres. Andere apparaten op het LAN kunnen het direct bereiken.
Wat is het verschil tussen een Docker-poort exposen en publishen?
EXPOSE in een Dockerfile is documentatie. Het verklaart op welke poort de applicatie luistert. Het opent die poort niet naar de host of de buitenwereld. --publish (of -p) tijdens runtime maakt een daadwerkelijke poortmapping van host naar container. Zonder -p bereikt geen extern verkeer de container.
# In your Dockerfile - this is documentation only
EXPOSE 8080
# This actually maps host:8080 -> container:8080
docker run -d -p 8080:8080 myapp
# This binds to localhost only - external traffic cannot reach it
docker run -d -p 127.0.0.1:8080:8080 myapp
Het 127.0.0.1-binding-patroon
Als je services achter een reverse proxy zoals Nginx, Traefik of Caddy draait , publiceer containerpoorten alleen op 127.0.0.1. Dit voorkomt directe toegang vanaf internet en dwingt al het verkeer door je reverse proxy waar TLS-terminatie, rate limiting en toegangscontrole plaatsvinden.
# docker-compose.yml - binding to localhost only
services:
app:
image: myapp:latest
ports:
- "127.0.0.1:3000:3000" # Only reachable from localhost
Verificeer de binding:
ss -tlnp | grep 3000
# LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("docker-proxy",...))
Let goed op: de output toont 127.0.0.1:3000, niet 0.0.0.0:3000. extern verkeer op het publieke IP van je VPS poort 3000 niet direct kan bereiken.
Zonder het 127.0.0.1-prefix publiceert Docker standaard op alle interfaces. Op een VPS met een publiek IP betekent dit dat je service blootgesteld is aan het internet, met omzeiling van je reverse proxy en mogelijk je firewall.
Port publishing snelreferentie
| Syntax | Bindt aan | Bereikbaar vanaf |
|---|---|---|
-p 8080:80 |
0.0.0.0:8080 + [::]:8080 |
Overal (IPv4 + IPv6) |
-p 127.0.0.1:8080:80 |
127.0.0.1:8080 |
Alleen localhost |
-p 0.0.0.0:8080:80 |
0.0.0.0:8080 |
Alle IPv4-interfaces |
-p [::1]:8080:80 |
[::1]:8080 |
Alleen IPv6 localhost |
Hoe isoleer je containernetwerken op een enkele VPS?
Maak aparte bridge-netwerken voor elke applicatiestack. Containers op verschillende netwerken kunnen niet communiceren tenzij je ze expliciet verbindt met een gedeeld netwerk. Voor databases en interne services gebruik je de internal: true-flag om alle uitgaande internettoegang te blokkeren.
Multi-netwerkarchitectuur
Een typische productie-setup op een VPS ziet er zo uit:
Internet
|
[Reverse Proxy]
/ \
[frontend] [api-net]
| |
webapp api-server
|
[db-net: internal]
|
postgres
De reverse proxy is verbonden met frontend en api-net. De API-server is verbonden met api-net en db-net. PostgreSQL zit alleen op db-net, zonder route naar internet.
Hier het Docker Compose-bestand voor deze setup:
services:
proxy:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
networks:
- frontend
- api-net
webapp:
image: mywebapp:latest
networks:
- frontend
api:
image: myapi:latest
environment:
- DATABASE_URL=postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
networks:
- api-net
- db-net
db:
image: postgres:16-alpine
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- db-net
networks:
frontend:
driver: bridge
api-net:
driver: bridge
db-net:
driver: bridge
internal: true # No internet access for the database network
volumes:
pgdata:
De internal: true op db-net betekent dat PostgreSQL het internet niet kan bereiken. Het kan geen updates downloaden, geen externe servers contacteren en niet misbruikt worden als draaipunt voor uitgaande verbindingen. De API-server kan de database bereiken omdat het aan zowel api-net als db-net is gekoppeld.
Netwerkisolatie verifiëren
Na het starten van de stack, bevestig de isolatie:
# List networks created by Compose
docker network ls --filter "name=myproject"
# Inspect a network to see which containers are connected
docker network inspect myproject_db-net --format '{{range .Containers}}{{.Name}} {{end}}'
# myproject-api-1 myproject-db-1
# Confirm the database cannot reach the internet
docker exec myproject-db-1 ping -c1 -W2 8.8.8.8
# ping: sendto: Network unreachable
Let goed op: "Network unreachable" bevestigt dat de internal: true-flag werkt. De databasecontainer heeft geen standaard gateway.
Hoe schakel je IPv6 in voor Docker-containers?
Schakel IPv6 globaal in via /etc/docker/daemon.json en vervolgens per netwerk met een subnet. Docker ondersteunt dual-stack (IPv4 + IPv6) standaard op Linux. De ip6tables-parameter, standaard ingeschakeld, beheert IPv6-firewallregels voor containers.
Stap 1: de daemon configureren
Bewerk /etc/docker/daemon.json:
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef::/64",
"ip6tables": true,
"default-address-pools": [
{ "base": "172.17.0.0/16", "size": 24 },
{ "base": "fd00:dead:beef::/48", "size": 64 }
]
}
fixed-cidr-v6 wijst een IPv6 /64-subnet toe aan de standaard bridge. Het blok default-address-pools vertelt Docker hoe subnetten worden toegewezen voor nieuwe netwerken: /24-blokken uit het IPv4-bereik en /64-blokken uit het IPv6-bereik.
Gebruik een ULA-prefix (fd00::/8) voor privé container-naar-container-verkeer. Als je VPS een publiek IPv6-bereik heeft van je provider, kun je een subnet daaruit gebruiken voor containers die publieke IPv6-adressen nodig hebben.
Herstart Docker:
sudo systemctl restart docker
Verificeer dat IPv6 is ingeschakeld:
docker network inspect bridge --format '{{.EnableIPv6}}'
# true
Stap 2: een dual-stack-netwerk aanmaken
docker network create --ipv6 --subnet 172.20.0.0/24 --subnet fd00:dead:beef:1::/64 app-v6
Of in Docker Compose:
networks:
app-v6:
enable_ipv6: true
ipam:
config:
- subnet: 172.20.0.0/24
- subnet: fd00:dead:beef:1::/64
Stap 3: dual-stack-connectiviteit verifiëren
docker run --rm --network app-v6 alpine ip -6 addr show eth0
# inet6 fd00:dead:beef:1::2/64 scope global
IPv6 wordt alleen ondersteund op Docker-daemons die op Linux draaien. Op oudere systemen moet je mogelijk de ip6_tables-kernelmodule laden voordat je IPv6-netwerken aanmaakt:
sudo modprobe ip6_tables
Hoe definieer je netwerken in Docker Compose?
Docker Compose maakt automatisch een standaardnetwerk voor elk project. Elke service in het Compose-bestand treedt toe tot dit netwerk en kan andere services resolven op servicenaam. Voor de meeste toepassingen met één stack is dit standaardgedrag voldoende.
Als je meerdere netwerken nodig hebt voor isolatie, definieer ze expliciet in de networks-sectie:
services:
web:
image: nginx:alpine
ports:
- "127.0.0.1:8080:80"
networks:
- public
app:
image: node:20-alpine
networks:
- public
- private
redis:
image: redis:7-alpine
networks:
- private
networks:
public:
driver: bridge
private:
driver: bridge
internal: true
In dit bestand kan web app bereiken via het public-netwerk. app kan zowel web als redis bereiken. redis kan alleen app bereiken via private en heeft geen internettoegang.
Host-networking in Compose
services:
node-exporter:
image: prom/node-exporter:latest
network_mode: host
pid: host
restart: unless-stopped
network_mode: host vervangt elke networks-definitie. Je kunt de host-modus niet combineren met aangepaste netwerken op dezelfde service.
Verbinden met externe netwerken
Als een netwerk buiten Compose is aangemaakt (door een andere stack of handmatig), refereer het als extern:
networks:
shared-proxy:
external: true
Dit laat meerdere Compose-projecten een netwerk delen. Een gangbaar patroon: één Compose-stack draait de reverse proxy en maakt het netwerk. Andere stacks verklaren het als extern en verbinden hun services ermee.
Docker-netwerkcommando's: snelreferentie
| Commando | Wat het doet |
|---|---|
docker network ls |
Alle netwerken weergeven |
docker network create mynet |
Een bridge-netwerk aanmaken |
docker network create --ipv6 --subnet fd00::/64 mynet |
Een dual-stack-netwerk aanmaken |
docker network inspect mynet |
Netwerkdetails tonen (subnet, containers, opties) |
docker network connect mynet container1 |
Een draaiende container aan een netwerk koppelen |
docker network disconnect mynet container1 |
Een container van een netwerk loskoppelen |
docker network prune |
Alle ongebruikte netwerken verwijderen |
docker network rm mynet |
Een specifiek netwerk verwijderen |
Schaallimieten
Bridge-netwerken worden instabiel wanneer meer dan 1.000 containers op een enkel netwerk zijn aangesloten. Dit is een Linux-kernelbeperking van het bridge-device. Als je veel containers draait, verdeel ze dan over meerdere netwerken per functie in plaats van alles op één bridge te zetten.
Iets werkt niet?
Containers kunnen elkaar niet op naam resolven.
Je zit waarschijnlijk op de standaard bridge. Maak een aangepast netwerk en koppel beide containers eraan. Controleer met docker inspect <container> --format '{{json .NetworkSettings.Networks}}'.
Poort is gepubliceerd maar niet bereikbaar van buitenaf.
Controleer of het gebonden is aan 127.0.0.1 in plaats van 0.0.0.0. Voer ss -tlnp | grep <port> uit op de host. Controleer ook je firewallregels.
Container kan het internet niet bereiken.
Het netwerk heeft mogelijk internal: true. Controleer met docker network inspect <network> --format '{{.Internal}}'. Als het true retourneert, blokkeert het netwerk uitgaand verkeer by design.
IPv6 werkt niet in containers.
Verificeer dat ip6tables is ingeschakeld in daemon.json. Op oudere kernels, laad de module: sudo modprobe ip6_tables. Controleer of het netwerk IPv6 heeft ingeschakeld: docker network inspect <network> --format '{{.EnableIPv6}}'.
"Address already in use" met host-networking.
Een ander proces (of container) gebruikt die poort al. Vind het met ss -tlnp | grep <port>. Host-networking doet geen poortmapping, dus conflicten zijn direct.
Volgende stappen
Je containernetwerk staat. Wat nu:
- Een reverse proxy toevoegen voor TLS en routing
- Dockers firewall-bypass oplossen zodat gepubliceerde poorten je UFW/nftables-regels respecteren
- Resourcelimieten en healthchecks toevoegen aan je Compose-services
- Terug naar het Docker-op-VPS-overzicht voor de volledige productiechecklist
Copyright 2026 Virtua.Cloud. Alle rechten voorbehouden. Deze inhoud is een origineel werk van het Virtua.Cloud-team. Reproductie, herpublicatie of herdistributie zonder schriftelijke toestemming is verboden.