Skip to content
DevOps devops containers 5 min read

Docker Volumes & Networks

By default, anything a container writes to its own filesystem disappears the moment you delete that container. That is fine for a stateless web app, but it is a disaster for a database — your data would vanish on every restart. Volumes solve this by storing data outside the container so it survives. Networks solve a different problem: letting one container (say, an app) talk to another (say, a database) by name. This page covers both, with real commands you can run on Ubuntu 22.04 or 24.04 LTS.

Why container storage is temporary

A container is a running instance of an image. Its filesystem is a thin, writable layer on top of the read-only image. When the container is removed (docker rm), that writable layer is deleted with it. There is no warning and no recovery.

Gotcha: Run a PostgreSQL container without a volume, do docker rm (or docker compose down), and every table, row, and user you created is gone forever. This is the single most common beginner mistake. Always attach a volume to any container that stores data.

Volumes vs bind mounts

Docker gives you two main ways to keep data outside the container. They look similar but are used for different jobs.

A named volume is storage that Docker creates and manages for you in its own directory on the host (the host is the Ubuntu machine running Docker). You refer to it by a name like pgdata, and Docker handles where the files actually live.

A bind mount maps a specific folder on your host directly into the container. You choose the exact host path, like /home/ubuntu/myapp/config.

FeatureNamed volumeBind mount
You pick the host path?No — Docker manages itYes — you give an exact path
Where data lives/var/lib/docker/volumes/Any folder you choose
Best forDatabase data, app stateSource code in development, config files
Portable across machinesYes (easy to back up)No (depends on host layout)
Managed by docker volume commandsYesNo

When to use a named volume: for anything the app produces and must keep — database files, uploaded images, logs you want to retain. This is the default choice for production data.

When to use a bind mount: for files you control on the host and want the container to read or edit live. The classic case is development, where you mount your source code so changes appear instantly without rebuilding the image. Avoid bind mounts for database data — host filesystem permissions often cause subtle corruption or “permission denied” errors.

Working with named volumes

Create a named volume and inspect it:

docker volume create pgdata
docker volume ls

Output:

DRIVER    VOLUME NAME
local     pgdata

Now run a PostgreSQL container that stores its data in that volume. The -v flag maps the volume name to the path inside the container where Postgres keeps its files:

docker run -d \
  --name db \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

To prove it works, stop and remove the container, then start a new one using the same volume. Your data is still there:

docker rm -f db
docker run -d --name db -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data postgres:16

Inspect where Docker actually stores it on the host:

docker volume inspect pgdata

Output:

[
    {
        "Name": "pgdata",
        "Driver": "local",
        "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
        "Scope": "local"
    }
]

Using a bind mount instead

For local development, mount your project folder into the container so edits on the host are seen instantly:

docker run -d --name web \
  -v /home/ubuntu/myapp:/usr/src/app \
  -p 3000:3000 \
  node:20 npm start

Here /home/ubuntu/myapp (host) appears as /usr/src/app inside the container.

Docker networks: containers talking to each other

When two containers need to communicate — for example, your app needs to reach your database — they must be on the same network (a virtual switch that connects containers). Docker has a default bridge network, but on it containers can only reach each other by IP address, which changes constantly.

The fix is a user-defined bridge network. On these, Docker provides built-in DNS (a name lookup service), so a container can reach another simply by its container name. This is the modern, recommended approach.

When to use this: any time you have more than one container that must talk — app plus database, app plus cache, and so on. Create one user-defined network per project.

Create the network, then attach both containers to it:

docker network create appnet
docker network ls

Output:

NETWORK ID     NAME      DRIVER    SCOPE
a1b2c3d4e5f6   appnet    bridge    local
b2c3d4e5f6a7   bridge    bridge    local
c3d4e5f6a7b8   host      host      local

Run the database and the app on appnet. Notice the app connects to the host named db — the database container’s name — with no IP address needed:

docker run -d --name db --network appnet \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data postgres:16

docker run -d --name web --network appnet \
  -e DATABASE_URL=postgres://postgres:secret@db:5432/postgres \
  -p 3000:3000 myapp:latest

Confirm the app can resolve the database by name:

docker exec web getent hosts db

Output:

172.18.0.2      db

Tip: Only publish ports you truly need. The -p 5432:5432 flag would expose Postgres to the whole internet (subject to your ufw firewall). Containers on the same network already reach db:5432 internally, so leave the database unpublished and only publish the web port.

Best Practices

  • Always attach a named volume to any container that stores data — databases, uploads, or persistent caches.
  • Use bind mounts for development source code and config files, not for database storage.
  • Create one user-defined bridge network per project so containers reach each other by name via Docker’s DNS.
  • Never publish a database port to the host unless an external tool genuinely needs it; keep it internal to the network.
  • Back up named volumes regularly — for Postgres, prefer pg_dump over copying the raw volume directory.
  • Name volumes and networks clearly (projectname_pgdata, projectname_net) so you know what each one belongs to.
  • Run docker volume prune carefully — it permanently deletes any volume not attached to a container.
Last updated June 15, 2026
Was this helpful?