AnyLearn
All lessons
Programmingadvanced

Networking and Storage: Connecting an Isolated Thing

Isolation is the point, and a container that can reach nothing and keep nothing is useless. This lesson covers how a network namespace is wired to the outside, why published ports and container-to-container traffic work completely differently, and where data has to live given that the writable layer disappears.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 8

An empty network namespace

A fresh network namespace contains a loopback interface and nothing else. No route to anywhere, no address, no connectivity.

Everything a container can reach is therefore something explicitly wired to it, and the wiring uses ordinary Linux facilities.

The standard arrangement is a virtual ethernet pair: two connected interfaces where anything sent into one comes out of the other. One end is placed inside the container's namespace and named eth0; the other stays on the host and is attached to a bridge, a software switch.

container ns:  eth0  <=====>  vethXXXX  :host
                                  |
                             docker0 bridge
                                  |
                              host routing

Every container on the same bridge gets an address on a private subnet, and the bridge switches traffic between them. Reaching the outside world goes through the host's routing table, with network address translation rewriting the private source address to the host's.

None of that is container technology. It is a virtual cable, a software switch and NAT, all of which the kernel has had for years, assembled automatically on your behalf.

Full lesson text

All 8 steps on one page, for reading, reference, and search.

Show

1. An empty network namespace

A fresh network namespace contains a loopback interface and nothing else. No route to anywhere, no address, no connectivity.

Everything a container can reach is therefore something explicitly wired to it, and the wiring uses ordinary Linux facilities.

The standard arrangement is a virtual ethernet pair: two connected interfaces where anything sent into one comes out of the other. One end is placed inside the container's namespace and named eth0; the other stays on the host and is attached to a bridge, a software switch.

container ns:  eth0  <=====>  vethXXXX  :host
                                  |
                             docker0 bridge
                                  |
                              host routing

Every container on the same bridge gets an address on a private subnet, and the bridge switches traffic between them. Reaching the outside world goes through the host's routing table, with network address translation rewriting the private source address to the host's.

None of that is container technology. It is a virtual cable, a software switch and NAT, all of which the kernel has had for years, assembled automatically on your behalf.

2. Publishing a port is a firewall rule

A container listening on port 8080 is listening in its own namespace, which has its own complete port space. Nothing outside can reach it, and two containers can both listen on 8080 without conflict.

Publishing a port creates a NAT rule on the host that forwards traffic to the container's address.

docker run -p 8080:80 nginx

That reads host port 8080 to container port 80, and the order catches people constantly. The rule installed is roughly "traffic arriving at host:8080 is destination-NATed to 172.17.0.2:80".

Three consequences follow.

Host ports genuinely conflict. Two containers can both use container port 80, and only one can publish to host port 8080, because that side is the host's real port space.

Publishing binds all interfaces by default, exposing the service on every address the host has. Restricting it to 127.0.0.1:8080:80 is the difference between a local development service and one reachable from the network, and the default surprises people.

The container sees the wrong source address. After NAT, connections appear to come from the gateway rather than the real client, which is why access logs are useless without a forwarded-header configuration on the proxy in front.

3. Containers talking to each other

Container-to-container traffic works completely differently from published ports, and mixing the two models is the most common networking mistake.

On a user-defined network, containers reach each other by name, resolved by an embedded DNS server, on the container's own port. No publishing is involved.

services:
  api:
    build: .
    environment:
      DB_HOST: db          # the service name, not localhost
      DB_PORT: "5432"      # the container port, not a published one
  db:
    image: postgres:16

The database publishes nothing. It does not need to: api and db share a network, so db:5432 resolves and connects directly.

Two errors follow from not seeing this.

Using localhost between containers. Inside a container, localhost is that container's own namespace, so localhost:5432 means "a database in this container", which there is not. This is the single most frequent container networking bug.

