Volume & Network #

Volumes and networks are two fundamental concepts that determine how containers store data and communicate. Docker Compose provides declarative ways to manage both, so you can focus on the application, not the infrastructure.

This article covers all volume types Compose supports, the available network topologies, and best practices for clean setups.

Volumes in Docker Compose #

Volumes are the mechanism for storing data outside containers. There are three main types: named volumes, bind mounts, and tmpfs.

Named Volumes #

Named volumes are managed by Docker, stored in /var/lib/docker/volumes/, and portable across hosts.

services:
  db:
    image: postgres:16-alpine
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

In the example above, db-data is a named volume. Docker creates it automatically (or uses an existing one) and mounts it to /var/lib/postgresql/data in the db container.

Named volume characteristics:

  • Docker-managed — lifecycle, location, and cleanup are handled by Docker.
  • Portable — can be backed up, restored, and moved between hosts.
  • Production-safe — doesn’t depend on the host’s filesystem layout.
  • Shareable between services by mounting the same volume.

Inspecting named volumes:

# List all volumes
docker volume ls

# Inspect a specific volume
docker volume inspect myproject_db-data

# See where the data is stored
docker volume inspect --format '{{ .Mountpoint }}' myproject_db-data
# Output: /var/lib/docker/volumes/myproject_db-data/_data

Bind Mounts #

Bind mounts mount a directory or file from the host directly into the container.

services:
  web:
    volumes:
      - ./html:/usr/share/nginx/html:ro
      - /opt/configs/app:/etc/app:ro

Format: host_path:container_path:options.

Bind mount characteristics:

  • Depends on host paths — must exist on every host where the stack runs.
  • Native performance — direct access to the host filesystem, no abstraction layer.
  • Great for development — host source code can be edited and immediately visible in the container.
  • Not portable — the host directory structure must match.

For development:

services:
  api:
    build: ./api
    volumes:
      - ./api/src:/app/src      # hot reload
      - ./api/tests:/app/tests

Code changes on the host are immediately visible in the container. For interpreted languages (Python, JavaScript, Ruby), this means no image rebuild needed.

Beware of permissions. Files on the host may be owned by the host user, but inside the container they’re owned by the container user. To avoid permission issues:

services:
  api:
    user: "1000:1000"  # match the host UID/GID
    volumes:
      - ./src:/app/src

Or set permissions in the Dockerfile:

RUN chown -R 1000:1000 /app

tmpfs Mounts #

tmpfs stores data in memory (RAM), not disk. Data disappears when the container stops.

services:
  api:
    tmpfs:
      - /tmp:size=100m
      - /run

tmpfs characteristics:

  • Very fast — RAM access.
  • Ephemeral — data disappears when the container stops/restarts.
  • Not shared between containers — each has its own tmpfs.
  • Good for caches, sessions, temporary file processing.
Performance comparison: tmpfs (RAM) > local volume (SSD) > bind mount (host FS) > network volume (NFS/EFS). Choose per performance and persistence needs.

Volume Drivers #

Named volumes support drivers for different storage backends.

volumes:
  db-data:
    driver: local
    driver_opts:
      type: nfs
      o: addr=10.0.0.5,rw
      device: ":/path/to/dir"
  
  shared-data:
    driver: aws/efs
    driver_opts:
      efs.id: fs-12345678

Common drivers:

  • local — default, stored on the local host.
  • nfs — Network File System, shared storage.
  • aws/efs — AWS Elastic File System.
  • gcs — Google Cloud Storage (FUSE).
  • azurefile — Azure File Storage.
Network volume drivers (NFS, EFS) add latency and potential failure points. For production databases, local SSD with application-level replication is better.

Networks in Docker Compose #

Compose automatically creates an internal network and registers all services on it. Services can communicate via hostname = service name.

The Default Network #

When you don’t define networks, Compose creates one default network.

services:
  api:
    image: myapi
  
  db:
    image: postgres:16

Docker creates the myproject_default network and both services join it. api can resolve db by hostname.

Inspect:

docker network ls
docker network inspect myproject_default

Custom Networks #

For more control, define explicit networks.

services:
  nginx:
    networks:
      - frontend
  
  api:
    networks:
      - frontend
      - backend
  
  db:
    networks:
      - backend

networks:
  frontend:
  backend:
    driver: bridge

With this configuration:

  • nginx and api are on frontend. They can communicate.
  • api and db are on backend. They can communicate.
  • db isn’t on frontend, so nginx can’t directly access db.
  • Everything must go through api.

This is the defense in depth pattern — separate public services from internal ones.

Network Drivers #

The default driver for custom networks in Compose is bridge. Other drivers are also available.

networks:
  frontend:
    driver: bridge
  
  backend:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: br-backend
    attachable: true

The bridge driver on Linux uses a kernel bridge interface. driver_opts enables advanced configuration.

