Neural Tech Daily
dev-tutorials

Docker fundamentals in 60 minutes: containerise a Python web app

Step-by-step tutorial: write a Dockerfile for a FastAPI app, run it with Docker Compose, and push the image to Docker Hub end-to-end in 60 minutes.

Updated ~12 min read
Share

The bottom line

A working containerised Python web app in 60 minutes is achievable for a developer with Docker Desktop installed, a terminal they’re comfortable in, and a free Docker Hub account. This tutorial walks through writing a Dockerfile for a small FastAPI service, building and running the image, layering Docker Compose on top so a database container can join later, and pushing the image to Docker Hub. The Dockerfile follows the structure the official FastAPI deployment docs recommend 1 , simplified so the reader can cross-reference the official template at any step. Every command is verifiable on a local machine; this is not a cloud-deploy tutorial.

Docker Compose v2 ships with Docker Desktop and is invoked as docker compose (with a space). The standalone Python docker-compose binary is the legacy v1 line and is no longer the canonical entry point 8 . This tutorial uses docker compose throughout.

Kubernetes, multi-stage builds, image-size optimisation, and production hardening are separate articles.

What you’ll build

The end state is a Python web app (a single-endpoint FastAPI service) packaged as a Docker image, running locally on port 8000, declared via a compose.yaml file so additional services can be added later, and published to Docker Hub under the reader’s namespace. The reader will write the Dockerfile by hand, run two docker commands, write one compose.yaml, run docker compose up, and run docker tag + docker push to publish.

Five files at the end:

  • app/main.py: the FastAPI app, three lines of routing.
  • requirements.txt: pinned dependencies.
  • Dockerfile: the container recipe.
  • .dockerignore: to keep the build context lean.
  • compose.yaml: the Compose declaration.

Prerequisites

Before starting, the reader needs:

  • Docker Desktop installed on macOS, Windows, or Linux, or Docker Engine + Docker Compose plugin on Linux. Confirm with docker --version and docker compose version. Docker Compose ships with Desktop on macOS and Windows; on Linux, the compose plugin is a separate package that lives in /usr/local/lib/docker/cli-plugins or $HOME/.docker/cli-plugins 8 .
  • Python 3.10 or newer is useful for running the app outside Docker first, though not strictly required since the container ships its own Python.
  • A free Docker Hub account. Sign up at hub.docker.com. The username chosen becomes the namespace for pushed images.
  • A terminal. Terminal.app or iTerm on macOS, any Linux terminal, PowerShell or WSL on Windows.
  • A code editor. VS Code, Cursor, or anything that highlights Python and YAML.

That’s the full prerequisite list. No cloud accounts, no Kubernetes, no registry beyond Docker Hub.

The FastAPI deployment-in-containers documentation page, showing the canonical Dockerfile template that this tutorial implements and extends

Image: FastAPI official “FastAPI in Containers - Docker” deployment page (fastapi.tiangolo.com/deployment/docker/), used for editorial coverage of the canonical Dockerfile template this tutorial follows.

The mental model

A Docker image is a read-only template that describes a filesystem and a default command. A container is a running instance of an image. The Dockerfile is the recipe that produces the image; docker build executes the recipe. docker run starts a container from an image. That’s the entire model the rest of the tutorial sits on top of.

Three Dockerfile instructions carry most of the weight. FROM picks the base image, usually an official language image like python:3.13-slim. COPY brings files from the build context (the directory docker build is invoked in) into the image. RUN executes a shell command at build time, typically to install dependencies. Two more shape the running container: WORKDIR sets the in-container working directory for subsequent instructions, and CMD declares the default command the container runs on docker run 2 . EXPOSE documents which port the container listens on but does not actually publish it; docker run -p does the publishing 2 .

That’s the full Dockerfile grammar a first containerisation needs.

Step 1: Project setup

Create a project directory and the four files the app needs:

mkdir docker-fastapi-demo
cd docker-fastapi-demo
mkdir app
touch app/main.py requirements.txt Dockerfile .dockerignore compose.yaml

Open the directory in an editor. The layout:

docker-fastapi-demo/
├── app/
│   └── main.py
├── requirements.txt
├── Dockerfile
├── .dockerignore
└── compose.yaml

Step 2: The FastAPI app

Paste the following into app/main.py:

from fastapi import FastAPI

app = FastAPI(title="docker-fastapi-demo")


@app.get("/")
def root() -> dict[str, str]:
    return {"status": "ok", "service": "docker-fastapi-demo"}


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "healthy"}

Two endpoints. / returns a hello payload; /health is the endpoint a container orchestrator’s health check will hit later (Kubernetes liveness probe, AWS ECS health check, Compose healthcheck block).

Pin dependencies in requirements.txt:

fastapi[standard]>=0.113.0,<0.114.0
pydantic>=2.7.0,<3.0.0

The fastapi[standard] extra installs the fastapi CLI and Uvicorn, which the Dockerfile’s CMD will invoke. These are the exact version pins the official FastAPI docker docs recommend at the time of writing 1 .

Step 3: The Dockerfile

Paste the following into Dockerfile:

FROM python:3.13-slim

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY ./app /code/app