Publishing everything. Databases and caches are routinely published to the host purely because their consumer could not reach them, which exposes them unnecessarily. If the consumer is a container on the same network, publishing is not the fix, and the port should not be exposed at all.

The rule: publish only what must be reachable from outside. Everything internal talks over the shared network by name.

4. Where data can live

Four destinations, and only the first is the default, which is why data loss surprises people.

Writable layer. Copy-on-write, per container, gone when it is removed. Fine for scratch files; wrong for anything else.

Named volume. A directory the runtime manages outside the container's layers. It survives removal, can be shared between containers, and is the correct default for persistent data. Because it bypasses the union filesystem, it also avoids the copy-up cost on writes.

Bind mount. A specific host path mounted into the container. Excellent for development, where mounting your source directory gives live editing without rebuilding. Poor for production, because it makes the container depend on the host's filesystem layout, which is exactly the portability the image was meant to provide.

tmpfs. Memory-backed and never written to disk, which is the right destination for secrets and scratch data that must not persist.

The decision is nearly always: volumes in production, bind mounts in development, tmpfs for secrets, and nothing important in the writable layer.

flowchart TD
A["A container writes a file"] --> B["Writable layer"]
A --> C["Named volume"]
A --> D["Bind mount"]
A --> E["tmpfs mount"]
B --> F["Deleted with the container"]
C --> G["Managed by the runtime, survives"]
D --> H["A host path, survives, host-dependent"]
E --> I["Memory only, never on disk"]

5. Configuration and secrets

Since an image should be identical across environments, everything that differs between them has to arrive at run time.

Environment variables are the usual mechanism, and they are convenient and leaky. They appear in docker inspect, in process listings, in crash dumps, and are inherited by every child process. That is acceptable for a log level and inadequate for a database password.

Mounted files are better for secrets. The value is a file the container reads, delivered by a tmpfs mount or a secrets mechanism, so it is not in the process environment and not in the image.

Baked into the image is the one to avoid absolutely, for the reason the previous lesson established: a secret in a layer is readable by anyone who pulls the image, permanently, and deleting it in a later instruction does not help.

The generalisation worth keeping is the build/run split. An image is built once and run in many places, so anything environment-specific belongs at run time, and anything at build time is baked into an artefact that will be copied everywhere.

The failure mode to watch for is building separate images per environment. It defeats the guarantee the image was for: the thing tested in staging is then not the thing running in production.

6. Logs go to stdout

The container convention for logging looks primitive and is deliberate: write to standard output and standard error, and do not manage files.

The reasoning follows from everything above. A log file written inside the container lands in the writable layer and disappears with it. Writing to a volume means rotation, permissions and disk management per container. And a container has no reliable identity or lifetime, so it is a poor place to accumulate anything.

Writing to stdout hands the problem to the runtime, which captures the stream and routes it wherever the platform collects logs. The application does one simple thing and the infrastructure does the hard part once.

Two practical points follow.

Structured output pays immediately. Emitting JSON rather than free text means the collector can index fields without parsing conventions, and container logs are aggregated across many instances where grep does not scale.

Buffering will bite you. Many runtimes buffer stdout when it is a pipe rather than a terminal, which is exactly the container case. The result is logs appearing late, in bursts, or not at all when a process crashes, and the fix is to disable buffering explicitly.

The same reasoning covers metrics and traces: emit, do not store.

7. Where the networking gets harder

Everything above is a single host. Across several, the model changes in ways worth knowing before meeting an orchestrator.

Addresses stop being useful. A container's IP is assigned from its host's bridge subnet and changes on every restart. Nothing can be configured to point at one.

Names must be resolved dynamically. Reaching a service means asking something where its instances currently are, which is service discovery.

Bridges do not span hosts. Connecting containers on different machines requires an overlay network, which encapsulates container traffic inside packets addressed between hosts and unwraps it at the far end.

Load balancing becomes necessary. With several instances of a service, something must distribute traffic and stop sending it to instances that have failed.

