Docker Deep Dive: Image Layers, Volumes, and Networking

Learn how image layers cache, persist data with volumes, wire containers together with custom networks, and reclaim disk space.

Once your first container is running on your server or VPS, the next step is understanding three things behind Docker: how images are layered, where data lives, and how containers talk to each other. This guide uses Ubuntu / Debian.

Image Layers and Caching

A Docker image is a stack of read-only layers, one per Dockerfile instruction. When a container runs, a thin writable layer is added on top. Grasping this speeds up your builds a lot.

The key rule: during a build, Docker compares layers one by one. As long as an instruction and its context are unchanged, it hits the cache. But once a layer changes, every layer after it is rebuilt. So put the steps that rarely change first.

# Anti-pattern: one code change reinstalls all deps
COPY . /app
RUN npm install

# Better: copy the manifest and install first, then the source
COPY package*.json /app/
RUN npm install
COPY . /app

Inspect an image's layer history:

docker history <image>
docker image ls        # see each image's size

Persisting Data: Volumes vs. Bind Mounts

Delete a container and everything in its writable layer is gone. To persist data you must keep it outside the container, and there are two ways.

  • Named volumes: managed by Docker, stored under /var/lib/docker/volumes/; ideal for databases and other production data.
  • Bind mounts: map a host directory straight into the container; handy in development for mounting source code or config files.
# Named volume
docker volume create pgdata
docker run -d --name pg -v pgdata:/var/lib/postgresql/data postgres

# Bind mount (absolute host path:container path)
docker run -d -v /srv/site:/usr/share/nginx/html nginx

Management commands:

docker volume ls
docker volume inspect pgdata
docker volume rm pgdata          # only removable when no container uses it

Network Modes and Linking Containers

Docker ships three networks out of the box:

  • bridge: the default; containers join a virtual bridge and are exposed with port mapping -p.
  • host: shares the host's network stack directly, so no port mapping is needed, but you lose isolation.
  • none: no networking at all, fully isolated.

On the default bridge, containers can only reach each other by IP. The right approach is a custom network: containers on the same one talk using the container name as a hostname, with built-in DNS resolution.

docker network create app-net
docker run -d --name db --network app-net postgres
docker run -d --name web --network app-net -p 8080:80 nginx
# Inside web, connect to the database at db:5432
docker network ls
docker network inspect app-net

Reclaiming Disk Space

Images, stopped containers, and dangling volumes quietly fill your disk. Clean up regularly:

docker system df                 # check usage first
docker system prune              # remove stopped containers, unused networks, dangling images
docker system prune -a --volumes # deeper: also unreferenced images and volumes (use with care)

-a --volumes deletes every unused image and volume, so make sure nothing you need is affected before you run it.

Summary

Images are layered and cached, so front-loading stable steps speeds up builds. Persist data with named volumes for production or bind mounts for development. Link containers over a custom network and address them by name, not IP. And don't forget docker system prune to keep disk usage in check. Nail these four points and you can confidently operate containerized apps on your own server.