→ From Coder to Shipper
From Coder to Shipper · DevOps

Docker — containers

Package your app with everything it needs into one little box — and ship the exact same box from your laptop to the production server.

← / → to navigate · EN/VI switch below

The problem

It works on my machine

The most famous sentence in software — and the reason containers exist. Same code, different machines, different results.

🧑‍💻 Your laptop

Python 3.11, Flask installed, everything tuned by you. The app runs.

✅ runs

👥 A teammate's laptop

Python 3.9, different packages, different versions. The same code crashes.

❌ crashes

🖥 The server

Nothing installed. "It works on my machine" is not an instruction manual.

❌ refuses

the code is identical · the environments are not — and the environment is what breaks

The idea

One consistent little box

A container packages the app together with everything it needs, so the box — not the machine — defines how the app behaves.

One box, everything inside

Your code, the language runtime (Python), every dependency, and the config — snapshotted into a single portable box.

Same box, any machine

Laptop, test server, production: all run the identical box, so the app behaves identically. The machine underneath is invisible to the app.

Not a mini computer

No operating system inside. A container borrows the host's OS kernel and carries only the app and its needs — that's why it's small and fast.

Throwaway by design

Containers are cheap to stop, delete and recreate. A broken one is not repaired — it's replaced by a fresh copy of the same box.

01
Part 1

The mental model

Image vs container, what's actually inside the box, and how a container differs from a virtual machine.

Mental model · image vs container

Image vs container — recipe vs dish

The most important pair of words in Docker. Get these two straight and the rest follows.

Image — the blueprint

A read-only recipe: base OS bits + runtime + your code + all dependencies, built once from a Dockerfile. Stored, shareable, immutable.

Think: a frozen cookie cutter — the shape, not a cookie.

Container — a running instance

The blueprint brought to life: a process running, its own files, its own network. Start it, stop it, delete it — the image is never touched.

Think: any number of cookies cut from that same cutter.

Mental model · picture it

One image, many containers

You build one image. Then you can run as many live copies as you like — they don't interfere with each other.

image: myapp:1.0
L3your code (app.py)
L2dependencies (flask + libs)
L1base (python 3.11-slim)

read-only blueprint

⟶
● container #1 — running :5000
● container #2 — running :5000
● container #3 — running :5000

image = the recipe · container = a meal cooked from it · many meals from one recipe

Mental model · the classic mix-up

Container vs virtual machine

Both "run software separately" — but they virtualize different things. That difference explains everything about container size and speed.

VM — a machine inside a machine

A hypervisor fakes whole hardware; the guest runs its own complete operating system, boots like a real computer, and weighs gigabytes.

Virtualizes: hardware. Boots in minutes.

Container — an app in a box

No fake hardware, no guest OS. The container's processes run directly on the host's kernel, carrying only app + dependencies — megabytes.

Virtualizes: the operating system (kernel is shared). Starts in milliseconds.

Mental model · inside the box

What's actually in the box

Four things, and one thing that's deliberately missing.

Your code

app.py, templates, static files — copied in from your project.

The runtime

Python 3.11 — the machine that executes your code, pinned to one exact version.

The dependencies

Flask and every library it drags in, installed with exact versions at build time.

Not in: the OS

No Linux distribution inside. The box shares the host's kernel — that's what makes it small and fast.

If it runs in the box on my laptop, it runs in the box on the server. The box doesn't change — only the machine underneath does.
the whole mental model in one sentence
02
Part 2

The Dockerfile — writing the recipe

A few short lines of plain text tell Docker exactly what to put in the box and what to run when it starts.

Dockerfile · the recipe

A whole app, in eight lines

This Dockerfile packages our Flask app from the previous topic. Read top to bottom: each line adds one layer.

Dockerfile
# recipe for our Flask app
FROM python:3.11-slim          # start from a ready-made base
WORKDIR /app
COPY app.py .                  # put your code in
COPY requirements.txt .
RUN pip install -r requirements.txt   # install deps
EXPOSE 5000                    # document the port
CMD ["python", "app.py"]        # what to run at start

The first line is the only required one. Everything else is optional seasoning.

Dockerfile · the toolbox

The five instructions you'll use 95%

Docker has about 15 instructions. These five cover virtually every app.

