Production deploy

api.nodetool.ai runs on the Docker host 46.224.131.110. Caddy routes traffic to app-1 and app-2 with cookie affinity. Both replicas use the same Supabase database, auth project, and storage buckets. Deployment configuration and the rolling release script live in the adjacent nodetool-deploy repository, checked out at /home/claude/nodetool-deploy on that host.

Release chain

A push to main starts the Docker image build and User Journeys independently. The Deploy to Docker workflow waits for both to succeed for that exact commit and rejects a commit superseded on main. It retains the historical filename fly-deploy.yml, but does not invoke Fly or use its credentials. Manual dispatch follows the same gates and uses the dispatch commit, never a mutable image tag.

The Deploy server job connects to the host over SSH and sends only the full commit SHA. A host-side forced command validates the SHA, rechecks the build gates and current main, and invokes the rolling script with ghcr.io/nodetool-ai/nodetool:main-<shortsha>. The host resolves this to an immutable digest. GitHub serializes release runs without cancelling a rollout in progress. The host script also takes a deployment lock.

web-deploy.yml follows Deploy to Docker and checks that its Deploy server job succeeded before releasing the same commit to Cloudflare Pages. A skipped or failed server release cannot promote the frontend. Cloudflare Pages credentials remain in web-production.

GitHub connection settings

Repository variables:

  • DOCKER_DEPLOY_HOST: the Docker host IP or hostname.
  • DOCKER_DEPLOY_USER: the SSH account that owns the deployment checkout.

Repository secrets:

  • DOCKER_DEPLOY_SSH_KEY: a dedicated Ed25519 private key.
  • DOCKER_DEPLOY_KNOWN_HOSTS: the host’s verified SSH public key entry.

The matching public key in authorized_keys uses restrict and forces /home/claude/nodetool-deploy/bootstrap/github-ssh-deploy.sh. It permits only a full release SHA or the read-only check probe, not shell commands, forwarding, or a terminal. The SSH client requires the pinned host key. Database credentials remain in the host’s mode-0600 .env and are not copied into Actions.

Rolling release

The script pulls the image and runs its database migrations once before changing either replica. It then verifies the peer is healthy, signals SIGUSR2 to drain the selected replica, waits for its turns and jobs to reach zero, and recreates it. Health must pass before the peer is drained. The saved image digest changes only after both replacements succeed. A failed migration or replacement stops the release. Migrations must remain compatible with the previous image during the handover.

Inspect the running deployment on the host:

cd /home/claude/nodetool-deploy
docker compose ps
node bootstrap/verify.mjs --public
docker compose logs --tail 100 app-1 app-2

For a manual rollback, review schema compatibility and run the rolling script on the host with a previously deployed immutable image digest. Rollbacks are not accepted by the restricted GitHub key, which only releases current main.

The deployment setup guide covers DNS, trigger-dispatch handover, pool sizing, and affinity checks. The retained Fly guide covers legacy rollback operations, not the active GitHub release destination.