Lesson 11 of 30 intermediate 5 min read

Before this:Multi-container apps with Compose

Container networking & volumes

Key takeaways Every container joins a network — usually a bridge — where Docker gives it an IP and lets containers reach each other by service name through built-in DNS. Publishing a port maps a container port to the host so the outside world can reach it. A container’s own filesystem is temporary; a volume — named or a bind mount — persists data on the host so it survives the container being replaced.

The Compose lesson showed ports: and volumes: lines without explaining the machinery under them. This lesson is that machinery: how containers get on the network, find each other, expose ports, and keep data alive.

Every container joins a network

When Docker starts a container it attaches it to a network and hands it a private IP. By default that’s a bridge network — a virtual switch on the host. Compose creates one network per project automatically, so all the services in a docker-compose.yml land on the same bridge and can talk to each other. Containers on different networks can’t see one another, which is how you keep, say, a database off the public side.

docker network ls                    # list networks
docker network inspect gophertrunk_default   # see who's attached

Containers find each other by name

The key trick: on a Docker network, you never hard-code IP addresses. Docker runs an internal DNS so a container can reach another by its service name. If your Compose file has a db service, the app container just connects to the host db:

services:
  app:
    environment:
      DATABASE_URL: "postgres://db:5432/app"   # "db" resolves to the db container
  db:
    image: postgres:16

Docker resolves db to that container’s current IP, even if it changes on restart. Names are stable; IPs are not — so you always use names.

Publishing ports exposes a container to the host

Container-to-container traffic stays on the internal network. To let something outside reach a container — your browser, another machine — you publish a port, mapping a host port to a container port. GopherTrunk publishes both its ports but binds them to localhost only:

ports:
  - "127.0.0.1:8080:8080"    # host 127.0.0.1:8080 -> container 8080
  - "127.0.0.1:50051:50051"  # gRPC, localhost only

The 127.0.0.1: prefix is the important detail: without it, - "8080:8080" binds to every host interface, exposing the API to the whole network. Binding to 127.0.0.1 keeps it reachable only from the host itself — you then put a reverse proxy in front to expose it safely.

host 127.0.0.1:8080 app db bridge network "db"
A published port forwards from the host to a container; on the bridge network, containers reach each other by service name.

Volumes: data that outlives the container

A container’s writable filesystem is ephemeral — remove the container (which happens on every upgrade) and everything it wrote is gone. A volume maps storage that lives on the host, so data survives. There are two kinds:

  Bind mount Named volume
You write ./recordings:/var/lib/... recdata:/var/lib/...
Location A host path you choose Docker-managed storage
Best for Config, code, files you edit Databases, data you don’t hand-edit

GopherTrunk uses bind mounts so the operator can see and edit the files directly:

volumes:
  - ./config.yaml:/etc/gophertrunk/config.yaml:ro   # read-only config
  - ./recordings:/var/lib/gophertrunk/recordings    # recordings persist
  - ./calls.db:/var/lib/gophertrunk/calls.db        # call database persists

The :ro on the config makes it read-only inside the container — the app can read its config but can’t overwrite it. Pull a new image, recreate the container, and the recordings and database are still there because they live on the host, not in the container. That persistence is the whole subject of backups & data.

Quick check: how does one container reach another on the same Docker network?

Recap

  • Containers join a bridge network and get a private IP; Compose makes one per project.
  • On a network, containers reach each other by service name via Docker’s DNS — never hard-code IPs.
  • Publishing a port maps a container port to the host; binding to 127.0.0.1 keeps it off the public network.
  • A volume (bind mount or named) persists data on the host so it survives the container being replaced; :ro makes a mount read-only.

Next up: giving a container exactly the access it needs — container security.

Frequently asked questions

How do containers talk to each other?

Containers on the same Docker network can reach each other by service name — Docker runs an internal DNS so the name ‘db’ resolves to that container’s IP. You don’t hard-code IP addresses; you use the service names from your Compose file. Containers on different networks can’t see each other unless you connect them, which is how you isolate a database from the outside world.

What is the difference between a named volume and a bind mount?

A bind mount maps a specific host directory into the container — you control the exact path, good for config files and code you edit. A named volume is managed by Docker in its own storage area, referenced by a name — better for data like databases where you don’t care about the host path, just that it persists. Both survive the container being removed.