Docker Compose networking dey work how?
Learn how Compose create the default bridge, use service-name DNS, share networks across projects, and why published ports fit bypass UFW.
Wetin Compose dey build before your app start
Docker Compose networking start with one rule: docker compose up dey create one private network for the project, attach every service to am, and allow those services reach each other by service name. You no need write even one networks: line to get this. Plenty confusion about Compose networking dey come from not knowing say default don already dey there.
Na small file be this. Save am as compose.yaml inside directory wey dem call shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleBring am up and check wetin Docker create:
docker compose up -d
docker network lsThe list now get one network wey dem call shop_default. Compose name am <project>_default, and the project name dey default to the directory name for lowercase. You fit override am with docker compose -p myproject up -d or with top-level name: myproject inside the file. The driver na bridge, wey be virtual switch inside the host. Each container get address for private subnet, and outbound traffic dey translate to the host address as e dey go out.
docker compose down dey delete that network again. Na why stale container from old project fit keep network open: Docker go refuse with error while removing network: network shop_default has active endpoints, and the fix na to stop or remove the container wey still attach to am.
If Compose still new to you, Compose file layout and lifecycle commands dey worth reading first, because everything wey follow assume say you fit start and stop project.
DNS by service name na di part beginners dey miss
For any user-defined network, Docker dey run one embedded DNS server wey every container fit see for 127.0.0.11. E dey resolve service names to the current container addresses. So web fit reach the database for hostname db, for port 5432, without any configuration.
docker compose exec web getent hosts dbE go print one line like 172.18.0.2 db. If e no print anything, the two services no dey for the same network.
The mistake almost everybody dey make once na to use localhost for the application config. Inside one container, localhost na that container, no be the host and no be the other service. Postgres clients dey report am clearly:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?The connection string suppose be postgresql://postgres:example@db:5432/postgres. The host part na the service name.
Two details fit save time later. Names dey resolve to wetin dey run now, so docker compose up -d --scale web=3 go give one name with three addresses, and any client wey dey cache DNS forever go pin itself to one dead container. And the legacy bridge network wey plain docker run dey use without --network no get name resolution at all. Na why advice about container links from 2016 no match wetin you dey see.
You no need ports: to connect two services
ports: dey publish one container port for the host. E dey handle network traffic wey dey enter from outside Docker. E no get anything to do with service-to-service traffic, because that one already dey work across the whole port range for the project network.
So the ports: - "5432:5432" wey many people dey add to their database service no dey help and e dey cause real harm: e exposes Postgres on the server public interface. Delete am. If you want make your laptop reach am for migration, bind am to loopback with "127.0.0.1:5432:5432" and reach am through an SSH tunnel. The difference between a listening socket, a published port, and a firewall rule dey covered for how ports and listening services dey work for Linux.
expose: na documentation only under Compose. E no open anything, because nothing dey closed between containers wey dey on the same network.
When network_mode host dey right, and wetin e cost
Host mode dey remove the container own network namespace and allow the process use the host interfaces directly.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityReal reasons dey to want this. Process wey need see broadcast or multicast traffic for the local network, like device discovery for media server or home automation hub, no fit see am from behind bridge, because the bridge no dey forward that traffic go the container. Monitoring agent wey dey read the host interface counters need the host interfaces. And you skip the address translation hop, wey matter when packet rate high.
The costs get specific effects.
ports: no longer dey work. Docker dey warn say published ports dey discarded when you use host network mode, and the container go bind any port wey its process bind. If two host mode containers want port 8080, dem go collide, and the second one go die with bind: address already in use.
Service-name resolution no longer dey work for both directions. The container no dey on the project network, so e no fit resolve db, and the other services no fit resolve am. E fit reach dem only through ports published on the host, usually for 127.0.0.1.
Isolation don disappear. Process wey bind 0.0.0.0 inside host mode container dey listen on every interface for your server, including the public one, exactly like package wey you install with apt. One benefit dey: this traffic dey follow the normal input path, so UFW rules dey apply to am. This one no true for published ports.
Host mode na Linux Docker Engine feature. Docker Desktop support am only from version 4.34 onward and only after you enable am. E still get limits: containers no fit bind host IP addresses, and na only TCP and UDP dem dey handle. If half your team dey use Linux servers and the other half dey use Docker Desktop, expect the same file to behave differently.
Use host mode when you need the host interfaces. No use am to fix connection problem, because e usually dey replace one problem with another one wey harder.
Connect Compose projects two together with external network
Network wey one project create no dey visible to another project. Na why reverse proxy for proxy/compose.yaml no fit see app for app/compose.yaml, even when dem dey the same server. The solution na network wey no project own.
Create am once, by hand:
docker network create edgeThen declare am as external for each project. The proxy side:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: trueThe application side:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true dey tell Compose make e connect to network wey already dey exist instead of creating new one, and make e leave am for docker compose down. The separate name: key matter pass as e first appear: without am, Compose go look for network wey name exactly edge, but with am, you fit use one name for your file and another name for the host.
If the network no exist, Compose no go start and e go report say the network was declared as external but e no fit find am. Create am first.
Notice wetin the application file dey do with internal. The database dey only on that project-local network, so the proxy no fit reach am and na only app fit reach am. Adding internal: true under a network go further and remove its route to outside world completely. This na good default for database, but know this cost before you set am: container for internal network no fit download anything, so entrypoint wey dey run apt-get update or pip install for startup go hang and later fail with timeout.
For complete worked setup with routing rules and certificates, see how to run several apps behind one Traefik instance.
Published ports dey bypass UFW
Na this part of Compose networking fit turn into security incident. You publish one port, you check say UFW dey active and dey deny everything except SSH, but the service still dey reachable from internet.
sudo ufw status
curl http://203.0.113.10:8080UFW dey say the port dey blocked. The curl still dey return the page. Nothing spoil. Docker dey write im own address translation and forwarding rules straight into iptables. Traffic wey go to published container port dey forward go the container instead of host. So e no dey pass through the chain wey UFW dey manage for traffic wey target the local host. Docker rules dey also match before UFW rules.
The short fix na to publish only where you need am:
ports:
- "127.0.0.1:8080:80"This one bind the host side to loopback. So the port dey reachable from the server itself and through an SSH tunnel, but nowhere else. Put the public entry point behind a reverse proxy wey publish 80 and 443 intentionally. The full explanation, including the DOCKER-USER chain for cases where you must filter a published port, dey inside why Docker dey publish straight past UFW and how to fix am.
How to debug am for four commands
Start by asking which network each container really dey on:
docker network inspect shop_defaultThe Containers block dey list every container wey attach, together with its address. If service no dey for that list, e dey another network, or e dey host mode, or e no dey run.
Test name resolution from one temporary container wey attach to the same network, so you no need any tooling inside your own images:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432nslookup wey fail dey point to name resolution or network membership. If nslookup succeed while nc fail, e mean say the service dey run but e no dey listen on that port, or e dey listen on 127.0.0.1 inside its own container instead of 0.0.0.0. This one common with development servers. The fix dey for the application's bind address, no be for Docker.
Another failure fit look like Docker bug. If containers fit talk to each other but dem no fit reach machine for your office or VPN network, the Docker subnet likely overlap that network. Docker dey allocate from 172.17.0.0/16 upward by default. Move the pool inside /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Then run sudo systemctl restart docker and recreate the affected networks, because existing network dey keep the subnet wey dem use create am.
FAQ
Why containers no fit reach each other by service name?
Dem no dey for the same network. Compose dey put every service for <project>_default automatically, but once you add networks: list to a service, that list go become the complete set of networks for am, and the default one no longer dey implied. Run docker network inspect <network> and check say both containers dey appear for the Containers block. Also check say neither service dey use network_mode: host, because host mode container dey on no Docker network and e no fit resolve service names.
I need publish ports make one service reach another?
No. For Compose network, every port of every container dey reachable by the other containers wey dey that network. ports: dey only to expose container to traffic from outside Docker, and expose: na documentation. Publishing database port na common and costly habit, because e dey put the database for your server public interface.
Wetin be the difference between bridge and host networking?
Bridge dey give the container im own network namespace and address for virtual switch, with automatic name resolution between containers and translated outbound traffic. Host dey give the container direct access to the host network stack: no separate address, no resolution by service name, no port publishing, and no isolation from the host other listeners. Bridge na the default and correct choice unless the process need the host interfaces.
How I fit connect containers from two different Compose files?
Create shared network with docker network create edge, then declare am for both files with external: true and attach the services wey need communicate. Compose no go create or delete am. If you skip the create step, Compose go refuse to start and report the network as declared external but not found.
Why my container dey reachable from internet when UFW block the port?
Because Docker dey handle published port with forwarding rules wey e add to iptables. Those rules dey match before UFW own rules, and forwarded traffic no dey pass through the chain wey UFW dey filter anyway. Bind the host side to loopback with "127.0.0.1:8080:80" and put anything public behind reverse proxy for ports 80 and 443.