The public FastAPI app is deployed as a prebuilt Railpack OCI image. GitHub Actions
builds the image, pushes it to GHCR, and asks Dokku to deploy the Git-generated
unambiguous short-SHA tag. Dokku's nginx proxy terminates TLS and forwards both the
static site and /api/compare to the same single Uvicorn process.
Configure two repository secrets:
GIT_REMOTE_URL: the Dokku app's SSH URL, including the host, SSH user, port, and static app name. For example:ssh://dokku@dokku.example.org:22/delta-track.SSH_PRIVATE_KEY: the private key matching a public key authorized on the Dokku host.
The workflow uses the built-in GITHUB_TOKEN for GHCR and grants it only
contents: read and packages: write; it is not a repository secret.
Ask the host maintainer whether these steps are already complete before changing the host.
-
Create the stateless app and keep it at one web process:
dokku apps:create delta-track dokku ps:scale delta-track web=1
Do not scale past one process. The slowapi counters in
web/app.pyuse in-memory, process-local storage, so multiple processes multiply the effective per-client request allowance. -
Make the GHCR image pullable. Public packages need no registry credential. If the GHCR image is private, provision a read-only GHCR token on the host once; do not add it as another GitHub repository secret:
dokku registry:login delta-track ghcr.io GITHUB_USERNAME GHCR_READ_TOKEN
Dokku stores per-app registry credentials under its own configuration. See Dokku registry management.
-
Point the app domain at the host:
dokku domains:set delta-track deltatrack.agoradmv.org
-
After the first successful HTTP deployment, install/configure the official Let's Encrypt plugin if the host does not already have it:
sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git sudo dokku letsencrypt:cron-job --add dokku letsencrypt:set delta-track email MAINTAINER_EMAIL dokku letsencrypt:enable delta-track
The plugin's HTTP-01 flow requires the deployed app and DNS to be reachable before
letsencrypt:enable. Dokku's default nginx TLS template redirects HTTP to HTTPS. See the official plugin instructions. -
Size the host and container limit for PDF diffing. Each upload can be 150 MB and the app admits two concurrent diffs. Set the memory limit from observed peak PDF workloads, leaving operating-system and nginx headroom; do not copy a speculative fixed value from this runbook. No database plugin or persistent volume is needed.
-
Keep nginx's request-body limit at least
157286400bytes and its proxy timeout above the app's 120-second diff timeout. These values must stay aligned withMAX_UPLOAD_BYTESandDIFF_TIMEOUT_Sinweb/app.py.
.github/workflows/deploy.yml runs on pushes to main. It:
-
Sets the lowercase
IMAGE_NAMEtoghcr.io/civictechdc/deltatrack, then builds and pushes it withiloveitaly/github-action-railpack. The action publishes the defaultlatesttag and Git's unambiguous short-SHA tag withGITHUB_TOKEN. -
Calls
dokku/github-actionwithGIT_REMOTE_URL,SSH_PRIVATE_KEY, and the short-SHA image. The action connects to the URL's app and runs the equivalent of:dokku git:from-image delta-track ghcr.io/civictechdc/deltatrack:SHORT_SHA
The Procfile binds Uvicorn to 0.0.0.0 and ${PORT:-5000}. It intentionally has no
--workers argument, so Uvicorn runs one worker.
Dokku's nginx must preserve Host, set X-Forwarded-Proto to the original client
scheme, and ensure the rightmost X-Forwarded-For address is proxy-controlled. The
application uses those signals for HTTPS redirects and rate-limit identity.
After setup or a proxy-template change, verify both schemes:
curl -sI http://deltatrack.agoradmv.org/index.html | head -5
curl -sI https://deltatrack.agoradmv.org/ | head -5The HTTP request should redirect once to HTTPS; the HTTPS request should return the
landing page without a redirect loop. If HTTPS loops, inspect the nginx
X-Forwarded-Proto value before changing application middleware.