At that point the single-host tooling has run out, and the functions being reinvented are exactly the ones the lesson Distributed Systems Fundamentals treats.

That is the boundary where orchestration begins. The container primitives stay identical; what changes is that scheduling, discovery and health become continuous problems rather than one-off setup, which is the subject of a separate path.

8. The defaults, and what to change

DefaultConsequenceWhat to do instead
Writable layer for dataLost on removalNamed volume
-p 8080:80 binds all interfacesExposed on every host address127.0.0.1:8080:80 for local only
Publishing to connect containersUnnecessary exposureShared network, connect by service name
localhost between containersConnection refusedUse the service name
Secrets in environment variablesVisible in inspect, dumps, childrenMounted file or tmpfs
Logs to a fileLost with the containerstdout, structured, unbuffered
Bind mounts in productionTies the image to a host layoutVolumes; bind mounts for development

The recurring theme is that the defaults optimise for getting something running quickly, and several of them are wrong for anything that has to keep running.

The unifying principle behind the right column is the one containers were built around: a container should be disposable. Anything that must survive it, whether data, configuration or logs, has to live outside it, and the mechanisms above are the four places outside it can mean.

What remains open is how much the isolation is actually worth. The wiring above deliberately punches holes in it, and the previous lesson noted that the kernel is shared. What that costs, and what to do about it, is the last lesson.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. What does `-p 8080:80` actually create?
    • A NAT rule on the host forwarding host port 8080 to the container's port 80
    • A shared socket between the host and the container
    • A bridge interface dedicated to that container
    • A DNS entry mapping the container name to port 8080
  2. Why does connecting to `localhost:5432` from inside a container fail to reach a database container?
    • Because the database has not published its port
    • Because `localhost` refers to that container's own network namespace
    • Because the bridge blocks loopback traffic between containers
    • Because DNS resolution is disabled on the default network
  3. Why is a named volume preferred over the writable layer for database storage?
    • It compresses data more efficiently
    • It allows the container to run as root
    • It survives container removal and bypasses the union filesystem's copy-up cost
    • It is automatically replicated across hosts
  4. Why are environment variables a poor place for secrets?
    • They are limited in length by the kernel
    • They are stored in the image's final layer
    • They cannot be changed after the container starts
    • They appear in inspect output, process listings and crash dumps, and are inherited by child processes
  5. Why do containers log to stdout rather than to files?
    • The runtime captures the stream and routes it, while a file in the container disappears with it
    • File writes are blocked by the mount namespace
    • stdout is the only stream the union filesystem supports
    • Log files cannot be rotated inside a container

Related lessons

Programming
advanced

The Runtime Stack, and What Isolation Is Worth

One command hides four layers of software and a set of standards that made them interchangeable. This lesson takes the stack apart, then asks the question the whole path has been building toward: given a shared kernel, how much is container isolation actually worth, and what has to be added before it is a security boundary.

8 steps·~12 min
Programming
advanced

Images: A Filesystem You Can Address by Hash

The kernel gives isolation for files already on the machine. The image format is what turned a directory tree into something you can build once, name by digest and run anywhere. This lesson covers layers and the union filesystem that stacks them, why the build cache behaves as it does, and the content addressing that makes it all work.

8 steps·~12 min
Programming
intermediate

A Container Is Not a Thing

There is no container object in the Linux kernel, no container system call, and no container data structure. A container is an ordinary process started with several isolation features switched on at once. This lesson establishes what those features are, what the word actually names, and why the distinction changes how you reason about them.

8 steps·~12 min
Programming
advanced

Pods, Services, and the Label That Joins Them

The object types look like an arbitrary vocabulary until you notice they are layers of controllers, each reconciling the one below. This lesson works up from the pod, explains why a Service can point at something whose address changes constantly, and shows that the whole system is joined by one mechanism: a label match.

8 steps·~12 min