Docker Compose — Bind Mounts in Compose

2 minute read

Bind mounts map a host directory directly into a container. They’re essential for development — edit code on your machine, see changes instantly in the container.


Named Volumes vs Bind Mounts

Named Volume:                    Bind Mount:
┌─────────────┐                 ┌─────────────┐
│  Container  │                 │  Container  │
│  /data ─────┼──→ Docker Vol   │  /app ──────┼──→ Host Dir
│             │    (managed)    │             │    (your code)
└─────────────┘                 └─────────────┘

• Docker manages storage          • YOU manage the path
• Good for databases              • Good for source code
• Not visible in host filesystem  • Visible and editable
Feature Named Volume Bind Mount
Managed by Docker You
Path Docker decides You specify
Performance Native speed May have overhead (macOS/Windows)
Use case Database data Source code, config files
In top-level volumes: Yes No

Basic Bind Mount

services:
  web:
    image: nginx:alpine
    volumes:
      - ./html:/usr/share/nginx/html     # Host path : Container path

The ./html is a relative path on your host machine. It’s relative to the compose file location.


Read-Only Bind Mounts

Append :ro to prevent the container from writing to the mount:

services:
  web:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro   # Read-only
      - ./html:/usr/share/nginx/html:ro          # Read-only

Use :ro for:

  • Configuration files (the container shouldn’t modify them)
  • Source code served by web servers
  • Any mount where writes aren’t needed

Development Workflow with Bind Mounts

The classic development pattern — mount your source code into the container:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app                   # Mount entire project
      - /app/node_modules        # Exclude node_modules (anonymous volume)
    command: npm run dev

The anonymous volume trick (/app/node_modules) prevents the host’s node_modules from overriding the container’s:

Host:                    Container:
├── src/          ──→    ├── src/          (from host)
├── package.json  ──→    ├── package.json  (from host)
├── node_modules/ ✗      ├── node_modules/ (from container)
                         │   └── (installed in container)

Multiple Bind Mounts

services:
  api:
    build: .
    volumes:
      - ./src:/app/src               # Source code
      - ./config:/app/config:ro      # Config (read-only)
      - ./logs:/app/logs             # Logs (writable)

Path Gotchas

Path Type Notes
./data:/container/path Bind mount Relative to compose file
/absolute/path:/container/path Bind mount Absolute host path
~/data:/container/path Bind mount Home directory expansion
data:/container/path Named volume NOT a bind mount (no ./)

The leading ./ or / is what distinguishes a bind mount from a named volume.


Summary

Feature Detail
Syntax ./host/path:/container/path
Read-only Append :ro
Relative paths Relative to compose.yml location
Dev workflow Mount source code, exclude deps
Named volume vs bind ./path = bind, name = volume

Quick Reference

Item Detail
Bind mount ./host/path:/container/path
Read-only ./config:/etc/config:ro
Relative paths Relative to compose.yml
Exclude deps /app/node_modules (anonymous volume)
Named volume data:/path (no ./ prefix)
Dev pattern Mount source, rebuild on change