The Data Persistence Problem
Containers are ephemeral. When a container is removed, everything written to its filesystem disappears — including your database data. Volumes solve this.
# Without a volume: data lost when container is removed
docker run --rm postgres:16-alpine
docker stop my-db && docker rm my-db # All data gone
# With a volume: data survives
docker run -v postgres_data:/var/lib/postgresql/data postgres:16-alpine
docker stop my-db && docker rm my-db # Data in postgres_data volume persists
docker run -v postgres_data:/var/lib/postgresql/data postgres:16-alpine # Data restored
Named Volumes vs Bind Mounts vs tmpfs
| Type | Syntax | Data location | Use case |
|---|---|---|---|
| Named volume | -v postgres_data:/data | Docker-managed (/var/lib/docker/volumes/) | Database data, persistent app state |
| Bind mount | -v $(pwd)/src:/app/src | Host filesystem | Development: live code reload |
| tmpfs | --tmpfs /app/cache | RAM only, not persisted | Temporary files, secrets in memory |
Named volumes are managed by Docker and work consistently across operating systems. Docker handles permissions and storage location.
Bind mounts expose a specific directory from your host machine inside the container. Essential for development workflows where you want code changes to be reflected immediately without rebuilding.
# Development: bind mount source for live reload
docker run -v $(pwd):/app -p 3000:3000 my-app-dev
# Production: named volume for database data
docker run -v db_data:/var/lib/postgresql/data postgres:16-alpine
Volume Commands
# Create a named volume
docker volume create my_data
# List all volumes
docker volume ls
# Inspect a volume (see location on host, labels)
docker volume inspect my_data
# Remove a volume (must not be in use)
docker volume rm my_data
# Remove all unused volumes (careful — data loss!)
docker volume prune
# Remove all unused volumes without confirmation prompt
docker volume prune -f
Docker Networks: Bridge, Host, None
Docker containers are isolated from each other by default. Networks connect them.
Bridge (Default)
Every container on the same bridge network can communicate with each other using container names as hostnames. Containers are isolated from the host network.
# Create a custom bridge network
docker network create my-app-net
# Run containers on the same network
docker run -d --name api --network my-app-net my-api
docker run -d --name db --network my-app-net postgres:16-alpine
# api can now reach db at hostname "db"
Docker Compose automatically creates a default bridge network for each project — containers in the same Compose file can reach each other by service name without any extra configuration.
Host
The container shares the host’s network stack directly. No network isolation.
docker run --network host nginx
# nginx binds to port 80 on the host directly — no -p needed
Use only for performance-critical scenarios or network debugging. Not available on Docker Desktop for Mac/Windows (Linux only).
None
The container has no network access at all. Useful for security-sensitive tasks.
docker run --network none my-secure-processor
Container DNS
When containers are on the same Docker network, Docker provides automatic DNS resolution — containers can reach each other using their container name or service name (in Compose) as the hostname.
# db container is reachable at hostname "db" from api container
docker run --name db --network my-net postgres:16-alpine
docker run --name api --network my-net \
-e DATABASE_URL=postgres://user:pass@db:5432/mydb \
my-api
This is why Compose connection strings use service names:
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
# ^^ service name = hostname
Exposing vs Publishing Ports
There’s an important distinction:
EXPOSE in Dockerfile — Documentation only. Declares which port the application listens on. Does not make the port accessible from outside the container.
-p (publish) flag — Actually maps a container port to a host port, making it accessible from outside.
# Publish port 3000 in container to port 3000 on host
docker run -p 3000:3000 my-app
# Map to a different host port
docker run -p 8080:3000 my-app # Access via localhost:8080
# Bind to a specific host interface (security: only localhost, not public)
docker run -p 127.0.0.1:3000:3000 my-app
# Publish all exposed ports to random host ports
docker run -P my-app
Rule of thumb: Only publish ports that need to be accessed from outside Docker (browser, curl, mobile app). Internal services (database, cache) should communicate via Docker networks only — never expose them to the host.
Connecting Containers in Compose
Docker Compose automatically creates one network per project and connects all services to it. No networks: section is needed for basic setups:
services:
api:
build: .
# Can reach "db" by hostname without any network config
db:
image: postgres:16-alpine
For more complex setups (isolating services, sharing a network with another Compose project):
services:
api:
build: .
networks:
- frontend
- backend
db:
image: postgres:16-alpine
networks:
- backend # Not reachable from frontend
nginx:
image: nginx
networks:
- frontend # Not reachable from backend
networks:
frontend:
backend:
Network management in Compose commands:
# List networks
docker network ls
# Inspect a Compose project's network
docker network inspect lifehack_default
# Connect a running container to a network
docker network connect my-net my-container
When To Use Named Volumes
Use named volumes when the data belongs to a service rather than to your source tree. Databases, caches, package registries, and local object stores usually fit this model. Docker manages the storage location, and your Compose file names the volume so it can be reused across container restarts.
Use bind mounts when you want a direct connection to files on your machine. Source code mounts are common during development because edits on the host appear inside the container immediately. Bind mounts are less portable because they depend on host paths.
Backup And Reset Habits
Volumes make local development convenient, but they can also hide stale state. If a database migration fails because old local data is hanging around, reset the volume intentionally instead of debugging ghosts.
Before deleting a volume, list it and confirm the project name:
docker volume ls
docker volume inspect myproject_postgres-data
For important local data, export it before removal. A database dump is safer than copying raw volume files because it captures data in the format the database understands.
Network Troubleshooting
Containers on the same Compose network can usually reach each other by service name. If your API service needs Postgres, the hostname is often db, not localhost. Inside a container, localhost points back to that same container.
If a service is reachable from another container but not from your browser, check port publishing. Internal networking and host port exposure are separate. A database can be available to the app inside Docker without being published to your laptop.
What I Would Do In Practice
I would use named volumes for stateful development services and bind mounts for source code. I would name volumes clearly, document the reset command, and avoid storing irreplaceable data only in Docker-managed local volumes.
For networking, I would keep the default Compose network unless there is a real reason to customize it. Simple service names and explicit ports are easier for teammates to understand than a hand-built network topology.
Practical Debugging Commands
When storage behaves strangely, list volumes, inspect the specific volume, and confirm which Compose project created it. Compose prefixes resource names with the project name, so two similar projects can create separate volumes that look almost identical.
When networking behaves strangely, inspect the container and check the network section. Confirm the service is attached to the expected network and that the application listens on 0.0.0.0, not only 127.0.0.1, when it needs to accept traffic from outside the container.
For local teams, document three commands: how to start the stack, how to reset state safely, and how to view logs. Those commands solve most day-to-day volume and networking confusion.