Sitemap

🐳 Docker Mastery Series β€” Part 3

13 min readSep 23, 2026

--

Press enter or click to view image in full size
Image created using Copilot

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 EXPOSE and port publishing.
  • Docker’s main network drivers.
  • How to create custom Docker networks.
  • How containers discover each other by name.
  • Why localhost behaves 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 file

Now imagine the container is removed:

docker rm my-container

The 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
β–Ό
Gone

If 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 disappears

This 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:

  1. Named volumes
  2. Bind mounts
  3. tmpfs mounts

There is also the container’s normal writable layer, which is useful for temporary data.

A simplified comparison looks like this:

Press enter or click to view image in full size

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 layer

we can store it in:

Container
↓
Docker Volume
↓
Persistent Storage

The container can be deleted while the volume remains.

5. Named Volumes

A named volume has an explicit name.

For example:

docker volume create mydata

Docker creates a volume called:

mydata

You can verify it:

docker volume ls

You might see:

DRIVER    VOLUME NAME

local mydata

The volume now exists independently of any particular container.

Using a Named Volume

Suppose a container expects its persistent data at:

/app/data

We can mount our volume there:

docker run \
-v mydata:/app/data \
myapp

The syntax is:

-v <volume-name>:<container-path>

So:

mydata:/app/data

means:

Docker Volume
β”‚
β”‚ mounted at
β–Ό
/app/data
β”‚
β–Ό
Container

6. 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-demo

First 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-data

Then run the container:

# Run a container and mount the "app-data" volume
docker run \
--name volume-container \
-v app-data:/app/data \
volume-demo

The application writes:

/app/data/message.txt

But that directory is backed by the Docker volume.

Now remove the container:

docker rm volume-container

The container is gone.

But the volume remains.

Check:

docker volume ls

You should still see:

app-data

Create another container using the same volume:

docker run \
--rm \
-v app-data:/app/data \
volume-demo

The volume can be reused independently of the previous container.

7. Managing Docker Volumes

Docker provides several commands for managing volumes.

List volumes

docker volume ls

Create a volume

docker volume create app-data

Inspect a volume

docker volume inspect app-data

Remove a volume

docker volume rm app-data

Remove unused volumes

docker volume prune

Be careful with:

docker volume prune

It 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-data

Docker 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 \
myapp

For 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-app

This 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-app

Now 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.

Press enter or click to view image in full size

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-name

Docker 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-name

Here:

  • type=bind β†’ creates a bind mount
  • source=$(pwd) β†’ uses the current directory on the host
  • target=/app β†’ mounts that directory at /app inside the container
  • image-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 \
myapp

The :ro means:

read-only

The 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 \
myapp

Read-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 \
myapp

Data written to:

/app/temp

exists 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 files

Because 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 data

Need your local source code inside the container?

Use a bind mount.

Local development
Live code editing
Configuration files

Need temporary in-memory data?

Consider tmpfs.

Temporary files
Short-lived sensitive data
Memory-backed scratch space

Need 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 ls

A typical installation might show:

NETWORK ID     NAME      DRIVER

abc123 bridge bridge
def456 host host
ghi789 none null

Docker provides several network drivers.

The most important ones for beginners are:

  • bridge
  • host
  • none

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:

bridge

If you run a container without specifying a network:

docker run nginx

Docker normally connects it to the default bridge network.

You can inspect it:

docker network inspect bridge

This 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-network

Verify it:

docker network ls

You should now see:

app-network

21. Connecting Containers to a Custom Network

Suppose we have two containers:

flask-api
postgres-db

Start PostgreSQL on the custom network:

docker run -d \
--name postgres-db \
--network app-network \
postgres

Then start the Flask application on the same network:

docker run -d \
--name flask-api \
--network app-network \
flask-app

Now both containers belong to:

app-network

They 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-db

From the Flask container, you generally do not connect to:

localhost

Instead, you use the PostgreSQL container’s network name:

postgres-db

For example:

DB_HOST = "postgres-db"

This is because:

localhost

inside 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 Container

Inside the Flask container:

localhost

means:

Flask Container

Inside the PostgreSQL container:

localhost

means:

PostgreSQL Container

Each 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-db

rather 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:5000

and:

--network app-network

They solve different problems.

Port publishing

-p 5000:5000

makes a container port accessible from the host.

Host
Port 5000
β”‚
β–Ό
Container
Port 5000

26. EXPOSE vs Port Publishing

Remember the EXPOSE instruction from Part 2?

For example:

EXPOSE 5000

This 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 myapp

Therefore:

EXPOSE
↓
Documentation / image metadata
-p
↓
Actual host-to-container port publishing

This 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 Volume

The 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 Services

This creates a tightly coupled environment.

A more container-friendly approach is:

Flask Container
β”‚
β”‚ network
β–Ό
PostgreSQL Container

Each 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

--

--

Written by Sabitha Manoj

AI researcher in medical imaging & radiomics at University Hospital Ulm. I build ML and deep learning models for clinical decision support.