π³ Docker Mastery Series β Part 3
Learn how Docker persists data, use volumes and bind mounts effectively, understand container networking, and connect multiple containers through practical Python and Flask examples.
Docker Mastery Series
Welcome back to the Docker Mastery Series.
In Part 1, we explored the fundamentals of Docker, including images, containers, Docker architecture, and essential Docker commands.
In Part 2, we moved one step further and learned how to build our own Docker images using Dockerfiles. We explored Dockerfile instructions, image layers, build caching, optimization techniques, security practices, and production-ready Dockerfile design.
Now itβs time to address two problems that appear as soon as we start building real applications:
Where should application data be stored?
and
How do multiple containers communicate with each other?
A container is designed to be disposable. This is one of Dockerβs strengths, but it also creates an important challenge: data stored inside a container can disappear when that container is removed.
At the same time, modern applications rarely consist of a single process. A typical application might contain a web application, a database, a cache, and several backend services. These components need a reliable way to communicate.
Docker provides solutions for both problems through Volumes and Networking.
In this article, weβll start with the basics and gradually build toward a multi-container application.
Docker Mastery Series
β Part 1 β Introduction to Docker
β Part 2 β Mastering Dockerfiles
β Part 3 β Mastering Docker Volumes & Networking (Current)
β‘οΈ Part 4 β Docker Compose
β‘οΈ Part 5 β Docker for Machine Learning
β‘οΈ Part 6 β Advanced Docker
What You Will Learn
By the end of this article, you will understand:
- Why container data is ephemeral(short-lived).
- How Dockerβs writable container layer works.
- What Docker volumes are.
- How named volumes work.
- How bind mounts work.
- The difference between volumes and bind mounts.
- How to create, inspect, use, and remove volumes.
- How containers communicate with each other.
- How Docker networks work.
- The difference between
EXPOSEand port publishing. - Dockerβs main network drivers.
- How to create custom Docker networks.
- How containers discover each other by name.
- Why
localhostbehaves differently inside containers. - How to connect a Flask application to PostgreSQL.
- How storage and networking work together in a realistic application.
- Common mistakes and best practices.
1. The Problem With Container Storage
Letβs start with a simple Python application.
Suppose our application writes a file:
with open("/app/data.txt", "w") as file:
file.write("Important application data")Inside the container, the file exists:
Container
/app
βββ app.py
βββ data.txt
#/app β directory/path inside the container
# app.py β application file
# data.txt β data fileNow imagine the container is removed:
docker rm my-containerThe container disappears.
The file disappears with it.
This can be surprising when youβre first learning Docker.
Containers Are Designed to Be Disposable
A useful way to think about containers is:
A container contains the running environment of an application, not necessarily its permanent data.
For example:
Docker Container
βββββββββββββββββββββββ
β β
β Application β
β β
β Python β
β Libraries β
β Temporary files β
β β
βββββββββββββββββββββββ
β
β delete
βΌ
GoneIf important data is stored only inside the container, deleting the container can delete that data.
This is perfectly acceptable for temporary files.
It is obviously not acceptable for something like a database.
Imagine storing a production PostgreSQL database entirely inside a disposable container without persistent storage.
If the container is deleted, the database files could disappear along with it.
Docker therefore provides several storage mechanisms that allow data to live independently from the container.
2. Understanding the Container Writable Layer
When Docker creates a container from an image, the image itself remains read-only.
Docker adds a thin writable layer on top.
Conceptually:
Container
βββββββββββββββββββββ
β Writable Layer β
β β
β Application Data β
βββββββββββββββββββββ
β²
βββββββββββββββββββββ
β Image Layer β
β β
β Application β
β Python β
β Libraries β
βββββββββββββββββββββThe underlying image layers are immutable.
Changes made by the running container are stored in the containerβs writable layer.
When the container is removed, that writable layer is removed as well.
Therefore:
Container created
β
Writable layer created
β
Application writes data
β
Container removed
β
Writable layer removed
β
Data disappearsThis is why persistent application data should normally be stored outside the containerβs writable layer.
3. Docker Storage Options
Docker provides several ways to handle data.
The most important ones are:
- Named volumes
- Bind mounts
- tmpfs mounts
There is also the containerβs normal writable layer, which is useful for temporary data.
A simplified comparison looks like this:
Letβs examine them individually.
4. Docker Volumes
A Docker volume is a storage location managed by Docker that exists independently of the container using it.
Instead of storing data inside:
Container
β
Writable layerwe can store it in:
Container
β
Docker Volume
β
Persistent StorageThe container can be deleted while the volume remains.
5. Named Volumes
A named volume has an explicit name.
For example:
docker volume create mydataDocker creates a volume called:
mydataYou can verify it:
docker volume lsYou might see:
DRIVER VOLUME NAME
local mydataThe volume now exists independently of any particular container.
Using a Named Volume
Suppose a container expects its persistent data at:
/app/dataWe can mount our volume there:
docker run \
-v mydata:/app/data \
myappThe syntax is:
-v <volume-name>:<container-path>So:
mydata:/app/datameans:
Docker Volume
β
β mounted at
βΌ
/app/data
β
βΌ
Container6. A Practical Volume Example
Letβs create a simple Python application.
import os
# Define the path where the data file will be stored inside the container
DATA_FILE = "/app/data/message.txt"
# Create the /app/data directory if it does not already exist
# exist_ok=True prevents an error if the directory already exists
os.makedirs("/app/data", exist_ok=True)
# Open the file in write mode ("w")
# If the file does not exist, it will be created
with open(DATA_FILE, "w") as file:
# Write the message to the file
file.write("This data should survive container deletion.")
# Display a confirmation message
print("Data written successfully.")Suppose this application is packaged into an image called:
volume-demoFirst create a volume:
# Create a named Docker volume called "app-data"
# The volume is managed by Docker and can persist data
# independently of the container
docker volume create app-dataThen run the container:
# Run a container and mount the "app-data" volume
docker run \
--name volume-container \
-v app-data:/app/data \
volume-demoThe application writes:
/app/data/message.txtBut that directory is backed by the Docker volume.
Now remove the container:
docker rm volume-containerThe container is gone.
But the volume remains.
Check:
docker volume lsYou should still see:
app-dataCreate another container using the same volume:
docker run \
--rm \
-v app-data:/app/data \
volume-demoThe volume can be reused independently of the previous container.
7. Managing Docker Volumes
Docker provides several commands for managing volumes.
List volumes
docker volume lsCreate a volume
docker volume create app-dataInspect a volume
docker volume inspect app-dataRemove a volume
docker volume rm app-dataRemove unused volumes
docker volume pruneBe careful with:
docker volume pruneIt removes unused local volumes.
If a volume contains important data, make sure it is no longer needed before deleting it.
8. Inspecting a Volume
Run:
docker volume inspect app-dataDocker returns metadata similar to:
[
{
"Name": "app-data",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/app-data/_data"
}
]The exact location can vary depending on the operating system and Docker configuration.
The important point is that Docker manages this storage location for you.
You normally donβt need to manipulate the underlying files directly.
9. Volumes in Dockerfile
In Part 2, we introduced the VOLUME Dockerfile instruction.
For example:
VOLUME ["/app/data"]This indicates that /app/data is intended to be used as a mount point for persistent data.
However, it is important to understand the distinction between declaring a volume mount point in an image and choosing a specific volume when running a container.
For example:
VOLUME ["/app/data"]declares the location.
At runtime, you can explicitly mount a named volume:
docker run \
-v app-data:/app/data \
myappFor modern applications, explicitly managing volumes at deployment time often provides clearer control over storage.
10. Bind Mounts
Named volumes are managed by Docker.
A bind mount, on the other hand, maps a specific directory or file from the host machine into the container.
For example:
docker run \
-v $(pwd):/app \
python-appThis maps the current directory on your computer to /app inside the container.
Conceptually:
Your computer
project/
βββ app.py
βββ requirements.txt
βββ Dockerfile
β
β bind mount
βΌ
Container/app
βββ app.py
βββ requirements.txt
βββ Dockerfile
The files are not copied into the image.
The container accesses the files directly through the mount.
11. Why Bind Mounts Are Useful
Bind mounts are particularly useful during development.
Suppose your Flask application contains:
print("Hello Docker")You start the container with your source directory mounted:
docker run \
-v $(pwd):/app \
flask-appNow edit the file on your host:
print("Hello Docker Volumes")The container sees the changed file because both the host and container are accessing the same mounted directory.
Get Sabitha Manojβs stories inΒ yourΒ inbox
Join Medium for free to get updates fromΒ thisΒ writer.
This is extremely useful for development workflows.
12. Named Volumes vs Bind Mounts
This distinction is important.
A useful rule of thumb is:
Use named volumes for application data that Docker should manage. Use bind mounts when you intentionally need to share specific host files or directories with a container.
13. Bind Mount Syntax
The traditional -v syntax is:
docker run \
-v /host/path:/container/path \
image-nameDocker also supports the more explicit --mount syntax:
# Run a container and bind-mount the current host directory to /app
docker run \
--mount type=bind,source=$(pwd),target=/app \
image-nameHere:
type=bindβ creates a bind mountsource=$(pwd)β uses the current directory on the hosttarget=/appβ mounts that directory at/appinside the containerimage-nameβ the Docker image used to create the container
14. Read-Only Mounts
Sometimes a container should be able to read files but should not be allowed to modify them.
Docker supports read-only mounts.
For example:
docker run \
-v $(pwd):/app:ro \
myappThe :ro means:
read-onlyThe container can read the files but cannot modify them through that mount.
With the --mount syntax:
docker run \
--mount type=bind,source=$(pwd),target=/app,readonly \
myappRead-only mounts can be useful when containers need access to configuration or reference data without requiring write permissions.
15. tmpfs Mounts
A tmpfs mount stores data in the host's memory rather than persistent disk storage.
For example:
docker run \
--tmpfs /app/temp \
myappData written to:
/app/tempexists only while the container is running.
When the container stops, the data is lost.
This can be useful for temporary data that does not need to persist.
For example:
Temporary processing
Temporary cache
Short-lived filesBecause the data is held in memory, available memory limits must also be considered.
16. Choosing the Right Storage Type
A simple decision process can help.
Need persistent application data?
Use a named volume.
Database
Uploaded files
Persistent application dataNeed your local source code inside the container?
Use a bind mount.
Local development
Live code editing
Configuration filesNeed temporary in-memory data?
Consider tmpfs.
Temporary files
Short-lived sensitive data
Memory-backed scratch spaceNeed temporary container-only data?
The normal writable layer may be sufficient.
17. Why Networking Matters
Weβve solved one major problem:
How do containers keep data after they are removed?
Now we need to solve another:
How do containers communicate with each other?
Modern applications are rarely single-container applications.
Consider a typical web application:
ββββββββββββββββ
β Browser β
ββββββββ¬ββββββββ
β
βΌ
ββββββββββββββββ
β Flask API β
ββββββββ¬ββββββββ
β
βββββββββββ΄ββββββββββ
βΌ βΌ
βββββββββββββββ βββββββββββββββ
β PostgreSQL β β Redis β
βββββββββββββββ βββββββββββββββEach component could run in its own container.
Those containers need to communicate reliably.
Docker Networking provides the infrastructure for this communication.
18. Docker Networking Basics
Docker creates virtual networks that allow containers to communicate.
List the available networks:
docker network lsA typical installation might show:
NETWORK ID NAME DRIVER
abc123 bridge bridge
def456 host host
ghi789 none nullDocker provides several network drivers.
The most important ones for beginners are:
bridgehostnone
Other drivers, such as overlay and macvlan, are useful in more specialized environments
19. The Default Bridge Network
When Docker is installed, it creates a default network called:
bridgeIf you run a container without specifying a network:
docker run nginxDocker normally connects it to the default bridge network.
You can inspect it:
docker network inspect bridgeThis displays information about the network and the containers connected to it.
20. Why Create a Custom Network?
Dockerβs default bridge network works for simple scenarios, but user-defined bridge networks provide better container-to-container communication.
Create a custom network:
docker network create app-networkVerify it:
docker network lsYou should now see:
app-network21. Connecting Containers to a Custom Network
Suppose we have two containers:
flask-api
postgres-dbStart PostgreSQL on the custom network:
docker run -d \
--name postgres-db \
--network app-network \
postgresThen start the Flask application on the same network:
docker run -d \
--name flask-api \
--network app-network \
flask-appNow both containers belong to:
app-networkThey can communicate over that Docker network.
Docker
β
βββ Container: flask-api
β βββ Flask application
β
βββ Container: postgres-db
βββ PostgreSQL databaseβββββββββββββββββββ app-network βββββββββββββββββββ
β flask-api β ββββββββββββββββββββββ> β postgres-db β
β β β β
β Flask API β β PostgreSQL β
βββββββββββββββββββ βββββββββββββββββββ22. Container-to-Container Communication
Hereβs an important Docker networking concept.
Suppose the PostgreSQL container is named:
postgres-dbFrom the Flask container, you generally do not connect to:
localhostInstead, you use the PostgreSQL containerβs network name:
postgres-dbFor example:
DB_HOST = "postgres-db"This is because:
localhostinside the Flask container refers to the Flask container itself.
It does not refer to the PostgreSQL container.
23. Understanding localhost in Docker
This is one of the most common Docker networking mistakes.
Imagine:
Host Computer
β
ββββββββββββββββ
βΌ βΌ
Flask Container PostgreSQL ContainerInside the Flask container:
localhostmeans:
Flask ContainerInside the PostgreSQL container:
localhostmeans:
PostgreSQL ContainerEach container has its own network namespace.
Therefore, if Flask needs to communicate with PostgreSQL, using:
DB_HOST = "localhost"is usually incorrect.
Instead:
DB_HOST = "postgres-db"allows Dockerβs user-defined network to route the connection to the PostgreSQL container.
24. Dockerβs Built-In Name Resolution
On a user-defined Docker network, Docker provides internal DNS-based service discovery.
For example:
app-networkβββββββββββββββββββββββ
β β
β flask-api β
β β
ββββββββββββ¬βββββββββββ
β
β postgres-db
βΌ
βββββββββββββββββββββββ
β β
β postgres-db β
β β
βββββββββββββββββββββββThe Flask application can connect using:
postgres-dbrather than needing to know the PostgreSQL containerβs IP address.
This is extremely useful because container IP addresses can change when containers are recreated.
Names provide a stable way for services to find each other.
25. Port Mapping vs Container Networking
Another common source of confusion is the difference between:
-p 5000:5000and:
--network app-networkThey solve different problems.
Port publishing
-p 5000:5000makes a container port accessible from the host.
Host
Port 5000
β
βΌ
Container
Port 500026. EXPOSE vs Port Publishing
Remember the EXPOSE instruction from Part 2?
For example:
EXPOSE 5000This documents that the application listens on port 5000.
It does not publish the port.
To make it accessible from the host, you still need:
docker run -p 5000:5000 myappTherefore:
EXPOSE
β
Documentation / image metadata-p
β
Actual host-to-container port publishingThis distinction is important.
27. A Practical Flask + PostgreSQL Architecture
Letβs combine everything weβve learned so far.
Our application will contain two containers:
Host Machine
β
β Port 5000
βΌ
βββββββββββββββββ
β Flask API β
β Container β
βββββββββ¬ββββββββ
β
app-network
β
βΌ
βββββββββββββββββ
β PostgreSQL β
β Container β
βββββββββ¬ββββββββ
β
βΌ
Docker VolumeThe responsibilities are separated:
Flask container
- Runs the application.
- Handles HTTP requests.
- Connects to PostgreSQL.
PostgreSQL container
- Runs the database.
- Stores persistent data in a volume.
Docker network
- Allows Flask and PostgreSQL to communicate.
Docker volume
- Keeps PostgreSQL data independent of the database container lifecycle.
This architecture is a major step toward real-world containerized applications.
28. Why Separate the Database Into Its Own Container?
A common beginner approach is to install everything into one container:
One Containerβββ Flask
βββ Python
βββ PostgreSQL
βββ Redis
βββ Other ServicesThis creates a tightly coupled environment.
A more container-friendly approach is:
Flask Container
β
β network
βΌ
PostgreSQL ContainerEach container has a focused responsibility.
This follows an important containerization principle:
One container should generally have one primary responsibility.
This doesnβt mean a container can never run multiple processes, but separating independently managed services usually makes systems easier to operate, scale, and maintain.
29. Conclusion
Docker containers are designed to be temporary and replaceable, but application data often needs to survive beyond the lifecycle of an individual container. Docker volumes provide a reliable way to persist data independently of containers, while bind mounts allow applications to work directly with files and directories on the host system.
Docker networking solves another important problem: allowing containers to communicate with each other reliably. With user-defined networks and Dockerβs built-in DNS-based service discovery, containers can communicate using container names instead of relying on hard-coded IP addresses.
Together, volumes and networks form two essential building blocks of multi-container applications:
Volumes provide persistent storage.
Networks provide communication between containers.
In this part, we explored these concepts individually and saw how they fit into a typical application architecture such as:
Host Machine
β
β HTTP
βΌ
βββββββββββββββ
β Flask API β
β Container β
ββββββββ¬βββββββ
β
app-network
β
βΌ
βββββββββββββββ
β PostgreSQL β
β Container β
ββββββββ¬βββββββ
β
app-data
β
βΌ
βββββββββββββββ
β Volume β
βββββββββββββββUnderstanding these building blocks makes it much easier to work with real-world containerized applications.
However, managing multiple containers manually can quickly become repetitive. Creating networks, starting containers, configuring environment variables, mapping ports, and attaching volumes requires multiple commands.
This is where Docker Compose becomes useful.
In Part 4 β Docker Compose, we will bring these concepts together and define a complete multi-container application using a single compose.yaml file. We will build a Flask + PostgreSQL application, configure its services, networks, volumes, environment variables, and dependencies, and then manage the entire application with simple Docker Compose commands.
Next: Part 4 β Docker Compose: Build and Manage Multi-Container Applications










