Build a Small Image With a Dockerfile
Write a Dockerfile for a small Python app, build it, run it and change it, and see which steps Docker reuses. After this, you can package your own app into an image.
Course content
Sign in to save your progress.
-
Before You Run Anything
-
What Docker Is and Why Containers Exist
-
Install Docker Desktop on Windows With WSL2
-
-
-
Your First Containers
-
Run, Stop and Remove a Container
-
Logs, Exec and Copying Files In and Out
-
-
-
Images
-
Pull an Image and See What Layers Are
-
Build a Small Image With a Dockerfile
-
-
-
Data and Networks
-
Volumes: Keep Data When a Container Dies
-
Networks: Let Two Containers Talk by Name
-
-
-
More Than One Service
-
Docker Compose: Two Services With One File
-
Clean Up: Images, Containers and Disk Space
-
When Something Won't Start
-
-
- Final Check: Docker for Beginners
Skip ahead: If you have written a Dockerfile before, skip to "When the port is open but nothing answers", then go on to the next lesson.
So far you have run images other people built. Now you build your own. A Dockerfile is a short text file with the steps that make an image. This lesson packs a 12-line Python web server into an image, runs it, and then changes it to show what the layer cache does. The same app comes back in the Compose lesson.
Make a new folder and create two files in it. Any editor works, as long as the file is named Dockerfile with no extension.
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
HOST = os.environ.get("HOST", "0.0.0.0")
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b"Hello from a container\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer((HOST, 8000), Handler).serve_forever()
FROM python:3.14-alpine
WORKDIR /app
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]
Line by line:
FROM python:3.14-alpinestarts from an existing image. Every Dockerfile begins with this.WORKDIR /appsets the working folder inside the image and creates it.COPY app.py .copiesapp.pyfrom your folder into the image's working folder.EXPOSE 8000documents which port the app listens on. It does not publish anything. You still need-pwhen you run it.CMD ["python", "app.py"]is the command the container runs. The square-bracket form runs the program directly, without a shell in between, sodocker stopcan reach it.
Build it
docker build -t hello-app:1.0 .
#1 [internal] load build definition from Dockerfile
#5 [1/3] FROM docker.io/library/python:3.14-alpine
#6 [2/3] WORKDIR /app
#7 [3/3] COPY app.py .
-t hello-app:1.0 names and tags the image. The dot at the end is the build context: the folder whose files the build may copy from. Docker sends that folder to the engine, so build from a folder that holds the app and not from your whole user profile. The numbered steps are your Dockerfile instructions, in order. Each COPY, RUN and WORKDIR produces a layer.
docker image ls hello-app
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
hello-app:1.0 15fbdfa41aea 82MB 20.4MB
Run it
docker run -d --name hello-app -p 8081:8000 hello-app:1.0
76cb1a3cbcea6e713148edd907d61b9abed07aa0c1a4ead2dab9304f7a548c0c
curl.exe -s http://localhost:8081
Hello from a container
docker logs hello-app
172.17.0.1 - - [15/Oct/2026 07:55:28] "GET / HTTP/1.1" 200 -
The log line is the Python server reporting the request, so you know the request reached your app and not some other program on port 8081. The ID printed by docker run and the text from curl are separate commands above. I split them because a server needs a second to start, and a command typed straight after docker run can arrive too early.
Change it and watch the cache
Edit app.py: change the text to "Hello again", then build again.
(Get-Content app.py) -replace 'Hello from a container', 'Hello again' | Set-Content app.py
docker build -t hello-app:1.1 .
#6 [2/3] WORKDIR /app
#6 CACHED
#7 [3/3] COPY app.py .
The WORKDIR step says CACHED. Docker saw that nothing before the COPY had changed, so it reused that layer, and it rebuilt only the layers from the changed file onward. That is why the order of lines in a Dockerfile matters. Put what changes rarely first (the base image, installed packages) and what changes often last (your code). A build that takes minutes the first time can take seconds the second time.
When the port is open but nothing answers
The most common first-container bug is a server that listens on 127.0.0.1 inside the container. From inside, that address means only the container itself, so a published port never reaches it. The app reads HOST from the environment, so you can reproduce the mistake.
docker run -d --name hello-local -e HOST=127.0.0.1 -p 8082:8000 hello-app:1.1
102bc7e879c1f3f298187e313292e897ba1cd6c01ff050972aa81c41294bc22a
curl.exe -sS http://localhost:8082
curl: (52) Empty reply from server
The container is running and the port is published, but the answer is an empty reply. The fix is to listen on 0.0.0.0 inside the container, which means "every network interface". Then your published port reaches the app. Run docker logs first when something like this happens. A server that is up shows no error, so the log will not blame anyone. Check which address the app binds to.
⚠ Warning: Never COPY secrets such as passwords or API keys into an image. Anyone who gets the image can read every layer, and deleting the file in a later step does not remove it from the earlier layer.
💡 Tip: A .dockerignore file next to the Dockerfile lists files the build must not send, such as .git and node_modules. It makes builds faster and keeps junk out of the image. Add it as soon as your folder holds more than the app.
docker rm -f hello-app hello-local
If you change one line near the top of a Dockerfile, which layers does Docker have to rebuild?
About
Write a Dockerfile for a small Python app, build it, run it and change it, and see which steps Docker reuses. After this, you can package your own app into an image.
- Duration
- 5 min
- Last update
- 08/03/2026
Comments (0)
Sign in to commentNo comments yet.