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 most famous sentence in software — and the reason containers exist. Same code, different machines, different results.
Python 3.11, Flask installed, everything tuned by you. The app runs.
✅ runs
Python 3.9, different packages, different versions. The same code crashes.
❌ crashes
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
A container packages the app together with everything it needs, so the box — not the machine — defines how the app behaves.
Your code, the language runtime (Python), every dependency, and the config — snapshotted into a single portable box.
Laptop, test server, production: all run the identical box, so the app behaves identically. The machine underneath is invisible to the app.
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.
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.
Image vs container, what's actually inside the box, and how a container differs from a virtual machine.
The most important pair of words in Docker. Get these two straight and the rest follows.
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.
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.
You build one image. Then you can run as many live copies as you like — they don't interfere with each other.
read-only blueprint
image = the recipe · container = a meal cooked from it · many meals from one recipe
Both "run software separately" — but they virtualize different things. That difference explains everything about container size and speed.
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.
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.
Four things, and one thing that's deliberately missing.
app.py, templates, static files — copied in from your project.
Python 3.11 — the machine that executes your code, pinned to one exact version.
Flask and every library it drags in, installed with exact versions at build time.
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.
A few short lines of plain text tell Docker exactly what to put in the box and what to run when it starts.
This Dockerfile packages our Flask app from the previous topic. Read top to bottom: each line adds one layer.
# 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.
Docker has about 15 instructions. These five cover virtually every app.
| Instruction | Its job | Example |
|---|---|---|
| FROM | Start from a base image — the foundation layer | FROM python:3.11-slim |
| COPY | Copy your files into the box | COPY app.py /app/ |
| RUN | Install & prepare — happens at build time, baked into the image | RUN pip install flask |
| EXPOSE | Document which port the app listens on | EXPOSE 5000 |
| CMD | The default command run when the container starts | CMD ["python", "app.py"] |
The #1 Docker confusion: what happens when you build, and what happens when you run. Two different clocks.
pip install runs once while building the image; the result is frozen into a layer. You never see it run again — until you rebuild.
The container's job, executed every time the container starts. This is your app: python app.py.
Change only app.py? Docker reuses every earlier layer and rebuilds just COPY onward. A rebuild can take seconds.
Four beats between a folder of code and a boxed image.
Top to bottom, line by line. Each line is one step in the assembly.
Pull FROM python:3.11-slim from the registry — or use the local copy if you already have it.
COPY copies, RUN installs… each finished line becomes a saved layer stacked on top of the previous one.
The stack of layers is your image, stored locally as myapp:1.0 — ready to run.
One command turns the folder into a boxed image.
An image is inert until someone runs it. docker run brings it to life — and a few flags decide what it can see.
Build makes the box; run makes a running instance of it. This is 90% of daily Docker.
# 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".
Learn these four and you can read any docker run command you'll ever see.
| Flag | It does | Example |
|---|---|---|
| -p 8080:5000 | Port map: host port → container port — makes the app reachable | curl http://127.0.0.1:8080 |
| -v name:/path | Volume: mount host storage into the box, so data survives | -v db-data:/var/lib/postgres |
| -d | Detach: run in the background, print the container ID | docker run -d … |
| --name web | Give it a name instead of a random ID | docker ps |
The container starts in the background; the port map is what makes it reachable.
Inside, the container is its own little universe. Outside, the host reaches it through exactly one door.
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
Three walls, one result: a container can't break the host, and the host can't sneak into the container.
The container's / is the image. The host's /usr and /etc are simply not visible from inside.
PID 1 is your app. Host processes and container processes don't see each other by default.
A private IP and its own port table. It reaches the outside only through mapped ports or the compose network.
Because nothing leaks in or out, a container can be deleted and re-created in seconds — the same box again.
One is a note; the other is a door. Confusing them is the most common Docker port mistake.
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.
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.
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.
Our Flask app plus its database. The same app as before — now with a real Postgres.
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.
That whole file is five ideas. Learn the keys and any compose file becomes readable.
Each entry under services: is one container. Here: web and db.
The same host:container mapping as -p, written in the file.
Named storage that lives on the host and survives container rebuilds — the database's home.
An ordering hint: start db before web. (A hint — not a guarantee the DB is fully ready.)
Pass configuration as environment variables — the same job as -e.
Up brings the family to life; down takes it away. The volumes stay either way.
Services in one compose file talk to each other by name — no IPs, no ports to open, no firewall.
from inside, web just connects to db:5432 — the service name. Docker resolves it. That's why containers need no IP configuration.
The one thing that must outlive the box: your database. Containers are disposable; data is not.
The same box has two kinds of storage, and they die at very different times.
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.
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.
The single most important table in this deck. Memorize the right column.
| Operation | What happens |
|---|---|
| docker stop / start | App frozen; everything stays |
| docker compose down | Containers removed; volumes survive |
| up --build after a code change | New code runs; old data stays in the volume |
| docker compose down -v | Containers AND volumes removed — data is truly gone |
Tear the system down and back up. The container is new; its memory is not.
This is the payoff: your Flask app becomes an image, and that image — not a folder of code — is what you ship.
the front door — HTTPS, static files, forwarding. Next topic.
build once on your laptop · the same bytes run on the test server and in production — with nginx in front (next topic)
Containers have a deep toolbox. These four are real — but they're not your next step.
A recipe trick: build in one fat stage, copy the result into a tiny final image. You'll meet it when images get big.
bridge, host, custom networks… compose already gives you the 95% you need; the rest is for exotic setups.
Orchestrating many containers across many machines. A whole other (exciting) field — not this course's path.
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.
Next: nginx — the front door that stands in front of the container.