Docker: images and containers
Package an app into a Docker image and run it as a container. Learn layers and caching, port mapping, environment variables, volumes, multi-stage builds and registries, plus the commands for logs and debugging.
What you'll build: a small Python web service, packaged as a Docker image and run as a container on your own machine. You will learn what images, containers and layers are, how to expose ports, pass settings, read logs, use volumes, make builds fast, and share an image through a registry.
Prerequisites: Docker installed and running. On Windows or macOS install Docker Desktop (free for personal learning use; check Docker's current terms if you use it at work). On Linux install Docker Engine. Check with docker --version. You should know the basics of the terminal and ports (see the two earlier lessons in this track). The backend track has a FastAPI-focused version, *Containerising a backend with Docker* in Backend Development; this lesson explains Docker itself in more depth and applies to any language.
Images, containers and layers
A Dockerfile is a recipe. Building it produces an image: a read-only package containing your code, the language runtime and every library, all frozen together. Running an image starts a container: an isolated process that thinks it has its own small Linux computer. You can start many containers from one image, like many cakes from one recipe.
Containers are not virtual machines. They share the host's operating system kernel, so they start in a second and use little memory. The promise is simple: if it runs in the container on your laptop, it runs the same way on a server.
Vocabulary
| Term | Meaning |
|---|---|
| Dockerfile | Text recipe that describes how to build an image |
| Image | The built package (read-only); identified by name and tag, e.g. python:3.12-slim |
| Container | A running (or stopped) instance of an image |
| Layer | Each Dockerfile instruction adds a cached layer; unchanged layers are reused |
| Registry | A server that stores images (Docker Hub, GitHub Container Registry, AWS ECR) |
| Volume | Storage that outlives a container |
| Port mapping | -p 8000:8000 connects a host port to a container port |
Step 1: a tiny app to package
Create a folder hello-docker with this structure: requirements.txt, app/main.py, Dockerfile, .dockerignore.
fastapi==0.115.6
uvicorn[standard]==0.34.0
Pinned versions keep builds repeatable. If pip cannot find these exact versions in the future, replace them with the current ones from pypi.org.
import os
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/hello")
def hello():
shop = os.environ.get("SHOP_NAME", "Brew & Bean")
return {"message": f"Welcome to {shop}"}
Reads the shop name from an environment variable, with a default. Configuration through environment variables is how containers get their settings.
Step 2: the Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
FROM picks the base image (slim is a smaller Debian-based Python). WORKDIR sets the working folder. We copy requirements.txt and install BEFORE copying the code, so the slow install layer is cached and only re-runs when dependencies change. USER runs the app as a normal user instead of root. EXPOSE documents the port (it does not publish it). CMD is the command the container runs; --host 0.0.0.0 is essential or the app is unreachable from outside the container.
__pycache__/
*.pyc
.venv/
.git/
.env
Dockerfile
Files listed here are not sent to the build, which keeps images small and stops secrets like .env from being copied into an image.
Step 3: build and run
docker build -t hello-docker:1.0 .
docker images
docker run -d --name hello -p 8000:8000 -e SHOP_NAME="Brew & Bean Indiranagar" hello-docker:1.0
docker ps
curl http://localhost:8000/hello-t names and tags the image. The final . is the build context (the current folder). In docker run: -d runs in the background, --name names the container, -p host:container publishes a port and -e sets an environment variable.
{"message":"Welcome to Brew & Bean Indiranagar"}If the build fails on the pinned versions, edit requirements.txt to versions that exist and build again. If port 8000 is already used on your machine, publish another host port, for example -p 8080:8000, and call localhost:8080.
Everyday container commands
Cheat sheet
| Command | What it does |
|---|---|
docker ps | List running containers (-a includes stopped ones) |
docker logs -f hello | Show and follow the container's output |
docker exec -it hello sh | Open a shell inside a running container (type exit to leave) |
docker stop hello | Stop it politely |
docker start hello | Start a stopped container again |
docker rm hello | Delete a stopped container (add -f to force) |
docker rmi hello-docker:1.0 | Delete an image |
docker system prune | Remove stopped containers, unused networks and dangling images (it asks first) |
Containers write logs to standard output, and docker logs reads them. That is why apps in containers should print logs to the console instead of writing log files. Containers are also disposable: anything written inside a container's own filesystem is lost when you remove it. For data you want to keep, use a volume.
docker volume create menu-data
docker run --rm -v menu-data:/data alpine sh -c "echo espresso > /data/menu.txt"
docker run --rm -v menu-data:/data alpine cat /data/menu.txtThe first container writes a file into the volume and is removed (--rm); the second container, a different one, still reads the file. Data lives in the volume, not in the container.
espressoMake images small and builds fast
Habits of good images
- 1Order the Dockerfile from least to most frequently changing: base image, dependencies, then your code.
- 2Use slim or alpine base images when your dependencies allow it, and pin a tag (
python:3.12-slim) rather thanlatest. - 3Add a
.dockerignoreso.git, virtual environments and secrets are never sent. - 4Use multi-stage builds when you need build tools but not in the final image (see below).
- 5Run as a non-root user, and never put secrets in the image: pass them at run time.
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
The first stage has Node and builds the site into dist. The second stage starts fresh from a tiny Nginx image and copies only the built files, so the final image contains no Node, no source and no node_modules.
Secrets in images
Anything you COPY or set with ENV in a Dockerfile can be read by anyone who gets the image, even if you delete the file in a later step (earlier layers still contain it). Never bake in API keys, passwords or .env files. Pass them at run time with -e, --env-file or your platform's secret store.
Share an image with a registry
docker login
docker tag hello-docker:1.0 YOUR-DOCKERHUB-USERNAME/hello-docker:1.0
docker push YOUR-DOCKERHUB-USERNAME/hello-docker:1.0
docker pull YOUR-DOCKERHUB-USERNAME/hello-docker:1.0docker tag gives the image a registry name. After push, any machine (a cloud server, a CI job, a teammate) can pull and run exactly the same image. Docker Hub has a free tier for public images; GitHub Container Registry and AWS ECR are alternatives.
Review this Dockerfile for a [language/framework] app. Point out problems with image size, build speed (layer caching), security (root user, secrets) and reliability. Then give an improved version and explain each change in one line.
[paste Dockerfile]What to expect: Check that the answer proposes caching dependencies before copying code, a pinned slim base, a non-root user and a .dockerignore. Test the improved version yourself before trusting it.
Troubleshooting: if a container exits straight away, run docker ps -a then docker logs <name> to read the error. If the app runs but you cannot reach it, check the port mapping with docker ps (the PORTS column) and that the app listens on 0.0.0.0. If a build is slow every time, a COPY . . before the dependency install is the usual cause.
How this comes up in interviews
Typical questions: the difference between an image and a container, between a container and a virtual machine, what layers and caching are, CMD versus ENTRYPOINT, how to persist data (volumes), how to keep images small (multi-stage, slim bases) and why you should not run as root or store secrets in images. Next: *Docker Compose for multi-service apps* runs several containers together.
Try it: Containerise one of your own projects
Write a Dockerfile and .dockerignore for a project you already have (Python, Node or Java). Build it, run it with a published port, view its logs and open a shell inside it. Then change one line of code and rebuild: check in the build output that the dependency install step says CACHED.
Self-check
- I can explain image vs container vs volume
- My Dockerfile installs dependencies before copying code
- My app listens on 0.0.0.0 inside the container
- I use .dockerignore and pass secrets at run time
- I can read logs and open a shell in a running container
Key takeaways
- An image is the frozen package; a container is a running instance; containers are disposable, volumes persist data.
- Order Dockerfile steps so slow, rarely-changing layers come first and stay cached.
- Publish ports with
-p host:containerand bind the app to0.0.0.0. - Pass configuration as environment variables; never bake secrets into an image.
docker logs,docker execanddocker ps -asolve most debugging problems.- Registries such as Docker Hub let any machine pull the exact same image.
Check yourself
Finishing the quiz marks the lesson as done.
1. What is the main difference between a Docker image and a container?
2. Why does the sample Dockerfile copy requirements.txt and run pip install before copying the app code?
3. Your container is running but http://localhost:8000 does not respond from your laptop. Which two things do you check first?
4. You write a file inside a container, then remove the container. What happens to the file?
5. Which practice keeps a database password safe when building images?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.