Other drivers:

  • host — shares the host network namespace. No network isolation.
  • overlay — for multi-host (Swarm mode).
  • macvlan — assigns MAC addresses to containers, making them look like physical devices.
For single-host Compose, bridge is enough. Other drivers are needed for special cases: host for network-sensitive performance, overlay for multi-host (Swarm), macvlan for physical-network integration.

Network Aliases #

A service can have several hostnames on a network.

services:
  api:
    networks:
      frontend:
        aliases:
          - api-service
          - myapi
          - backend-api

api can now be reached via api, api-service, myapi, or backend-api on the frontend network. Useful for compatibility with legacy code hardcoding specific hostnames.

External Networks #

For connecting a Compose stack with resources outside Compose.

services:
  api:
    networks:
      - shared-network

networks:
  shared-network:
    external: true
    name: my-existing-network

The my-existing-network already exists in Docker (created manually or by another stack). Compose won’t remove it on down.

Use cases:

  • Stacks shared between multiple Compose projects.
  • Standalone monitoring or logging networks.
  • Legacy services managed outside Compose.

A Complete Setup Example #

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
      - html:/usr/share/nginx/html
    networks:
      - frontend
    depends_on:
      - api
    restart: unless-stopped

  api:
    build: ./api
    environment:
      - DATABASE_URL=postgres://app:pass@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy
    networks:
      - frontend
      - backend
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=myapp
    volumes:
      - pg-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    volumes:
      - cache-data:/data
    networks:
      - backend
    restart: unless-stopped

  worker:
    build: ./worker
    environment:
      - DATABASE_URL=postgres://app:pass@db:5432/myapp
    networks:
      - backend
    restart: unless-stopped

volumes:
  pg-data:
  cache-data:
  html:

networks:
  frontend:
  backend:
    internal: false  # exposed to the host? or leave false

This stack has 5 services, 3 named volumes, and 2 networks. Database and cache are isolated on backend. Only nginx is exposed to the host via ports.

Best Practices #

Use Named Volumes for Data #

Named volumes are the safe default. They’re portable, Docker-managed, and support backups.

# GOOD
volumes:
  - db-data:/var/lib/postgresql/data

# AVOID for production
volumes:
  - /var/lib/postgresql/data:/var/lib/postgresql/data

Bind mounts to absolute host paths are fragile — if a host has a different layout, the container fails to start.

Bind Mounts for Development #

For development, bind-mount the source code so hot reload works.

# docker-compose.dev.yml
services:
  api:
    volumes:
      - ./src:/app/src  # hot reload
      - /app/node_modules  # excluded from the bind mount

/app/node_modules is mounted as an anonymous volume to prevent the host’s node_modules (if any) from overriding the container’s.

Separate Networks by Role #

networks:
  public:    # exposed to the internet
  internal:  # service-to-service only
  management: # monitoring, debugging

Separate networks by who’s allowed access. Databases should never be on a public network.

Set Resource Limits #

For containers mounting lots of data or sharing large networks.

services:
  db:
    volumes:
      - db-data:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          memory: 4G

Back Up Volumes #

Use a separate container for backups.

# Back up a volume to a file
docker run --rm \
  -v myproject_pg-data:/source:ro \
  -v $(pwd)/backups:/backup \
  alpine tar -czf /backup/pg-data-$(date +%F).tar.gz -C /source .

# Restore from a file
docker run --rm \
  -v myproject_pg-data:/target \
  -v $(pwd)/backups:/backup:ro \
  alpine tar -xzf /backup/pg-data-2024-01-15.tar.gz -C /target
Clean separation: data via volumes, services via Compose, backups via a separate container. This allows backing up without disturbing services, and services can be recreated without losing data.

Volume Mount Reference Table #

A quick reference for all ways to mount volumes in Compose.

TypeSyntaxExample
Named volumename:pathdb-data:/var/lib/data
Named volume (explicit)type:volume,source:...,target:...Long form
Bind mounthost:path:container:path./src:/app/src
Bind mount (explicit)type:bind,source:...,target:...Long form
tmpfs (shorthand)- /tmp- /tmp
tmpfs (explicit)type:tmpfs,target:...Long form
Anonymous volume- /var/lib/data- /var/lib/data

Long-form syntax example:

volumes:
  # Named volume
  - type: volume
    source: db-data
    target: /var/lib/postgresql/data

  # Bind mount with read-only
  - type: bind
    source: ./configs
    target: /etc/app/configs
    read_only: true

  # tmpfs with a size limit
  - type: tmpfs
    target: /tmp
    tmpfs:
      size: 100m

Long form is useful for options unavailable in shorthand, like bind propagation, tmpfs.size, or volume nocopy.

Network Reference Table #

