Writing a Dockerfile: Containerise a Python App
Write your first Dockerfile for a Python Flask API, build it into an image, run it, and avoid the classic “listening on localhost” mistake.
On this page 10 sections
Introduction
So far you have run images other people built. Now you will build your own. A Dockerfile is a plain text file with step-by-step instructions; docker build follows them and produces an image you can run anywhere.
- How to write a Dockerfile with FROM, WORKDIR, COPY, RUN, EXPOSE and CMD.
- How to build an image with docker build and give it a name.
- How to run and test your own containerised app.
- Why a web app in a container must listen on 0.0.0.0.
The App
Priya’s Quotes API is a small Flask web app with two endpoints. Her project folder quotes-api contains three files:
quotes-api/├── app.py├── Dockerfile└── requirements.txtimport osimport socket
from flask import Flask, jsonify
app = Flask(__name__)
QUOTES = [ "Talk is cheap. Show me the code.", "First, solve the problem. Then, write the code.", "Simplicity is prerequisite for reliability.",]
@app.route("/")def home(): greeting = os.environ.get("GREETING", "Hello from Docker!") return jsonify(message=greeting, container=socket.gethostname())
@app.route("/quotes")def quotes(): return jsonify(QUOTES)
if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)flask==3.1.3Notice host="0.0.0.0" on the last line — it matters a lot, as you will see below.
The Dockerfile, Line by Line
The file must be called Dockerfile (no extension) and sits in the project folder:
FROM python:3.14-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 5000
CMD ["python", "app.py"]| Instruction | What it does here |
|---|---|
| FROM python:3.14-slim | Start from the official slim Python 3.14 image — Python is already installed |
| WORKDIR /app | Create /app inside the image and make it the current folder for the following steps |
| COPY . . | Copy everything from the project folder (the build context) into /app |
| RUN pip install ... | Run a command while building — here, install Flask into the image. --no-cache-dir keeps pip’s download cache out of the image |
| EXPOSE 5000 | Document that the app listens on port 5000 (it does not publish the port) |
| CMD ["python", "app.py"] | The command a container runs when it starts |
Write CMD ["python", "app.py"] rather than CMD python app.py. The JSON (exec) form runs your program directly, so it receives stop signals properly and shuts down cleanly when you run docker stop.
Building the Image
From inside the project folder, build the image and name it with -t (tag). The final . is the build context — the folder whose files COPY can use:
cd quotes-apidocker build -t quotes-api .Docker prints each step as it runs: loading the Dockerfile, pulling python:3.14-slim if needed, then [1/4] FROM, [2/4] WORKDIR, [3/4] COPY and [4/4] RUN pip install, and finally naming to docker.io/library/quotes-api:latest. If a step fails, the build stops and shows that step’s error output. Check the result:
docker images quotes-apiRunning the Container
Run it like any other image — in the background, with a name and a published port:
docker run -d --name api -p 5000:5000 quotes-apicurl http://localhost:5000/curl http://localhost:5000/quotesThe first request returns JSON with the greeting and the container’s hostname (its short ID); the second returns the list of quotes. docker logs api shows Flask starting up and one line per request. Flask also warns that it is a development server — lesson 14 replaces it with a production server.
If app.py used app.run(port=5000) without host="0.0.0.0", Flask would listen only on 127.0.0.1 inside the container. Requests forwarded by -p arrive on the container’s network interface, not its loopback, so curl fails with “Connection reset by peer” even though docker logs shows the app running. Any server in a container must listen on 0.0.0.0.
Priya runs docker build -t quotes-api from inside her project folder and Docker reports an error about a missing argument. What is missing?
Dockerfile Instructions Reference
| Instruction | Purpose |
|---|---|
| FROM | The base image to start from (every Dockerfile begins with it) |
| WORKDIR | Set the working folder for later instructions and for the container |
| COPY / ADD | Copy files into the image (prefer COPY; ADD can also fetch URLs and unpack archives) |
| RUN | Run a command at build time, e.g. install packages |
| ENV | Set an environment variable for build steps and containers |
| ARG | A variable available only while building (lesson 9) |
| EXPOSE | Document the port the app listens on |
| USER | Run later steps and the container as a given user (lesson 14) |
| CMD | The default command when a container starts |
| ENTRYPOINT | A fixed command that CMD’s values are passed to (lesson 6) |
| HEALTHCHECK | A command Docker runs to check the app is healthy (lesson 14) |
Put the lines of a simple Python Dockerfile in the right order.
- EXPOSE 5000
- WORKDIR /app
- COPY . .
- FROM python:3.14-slim
- RUN pip install --no-cache-dir -r requirements.txt
- CMD ["python", "app.py"]
Common Mistakes
- Forgetting the . (build context) at the end of docker build.
- A server listening on 127.0.0.1 instead of 0.0.0.0 inside the container.
- Saving the file as Dockerfile.txt — it must be exactly Dockerfile.
- Rebuilding but running an old container — remove the old container and start a new one from the new image.
Frequently Asked Questions
No. The image contains a copy of your files from build time. Rebuild with docker build after every change, then start a new container.
Yes, with docker build -f Dockerfile.dev -t myapp . — useful for keeping separate development and production versions.
python on its own means python:latest, which can change version without warning and is much larger. Pinning 3.14-slim gives a predictable, smaller image.
Key Takeaways
- A Dockerfile lists the steps to build an image: FROM, WORKDIR, COPY, RUN, EXPOSE, CMD.
- docker build -t name . builds an image from the current folder.
- Run your image like any other: docker run -d -p 5000:5000 --name api quotes-api.
- Servers in containers must listen on 0.0.0.0.
- Use the JSON form of CMD so your app shuts down cleanly.
Summary
You have containerised your first app. Next you will make builds fast with the build cache, keep junk out of images with .dockerignore, and version your images with tags.