Skip to content

Deploy

Build the release image from the repository root:

Terminal window
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}}'
Terminal window
docker pull ghcr.io/brownjuly2003-code/ab-test-research-designer:latest
docker run --rm -p 8008:8008 ghcr.io/brownjuly2003-code/ab-test-research-designer:latest

.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:

  1. Push a release tag such as v1.1.0.
  2. Wait for the Publish Docker image workflow to finish successfully.
  3. 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.
  4. Verify anonymous pull from a clean machine:
Terminal window
docker pull ghcr.io/brownjuly2003-code/ab-test-research-designer:v1.1.0

Notes:

  • 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.

Set <REGISTRY> explicitly for your target registry namespace, for example ghcr.io/<owner> or docker.io/<user>.

Terminal window
docker tag ab-test-research-designer:1.0.0 <REGISTRY>/ab-test-research-designer:1.0.0
docker tag ab-test-research-designer:latest <REGISTRY>/ab-test-research-designer:latest

Do not run push until registry credentials, target namespace, and image scan are ready.

Terminal window
docker push <REGISTRY>/ab-test-research-designer:1.0.0
docker push <REGISTRY>/ab-test-research-designer:latest

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:

Terminal window
docker volume create ab-test-data
docker run --rm -v ab-test-data:/app/data -p 8008:8008 ab-test-research-designer:1.0.0

To bind-mount a host directory instead, chown it to uid 1000 first: mkdir -p ./data && sudo chown 1000:1000 ./data.

Open mode:

Terminal window
docker run --rm --name ab-test-v1-open -p 8008:8008 ab-test-research-designer:1.0.0

Secure mode:

Terminal window
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.0

Dual-token mode:

Terminal window
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.0

Signed workspace backup mode:

Terminal window
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.0

Slack App mode:

Terminal window
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.0

Create 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 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.

Terminal window
fly apps create <fly-app-name>
fly volumes create ab_test_data --region ams --size 1
fly deploy

Notes:

  • fly.toml keeps app = "ab-test-research-designer" as a placeholder; replace it after fly apps create or pass fly 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 console opens a root shell, the app process does not):
Terminal window
fly ssh console -C "chown -R 1000:1000 /data"
  • Demo seeding is a manual post-deploy step because Fly release_command Machines do not mount persistent volumes:
Terminal window
fly ssh console -C "python scripts/seed_demo_workspace.py --idempotent"

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.

Terminal window
fly secrets set AB_API_TOKEN=... AB_READONLY_API_TOKEN=... AB_WORKSPACE_SIGNING_KEY=...
fly deploy

Open runtime:

Terminal window
curl http://127.0.0.1:8008/health
curl http://127.0.0.1:8008/readyz
curl http://127.0.0.1:8008/api/v1/diagnostics
curl http://127.0.0.1:8008/

Expected responses:

  • GET /health -> 200 with "status":"ok" and the package version string.
  • GET /readyz -> 200 with "status":"ready" and all readiness checks marked ok.
  • GET /api/v1/diagnostics -> 200 and storage.write_probe_ok=true.
  • GET / -> 200 and HTML title AB Test Research Designer.

Secure runtime:

Terminal window
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/calculate returns 401.
  • With Authorization: Bearer <WRITE_TOKEN>, POST /api/v1/calculate returns 200.
  • In dual-token mode, use <READONLY_TOKEN> for read-only diagnostics and <WRITE_TOKEN> for mutating endpoints.

Stop the current container, pull or retag the previous release, and run the previous tag again.

Terminal window
docker stop ab-test-v1-open
docker 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:

Terminal window
docker tag <REGISTRY>/ab-test-research-designer:<PREVIOUS_TAG> ab-test-research-designer:rollback
docker run --rm --name ab-test-v1-rollback -p 8008:8008 ab-test-research-designer:rollback