| scripts | ||
| serve | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| compose.yml | ||
| Dockerfile | ||
| pyproject.toml | ||
| README.md | ||
Serve
Serve is a small Python application packaged as Docker images whose only job is to gather, build, and serve a static website with minimal setup.
It supports:
- Git gather mode: clone a repository, poll for updates, rebuild, and redeploy.
- Local gather mode: watch a mounted folder, rebuild, and redeploy on changes.
- Optional build step: run any shell command before publishing.
- Caddy serving: serve the chosen output directory through Caddy with gzip/zstd enabled.
- Multiple image variants: keep the runtime identical while swapping in language toolchains.
Image variants
Serve uses python:3.12-slim as its base and installs the official Caddy 2.9.1 binary on top. That keeps the runtime Python-native while still shipping a real Caddy server in every image.
| Tags | Stage | Includes | Best for |
|---|---|---|---|
serve:base, serve:latest |
base |
Caddy, Python 3, pip, git, OpenSSH client, bash, curl, xz-utils | Pure static sites or projects that already vendor their build tools |
serve:npm, serve:node |
npm |
Everything in base, plus Node.js, npm, npx, corepack, pnpm, yarn, build-essential |
Next.js export, Vite, Astro, VuePress, Docusaurus, and other JS static builds |
serve:python, serve:py |
python |
Everything in base, plus conda, mamba, uv, pipx, virtualenv, poetry, hatch, build-essential |
MkDocs, Sphinx, Pelican, Jupyter Book, and other Python static builds |
How it works
- Gather the source from either a Git remote or a local mounted folder.
- Copy the source into a timestamped deployment workspace.
- Run
BUILD_COMMANDwhen it is set. - Resolve
SERVE_PATHinside that deployment. - Atomically repoint
/srv/currentto the new output so Caddy serves the new release immediately.
The Python code is now built around a small step DSL:
serve/environment.py: reusable environment parsing primitivesserve/pipeline.py: the program/recipe/execution abstractionsserve/steps/gather.py: gather env loading, validation, and trigger strategyserve/steps/build.py: optional build env loading and build executionserve/steps/serve.py: serve env loading, publish-root resolution, and Caddy wiringserve/deploy.py: deployment lifecycle that consumes the loaded stepsserve/runtime.py: the application shell that composes and runs the step program
The default runtime is declared as a program composed of GatherStep, BuildStep, and ServeStep. Adding another step means implementing another module that loads its own environment and applies itself to the shared runtime recipe before execution.
That also means extension is just composition. A future anubis.py step could be inserted by building a custom program such as RuntimeProgram.composed_of(GatherStep(), BuildStep(), AnubisStep(), ServeStep()) and passing it to ServeApplication.from_env(..., program=...).
Configuration
These environment variables control the whole runtime:
##################
# === GATHER === #
##################
# GATHER from Git repo
GATHER_FROM: https://github.com/Malasaur/MySite
GATHER_GIT_PAT: ...
GATHER_EVERY: 1m
# GATHER from local folder
GATHER_FROM: /folder
GATHER_EVERY: 1m
# GATHER_FROM defaults to /data
#################
# === BUILD === #
#################
BUILD_COMMAND: npm run site_build
#################
# === SERVE === #
#################
SERVE_PATH: ./dist
SERVE_PORT: 80
SERVE_ON: 8000
Defaults and behavior
| Variable | Default | Notes |
|---|---|---|
GATHER_FROM |
/data |
Local directory by default |
GATHER_GIT_PAT |
unset | Used only for HTTP(S) Git remotes |
GATHER_EVERY |
1m for Git, unset for local folders |
Local folders use event-driven watching when this is unset |
BUILD_COMMAND |
unset | Runs inside the copied deployment workspace |
SERVE_PATH |
. |
Resolved relative to the deployment workspace unless absolute |
SERVE_PORT |
unset in the app, 80 in compose.yml |
Used by Compose for host-to-container port mapping |
SERVE_ON |
8000 |
Port Caddy listens on inside the container |
Compose usage
The included compose.yml works out of the box for local-folder mode:
docker compose up --build
By default it:
- builds
serve:latestfrom thebasetarget - mounts
./siteto/data - serves the site on
http://localhost:80
Local folder example
services:
serve:
image: serve:node
environment:
GATHER_FROM: /data
BUILD_COMMAND: pnpm install && pnpm run build
SERVE_PATH: dist
SERVE_PORT: 80
SERVE_ON: 8000
ports:
- "80:8000"
volumes:
- ./site:/data
- serve-state:/var/lib/serve
Git repository example
services:
serve:
image: serve:py
environment:
GATHER_FROM: https://github.com/Malasaur/MySite
GATHER_GIT_PAT: ${GATHER_GIT_PAT:-}
GATHER_EVERY: 1m
BUILD_COMMAND: poetry install && poetry run mkdocs build
SERVE_PATH: site
SERVE_PORT: 8080
SERVE_ON: 8000
ports:
- "8080:8000"
volumes:
- serve-state:/var/lib/serve
Building the images
Build every published tag with:
./scripts/build-images.sh
Or build a single target manually:
docker build --target base -t serve:base -t serve:latest .
docker build --target npm -t serve:npm -t serve:node .
docker build --target python -t serve:python -t serve:py .
Extending Serve
To add another toolchain image, extend the base stage in Dockerfile, install the extra tools, and tag that new stage. The runtime code does not need to change unless the gather/build/deploy behavior itself changes.