Docker Volumes, Networks and Environment Config for Containers
DevOpsLesson 3 of 45 min read
What you'll learn
- Choose between named volumes, bind mounts and tmpfs mounts
- Connect containers over a user-defined network using container names as hostnames
- Publish ports deliberately and explain what localhost means inside a container
- Pass configuration with environment variables and env files without baking secrets into images
On this page
A clinic's appointment queue runs as one app container and one Postgres container. On Monday the receptionist books forty visits. On Tuesday an engineer recreates the database container to bump a minor version, and the queue is empty. Nothing crashed. Docker did exactly what it was told: a container's filesystem belongs to that container. This lesson fixes that story and the two related questions it raises: how does the app find the database, and where do the passwords go?
Three ways to put storage in a container
| Kind | Lives where | Use it for | Watch out for |
|---|---|---|---|
| Named volume | Managed by Docker on the host | Database files, uploads, anything that must survive container replacement | Deleted by docker volume rm or docker compose down -v |
| Bind mount | A host directory you choose | Live-editing source code in development, mounting a config file read-only | Host paths and permissions differ per machine; the container can modify host files unless readonly |
| tmpfs mount | Host memory (Linux) | Scratch data that should never touch disk | Gone when the container stops; counts against memory |
Named volumes are the default answer for persistent data because Docker handles location and initial population. Bind mounts are for sharing with the host. The following session proves persistence end to end using the official Postgres image. The password lives in a local env file for this throwaway experiment.
# db.env (local experiment only; do not commit real credentials)
# POSTGRES_USER=clinic
# POSTGRES_PASSWORD=change-me-local-only
# POSTGRES_DB=queue
docker network create clinic-net
docker volume create clinic-pgdata
docker run -d --name clinic-db --network clinic-net \
--env-file db.env \
-v clinic-pgdata:/var/lib/postgresql/data \
postgres:16-alpine
# Wait a few seconds for startup, then create and fill a table
docker exec clinic-db psql -U clinic -d queue -c "create table visits(id serial primary key, patient text)"
docker exec clinic-db psql -U clinic -d queue -c "insert into visits(patient) values ('A. Rao'), ('M. Chen')"
# Destroy the container completely, then start a new one on the same volume
docker rm -f clinic-db
docker run -d --name clinic-db --network clinic-net \
--env-file db.env \
-v clinic-pgdata:/var/lib/postgresql/data \
postgres:16-alpine
docker exec clinic-db psql -U clinic -d queue -c "select count(*) from visits"Output
count
-------
2
(1 row)The rows survived because the data directory was a volume, not part of the container's writable layer. The data directory path is specific to the Postgres image and its major version, so check the image's documentation when you change versions. Whitespace in the table that psql prints may differ slightly from the block above.
Networks: how containers find each other
Containers on the default bridge network can reach each other only by IP address. Containers on a user-defined bridge network (like clinic-net above) get automatic DNS: any container can reach another by its name. So the app does not need to know an IP. It connects to the host clinic-db on port 5432, and Docker resolves that name.
# Start the app on the same network; it will connect to host "clinic-db"
docker run -d --name clinic-app --network clinic-net \
-e DATABASE_URL=postgres://clinic:change-me-local-only@clinic-db:5432/queue \
-p 127.0.0.1:8080:3000 \
clinic-queue:1
# Confirm the name resolves from inside the app container
docker exec clinic-app getent hosts clinic-dbOutput
172.18.0.2 clinic-db
The address printed is only an example; Docker assigns addresses from its own range. The -p 127.0.0.1:8080:3000 flag is port publishing: it maps host port 8080 to container port 3000, and the leading 127.0.0.1 restricts it to your own machine. Without that prefix Docker binds on all interfaces. Notice that the database has no -p at all. Other containers on clinic-net can reach it, and nothing outside can. That is a security default worth keeping.
Configuration: environment variables and what not to bake in
The twelve-factor habit is to build one image and change its behaviour through configuration at run time. Docker gives you three layers of it. ENV in a Dockerfile sets a default that lives in the image. -e KEY=value or --env-file at docker run overrides it for one container. ARG exists only at build time and is for things like a version number to compile in.
- Anything set with
ENVorARGcan be read back later withdocker inspectordocker history. Never put a real password or API key there. - An env file keeps values out of shell history and out of your command line. Add it to
.gitignoreand to.dockerignore. - For genuinely sensitive values in production, prefer your platform's secret mechanism (Docker secrets, Kubernetes Secrets, a cloud secrets manager) and read the value from a mounted file or injected variable at start-up.
- Mount configuration files as bind mounts with
:ro(read-only), for example-v "$(pwd)/app.toml:/etc/clinic/app.toml:ro".
Common mistakes and the symptoms they cause
- Anonymous volumes everywhere. Symptom:
docker volume lsshows dozens of hash-named volumes and you cannot tell which holds your data. Fix: always give volumes names. - Bind-mounting over files baked into the image. Symptom: the app crashes because a directory it expected (such as
node_modules) is suddenly empty. A bind mount hides whatever the image had at that path. - Publishing the database port to the world. Symptom: scanners hit your Postgres from the internet. Fix: do not publish it, or bind to
127.0.0.1. - Using `localhost` as a hostname between containers. Symptom:
connection refusedwhile the other container is clearly healthy. - Permission errors on mounts. Symptom:
permission deniedwriting to a bind mount because the container's user ID does not own the host directory. Fix: align the UID or use a named volume.
How this shows up in real projects and interviews
"Where does the data go when the container is deleted?" is a standard screening question, and the best answers name the writable layer and the volume types. In day-to-day work you will constantly debug connectivity: can the app container resolve the database name, is the port published, is the right env variable set? docker exec into a container and test the name with getent hosts or a quick connection, then inspect with docker inspect and docker network inspect. The next lesson turns all of these flags into a single declarative file.
Try it yourself
Reproduce the clinic experiment with a different stateful image of your choice (for example Redis with persistence enabled, or MySQL). Create a named volume, write some data, remove the container, start a new one on the same volume and confirm the data is still there. Then run a second container on the same user-defined network and reach the first one by name.
Show a hint
Read the image's documentation for the data directory path. Use docker exec <name> getent hosts <other-name> or install a client in a throwaway container with docker run --rm --network <net>.
Key takeaways
- Use named volumes for persistent data, bind mounts for host sharing, and tmpfs for scratch memory.
- User-defined networks give containers DNS by name; the default bridge does not.
- Publish only the ports clients need, and bind to 127.0.0.1 when only local access is intended.
- Inside a container, localhost is the container itself.
- Configure at run time with env vars or files, and never bake secrets into ENV, ARG or image layers.
Test yourself
A trick from this lesson's ride: The container's own layer is a sticky note on the fridge. Valuables go in a named volume. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is the difference between a Docker volume and a bind mount?
A named volume is created and managed by Docker in its own storage area, and is the recommended choice for persistent data. A bind mount maps a specific host path into the container, which is handy for development but depends on the host's layout and permissions.
How do two Docker containers talk to each other?
Put them on the same user-defined network and use the other container's name (or Compose service name) as the hostname. Docker's built-in DNS resolves it. You only need to publish a port with -p when something outside Docker must reach the container.
Is it safe to put passwords in Docker environment variables?
It is acceptable for local development, but environment variables can be read with docker inspect and often leak into logs. For production, use a secrets mechanism such as Docker secrets, Kubernetes Secrets or a cloud secrets manager, and never bake values into the image.