7 The server in Docker
Build it yourself. Read the tutorial is the other way to read this part.
This walk goes through rahuldave/cheesecracker on GitHub. This part starts from the code at step-06. The code before this part
If you start at this part, make your clone as Get the project says, and then run this command. It puts in your clone the files that this part starts from. A file that the project deleted before this part stays, so delete it if git check lists it:
git fetch --quiet --tags --force origin; git rm -rq --cached --ignore-unmatch -- :/ && git restore --source step-06 --staged --worktree -- :/just setupThe server runs on this machine, with its .venv and this version of macOS or Linux. Production is another machine,
such as a server on Google Cloud. A Docker image holds the server with everything it needs, so it runs the same on
any machine with Docker. The run image is the image that production runs, our deployment.
7.1 what docker build leaves out
Docker runs programs in containers. A container is a process with its own files, cut off
from the rest of the machine. A Dockerfile lists the instructions that build an image. It starts from a base system, such
as Debian with Python, and adds the files and the commands. docker build makes the image, and docker run starts a
container from it. On macOS and Windows, the app Docker Desktop runs Docker.
docker build sends the files of the folder to Docker. The folder has .venv, the caches of the tools, git and maybe
secrets. None of them belongs in an image. .dockerignore lists them.
Apply the patch to .dockerignore, from the top of the project:
curl -fsSL https://github.com/rahuldave/cheesecracker/commit/2a28a5c704d6d9ed8200aeb8962aa01e5c2ac552.diff | git apply --include=.dockerignoreApply the patch, and read its list.
cat .dockerignorecat prints the list.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check 2a28a5c704d6If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git fetch --quiet --tags --force origin; git restore --source step-06 --staged -- :/ && git restore --source 2a28a5c704d6 --staged --worktree -- :/What changed
.dockerignore: whatdocker buildnever sends to Docker. New, 10 lines. The file- look at
.venv/
- look at
The patch to .dockerignore
@@ -0,0 +1,10 @@
+# What docker build never sends to Docker: the environment, the caches, git and the secrets
+.venv/
+__pycache__/
+.pytest_cache/
+.ruff_cache/
+.coverage
+htmlcov/
+.git/
+secrets/
+.env7.2 the run image, the server as it runs in production
The run image needs Python, the dependencies of the server, and the code. It does not need uv, the tests, ruff or
pre-commit. The Dockerfile builds the image in stages, and the run image takes only what the server needs.
Apply the patch to Dockerfile, from the top of the project:
curl -fsSL https://github.com/rahuldave/cheesecracker/commit/0acb4556f724cd94e4a71ab4fff739a974413fc0.diff | git apply --include=DockerfileApply the patch, and read the three stages, which start at each FROM. Read the line RUN uv sync in the stage
build, and the line COPY --from=build and the CMD in the stage run.
docker build --target run --tag cheesecracker-run .Docker prints each line of the Dockerfile as it runs. Near the end, the line naming to docker.io/library/cheesecracker-run names the image.
docker image ls cheesecracker-runThe image is about 173 MB, nearly all of it Python and the dependencies of the server.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check 0acb4556f724If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git restore --source 2a28a5c704d6 --staged -- :/ && git restore --source 0acb4556f724 --staged --worktree -- :/What changed
Dockerfile: the stagesuv,buildandrun. New, 34 lines. The file- look at
RUN uv sync --locked --no-default-groups
- look at
The patch to Dockerfile
@@ -0,0 +1,34 @@
+# The images of the API server, built from uv.lock, so they hold the versions that the lockfile names.
+#
+# docker build --target run --tag cheesecracker-run .
+# the production image: Python, the dependencies of the server and its code. It starts the server
+#
+# The environment lives in /opt/venv, never in the project folder.
+
+# uv itself, copied from its own image
+FROM ghcr.io/astral-sh/uv:0.12.13 AS uv
+
+# The environment of the server, without the dependency groups: no tests, no ruff, no pre-commit
+FROM python:3.12-slim-bookworm AS build
+COPY --from=uv /uv /usr/local/bin/uv
+ENV UV_PROJECT_ENVIRONMENT=/opt/venv \
+ UV_PYTHON_DOWNLOADS=never \
+ UV_COMPILE_BYTECODE=1 \
+ UV_LINK_MODE=copy \
+ UV_NO_CACHE=1
+WORKDIR /app
+COPY pyproject.toml uv.lock .python-version ./
+RUN uv sync --locked --no-default-groups
+
+# The run image: Python, the environment from the build stage, and the code. No uv, and no dev tools
+FROM python:3.12-slim-bookworm AS run
+RUN useradd --create-home --uid 1000 app
+COPY --from=build /opt/venv /opt/venv
+WORKDIR /app
+COPY src/api-service/api ./api
+ENV PATH=/opt/venv/bin:$PATH \
+ PYTHONUNBUFFERED=1
+USER app
+EXPOSE 9000
+# Four workers, and no restart on a change: the server as it runs in production
+CMD ["uvicorn", "api.service:app", "--host", "0.0.0.0", "--port", "9000", "--workers", "4"]7.3 just docker-build, just docker-serve and just docker-stop
Three recipes give the Docker commands short names. docker-build builds an image of the Dockerfile.
docker-serve starts the server of the run image in the background, at port 9000, and docker-stop stops it.
Apply the patch to justfile, from the top of the project:
curl -fsSL https://github.com/rahuldave/cheesecracker/commit/d444e0387d256c62024581e98f59d670cbc93296.diff | git apply --include=justfileRead the recipes docker-build, docker-serve and docker-stop at the end of the justfile.
just docker-build runDocker takes each layer that has not changed since the last build from its cache, and prints CACHED for it.
just docker-serveThe recipe prints docker-serve: http://localhost:9000, in the container cheesecracker-run. Ask the server for its
health.
curl -s localhost:9000/healthThe server in Docker answers {"status":"healthy"}, as the server on this machine did. The container runs in the
background, so you do not see what it prints. Docker keeps that output, and docker logs shows it.
docker logs cheesecracker-runuvicorn printed Uvicorn running on http://0.0.0.0:9000, and started four server processes, its workers.
just docker-stopDocker prints the name of the container, cheesecracker-run, and the server stops.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check d444e0387d25If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git fetch --quiet --tags --force origin; git restore --source 0acb4556f724 --staged -- :/ && git restore --source step-07 --staged --worktree -- :/What changed
justfile: the recipesdocker-build,docker-serveanddocker-stop. 20 lines added. The file- look at
docker run --rm --detach --name cheesecracker-run --publish 9000:9000 cheesecracker-run > /dev/null || exit 1
- look at
The patch to justfile
@@ -62,3 +62,23 @@ check:
just fmt --check
just lint
just test -q
+
+# Build an image of the Dockerfile: just docker-build run
+docker-build kind:
+ docker build --target "$1" --tag "cheesecracker-$1" .
+
+# Start the server in the run image, in the background, at http://localhost:9000, as it runs in production. just docker-stop stops it
+docker-serve:
+ #!/usr/bin/env sh
+ if curl -sf localhost:9000/health > /dev/null; then echo "docker-serve: a server already answers at http://localhost:9000" >&2; exit 1; fi
+ docker run --rm --detach --name cheesecracker-run --publish 9000:9000 cheesecracker-run > /dev/null || exit 1
+ for i in $(seq 1 30); do
+ if curl -sf localhost:9000/health > /dev/null; then echo "docker-serve: http://localhost:9000, in the container cheesecracker-run"; exit 0; fi
+ sleep 1
+ done
+ echo "docker-serve: no answer after 30 seconds; read docker logs cheesecracker-run" >&2
+ exit 1
+
+# Stop the server of just docker-serve, and remove its container
+docker-stop:
+ docker stop cheesecracker-runThis part ends at the code at step-07. The code after this part