Laptop to Production

Data and networking

Volumes, bind mounts and container networks.

Time
40 minutes
Level
Beginner

Part 1: The problem

QuickCart, a database on every laptop

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:

KindWhat it isSurvives docker rm?Good for
Writable layerThe container’s own scratch spaceNoTemporary files
Named volumeStorage Docker creates and manages, with a name you give itYesA database’s files, anything that must survive
Bind mountA folder on your computer, shown inside the containerYes; it’s your folderCode 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.

Key point: Anything a container writes outside a volume is deleted with the container.

Two ways to write it

docker run accepts volumes and bind mounts in two styles. They do the same thing:

You writeMeans
-v db-data:/var/lib/mysqlMount 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/mysqlThe same, spelled out
--mount type=bind,source=/home/maya/order-service,target=/codeShow 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.

FromToHow
Your computerA containerPublish a port: -p 8080:8080 connects port 8080 on your computer to port 8080 in the container
A containerAnother containerPut both on the same network you created, and use the other’s name
A containerThe internetWorks by default. Docker shares your computer’s connection

Publishing ports

-p takes the host’s port first, then the container’s:

You writeMeans
-p 8080:8080Port 8080 on your computer, on every network it’s connected to, leads to port 8080 in the container
-p 8081:8080Port 8081 on your computer leads to port 8080 in the container
-p 127.0.0.1:8080:8080Only 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.

NetworkReach others by name?Use it for
The default bridgeNo, IP onlyQuick one-off containers
A network you create, like quickcartYesAnything with more than one container
Separate networksNo: they can’t reach each other at allKeeping 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.

