Skip to content
Serverküche
Search

Loading search … (only available on the published site).

Basics Difficulty: Intermediate

Understanding Docker Networks: bridge, internal DNS & the proxy network

Why does every app recipe declare a proxy network? This guide explains Docker networks, internal DNS and container isolation with real commands.

· 7 min read ·Duration: approx. 30 minutes
Table of contents

networks: [proxy] – this half-sentence shows up in almost every Serverküche app recipe, usually without comment. Time to explain it once, centrally. Afterwards you’ll understand how containers find each other, why some don’t need internet access and how Traefik reaches them at all.

What are we building?

Not a new service, but the networking model behind every Docker stack. By the end you’ll know why you should create your own networks instead of the default bridge, how containers reach each other by name instead of IP (the internal DNS), what an internal network without internet access is good for, and why the shared proxy network is the trick that lets Traefik reach every app. All output comes from a real Debian 13 server running Docker 29.

Diagram: internet via Traefik into the proxy network, the app also on the internal network to the database with no internet access
Two networks: proxy connects Traefik and app, intern connects app and database – the DB has no internet access

Prerequisites

🍳 Recommendation Ad

VPS 1000 G12

4 vCores · 8 GB RAM · 256 GB NVMe

from €10.36/month

A small VPS is enough to work through several container networks.

Go to netcup →

💶 5 € voucher for new netcup customers:36nc17844976032 (new customers only, no domains)

Step by step

Step 1: What’s already there

After installation Docker ships with three networks:

Terminal
docker network ls
Ausgabe
NETWORK ID     NAME      DRIVER    SCOPE
9d087537e327   bridge    bridge    local
b622109fef73   host      host      local
e12e562ad7f6   none      null      local
  • bridge – the default network. Containers with no explicit network land here. It works, but has one crucial catch (step 3).
  • host – the container shares the host’s network stack directly (no isolation layer, no port mapping). For special cases only.
  • none – no network at all. For containers that should deliberately be offline.

For your own stacks you don’t use any of these directly – you create your own networks. Why is shown in step 3.

Step 2: Creating your own network

Terminal
docker network create demo-net

Docker creates a bridge network and assigns a subnet automatically:

Terminal
docker network inspect demo-net --format 'name={{.Name}} driver={{.Driver}} subnet={{range .IPAM.Config}}{{.Subnet}}{{end}}'
Ausgabe
name=demo-net driver=bridge subnet=172.20.0.0/16

Every container in this network gets an IP from 172.20.0.0/16. But you don’t want to work with IPs at all – they change on every restart. The real win is something else.

Step 3: The internal DNS – finding containers by name

Start two containers in demo-net: a web server and a client.

Terminal
docker run -d --name web --network demo-net nginx:1.29-alpine
docker run -d --name client --network demo-net alpine:3.20 sleep 600

Now the crucial test – the client reaches the web server by its name:

Terminal
docker exec client ping -c 2 web
Ausgabe
PING web (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.171 ms
64 bytes from 172.20.0.2: seq=1 ttl=64 time=0.095 ms

Docker automatically resolves the container name web to the right IP. The same works for HTTP:

Terminal
docker exec client wget -qO- http://web/
Ausgabe
<title>Welcome to nginx!</title>

This is exactly why Compose files say DB_HOST=db instead of an IP: db is the service name, and Docker turns it into the right address. Comparing with the default bridge shows why you avoid it – there’s no name resolution there:

Terminal
docker run -d --name legacy1 nginx:1.29-alpine
docker run --rm alpine:3.20 ping -c 1 legacy1
Ausgabe
ping: bad address 'legacy1'

Compose does this automatically

A docker compose up automatically creates a dedicated named network for the stack (e.g. myapp_default) – with working DNS. That’s why the services of a Compose file always reach each other by name without you doing anything. The default bridge only affects containers you start manually with docker run and no --network.

Step 4: Segmenting – a database with no internet

Not every container needs to see every other one. A database should only be reachable by its app and needs no internet itself. That’s what internal networks are for:

Terminal
docker network create --internal demo-intern

internal: true cuts the route to the outside. The proof:

Terminal
docker run --rm --network demo-intern alpine:3.20 \
  sh -c "wget -qT4 -O- http://example.com >/dev/null 2>&1 && echo REACHED || echo BLOCKED"
Ausgabe
BLOCKED

This is a strong security pattern: even if an attacker compromises the database, it can’t open a connection to the outside (no pulling in malware, no data exfiltration). A typical stack therefore uses two networks: an internal network between app and database, and the public proxy network only between app and Traefik.

Step 5: The proxy network – how Traefik reaches every app

Now the most-copied half-sentence of the catalog. Traefik runs in its own container and has to talk to every app to forward its requests. But containers only talk to each other when they’re on the same network. The solution: a shared, permanent network called proxy.

Terminal
docker network inspect proxy --format 'name={{.Name}} driver={{.Driver}} subnet={{range .IPAM.Config}}{{.Subnet}}{{end}} internal={{.Internal}}'
Ausgabe
name=proxy driver=bridge subnet=172.19.0.0/16 internal=false

Traefik stays permanently on this network. Every new app joins it – in Compose like this:

YAML
services:
  myapp:
    image: example/app:1.0
    networks:
      - proxy        # so Traefik can reach the app
      - intern       # private link to the database
    labels:
      - "traefik.enable=true"
      # ... the router/service labels

networks:
  proxy:
    external: true   # created by Traefik, do NOT recreate
  intern:
    internal: true

Two things are key here: external: true tells Docker “this network already exists, just attach to it” – if you accidentally recreate it, the app lands in a different proxy network than Traefik and can’t be reached via the domain (the classic 502 troubleshooting case). And through the second network intern the app reaches its database without that database ever sitting on the public network.

Step 6: Publishing ports – only where needed

In all steps so far, containers talk to each other without publishing a single port. A port only needs to go outside when the host or the internet should access it directly:

Terminal
docker run -d --name pub -p 8099:80 nginx:1.29-alpine
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8099/
Ausgabe
200

-p 8099:80 binds host port 8099 to container port 80. In Compose that’s ports: ["8099:80"]. The important difference to expose: expose only makes a port visible to other containers (network-internal), ports opens it to the host.

Behind Traefik, don't publish app ports

When an app runs behind Traefik it needs (and wants) no ports:. Traefik reaches it via the proxy network; an extra published port would bypass Traefik and thus HTTPS, middlewares and firewall logic. Only publish Traefik’s own ports (80/443). Everything else stays network-internal.

When things go wrong

Container can’t reach another by name (bad address). Both aren’t on the same named network, or one uses the default bridge (no DNS). Check which networks a container is on with docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' CONTAINER.

Traefik returns 502 Bad Gateway. Almost always Traefik and the app are on different proxy networks because the network was created twice. Make sure external: true is set in Compose and check with docker network inspect proxy whether both containers are listed.

network proxy not found on up. The external network doesn’t exist yet. Create it once: docker network create proxy (or start Traefik first, which creates it).

Database has internet despite internal. It’s also attached to a non-internal network (e.g. proxy). A container has the sum of the access of all its networks – the DB belongs only on the internal network, never on the proxy network.

Two stacks collide on the subnet. Docker assigns 172.x ranges automatically, but with many networks the pool can run short (could not find an available, non-overlapping IPv4 address pool). Clean up unused networks: docker network prune.

Maintenance & backups

  • Networks need no backup – they’re recreated from the Compose file at any time. What matters is that your compose.yaml and the networks: declarations are in your backup, not the network state itself.
  • Clean up orphaned networks. After lots of testing, unused networks pile up. docker network ls shows them, docker network prune removes all without attached containers. The permanent proxy network survives this as long as Traefik is running.
  • The proxy network is infrastructure. Treat it like Traefik itself: create it once, then leave it alone. Deleting it by accident (network prune with Traefik stopped) forces you to reconnect all apps. More on clean-up in Keeping your Docker stack updated.

You might also like

Forgejo: your own Git server behind Traefik
Applications Intermediate

Forgejo: your own Git server behind Traefik

Set up Forgejo with Docker & Traefik: your own Git server with HTTPS, repos via web UI, cloning over HTTPS and SSH – the …

· 11 min read