Docker

Buildx multi-target images, Compose profiles, and Cloud Run production contract for the PYNE Pro API.

This page

Docker

Abstract

Containers package the Pro API (Flask + evaluator), the CLI (pynescript), and the LSP for reproducible local runs and Cloud Run deploys. A single multi-stage Dockerfile exposes named targets; Docker Buildx Bake (docker-bake.hcl) drives local loads and multi-platform release builds. Compose wires volume mounts and optional Redis/LSP/CLI profiles for desk development, with a production overlay for gunicorn.

Conceptual model

Diagram

Rendering…

Interface surface

Dockerfile targets

TargetProcess modelAudience
apientrypoint-api.sh → gunicorn (0.0.0.0:$PORT)Production / Cloud Run / compose prod
api-devpython -m backend.app with HOST=0.0.0.0Local compose (source mounts)
lspENTRYPOINT pyne-lsp (stdio)Compose profile lsp; GHCR ghcr.io/hoox-sh/pyne/lsp
clientrypoint-cli.shpyne (fallback pynescript; ephemeral; /work CWD)CI gates, one-shot parse/lint/compile/run

All targets: Python 3.12-slim, non-root appuser (uid 1000). API targets also set API_KEY_STORE=/data/api_keys.json, MPLBACKEND=Agg, healthcheck via curl. The cli image installs .[compile,data] only (no Flask/matplotlib) and is smaller/faster to build than api.

Buildx Bake (docker-bake.hcl)

# Local load (host platform)
docker buildx bake api                 # production image → pynescript-api:latest
docker buildx bake cli                 # CLI image → pynescript-cli:latest (ENTRYPOINT: pyne)
docker buildx bake lsp                 # LSP image → pynescript-lsp:latest (ENTRYPOINT: pyne-lsp)
docker buildx bake                     # default group: api + api-dev
docker buildx bake all                 # api + api-dev + lsp + cli

# Multi-platform (amd64 + arm64). Set REGISTRY to push:
REGISTRY=ghcr.io/hoox-sh/pyne TAG=0.6.4 docker buildx bake release
# release group = api-release + cli-release + lsp-release
# → ghcr.io/hoox-sh/pyne/{api,cli,lsp}:0.6.4
# Prefer: make docker-push-ghcr  (workflow GHCR on v* tags)

Make wrappers:

make docker-build       # bake api
make docker-build-cli   # bake cli
make docker-build-lsp   # bake lsp
make docker-build-all   # bake all targets
make docker-buildx      # bake release (multi-platform)
make docker-push-ghcr   # gh workflow run ghcr.yml
make docker-cli ARGS="check script.pine"   # compose profile cli

One-time multi-platform builder (if needed):

docker buildx create --use --name pynescript

Compose

cp .env.example .env   # optional overrides

make docker-up         # api-dev on host :5002
make docker-up-full    # + redis profile (api + redis)
make docker-prod       # requires ADMIN_TOKEN; gunicorn, no source mounts
make docker-smoke      # curl http://127.0.0.1:5002/
make docker-down

# stdio LSP (not a long-running compose service)
docker compose --profile lsp run --rm lsp

# CLI (ephemeral; mounts repo at /work)
docker compose --profile cli run --rm cli check script.pine
# ≡ make docker-cli ARGS="check script.pine"
ServiceProfileNotes
apidefaultPort ${API_PORT:-5002}:8080, ro mounts of src/ + backend/, volume api_data/data
redisredisredis:7-alpine, AOF, ${REDIS_PORT:-6379}
lsplspstdio; use docker compose run --rm lsp (not detached up)
cliclientrypoint-cli.shpyne; mounts .:/work + src + cli_data

Network name: pynescript-dev.

Production overlay (docker-compose.prod.yml): target api, volumes: !override so only api_data:/data remains (dev source binds are dropped), SQLite key store on /data, mem/cpu caps, requires ADMIN_TOKEN.

Environment variables

