Docker Image¶
The underwrite runtime ships as a multi-stage Docker image. The image is non-root, has a healthcheck, and is published to GitHub Container Registry on tagged releases.
Build¶
The Dockerfile is the source of truth. Build locally with:
./scripts/build-image.sh # underwrite:dev
./scripts/build-image.sh v0.9.0 # underwrite:v0.9.0
./scripts/build-image.sh v0.9.0 --push # tag and push
Build args:
| Arg | Default | Description |
|---|---|---|
PYTHON_VERSION |
3.12 |
Python base image |
EXTRAS |
serve,otlp,vault |
pip extras to install |
BUILD_VERSION |
0.1.0 |
OCI image version label |
GIT_COMMIT |
dev |
OCI image revision label |
BUILD_DATE |
unknown |
OCI image created label |
The build:
- Compiles the wheel in a
builderstage withcryptographyandpydanticprecompiled wheels. - Installs the wheel + the production extras in a clean layer.
- Strips debug info from the
.sofiles to shrink the image by ~30 MB. - Copies the installed packages and the
underwriteCLI entrypoint to a cleanpython:3.12-slimruntime. - Creates a non-root
underwriteuser (UID 1001) and gives it ownership of/data. - Adds a
HEALTHCHECKthat pings/healthzevery 30s with a 5s timeout. - Sets the OCI labels (
org.opencontainers.image.*) for registry compatibility.
Run¶
The image's default command is underwrite serve:
For a full configuration:
docker run --rm \
-p 8080:8080 \
-v $PWD/underwrite.json:/app/underwrite.json:ro \
-v $PWD/data:/data \
-e UNDERWRITE_REQUIRE_AUTH=true \
-e UNDERWRITE_API_TOKEN=$(cat /etc/underwrite/api-token) \
underwrite:dev \
--config /app/underwrite.json \
serve --host 0.0.0.0 --port 8080
The CLI is also available — override the entrypoint:
docker run --rm --entrypoint underwrite underwrite:dev --help
docker run --rm --entrypoint underwrite underwrite:dev init /app/config.json
docker run --rm --entrypoint underwrite underwrite:dev run mechanism audit
docker-compose¶
docker-compose.yml is the reference local-deployment
manifest: it brings up the runtime plus HashiCorp Vault 1.18 in
dev mode and an OpenTelemetry Collector. The runtime uses a
file-backed SQLite database mounted at /data and connects to
the in-network Vault via the service name; environment variables
on the underwrite service mirror the UNDERWRITE_* namespace
in the runtime config.
Vault's dev root token defaults to devroot; override with
VAULT_TOKEN=… in the environment. The underwrite_data
named volume persists the file-store state across container
restarts.
CI¶
.github/workflows/docker.yml builds the image on every push
to master and on every tag (v*.*.*). The job:
- Sets up QEMU + Buildx
- Builds the image with
--cache-from type=ghaso unchanged layers are pulled from the GitHub Actions cache - Smoke-tests the image by running
servein the background and curling/healthz - Smoke-tests the CLI by running
underwrite --help - Pushes the image to
ghcr.io/${{ github.repository }}on tag releases (no push on branch builds)
The smoke test uses UNDERWRITE_REQUIRE_AUTH=false so the
healthcheck endpoint is reachable without a token. Production
deployments set UNDERWRITE_REQUIRE_AUTH=true and the
UNDERWRITE_API_TOKEN env var; the image's HEALTHCHECK does
not require auth (it hits /healthz, not /v1/publish).
Image size¶
| Variant | Size |
|---|---|
underwrite:dev (Python 3.12, runtime + production extras) |
~280 MB |
underwrite:dev (multi-arch amd64 + arm64) |
~290 MB |
underwrite:dev (distroless python:3.12-distroless) |
~210 MB (not yet wired) |
The distroless variant is on the v1.0 roadmap; the current slim variant is the recommended target for v0.9 / v0.10.