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
Rendering…
Interface surface
Dockerfile targets
| Target | Process model | Audience |
|---|---|---|
api | entrypoint-api.sh → gunicorn (0.0.0.0:$PORT) | Production / Cloud Run / compose prod |
api-dev | python -m backend.app with HOST=0.0.0.0 | Local compose (source mounts) |
lsp | ENTRYPOINT pyne-lsp (stdio) | Compose profile lsp; GHCR ghcr.io/hoox-sh/pyne/lsp |
cli | entrypoint-cli.sh → pyne (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"
| Service | Profile | Notes |
|---|---|---|
api | default | Port ${API_PORT:-5002}:8080, ro mounts of src/ + backend/, volume api_data → /data |
redis | redis | redis:7-alpine, AOF, ${REDIS_PORT:-6379} |
lsp | lsp | stdio; use docker compose run --rm lsp (not detached up) |
cli | cli | entrypoint-cli.sh → pyne; 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
| Variable | Default | Meaning |
|---|---|---|
PORT | 8080 | Listen port inside the container |
HOST | 0.0.0.0 | Bind address (dev Flask runner) |
API_PORT | 5002 | Host port published to container 8080 |
FLASK_ENV | production / development | App mode by target |
STORE_BACKEND | json (prod overlay: sqlite) | json | sqlite | redis |
API_KEY_STORE | /data/api_keys.json | JSON key store path (STORE_BACKEND=json) |
API_KEY_STORE_SQLITE | /data/api_keys.db | SQLite path (STORE_BACKEND=sqlite) |
ALLOWED_ORIGINS | compose default includes https://hoox.sh, https://hoox.sh/axis, and local AXIS :8081 | CORS allow-list. VPS AXIS is :80 — do not copy 8081 into VPS env |
ADMIN_TOKEN | unset | Required for POST /auth/create_key (X-Admin-Token) and prod compose |
PYNE_RUNNER | 0 | Optional hosted runner (docs). 1 enables /scripts + /cron/* |
PYNE_RUNNER_SCHEDULER | 0 | In-process bar-close poll. Prefer GUNICORN_WORKERS=1 when on |
PYNE_RUNNER_DB | /data/runner.db | SQLite on the api_data volume |
REDIS_URL | empty in prod; compose dev default redis://redis:6379/0 | Required when STORE_BACKEND=redis |
GUNICORN_WORKERS / GUNICORN_THREADS / GUNICORN_TIMEOUT | 2 / 4 / 120 (entrypoint default; prod compose overlay defaults to 60) | Prod entrypoint knobs |
GUNICORN_BIND | 0.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
- base-os / base — slim image, non-root
appuser; API/LSPbasealso installscurl+ freetype/png - builder —
build-essential, pip cache mounts, installbackend/requirements.txtthenpip install ".[lsp]"into/install - lsp-builder —
pip install ".[lsp]"only (no Flask / matplotlib) - cli-builder —
pip install ".[compile,data]"only (no Flask stack) - runtime / api / api-dev / lsp / cli — final CMDs / ENTRYPOINTs and healthchecks (
lsphas 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
- 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. - Read-only mounts in compose mean dependency changes need image rebuild; source edits are visible via
PYTHONPATHprecedence. - Production target is immutable — no live mounts; use the prod overlay or Cloud Run.
- Non-root
appusercannot write arbitrary paths; keepAPI_KEY_STOREunder/data. - Cloud Build always builds
--target api(gunicorn). Do not point it atapi-dev. - LSP is stdio — not an HTTP service; use
docker compose run --rm lsprather than expecting a published port. - CLI is ephemeral —
entrypoint-cli.shexecspyne(fallbackpynescript); pass subcommands after the image name. Workdir defaults to/work(compose mounts the repo there). - Prod overlay is fail-closed —
ADMIN_TOKENis required (:?interpolation);volumes: !overridedrops the dev source binds so onlyapi_data:/dataremains.
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
| Symptom | Cause | Fix |
|---|---|---|
| Healthcheck fails | process not listening / curl missing | Use image targets from this Dockerfile; check docker compose logs api |
| Connection refused on host port | Flask bound to 127.0.0.1 | Dev target sets HOST=0.0.0.0; do not override to localhost |
| Permission denied writing keys | Running as appuser outside /data | Set API_KEY_STORE=/data/... and mount api_data |
| Import errors after mount | PYTHONPATH / stale site-packages | PYTHONPATH=/app/src:/app; rebuild after dependency changes |
| OOM under load | Small memory cap | Raise API_MEM_LIMIT / Cloud Run memory; reduce concurrency |
| CORS failures from PWA | Origin not allowed | Set ALLOWED_ORIGINS |
| Huge build context | Missing .dockerignore | Ensure .dockerignore is present (corpus/docs excluded) |