MySQL keeps its files
The QuickCart app saves orders to a MySQL container. Inside the container is MySQL and the container's own writable layer. Outside it, on the host, is a named volume called db-data, which can be mounted into the container at /var/lib/mysql.Your machineQuickCart appsaves ordersContainer quickcart-db-1started from the mysql:8.4 imageMySQLwrites its files to /var/lib/mysqlWritable layer: the container's ownNo orders yetRemovedthe container and its writable layer are deletedmountedVolume db-datanot used
ReadyMySQL is keeping its files inside the container. Save an order or two, then remove the container and start a new one.

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.

  1. Watch a file disappear

    Start a container that writes a file and finishes:

    Terminal
    docker run --name scratch python:3.12-slim sh -c "echo hello > /note.txt"
    PartWhat it means
    docker run --name scratchstart a container called scratch. No --rm, so it’s kept after it finishes
    python: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 hello into the file /note.txt. The > sends what echo prints into the file

    It prints nothing and finishes straight away. The file is in the container’s writable layer. Now remove the container:

    Terminal
    docker rm scratch
    PartWhat it means
    docker rm scratchdelete the container, and its writable layer with it
    You should see
    scratch

    Start a new container from the same image and look for the file:

    Terminal
    docker run --rm python:3.12-slim cat /note.txt
    PartWhat it means
    --rmdelete this container as soon as it finishes
    cat /note.txtprint the file
    You should see
    cat: /note.txt: No such file or directory

    The file went with the container. The image never had it.

  2. Keep it with a named volume

    Terminal
    docker volume create notes
    PartWhat it means
    docker volume createmake a volume
    notesits name
    You should see
    notes

    Write the file into the volume this time:

    Terminal
    docker run --rm -v notes:/data python:3.12-slim sh -c "echo hello > /data/note.txt"
    PartWhat it means
    -v notes:/datamount the volume notes at /data inside the container
    echo hello > /data/note.txtwrite the file inside /data, which is the volume

    That container is already gone, because of --rm. Start a different one with the same volume:

    Terminal
    docker run --rm -v notes:/data python:3.12-slim cat /data/note.txt
    PartWhat it means
    -v notes:/dataa brand-new container, given the same volume
    cat /data/note.txtprint the file from the volume
    You should see
    hello

    A new container read what an old, deleted one wrote.

  3. Look into your computer with a bind mount

    A bind mount needs the full path of a folder. Ask the terminal where you are:

    Terminal
    pwd
    PartWhat it means
    pwdprint working directory: the full path of the folder you’re in
    Something like
    /home/maya/order-service

    Copy what yours prints, and use it in place of FULL_PATH:

    Terminal
    docker run --rm --mount type=bind,source=FULL_PATH,target=/code,readonly python:3.12-slim ls /code
    PartWhat it means
    --mount type=bindshow a folder from your computer inside the container
    source=FULL_PATHwhich folder: the path pwd printed
    target=/codewhere it appears inside the container
    readonlythe container can read the folder but not change it
    ls /codelist what’s there
    You should see

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

  4. Connect two containers by name

    Build the order service image again. You removed it at the end of the last lesson:

    Terminal
    docker build -t order-service:v1 .
    PartWhat it means
    docker build -t order-service:v1 .build the image from the Dockerfile in this folder, named order-service:v1
    You should see

    The build finishes with a naming to docker.io/library/order-service:v1 line.

    Create a network:

    Terminal
    docker network create quickcart
    PartWhat it means
    docker network createmake a new bridge network, with its own DNS
    quickcartits name
    You should see

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

    Terminal
    docker run -d --name orders --network quickcart order-service:v1
    PartWhat it means
    -d --name ordersrun it in the background, called orders
    --network quickcartconnect it to the quickcart network instead of the default one

    Now start a second container on the same network, running curl, and call the first one by its name:

    Terminal
    docker run --rm --network quickcart curlimages/curl -sS http://orders:8080/health
    PartWhat 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 /health
    You should see
    {"status":"ok","version":"v1"}

    Try the same from a container on the default network:

    Terminal
    docker run --rm curlimages/curl -sS --max-time 3 http://orders:8080/health
    PartWhat it means
    no --networkthis container goes on the default bridge network
    --max-time 3give up after 3 seconds
    You should see
    curl: (6) Could not resolve host: orders

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

    Terminal
    docker rm -f orders
    PartWhat it means
    docker rm -fstop and delete the container in one step
    You should see
    orders
  5. Describe the app and MySQL in compose.yaml

    In the order-service folder, create compose.yaml:

    compose.yaml
    services:
      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:
    LineWhat it doesIf you leave it out
    services:The containers in this project. Each name becomes a hostname on the project’s networkNothing to run
    app: → build: .Build the image from the Dockerfile in this folderCompose would need an image that already exists
    image: order-service:v1The name to give that imageCompose 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 colonYou couldn’t reach the app from your computer
    depends_on: → condition: service_healthyStart the app only once the database’s health check passesThe app might start before MySQL is ready to accept connections
    db: → image: mysql:8.4MySQL 8.4, a long-term support releaseRequired
    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 createMySQL refuses to start without a root password setting
    volumes: - db-data:/var/lib/mysqlMount the named volume db-data where 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_healthy would have nothing to wait for
    interval, 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 upDocker’s defaults check every 30 seconds, which makes startup slow
    volumes: db-data: at the endDeclare the named volume, so Compose creates itCompose 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 name db.

  6. Start it and prove the names work

    Terminal
    docker compose up -d --wait
    PartWhat 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 see

    A short list of what was created: a network called order-service_default, a volume called order-service_db-data, and containers called order-service-db-1 and order-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:

    Terminal
    curl localhost:8080/health
    PartWhat 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 db means:

    Terminal
    docker compose exec app python -c "import socket; print(socket.gethostbyname('db'))"
    PartWhat it means
    docker compose exec apprun a command inside the running app service
    python -c "…"run a one-line Python program
    socket.gethostbyname('db')look up the name db the way the app would, and print the address it finds
    An address like
    172.18.0.2

    The name db means the database container, found through Compose’s network. That’s the address the app would use instead of localhost.

  7. Save an order, then recreate everything

    Create a table and save one order, with the MySQL client that’s inside the db container:

    Terminal
    docker 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');"
    PartWhat it means
    docker compose exec dbrun a command inside the db service
    mysqlthe MySQL command-line client
    -uquickcart -plocal-dev-onlylog in as user quickcart with its password. No space after -u or -p
    quickcartthe database to use
    -e "…"run this SQL and exit: create a table, then add one row
    You should see
    mysql: [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:

    Terminal
    docker compose down
    PartWhat it means
    docker compose downstop and remove the project’s containers and network. Named volumes are kept
    You should see

    Both containers and the network listed as Removed. The volume isn’t mentioned: it’s still there.

    Start everything again. These are brand-new containers:

    Terminal
    docker compose up -d --wait
    PartWhat it means
    docker compose up -d --waitcreate fresh containers and wait until they’re healthy

    Ask the new database for the order:

    Terminal
    docker compose exec db mysql -uquickcart -plocal-dev-only quickcart -e "SELECT * FROM orders;"
    PartWhat it means
    -e "SELECT * FROM orders;"print every row in the orders table
    You should see
    mysql: [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.

  8. Clean-up step: Clean up

    Remove the Compose project, including its volume. This deletes the test order for good:

    Terminal
    docker compose down -v
    PartWhat it means
    docker compose downremove the containers and the network
    -valso remove the named volumes in compose.yaml: here, db-data and everything in it
    You should see

    The containers, the network and the volume order-service_db-data, all listed as Removed.

    Remove the network and volume from the earlier steps:

    Terminal
    docker network rm quickcart
    PartWhat it means
    docker network rmdelete a network. It must have no containers left on it
    You should see
    quickcart
    Terminal
    docker volume rm notes
    PartWhat it means
    docker volume rmdelete a volume, and every file in it
    You should see
    notes

    Last, remove the images you downloaded for this lab:

    Terminal
    docker image rm mysql:8.4 curlimages/curl
    PartWhat it means
    docker image rmdelete images from your computer
    mysql:8.4 curlimages/curlthe two images this lab downloaded
    You should see

    Untagged: and Deleted: lines for each. Keep order-service:v1 and your order-service folder: 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

On one card
Command or settingWhat it does
Writable layerThe container’s own files. Deleted by docker rm
docker volume create NAMEMake a named volume
-v NAME:/pathMount a named volume inside the container
--mount type=bind,source=FULL_PATH,target=/pathShow a folder from your computer inside the container
-p 8081:8080Port 8081 on your computer leads to 8080 in the container
-p 127.0.0.1:8080:8080Only reachable from your own computer
docker network create NAMEA network where containers find each other by name
localhost in a containerThat container itself. Use the other container’s name
docker compose up -d --waitStart everything in compose.yaml and wait until healthy
docker compose exec SERVICE COMMANDRun a command inside a service
docker compose downRemove containers and network. Keeps volumes
docker compose down -vAlso deletes the volumes and their data

Finished the build and the quiz? Mark it done to fill in its stop on the route map.