EXPOSE 8000

CMD ["fastapi", "run", "app/main.py", "--port", "8000"]

Six instructions, in the order Docker recommends for build-cache efficiency 1 :

  1. FROM python:3.13-slim: the official Python image, slim variant (smaller footprint than the default, large enough that most C extensions still install). The FastAPI docs use python:3.14 as an example 1 ; 3.13-slim is a smaller, conservative choice for a first build.
  2. WORKDIR /code: sets the working directory for subsequent COPY / RUN / CMD instructions 2 . The directory is created if it doesn’t exist.
  3. COPY ./requirements.txt /code/requirements.txt: copies just the dependency manifest, not the code yet.
  4. RUN pip install ...: installs the dependencies. Because this layer depends only on requirements.txt, Docker caches the result; subsequent builds skip the install unless the manifest changes. This is the key cache-optimisation pattern.
  5. COPY ./app /code/app: copies the application code last, because it changes most often and would invalidate the cache for everything below it.
  6. CMD ["fastapi", "run", ...]: the default command, in JSON-array form (the “exec form”). FastAPI docs flag this explicitly: always use the exec form so the container handles SIGTERM correctly and FastAPI’s lifespan events fire 1 .

Add a .dockerignore so unrelated files don’t slow the build:

__pycache__
*.pyc
.git
.venv
.env
.pytest_cache
.ruff_cache
.DS_Store

.dockerignore works like .gitignore; files matching its patterns are excluded from the build context Docker uploads to the daemon, which keeps builds fast and prevents accidentally baking secrets into images.

Step 4: Build and run

From the project root, build the image:

docker build -t docker-fastapi-demo:0.1 .

The -t flag tags the image with a name and version; the . is the build context (the current directory). The first build takes 30–60 seconds depending on network speed (pulling the Python base image is the bulk of it). Subsequent builds with only code changes finish in 2–3 seconds because the dependency layer is cached.

Run the container:

docker run --rm -p 8000:8000 docker-fastapi-demo:0.1

Three flags worth knowing: --rm removes the container when it exits (handy for one-off runs); -p 8000:8000 publishes the container’s port 8000 to the host’s port 8000 (the format is host:container); the image name and tag come last.

In another terminal, hit the endpoint:

curl http://localhost:8000/health

The response should be {"status":"healthy"}. Stop the container with Ctrl+C.

If curl returns “connection refused”, check that the docker run terminal is still showing Uvicorn logs. If docker run exited immediately, run docker logs $(docker ps -lq) against the most recent container to see the crash message.

The official Docker Dockerfile reference page on docs.docker.com listing every instruction (FROM, COPY, RUN, CMD, WORKDIR, EXPOSE, ENV, ENTRYPOINT) that this tutorial uses

Image: Docker official Dockerfile reference (docs.docker.com/reference/dockerfile/), used for editorial coverage of the instruction set this tutorial implements.

Step 5: Add Docker Compose

Compose declares the run-time config of one or more containers in a single YAML file, so the reader doesn’t have to remember the -p flag, environment variables, or volume mounts on every docker run. For a single-container app it’s overkill; for any app that grows a database, a worker, or a cache, it’s the natural extension.

Paste into compose.yaml:

services:
  api:
    build: .
    image: docker-fastapi-demo:0.1
    ports:
      - "8000:8000"
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped

Five blocks:

  • build: . tells Compose to build the image from the Dockerfile in the current directory if it isn’t already present.
  • image: names the image Compose creates (same tag the manual docker build used).
  • ports: publishes 8000 on host to 8000 in container, same as -p 8000:8000.
  • healthcheck: Compose’s built-in periodic check. The test runs a Python one-liner that hits /health; if it fails three times in a row, Compose marks the container unhealthy.
  • restart: unless-stopped restarts the container on crashes (but not when the reader runs docker compose stop).

Start the stack:

docker compose up

The first run builds the image; subsequent runs reuse it. docker compose up -d runs in the background (detached); docker compose logs -f api follows the logs; docker compose down stops everything. The compose-file schema accepts dozens more fields (volumes, secrets, depends_on, networks); the Compose file reference is the canonical place to look them up 9 .

Step 6: Push to Docker Hub

The image lives only on the local machine until it’s pushed to a registry. Docker Hub is the default registry for docker pull (when no registry is named, Docker assumes Hub), and the free tier is enough for a personal portfolio repo.

Log in from the terminal:

docker login

The CLI prompts for the Docker Hub username and password (or a personal access token, which is the recommended path; generate one under Account Settings → Security on hub.docker.com). The session is cached so subsequent pushes don’t re-prompt.

Tag the image with the Docker Hub namespace. The format is [NAMESPACE/]REPOSITORY[:TAG]; if no namespace is given, Docker uses library, which is reserved for official images 4 . Replace myuser with the actual Docker Hub username:

docker tag docker-fastapi-demo:0.1 myuser/docker-fastapi-demo:0.1
docker tag docker-fastapi-demo:0.1 myuser/docker-fastapi-demo:latest

Two tags now point at the same image: 0.1 and latest. If no tag is provided on push or pull, Docker defaults to latest 4 .

Push:

