No description
Find a file
2026-05-19 16:15:52 +02:00
scripts Initial Serve implementation 2026-05-18 19:00:42 +02:00
serve Format everything 2026-05-19 16:15:52 +02:00
tests Refactor runtime into step modules 2026-05-19 15:38:50 +02:00
.dockerignore Initial Serve implementation 2026-05-18 19:00:42 +02:00
.gitignore Initial Serve implementation 2026-05-18 19:00:42 +02:00
compose.yml Initial Serve implementation 2026-05-18 19:00:42 +02:00
Dockerfile Initial Serve implementation 2026-05-18 19:00:42 +02:00
pyproject.toml Initial Serve implementation 2026-05-18 19:00:42 +02:00
README.md Refactor runtime into step modules 2026-05-19 15:38:50 +02:00

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

  1. Gather the source from either a Git remote or a local mounted folder.
  2. Copy the source into a timestamped deployment workspace.
  3. Run BUILD_COMMAND when it is set.
  4. Resolve SERVE_PATH inside that deployment.
  5. Atomically repoint /srv/current to 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 primitives
  • serve/pipeline.py: the program/recipe/execution abstractions
  • serve/steps/gather.py: gather env loading, validation, and trigger strategy
  • serve/steps/build.py: optional build env loading and build execution
  • serve/steps/serve.py: serve env loading, publish-root resolution, and Caddy wiring
  • serve/deploy.py: deployment lifecycle that consumes the loaded steps
  • serve/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:latest from the base target
  • mounts ./site to /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.