InstructionIts jobExample
FROMStart from a base image — the foundation layerFROM python:3.11-slim
COPYCopy your files into the boxCOPY app.py /app/
RUNInstall & prepare — happens at build time, baked into the imageRUN pip install flask
EXPOSEDocument which port the app listens onEXPOSE 5000
CMDThe default command run when the container startsCMD ["python", "app.py"]
Dockerfile · two clocks

Build time vs run time

The #1 Docker confusion: what happens when you build, and what happens when you run. Two different clocks.

RUN — at build time

pip install runs once while building the image; the result is frozen into a layer. You never see it run again — until you rebuild.

CMD — at start time

The container's job, executed every time the container starts. This is your app: python app.py.

Layers are cached

Change only app.py? Docker reuses every earlier layer and rebuilds just COPY onward. A rebuild can take seconds.

Dockerfile · the build

What docker build actually does

Four beats between a folder of code and a boxed image.

  1. 1

    Read the Dockerfile

    Top to bottom, line by line. Each line is one step in the assembly.

  2. 2

    Fetch the base

    Pull FROM python:3.11-slim from the registry — or use the local copy if you already have it.

  3. 3

    Execute each line

    COPY copies, RUN installs… each finished line becomes a saved layer stacked on top of the previous one.

  4. 4

    Tag the result

    The stack of layers is your image, stored locally as myapp:1.0 — ready to run.

Dockerfile · in the terminal

Build it

One command turns the folder into a boxed image.

ops@vps — zsh
$ docker build -t myapp:1.0 .
=> [1/6] FROM docker.io/library/python:3.11-slim
=> CACHED [2/6] WORKDIR /app
=> [5/6] RUN pip install -r requirements.txt
$ docker images
REPOSITORY TAG SIZE
myapp 1.0 148MB
the folder is now a boxed image, stored locally
03
Part 3

Running a container

An image is inert until someone runs it. docker run brings it to life — and a few flags decide what it can see.

Running · the two commands

The two commands that matter

Build makes the box; run makes a running instance of it. This is 90% of daily Docker.

bash
# 1. turn the Dockerfile into an image
docker build -t myapp:1.0 .

# 2. turn the image into a running container
docker run \
  -d --name web \
  -p 8080:5000 \
  -v myapp-data:/app/data \
  myapp:1.0

-t tags the image; the trailing dot means "read the Dockerfile from this folder".

Running · the flags

Four flags, 90% of the work

Learn these four and you can read any docker run command you'll ever see.

FlagIt doesExample
-p 8080:5000Port map: host port → container port — makes the app reachablecurl http://127.0.0.1:8080
-v name:/pathVolume: mount host storage into the box, so data survives-v db-data:/var/lib/postgres
-dDetach: run in the background, print the container IDdocker run -d …
--name webGive it a name instead of a random IDdocker ps
Running · in the terminal

Run it and look around

The container starts in the background; the port map is what makes it reachable.

ops@vps — zsh
$ docker run -d --name web -p 8080:5000 myapp:1.0
a1b2c3d4e5f6
$ docker ps
CONTAINER ID IMAGE STATUS PORTS
a1b2c3d4e5f6 myapp:1.0 Up 5 seconds 0.0.0.0:8080->5000/tcp
$ curl http://127.0.0.1:8080/
<h1>Hello, shipper!</h1>
host port 8080 knocked; container port 5000 answered
Running · isolation

What a container sees

Inside, the container is its own little universe. Outside, the host reaches it through exactly one door.

CONTAINER — ITS OWN UNIVERSE
its own / — app.py · python · flask
its own processes — PID 1 = python app.py
its own network — IP 172.17.0.2 · port 5000
🚪 8080 → 5000
(port map)
⟷

🖥 The host

Your laptop or the server: its own files, its own processes, its own ports. It can only reach in through the mapped port.

the container cannot see the host's files or processes; the host reaches it only through the mapped port — that's isolation

Running · isolation

Why isolation matters

Three walls, one result: a container can't break the host, and the host can't sneak into the container.

Its own filesystem

The container's / is the image. The host's /usr and /etc are simply not visible from inside.

Its own processes

PID 1 is your app. Host processes and container processes don't see each other by default.

Its own network

A private IP and its own port table. It reaches the outside only through mapped ports or the compose network.

Cheap to replace

Because nothing leaks in or out, a container can be deleted and re-created in seconds — the same box again.

Running · ports

EXPOSE vs -p — two different things

One is a note; the other is a door. Confusing them is the most common Docker port mistake.

EXPOSE — documentation

A note inside the image: "the app listens on 5000". Alone, it opens nothing — the host still cannot reach the container.

Useful for humans and for compose. It is not a port mapping.

-p — the actual door

docker run -p 8080:5000 connects a host port to a container port. Traffic hitting the host's 8080 lands inside on 5000.

Without -p, only other containers (via the compose network) can reach it.

04
Part 4

docker compose — a family of containers

Real apps are rarely one box: a web app + a database + (later) nginx. Compose describes the whole family in one file and starts it with one command.

Compose · the file

One file, a whole system

Our Flask app plus its database. The same app as before — now with a real Postgres.

docker-compose.yml
services:
  web:
    build: .
    ports:
      - "8080:5000"
    depends_on:
      - db
  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data
volumes:
  db-data:

web is built from our Dockerfile; db comes ready-made from the registry.

Compose · the keys

The five compose keys

That whole file is five ideas. Learn the keys and any compose file becomes readable.

services

Each entry under services: is one container. Here: web and db.

ports

The same host:container mapping as -p, written in the file.

volumes

Named storage that lives on the host and survives container rebuilds — the database's home.

depends_on

An ordering hint: start db before web. (A hint — not a guarantee the DB is fully ready.)

environment

Pass configuration as environment variables — the same job as -e.

Compose · in the terminal

One command, whole system

Up brings the family to life; down takes it away. The volumes stay either way.

ops@vps — zsh
$ docker compose up -d
✔ Network docker_default Created
✔ Container docker-db-1 Started
✔ Container docker-web-1 Started
$ docker compose ps
web Up 0.0.0.0:8080->5000/tcp
db Up 5432/tcp
$ docker compose down # remove the containers
Compose · picture it

The family, running

Services in one compose file talk to each other by name — no IPs, no ports to open, no firewall.

HOST MACHINE
web — Flask app listens on :5000
internal network
web → db:5432 (by name)
db — Postgres listens on :5432
🗄 db-data — volume on host disk · survives rebuilds

from inside, web just connects to db:5432 — the service name. Docker resolves it. That's why containers need no IP configuration.

05
Part 5

Where data lives

The one thing that must outlive the box: your database. Containers are disposable; data is not.

Data · two filesystems

Two filesystems, two different lives

The same box has two kinds of storage, and they die at very different times.

The container's filesystem

The read-only image plus a thin writable layer. Delete the container and that layer vanishes — the database file with it.

Life expectancy: the container's.

A volume

Named storage on the host disk, mounted into the box. Survives stop, restart, delete — and even image rebuilds.

Life expectancy: until someone explicitly removes it.

Data · the golden rule

What survives what

The single most important table in this deck. Memorize the right column.

OperationWhat happens
docker stop / startApp frozen; everything stays
docker compose downContainers removed; volumes survive
up --build after a code changeNew code runs; old data stays in the volume
docker compose down -vContainers AND volumes removed — data is truly gone
Data · in the terminal

Watch data outlive its container

Tear the system down and back up. The container is new; its memory is not.

ops@vps — zsh
$ docker compose up -d
$ docker compose down
$ docker volume ls
LOCAL VOLUME
docker_db-data
$ docker compose up -d
the container is new; its data is not
Big picture · shipping

The same box, from laptop to production

This is the payoff: your Flask app becomes an image, and that image — not a folder of code — is what you ship.

🌐 Browser

▶

🚪 nginx

the front door — HTTPS, static files, forwarding. Next topic.

▶
🐳 container: Flask app + Python + deps
🗄 myapp-data — volume (data)
💻 laptop
image: myapp:1.0
▶
🧪 test server
image: myapp:1.0
▶
🚀 production
image: myapp:1.0

build once on your laptop · the same bytes run on the test server and in production — with nginx in front (next topic)

Big picture · out of scope

What we're deliberately skipping

Containers have a deep toolbox. These four are real — but they're not your next step.

Multi-stage builds

A recipe trick: build in one fat stage, copy the result into a tiny final image. You'll meet it when images get big.

Networking modes

bridge, host, custom networks… compose already gives you the 95% you need; the rest is for exotic setups.

Kubernetes & Swarm

Orchestrating many containers across many machines. A whole other (exciting) field — not this course's path.

Image registries

Docker Hub & friends: where images are stored and shared between machines. We build locally first — registries come later.

You don't ship code anymore. You ship a box. The same box on your laptop, on the test server, and in production — that's the whole deal.
the shipper's creed

Build. Run. Ship.

Next: nginx — the front door that stands in front of the container.