VariableDefaultMeaning
PORT8080Listen port inside the container
HOST0.0.0.0Bind address (dev Flask runner)
API_PORT5002Host port published to container 8080
FLASK_ENVproduction / developmentApp mode by target
STORE_BACKENDjson (prod overlay: sqlite)json | sqlite | redis
API_KEY_STORE/data/api_keys.jsonJSON key store path (STORE_BACKEND=json)
API_KEY_STORE_SQLITE/data/api_keys.dbSQLite path (STORE_BACKEND=sqlite)
ALLOWED_ORIGINScompose default includes https://hoox.sh, https://hoox.sh/axis, and local AXIS :8081CORS allow-list. VPS AXIS is :80 — do not copy 8081 into VPS env
ADMIN_TOKENunsetRequired for POST /auth/create_key (X-Admin-Token) and prod compose
PYNE_RUNNER0Optional hosted runner (docs). 1 enables /scripts + /cron/*
PYNE_RUNNER_SCHEDULER0In-process bar-close poll. Prefer GUNICORN_WORKERS=1 when on
PYNE_RUNNER_DB/data/runner.dbSQLite on the api_data volume
REDIS_URLempty in prod; compose dev default redis://redis:6379/0Required when STORE_BACKEND=redis
GUNICORN_WORKERS / GUNICORN_THREADS / GUNICORN_TIMEOUT2 / 4 / 120 (entrypoint default; prod compose overlay defaults to 60)Prod entrypoint knobs
GUNICORN_BIND0.0.0.0:${PORT}Optional gunicorn bind override

Published image names

GitHub Container Registry (workflow GHCR, tags v*):

ghcr.io/hoox-sh/pyne/api:0.6.4
ghcr.io/hoox-sh/pyne/cli:0.6.4
ghcr.io/hoox-sh/pyne/lsp:0.6.4

Cloud Build (cloudbuild.yaml, --target api only):

gcr.io/$PROJECT_ID/pynescript/pynescript-pro-api:$COMMIT_SHA
gcr.io/$PROJECT_ID/pynescript/pynescript-pro-api:latest

Substitution _PYNESCRIPT_VERSION in that YAML currently defaults to "0.3.0" — override at submit time to match __about__.py (0.6.4).

Internals

Build stages

  1. base-os / base — slim image, non-root appuser; API/LSP base also installs curl + freetype/png
  2. builderbuild-essential, pip cache mounts, install backend/requirements.txt then pip install ".[lsp]" into /install
  3. lsp-builderpip install ".[lsp]" only (no Flask / matplotlib)
  4. cli-builderpip install ".[compile,data]" only (no Flask stack)
  5. runtime / api / api-dev / lsp / cli — final CMDs / ENTRYPOINTs and healthchecks (lsp has none — stdio)

.dockerignore

Keeps context small by excluding tests/ (corpus), docs/, brand/, node modules, compiler *.nbc/*.nbi caches, venvs, and IDE cruft.

Compose healthchecks

API: curl -fsS http://127.0.0.1:8080/. Redis: redis-cli ping.

Invariants & edge cases

  1. Host port 5002 vs container 8080. Local compose publishes API on :5002. Cloud Run uses :8080. VPS deploy (scripts/deploy_vps.sh) health-checks API :5002 and AXIS :80. Local AXIS PWA is :8081 — do not treat 8081 as the VPS PWA port.
  2. Read-only mounts in compose mean dependency changes need image rebuild; source edits are visible via PYTHONPATH precedence.
  3. Production target is immutable — no live mounts; use the prod overlay or Cloud Run.
  4. Non-root appuser cannot write arbitrary paths; keep API_KEY_STORE under /data.
  5. Cloud Build always builds --target api (gunicorn). Do not point it at api-dev.
  6. LSP is stdio — not an HTTP service; use docker compose run --rm lsp rather than expecting a published port.
  7. CLI is ephemeralentrypoint-cli.sh execs pyne (fallback pynescript); pass subcommands after the image name. Workdir defaults to /work (compose mounts the repo there).
  8. Prod overlay is fail-closedADMIN_TOKEN is required (:? interpolation); volumes: !override drops the dev source binds so only api_data:/data remains.

Worked examples

Production-like local API (bake)

docker buildx bake api
docker run --rm -p 8080:8080 \
  -e FLASK_ENV=production \
  -e ADMIN_TOKEN=change-me \
  -e ALLOWED_ORIGINS=https://hoox.sh,https://hoox.sh/axis \
  pynescript-api:latest
curl -s http://127.0.0.1:8080/ | jq .

Compose with Redis

docker compose --profile redis up --build

Optional LSP container

docker compose --profile lsp run --rm lsp

CLI image

docker buildx bake cli
docker run --rm pynescript-cli:latest --help
docker run --rm -v "$PWD:/work" -w /work pynescript-cli:latest check script.pine
docker run --rm -v "$PWD:/work" -w /work pynescript-cli:latest lint script.pine --json
# or via compose:
make docker-cli ARGS="run script.pine --bars 50"

Production overlay

export ADMIN_TOKEN=change-me
docker compose -f docker-compose.yml -f docker-compose.prod.yml up --build -d

Failure modes

SymptomCauseFix
Healthcheck failsprocess not listening / curl missingUse image targets from this Dockerfile; check docker compose logs api
Connection refused on host portFlask bound to 127.0.0.1Dev target sets HOST=0.0.0.0; do not override to localhost
Permission denied writing keysRunning as appuser outside /dataSet API_KEY_STORE=/data/... and mount api_data
Import errors after mountPYTHONPATH / stale site-packagesPYTHONPATH=/app/src:/app; rebuild after dependency changes
OOM under loadSmall memory capRaise API_MEM_LIMIT / Cloud Run memory; reduce concurrency
CORS failures from PWAOrigin not allowedSet ALLOWED_ORIGINS
Huge build contextMissing .dockerignoreEnsure .dockerignore is present (corpus/docs excluded)

See also