Fast, full-stack Python application with quick build times.
In this post, we’ll look at my Dockerfile:
# Dockerfile
FROM ghcr.io/astral-sh/uv:latest AS uv
FROM python:3.12-slim
# Install the required system packages
RUN apt-get update && apt-get install -y \
\
git \
ffmpeg && apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Set the environment variables
ENV PATH=/root/.local/bin:$PATH
# Set the working directory
WORKDIR /app
# Copy the files
COPY readme.md /app/readme.md
COPY pyproject.toml /app/pyproject.toml
COPY .git /app/.git
COPY src /app/src
# Install the Python packages
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=from=uv,source=/uv,target=./uv \
./uv pip install 'my-package @ .' --system --upgrade
This allow for a twelve-factor-style Python application deployed with Docker. I use a Docker compose YAML file with a backend server and GUI container, both running the same image with different CLI commands (e.g. my-package server
and my-package gui
) and mounting a ~/.my-package
directory for secrets and persistent data.
My rough Python stack includes:
- FastAPI: web server + continuous data ingestion and ETL
- Typer: CLI
- SQLite and/or Postgres: OLTP database
- DuckDB: OLAP database (views on top of the OLTP tables)
- Ibis: table management and queries
- Shiny for Python: GUI
Of course, many other tools and Python packages are used along the way (and you can use whatever you like!), but this stack contained in a single Dockerfile allows deploying a single-node full-stack Python application with minimal build times locally, on a Raspberry Pi, and on a cloud VM with ease.
Some miscellaneous notes:
- order: the Dockefile is ordered so that frequently changing parts are toward the bottom
- system installs: if you don’t need these, you can remove them. I also need to re-add Postgres here if using it (I’m not currently in the project I copied this from)
- cache: the big point is the caching line, which I stole from somewhere (here?)
- git: I wouldn’t recommend copying the
.git
directory (or intallinggit
), but I’m kinda-sorta using it (I might remove it) - readme.md: the
pyproject.toml
is needed as it defines the installation of the package (and CLI), and thereadme.md
is needed because it’s referenced in thepyproject.toml
- src: this is the source code, in something like
src/my_package
, and obviously needs to be copied - pinning verions: in general I’m not pinning (or using a
uv
lockfile), but I would obviously recommend doing so for production workloads and having a process for updating dependencies
Combining this with a few justfile
commands makes development and deployment very easy.