Cloud & DevOps
Hands-onBeginner 40 min· Containers and pipelines · 4 of 16

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

TermMeaning
DockerfileText recipe that describes how to build an image
ImageThe built package (read-only); identified by name and tag, e.g. python:3.12-slim
ContainerA running (or stopped) instance of an image
LayerEach Dockerfile instruction adds a cached layer; unchanged layers are reused
RegistryA server that stores images (Docker Hub, GitHub Container Registry, AWS ECR)
VolumeStorage 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.

requirements.txt
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.

app/main.py
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

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.

.dockerignore
__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

Terminal
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.

Output
{"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

CommandWhat it does
docker psList running containers (-a includes stopped ones)
docker logs -f helloShow and follow the container's output
docker exec -it hello shOpen a shell inside a running container (type exit to leave)
docker stop helloStop it politely
docker start helloStart a stopped container again
docker rm helloDelete a stopped container (add -f to force)
docker rmi hello-docker:1.0Delete an image
docker system pruneRemove 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.

Terminal
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.txt

The 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.

Output
espresso

Make images small and builds fast

Habits of good images

  1. 1Order the Dockerfile from least to most frequently changing: base image, dependencies, then your code.
  2. 2Use slim or alpine base images when your dependencies allow it, and pin a tag (python:3.12-slim) rather than latest.
  3. 3Add a .dockerignore so .git, virtual environments and secrets are never sent.
  4. 4Use multi-stage builds when you need build tools but not in the final image (see below).
  5. 5Run as a non-root user, and never put secrets in the image: pass them at run time.
Dockerfile for a React + Vite site (multi-stage)
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

Terminal
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.0

docker 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.

Prompt · Review my Dockerfile
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

  1. I can explain image vs container vs volume
  2. My Dockerfile installs dependencies before copying code
  3. My app listens on 0.0.0.0 inside the container
  4. I use .dockerignore and pass secrets at run time
  5. I can read logs and open a shell in a running container

Key takeaways

  1. An image is the frozen package; a container is a running instance; containers are disposable, volumes persist data.
  2. Order Dockerfile steps so slow, rarely-changing layers come first and stay cached.
  3. Publish ports with -p host:container and bind the app to 0.0.0.0.
  4. Pass configuration as environment variables; never bake secrets into an image.
  5. docker logs, docker exec and docker ps -a solve most debugging problems.
  6. 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.

Start free