TypeSyntaxExample
Default network(automatic)Created automatically
Custom networkname: ... at the top levelfrontend: ...
External networkexternal: trueexternal: true, name: my-net
Network with a driverdriver: ...driver: bridge
Network with a subnetipam: ...ipam: { config: [...] }

Networks with static IPs:

networks:
  backend:
    ipam:
      driver: default
      config:
        - subnet: 10.5.0.0/16

services:
  db:
    networks:
      backend:
        ipv4_address: 10.5.0.5

Useful for services needing fixed IPs — usually for legacy systems hardcoding IPs.

Volume Lifecycle Management #

Prune Unused Volumes #

# Remove volumes not mounted by any container
docker volume prune

# Remove all volumes not in the Compose file
docker compose down --volumes

docker compose down --volumes removes all volumes declared in the Compose file. Safer than docker volume prune --all.

Backup Strategy #

Backups should be part of the operational routine. Example Postgres script:

#!/bin/bash
BACKUP_DIR="/backups/$(date +%F)"
mkdir -p "$BACKUP_DIR"

# Back up the database with pg_dump
docker compose exec -T db pg_dump -U app myapp | gzip > "$BACKUP_DIR/db.sql.gz"

# Back up a raw volume (for file-based data, not databases)
docker run --rm \
  -v myproject_pg-data:/source:ro \
  -v "$BACKUP_DIR":/backup \
  alpine tar -czf /backup/pg-data.tar.gz -C /source .

# Upload to S3
aws s3 sync "$BACKUP_DIR" "s3://my-backups/$(date +%F)/"

Test the restore procedure periodically — a backup that’s never been restored is useless.

For databases, use pg_dump/mysqldump rather than copying volumes directly. Databases have internal state (WAL, redo logs, etc.) that must stay consistent. pg_dump guarantees a consistent snapshot. Copying raw volumes can produce corrupted backups.

Volume Migration #

To move volumes between hosts or clouds.

# Stop the service using the volume
docker compose stop db

# Export the volume to a file
docker run --rm \
  -v myproject_pg-data:/from:ro \
  -v $(pwd):/to \
  alpine tar -czf /to/pg-data.tar.gz -C /from .

# Transfer the file to the new host (scp, rsync, etc.)
rsync -avz pg-data.tar.gz user@newhost:/tmp/

# On the new host, import
docker volume create pg-data
docker run --rm \
  -v pg-data:/to \
  -v /tmp:/from:ro \
  alpine tar -xzf /from/pg-data.tar.gz -C /to

# Start the service on the new host
docker compose up -d db

Network Performance Tuning #

MTU and Drivers #

The default MTU for bridge networks is 1500. For networks with encapsulation overhead (overlay, VXLAN), you may need to lower it.

networks:
  backend:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.mtu: 1450

DNS Resolution #

Compose automatically sets up DNS for services on the network. Resolution happens via Docker’s embedded DNS server.

# Test DNS resolution from inside a container
docker compose exec api nslookup db
docker compose exec api nslookup redis

If nslookup fails, there’s usually a network setup problem or the service isn’t on the same network.

Connection Limits #

For high-traffic services, tune ulimits (file descriptors).

services:
  api:
    ulimits:
      nofile:
        soft: 65536
        hard: 65536

Or tune sysctls inside the container (if the image supports it):

services:
  api:
    sysctls:
      - net.core.somaxconn=1024
      - net.ipv4.tcp_max_syn_backlog=1024
Compose’s default network is enough for most cases. Tuning MTU, ulimits, and sysctls is optimization for high-performance workloads. For development and small production, the defaults are more than sufficient.

Recap Cheatsheet #

AspectRecommendation
Persistent dataNamed volume
Host source codeBind mount
Ephemeral cachestmpfs
Default networkAutomatic
Multi-tier isolationCustom networks per role
BackupsSeparate container + upload to S3
Pruningdocker compose down --volumes
Production driverlocal (SSD) or a managed DB

Summary #

  • Volumes are how Docker stores data outside containers. Three types: named volumes, bind mounts, tmpfs.
  • Named volumes are the safe default — portable, Docker-managed, backup-supported. Use them for persistent data (databases, uploads).
  • Bind mounts for development (source code hot reload) or configuration needing direct host access. Avoid in production because they’re not portable.
  • tmpfs for ephemeral in-memory data. Very fast but lost on restart.
  • Networks in Compose are created automatically. Services in the same file resolve each other by hostname.
  • Custom networks to separate services by role (public, internal, management). Defense in depth.
  • Network drivers: bridge for single host. overlay for multi-host (Swarm). host for performance.
  • Aliases for multiple hostnames on one network. External networks for integrating with resources outside Compose.
  • Back up volumes via a separate container. Restore also via containers, not by editing the host directly.
  • Best practices: named volumes for data, bind mounts for development code, role-separated networks, resource limits, and a backup strategy.

← Previous: Environment Variable   Next: Depends On & Startup Order →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact