Why Containers, and a First Dockerfile
Here's the problem we're solving, stated plainly. Your app depends on things that aren't in the app: a specific Python version, a web framework, a database driver, maybe a system library or two. When you run it, all of those happen to be on your machine. When someone else runs it, some of them aren't - or they're the wrong version. The app doesn't care that it's "the same code." It breaks.
A container fixes this by shipping the app and everything it depends on as one sealed unit. Inside the container is a tiny, predictable Linux system with exactly the Python and libraries your app needs. Outside, the host machine's setup stops mattering. Whoever runs the container gets your environment, not theirs.
Two words you'll see constantly:
- An image is the recipe - a frozen, read-only snapshot of the filesystem with your app and its dependencies baked in.
- A container is a running instance of an image. You can start many containers from one image, like many objects from one class.
You write a Dockerfile to describe the image. Docker reads it top to bottom and builds the image. Let's make one.
The app
Make a project folder and drop two files in it. First the app - a Flask server with a single route:
# app.py
=
return
One detail that trips everyone up: host="0.0.0.0". Inside a container, the default 127.0.0.1 means "only listen to traffic from inside this container" - which is nobody, since you're connecting from outside. 0.0.0.0 tells Flask to accept connections on all interfaces so your host can reach it.
Now declare the dependency:
# requirements.txt
flask==3.0.3
You could run this locally right now (pip install -r requirements.txt && python app.py), and if you want to confirm the app itself works before containerizing, go ahead. But the point is to stop depending on your machine's Python. So let's containerize it.
The Dockerfile
Create a file named exactly Dockerfile (no extension) next to app.py:
# Dockerfile
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
Read it line by line, because every line is doing real work:
FROM python:3.12- start from an official image that already has Python 3.12 installed. This is your base layer; you build on top of it.WORKDIR /app- set the working directory inside the image. Commands after this run here, and it's created if it doesn't exist.COPY requirements.txt .- copy the requirements file from your folder into the image's/app.RUN pip install -r requirements.txt- install Flask inside the image. This runs at build time, so the dependency is baked in.COPY . .- copy the rest of your project (includingapp.py) into the image.EXPOSE 5000- document that the app listens on port 5000. (This is a label; it doesn't actually publish the port - you do that when you run.)CMD [...]- the default command to run when a container starts. This one launches your app.
Build it
From the project folder, build the image and give it a name with -t (for "tag"):
The . at the end is the build context - the folder Docker sends to the build, and the root for those COPY lines. You'll watch Docker pull the base image (once - it's cached after that) and run each step. At the end you'll see something like naming to docker.io/library/myapp.
Confirm the image exists:
You should see myapp in the list with a size - probably around a gigabyte, because python:3.12 is a full image. We'll shrink that dramatically in Phase 2.
Run it
Start a container from the image:
The -p 8080:5000 is the bridge between worlds: it maps port 8080 on your machine to port 5000 inside the container (the order is host:container). The EXPOSE line alone wouldn't do this - -p is what actually opens the door.
Open a browser to http://localhost:8080 (or run curl http://localhost:8080). You should see:
Hello from inside a container!
That response came from a Python that may not even be installed on your machine, running inside an isolated Linux environment, reachable because you mapped a port. The container is running in your terminal - press Ctrl+C to stop it.
To run it in the background instead, add -d (detached) and name it:
&&
Where you are
You have a working image and a container serving HTTP. Anyone with Docker can now run your exact app - no Python setup, no dependency install - with docker build and docker run. That's the "works on my machine" problem solved in its crudest form.
It's also wasteful: a gigabyte for a Hello World, and a full reinstall of Flask every time you change one line of app.py. In Phase 2 we turn this rough Dockerfile into one you'd be happy to put your name on.