Deploy
Deploy
Section titled “Deploy”Build the release image from the repository root:
docker build -t ab-test-research-designer:1.0.0 -t ab-test-research-designer:latest .docker inspect ab-test-research-designer:1.0.0 --format '{{.Size}}'Pulling from GHCR
Section titled “Pulling from GHCR”docker pull ghcr.io/brownjuly2003-code/ab-test-research-designer:latestdocker run --rm -p 8008:8008 ghcr.io/brownjuly2003-code/ab-test-research-designer:latestAutomated publish via GitHub Actions
Section titled “Automated publish via GitHub Actions”.github/workflows/docker-publish.yml publishes the multi-arch image (linux/amd64, linux/arm64) to ghcr.io/brownjuly2003-code/ab-test-research-designer on every pushed tag matching v*. The same workflow also supports manual workflow_dispatch: provide a specific tag or leave the input empty to republish the latest v* tag already present in git.
First GHCR release checklist:
- Push a release tag such as
v1.1.0. - Wait for the
Publish Docker imageworkflow to finish successfully. - Open https://github.com/brownjuly2003-code/ab-test-research-designer/pkgs/container/ab-test-research-designer, go to
Settings, and switch package visibility to Public. - Verify anonymous pull from a clean machine:
docker pull ghcr.io/brownjuly2003-code/ab-test-research-designer:v1.1.0Notes:
- GHCR creates the package as private on the first push even when it is linked to the repository, so the visibility switch is a one-time manual step.
- The workflow authenticates with the repository
GITHUB_TOKEN; no personal access token is required. - Build cache uses
type=gha; GitHub may evict it after about 7 days of inactivity, so the first build after an idle period can be slower.
Tag For Registry
Section titled “Tag For Registry”Set <REGISTRY> explicitly for your target registry namespace, for example ghcr.io/<owner> or docker.io/<user>.
docker tag ab-test-research-designer:1.0.0 <REGISTRY>/ab-test-research-designer:1.0.0docker tag ab-test-research-designer:latest <REGISTRY>/ab-test-research-designer:latestDo not run push until registry credentials, target namespace, and image scan are ready.
docker push <REGISTRY>/ab-test-research-designer:1.0.0docker push <REGISTRY>/ab-test-research-designer:latestRun Locally
Section titled “Run Locally”The container runs as the unprivileged user app (uid 1000), not root. /app/data is the only writable path in the image. To persist SQLite across runs, prefer a named volume — Docker seeds it from the image directory and keeps its ownership — because a host bind source that the daemon creates is root-owned and the app user cannot write to it:
docker volume create ab-test-datadocker run --rm -v ab-test-data:/app/data -p 8008:8008 ab-test-research-designer:1.0.0To bind-mount a host directory instead, chown it to uid 1000 first: mkdir -p ./data && sudo chown 1000:1000 ./data.
Open mode:
docker run --rm --name ab-test-v1-open -p 8008:8008 ab-test-research-designer:1.0.0Secure mode:
docker run --rm --name ab-test-v1-secure -e AB_API_TOKEN=replace-with-a-write-token -p 8008:8008 ab-test-research-designer:1.0.0Dual-token mode:
docker run --rm --name ab-test-v1-dual -e AB_API_TOKEN=replace-with-a-write-token -e AB_READONLY_API_TOKEN=replace-with-a-readonly-token -p 8008:8008 ab-test-research-designer:1.0.0Signed workspace backup mode:
docker run --rm --name ab-test-v1-signed -e AB_WORKSPACE_SIGNING_KEY=replace-with-a-long-random-secret -p 8008:8008 ab-test-research-designer:1.0.0Slack App mode:
docker run --rm --name ab-test-v1-slack ^ -e AB_SLACK_CLIENT_ID=... ^ -e AB_SLACK_CLIENT_SECRET=... ^ -e AB_SLACK_SIGNING_SECRET=... ^ -p 8008:8008 ab-test-research-designer:1.0.0Create the app from slack/app-manifest.yml, replace {DEPLOY_HOST} with the public HTTPS host, then open /slack/install. Rotate Slack credentials in the Slack App configuration, update runtime secrets, restart the backend, delete the affected slack_installations row if the bot token is being replaced, and reinstall the app.
Publication path (GitHub + local)
Section titled “Publication path (GitHub + local)”Publication and acceptance for this project are GitHub-only (source, Actions,
Pages, Releases, GHCR) plus the supported local runtime
(python scripts/run_local.py). Hugging Face is not a supported publication
or demo target (owner decision 2026-07-30). Optional legacy HF snapshot/deploy
helpers may still exist in the repository for historical reference; they are
outside closure and must not be treated as an active host of record.
Operator mode (?admin=1): the public app shows only the planning wizard. All
operator surfaces — the saved-project sidebar (projects, history, revisions,
archived, workspace backup) and the System / API keys tabs (backend
status, API session token, AB_ADMIN_TOKEN, Slack App, diagnostics, audit log,
webhooks, raw endpoints) — are hidden and live behind admin mode. Open
http://127.0.0.1:8008/?admin=1 (or the equivalent self-hosted URL) to reveal
them (persisted to localStorage['ab-test:admin']; clear with ?admin=0).
Logic: app/frontend/src/lib/adminMode.ts. The smoke flows
(app/frontend/src/test/e2e-smoke.spec.ts, scripts/run_local_smoke.py)
open /?admin=1 so they can exercise those surfaces.
Fly.io Demo Deploy (optional self-host — not exercised by CI)
Section titled “Fly.io Demo Deploy (optional self-host — not exercised by CI)”The Fly.io path below is a documented optional self-host recipe. It is not exercised by CI and is not required for project closure.
Open mode is recommended for a public showcase. This keeps the hosted demo anonymous and matches the default open runtime in the app.
fly apps create <fly-app-name>fly volumes create ab_test_data --region ams --size 1fly deployNotes:
fly.tomlkeepsapp = "ab-test-research-designer"as a placeholder; replace it afterfly apps createor passfly deploy -a <fly-app-name>.- The Fly volume is mounted at
/data, and SQLite is pointed at/data/projects.sqlite3. - The image runs as the unprivileged user
app(uid 1000). A freshly created Fly volume is mounted root-owned, so chown it once before the app can write SQLite there (fly ssh consoleopens a root shell, the app process does not):
fly ssh console -C "chown -R 1000:1000 /data"- Demo seeding is a manual post-deploy step because Fly
release_commandMachines do not mount persistent volumes:
fly ssh console -C "python scripts/seed_demo_workspace.py --idempotent"Fly.io Deploy (Secure)
Section titled “Fly.io Deploy (Secure)”Secure mode is private by default. Once secrets are set, callers must present the configured token; if you share a readonly token, it enables safe GET access but it is no longer an anonymous public demo.
fly secrets set AB_API_TOKEN=... AB_READONLY_API_TOKEN=... AB_WORKSPACE_SIGNING_KEY=...fly deployHealth / Verification
Section titled “Health / Verification”Open runtime:
curl http://127.0.0.1:8008/healthcurl http://127.0.0.1:8008/readyzcurl http://127.0.0.1:8008/api/v1/diagnosticscurl http://127.0.0.1:8008/Expected responses:
GET /health->200with"status":"ok"and the package version string.GET /readyz->200with"status":"ready"and all readiness checks markedok.GET /api/v1/diagnostics->200andstorage.write_probe_ok=true.GET /->200and HTML titleAB Test Research Designer.
Secure runtime:
curl -X POST http://127.0.0.1:8008/api/v1/calculate -H "Content-Type: application/json" -d '{"metric_type":"binary","baseline_value":0.1,"mde_pct":5,"alpha":0.05,"power":0.8,"expected_daily_traffic":1000,"audience_share_in_test":1.0,"traffic_split":[50,50],"variants_count":2}'curl -X POST http://127.0.0.1:8008/api/v1/calculate -H "Authorization: Bearer <WRITE_TOKEN>" -H "Content-Type: application/json" -d '{"metric_type":"binary","baseline_value":0.1,"mde_pct":5,"alpha":0.05,"power":0.8,"expected_daily_traffic":1000,"audience_share_in_test":1.0,"traffic_split":[50,50],"variants_count":2}'Expected auth behavior:
- Without a write token,
POST /api/v1/calculatereturns401. - With
Authorization: Bearer <WRITE_TOKEN>,POST /api/v1/calculatereturns200. - In dual-token mode, use
<READONLY_TOKEN>for read-only diagnostics and<WRITE_TOKEN>for mutating endpoints.
Rollback
Section titled “Rollback”Stop the current container, pull or retag the previous release, and run the previous tag again.
docker stop ab-test-v1-opendocker run --rm --name ab-test-v1-rollback -p 8008:8008 <REGISTRY>/ab-test-research-designer:<PREVIOUS_TAG>If the previous image already exists locally, retag it first:
docker tag <REGISTRY>/ab-test-research-designer:<PREVIOUS_TAG> ab-test-research-designer:rollbackdocker run --rm --name ab-test-v1-rollback -p 8008:8008 ab-test-research-designer:rollback