docker push myuser/docker-fastapi-demo:0.1
docker push myuser/docker-fastapi-demo:latest

Or push all tags in one call:

docker image push --all-tags myuser/docker-fastapi-demo

The --all-tags flag is documented in the docker image push CLI reference 3 . The Docker Hub web UI under hub.docker.com/r/myuser/docker-fastapi-demo should show both tags within seconds.

To prove the round-trip works, on a different machine (or after deleting the local image with docker rmi), run:

docker run --rm -p 8000:8000 myuser/docker-fastapi-demo:0.1

Docker pulls the image from Hub and runs it. The reader’s container is now portable.

Things that commonly go wrong

A short list of things that bite first-timers, with the fix for each:

  • docker: command not found: Docker Desktop isn’t running (on macOS / Windows) or the user isn’t in the docker group (on Linux). On Linux, sudo usermod -aG docker $USER and log out / back in.
  • Cannot connect to the Docker daemon: same root cause. Start Docker Desktop, or sudo systemctl start docker on Linux.
  • port is already allocated: something else is bound to 8000. Either stop it or change the host side: -p 8001:8000.
  • docker compose: 'compose' is not a docker command: the Compose plugin isn’t installed. On Docker Desktop, update to the latest version; on standalone Linux installs, install the docker-compose-plugin package from Docker’s apt/yum repo.
  • Image builds, container exits immediately: docker logs $(docker ps -lq) shows the stack trace. Common cause: typo in app/main.py or a missing import.
  • denied: requested access to the resource is denied on push: either docker login hasn’t been run, or the tag’s namespace doesn’t match the logged-in username.

What was deliberately skipped

This tutorial covers the smallest end-to-end loop. A production image would also use a multi-stage build (a separate build stage with build dependencies, a final runtime stage with only the wheels), a non-root user (USER appuser after creating one), a tighter base image (python:3.13-slim-bookworm or distroless), explicit healthcheck and resource limits, and signed images via Docker Content Trust or Cosign. None of those change the mental model; they harden it.

Layer caching, build args, multi-arch builds with Buildx, and registry alternatives (GitHub Container Registry, AWS ECR, Google Artifact Registry) are also separate articles.

The Docker Compose overview page on docs.docker.com documenting the docker compose CLI plugin that this tutorial uses to declare the FastAPI service

Image: Docker official Compose overview (docs.docker.com/compose/), used for editorial coverage of the docker compose plugin this tutorial sets up.

Recap

A FastAPI container in five files, three commands. The Dockerfile copies dependencies before code so the build cache stays warm; Compose adds a single-file run config that scales to multiple services; Docker Hub gives the image a permanent home that any other machine can pull. The FastAPI official Docker deployment docs are the canonical reference for the Dockerfile pattern 1 ; the Docker Dockerfile reference 2 , push CLI reference 3 , and Compose docs 7 cover the underlying CLI in full detail.

The Docker Hub push-images page on docs.docker.com documenting the docker push command, namespace format, and tag conventions this tutorial uses

Image: Docker official “Push images to a repository” docs (docs.docker.com/docker-hub/repos/manage/hub-images/push/), used for editorial coverage of the docker push workflow this tutorial follows.

How this article was made: an autonomous AI pipeline researched, drafted, fact-checked, and reviewed this piece, aggregating publicly-available information from the sources consulted below. AI (artificial intelligence) can make mistakes, so please cross-check the consulted sources before acting on anything here. Neural Tech Daily is not liable for decisions or outcomes based on this article.

Sources consulted

Cited Sources

  1. 1. FastAPI — official "FastAPI in Containers - Docker" deployment docs (canonical Dockerfile template with FROM python, WORKDIR /code, COPY requirements before code, fastapi run CMD in exec form) (accessed )
  2. 2. Docker — official Dockerfile reference (canonical specification for FROM, WORKDIR, COPY, RUN, CMD, EXPOSE instructions and cache behaviour) (accessed )
  3. 3. Docker — docker image push CLI reference (push command syntax, --all-tags flag documentation) (accessed )
  4. 4. Docker — docker image tag CLI reference (tag format NAMESPACE/REPOSITORY:TAG, latest default behaviour, library namespace for official images) (accessed )
  5. 5. Docker — "Build, tag, and publish an image" getting-started tutorial (the canonical end-to-end build + tag + push sequence) (accessed )
  6. 6. Docker — "Push images to a repository" Docker Hub docs (docker login, tag format with username namespace, push verification on Hub UI) (accessed )
  7. 7. Docker — Docker Compose overview (the canonical entry point for the docker compose plugin) (accessed )
  8. 8. docker/compose — official GitHub repository (Compose v2 written in Go, invoked as docker compose; v5.1.3 latest release as of April 2026; install location $HOME/.docker/cli-plugins on Linux) (accessed )
  9. 9. Docker — Compose file reference (canonical schema for services, build, image, ports, healthcheck, restart, volumes, networks) (accessed )

Anonymous · no cookies set

Report a problem with this article

Articles are produced by an autonomous AI pipeline; mistakes do happen. Tell us what's wrong and the editorial review will revisit the claim.

Category

Found this useful? Share it.