Data and networking
Volumes, bind mounts and container networks.
Part 1: The problem
To test the order service properly, Priya ran MySQL in a container on her laptop. She spent an afternoon filling it with realistic test orders. That evening, tidying up, she removed every container she had.
The next morning, the database was empty. The MySQL image was still there; the orders were not.
At the same time, Leo was trying to connect the order service to that database. The app was set to connect to localhost:3306, which worked when both ran directly on his laptop. As soon as the app ran in a container, it failed with connection refused, even though MySQL was running and healthy.
Both problems come from the same idea: a container is its own small, separate machine. It has its own disk, which is thrown away with it, and its own network, with its own localhost. This lesson is about reaching outside both.
Part 2: The idea
Where a container’s files go
An image is read-only. When a container starts, Docker gives it a thin writable layer on top of the image. Everything the container writes lands there: logs, uploads, a database’s files.
That layer belongs to that one container. Stopping the container keeps it. Removing the container deletes it. A new container from the same image starts with an empty layer again.
Most of the time that’s what you want: a container you can throw away and replace. But some data must outlive the container. Docker has two ways to keep it:
| Kind | What it is | Survives docker rm? | Good for |
|---|---|---|---|
| Writable layer | The container’s own scratch space | No | Temporary files |
| Named volume | Storage Docker creates and manages, with a name you give it | Yes | A database’s files, anything that must survive |
| Bind mount | A folder on your computer, shown inside the container | Yes; it’s your folder | Code you’re editing, config files, local development |
A volume or bind mount is attached at a folder inside the container, called its mount point. MySQL always writes to /var/lib/mysql. Mount a volume there, and MySQL’s files go into the volume without MySQL knowing anything changed.
Two ways to write it
docker run accepts volumes and bind mounts in two styles. They do the same thing:
| You write | Means |
|---|---|
-v db-data:/var/lib/mysql | Mount the named volume db-data at /var/lib/mysql. Docker creates it if it doesn’t exist |
--mount type=volume,source=db-data,target=/var/lib/mysql | The same, spelled out |
--mount type=bind,source=/home/maya/order-service,target=/code | Show that folder on your computer inside the container, at /code |
--mount is longer but harder to get wrong. With -v, if the left side doesn’t look like a path, Docker quietly treats it as a volume name. With --mount type=bind, a path that doesn’t exist is an error, which is what you want.
Every container has its own network
Each container gets its own network card, its own IP address, and its own localhost. Inside a container, localhost means that container, and nothing else.
| From | To | How |
|---|---|---|
| Your computer | A container | Publish a port: -p 8080:8080 connects port 8080 on your computer to port 8080 in the container |
| A container | Another container | Put both on the same network you created, and use the other’s name |
| A container | The internet | Works by default. Docker shares your computer’s connection |
Publishing ports
-p takes the host’s port first, then the container’s:
| You write | Means |
|---|---|
-p 8080:8080 | Port 8080 on your computer, on every network it’s connected to, leads to port 8080 in the container |
-p 8081:8080 | Port 8081 on your computer leads to port 8080 in the container |
-p 127.0.0.1:8080:8080 | Only programs on your own computer can use it, not other devices on your Wi-Fi |
Containers talking to each other on the same network don’t need -p at all. Publishing is only for reaching a container from outside Docker. That’s why QuickCart’s database will have no -p: only the app needs to reach it.
Networks and names
Docker starts every container on a network called bridge unless you say otherwise. Containers on it can reach each other only by IP address, and addresses change every time a container is recreated.
A network you create is also a bridge network, with one important extra: built-in DNS. Every container on it can reach the others by name.
| Network | Reach others by name? | Use it for |
|---|---|---|
The default bridge | No, IP only | Quick one-off containers |
A network you create, like quickcart | Yes | Anything with more than one container |
| Separate networks | No: they can’t reach each other at all | Keeping groups of containers apart |
This is the same idea as security groups: who can reach whom is decided by the network around the app, not by the app.
Docker Compose: all of it in one file
Starting a database and an app by hand takes a network, a volume, and two long docker run commands, typed in the right order. Docker Compose describes all of that in one file, compose.yaml, and starts it with one command.
Compose also creates a network for the project, and each service name becomes a hostname on it. A service called db is reachable as db.
In the cloud
On your computer, a volume is a folder Docker manages on your disk. In production there’s no laptop, and containers move between machines, so QuickCart doesn’t keep its database in a container at all. It uses a managed database, RDS, and files go to S3. Containers on ECS and Kubernetes get their own networking too, covered in ECS on Fargate. What you learn here is the local version of the same ideas.
Part 3: See it
Below is a MySQL container on your machine. Save a few orders, remove the container, and start a new one. Then switch to In a named volume and do exactly the same.
What to notice: MySQL behaved identically both times. It wrote to the same folder, /var/lib/mysql. The only difference was whether that folder was part of the container or a volume mounted into it, and that decided whether the orders survived.
Part 4: Do it
Free — everything runs on your own computer.
You’ll lose a file on purpose, keep one with a volume, look into your computer from a container, connect two containers by name, and then run QuickCart’s app with MySQL using Compose. About 30 minutes.
You need Docker running, and the order-service folder from Writing a Dockerfile. Open a terminal in that folder.
Watch a file disappear
Start a container that writes a file and finishes:
Terminaldocker run --name scratch python:3.12-slim sh -c "echo hello > /note.txt"Part What it means docker run --name scratchstart a container called scratch. No--rm, so it’s kept after it finishespython:3.12-slimthe image; you already have it from the last lesson sh -c "…"run the text in quotes with the container’s shell echo hello > /note.txtinside the container: write hellointo the file/note.txt. The>sends whatechoprints into the fileIt prints nothing and finishes straight away. The file is in the container’s writable layer. Now remove the container:
Terminaldocker rm scratchPart What it means docker rm scratchdelete the container, and its writable layer with it You should seescratchStart a new container from the same image and look for the file:
Terminaldocker run --rm python:3.12-slim cat /note.txtPart What it means --rmdelete this container as soon as it finishes cat /note.txtprint the file You should seecat: /note.txt: No such file or directoryThe file went with the container. The image never had it.
Keep it with a named volume
Terminaldocker volume create notesPart What it means docker volume createmake a volume notesits name You should seenotesWrite the file into the volume this time:
Terminaldocker run --rm -v notes:/data python:3.12-slim sh -c "echo hello > /data/note.txt"Part What it means -v notes:/datamount the volume notesat/datainside the containerecho hello > /data/note.txtwrite the file inside /data, which is the volumeThat container is already gone, because of
--rm. Start a different one with the same volume:Terminaldocker run --rm -v notes:/data python:3.12-slim cat /data/note.txtPart What it means -v notes:/dataa brand-new container, given the same volume cat /data/note.txtprint the file from the volume You should seehelloA new container read what an old, deleted one wrote.
Look into your computer with a bind mount
A bind mount needs the full path of a folder. Ask the terminal where you are:
TerminalpwdPart What it means pwdprint working directory: the full path of the folder you’re in Something like/home/maya/order-serviceCopy what yours prints, and use it in place of
FULL_PATH:Terminaldocker run --rm --mount type=bind,source=FULL_PATH,target=/code,readonly python:3.12-slim ls /codePart What it means --mount type=bindshow a folder from your computer inside the container source=FULL_PATHwhich folder: the path pwdprintedtarget=/codewhere it appears inside the container readonlythe container can read the folder but not change it ls /codelist what’s there You should seeYour folder’s files:
Dockerfile,app.py,requirements.txt, and any other files from the last lesson. The container is looking straight at your computer’s folder. Nothing was copied.Connect two containers by name
Build the order service image again. You removed it at the end of the last lesson:
Terminaldocker build -t order-service:v1 .Part What it means docker build -t order-service:v1 .build the image from the Dockerfile in this folder, named order-service:v1You should seeThe build finishes with a
naming to docker.io/library/order-service:v1line.Create a network:
Terminaldocker network create quickcartPart What it means docker network createmake a new bridge network, with its own DNS quickcartits name You should seeA long ID: the new network’s.
Start the order service on it. There’s no
-p, so your computer can’t reach it, only containers on the network can:Terminaldocker run -d --name orders --network quickcart order-service:v1Part What it means -d --name ordersrun it in the background, called orders--network quickcartconnect it to the quickcartnetwork instead of the default oneNow start a second container on the same network, running
curl, and call the first one by its name:Terminaldocker run --rm --network quickcart curlimages/curl -sS http://orders:8080/healthPart What it means --network quickcartput this container on the same network curlimages/curla tiny image that contains only curl. Docker downloads it the first time-sSsilent, but still show an error if there is one http://orders:8080/healththe container called orders, port 8080, page/healthYou should see{"status":"ok","version":"v1"}Try the same from a container on the default network:
Terminaldocker run --rm curlimages/curl -sS --max-time 3 http://orders:8080/healthPart What it means no --networkthis container goes on the default bridgenetwork--max-time 3give up after 3 seconds You should seecurl: (6) Could not resolve host: ordersIt can’t even find the name. It’s on a different network, with no DNS for
orders.Remove the container before moving on, so it doesn’t hold the name:
Terminaldocker rm -f ordersPart What it means docker rm -fstop and delete the container in one step You should seeordersDescribe the app and MySQL in compose.yaml
In the
order-servicefolder, createcompose.yaml:compose.yamlservices: app: build: . image: order-service:v1 ports: - "8080:8080" depends_on: db: condition: service_healthy db: image: mysql:8.4 environment: MYSQL_ROOT_PASSWORD: local-root-only MYSQL_DATABASE: quickcart MYSQL_USER: quickcart MYSQL_PASSWORD: local-dev-only volumes: - db-data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"] interval: 5s timeout: 3s retries: 20 start_period: 30s volumes: db-data:Line What it does If you leave it out services:The containers in this project. Each name becomes a hostname on the project’s network Nothing to run app:→build: .Build the image from the Dockerfile in this folder Compose would need an image that already exists image: order-service:v1The name to give that image Compose makes up a name from the folder and service ports: "8080:8080"Publish port 8080 on your computer to the app. Quoted, because YAML can misread some number pairs with a colon You couldn’t reach the app from your computer depends_on:→condition: service_healthyStart the app only once the database’s health check passes The app might start before MySQL is ready to accept connections db:→image: mysql:8.4MySQL 8.4, a long-term support release Required environment:Settings the MySQL image reads the first time it starts with an empty data folder: the root password, and a database and user to create MySQL refuses to start without a root password setting volumes: - db-data:/var/lib/mysqlMount the named volume db-datawhere MySQL keeps its filesThe data lives in the writable layer and dies with the container healthcheck:→test:Ask MySQL “are you alive?” over the network with mysqladmin ping. It answers without a passwordservice_healthywould have nothing to wait forinterval,timeout,retries,start_periodCheck every 5 seconds, wait 3 seconds for an answer, allow 20 failures in a row, and ignore failures in the first 30 seconds while MySQL sets itself up Docker’s defaults check every 30 seconds, which makes startup slow volumes: db-data:at the endDeclare the named volume, so Compose creates it Compose stops with an error: the service refers to an undefined volume The database has no
ports:line. Only the app can reach it, over the project’s network, by the namedb.Start it and prove the names work
Terminaldocker compose up -d --waitPart What it means docker compose upcreate the network and volume, build the app image if needed, and start both services -din the background --waitdon’t return until the services are running and healthy You should seeA short list of what was created: a network called
order-service_default, a volume calledorder-service_db-data, and containers calledorder-service-db-1andorder-service-app-1, ending with both Healthy or Started. Compose names things after your folder. The first time, it downloads MySQL, which takes a minute.The app answers on your computer, as before:
Terminalcurl localhost:8080/healthPart What it means curl localhost:8080/healthfetch the health page through the published port You should see{"status":"ok","version":"v1"}Now ask the app container what the name
dbmeans:Terminaldocker compose exec app python -c "import socket; print(socket.gethostbyname('db'))"Part What it means docker compose exec apprun a command inside the running appservicepython -c "…"run a one-line Python program socket.gethostbyname('db')look up the name dbthe way the app would, and print the address it findsAn address like172.18.0.2The name
dbmeans the database container, found through Compose’s network. That’s the address the app would use instead oflocalhost.Save an order, then recreate everything
Create a table and save one order, with the MySQL client that’s inside the
dbcontainer:Terminaldocker compose exec db mysql -uquickcart -plocal-dev-only quickcart -e "CREATE TABLE orders (id INT PRIMARY KEY, item VARCHAR(50)); INSERT INTO orders VALUES (1, 'pad thai');"Part What it means docker compose exec dbrun a command inside the dbservicemysqlthe MySQL command-line client -uquickcart -plocal-dev-onlylog in as user quickcartwith its password. No space after-uor-pquickcartthe database to use -e "…"run this SQL and exit: create a table, then add one row You should seemysql: [Warning] Using a password on the command line interface can be insecure.That warning is MySQL reminding you that a password typed in a command is visible to anyone who can see your command history. Fine for a local lab.
Now remove both containers and the network, but not the volume:
Terminaldocker compose downPart What it means docker compose downstop and remove the project’s containers and network. Named volumes are kept You should seeBoth containers and the network listed as Removed. The volume isn’t mentioned: it’s still there.
Start everything again. These are brand-new containers:
Terminaldocker compose up -d --waitPart What it means docker compose up -d --waitcreate fresh containers and wait until they’re healthy Ask the new database for the order:
Terminaldocker compose exec db mysql -uquickcart -plocal-dev-only quickcart -e "SELECT * FROM orders;"Part What it means -e "SELECT * FROM orders;"print every row in the orderstableYou should seemysql: [Warning] Using a password on the command line interface can be insecure. +----+----------+ | id | item | +----+----------+ | 1 | pad thai | +----+----------+The containers are new; the order isn’t. The new MySQL found its files in the volume.
Clean-up step: Clean up
Remove the Compose project, including its volume. This deletes the test order for good:
Terminaldocker compose down -vPart What it means docker compose downremove the containers and the network -valso remove the named volumes in compose.yaml: here,db-dataand everything in itYou should seeThe containers, the network and the volume
order-service_db-data, all listed as Removed.Remove the network and volume from the earlier steps:
Terminaldocker network rm quickcartPart What it means docker network rmdelete a network. It must have no containers left on it You should seequickcartTerminaldocker volume rm notesPart What it means docker volume rmdelete a volume, and every file in it You should seenotesLast, remove the images you downloaded for this lab:
Terminaldocker image rm mysql:8.4 curlimages/curlPart What it means docker image rmdelete images from your computer mysql:8.4 curlimages/curlthe two images this lab downloaded You should seeUntagged:andDeleted:lines for each. Keeporder-service:v1and yourorder-servicefolder: Storing images: ECR pushes that image to AWS.
Part 5: Check yourself
Priya runs MySQL in a container with no volume, saves some orders, then runs docker rm on the container. What happens to the orders?
Two containers on Docker's default network can't reach each other by name. What's the fix?
What does -p 8081:8080 mean on docker run?
The order service runs in one container and MySQL in another. The app is set to connect to localhost:3306 and gets "connection refused". Why?
What's the difference between docker compose down and docker compose down -v?
Which is the right tool for each job?
Part 6: Cheat sheet
| Command or setting | What it does |
|---|---|
| Writable layer | The container’s own files. Deleted by docker rm |
docker volume create NAME | Make a named volume |
-v NAME:/path | Mount a named volume inside the container |
--mount type=bind,source=FULL_PATH,target=/path | Show a folder from your computer inside the container |
-p 8081:8080 | Port 8081 on your computer leads to 8080 in the container |
-p 127.0.0.1:8080:8080 | Only reachable from your own computer |
docker network create NAME | A network where containers find each other by name |
localhost in a container | That container itself. Use the other container’s name |
docker compose up -d --wait | Start everything in compose.yaml and wait until healthy |
docker compose exec SERVICE COMMAND | Run a command inside a service |
docker compose down | Remove containers and network. Keeps volumes |
docker compose down -v | Also deletes the volumes and their data |