Deployment

Deploy the client and server apps

Updated August 22nd, 202610 views

Silverbullet deploys to two places at once: the client to Cloudflare Workers, and the API, queue, and cron apps as Docker containers on a dedicated server managed by Coolify.

The automated path

Deployment is driven by .github/workflows/deploy-coolify.yaml. It runs on a push to main (production) or staging (staging), and can also be triggered manually with workflow_dispatch.

On each run it:

  1. Builds Docker images for the server apps from deploy/{api,queue,cron}/Dockerfile-lite and pushes them to Docker Hub.

  2. Runs database migrations over an SSH tunnel.

  3. Triggers a Coolify redeploy for each server app.

  4. Builds and deploys the client to Cloudflare.

Enable deploys in a new project

Projects created by new-project.sh ship with deploys disabled on purpose, so a first push cannot deploy to infrastructure that does not exist yet.

The scaffolding leaves you with two workflow files. The original deploy-coolify.yaml is disabled with branches: [DISABLED_DEPLOY] and kept as the target for template updates. A copy named <name>-coolify.yaml targets your renamed client app and is also disabled.

When your infrastructure is ready, edit <name>-coolify.yaml and set branches back to [main, staging]. Leave the original file disabled.

Configure GitHub Actions secrets

Secrets are injected at build time from Settings → Secrets and variables → Actions. You do not run wrangler secret put for backend values.

Single-value secrets, shared across environments:

Plain Text
BETTER_AUTH_SECRET   MAILERSEND_API_KEY   ENCRYPTION_KEY
S3_ACCESS_KEY        S3_SECRET_KEY        OPENAI_API_KEY
GOOGLE_CLIENT_ID     GOOGLE_CLIENT_SECRET
APPLE_CLIENT_ID      APPLE_CLIENT_SECRET  APPLE_APP_BUNDLE_IDENTIFIER
REDIS_HOST           REDIS_USERNAME       REDIS_PASSWORD

Per-environment secrets:

Plain Text
DATABASE_URL_PROD          DATABASE_URL_STAGING
PADDLE_API_KEY_PROD        PADDLE_API_KEY_STAGING
PADDLE_WEBHOOK_SECRET_PROD PADDLE_WEBHOOK_SECRET_STAGING

Infrastructure credentials:

Plain Text
CLOUDFLARE_ACCOUNT_ID   CLOUDFLARE_API_TOKEN
DOCKER_HUB_USERNAME     DOCKER_HUB_PASSWORD
COOLIFY_TOKEN           HETZNER_HOST   HETZNER_SSH_PRIVATE_KEY

A missing secret usually surfaces as a build-time failure rather than a broken runtime, which is the failure mode you want.

Deploy the client manually

You can push the client without going through CI:

Bash
pnpm --filter=<name>-client deploy:staging
pnpm --filter=<name>-client deploy:production

Each script sets CLOUDFLARE_ENV, builds with Vite, and deploys with Wrangler. Always go through the package's own scripts — calling wrangler directly skips the environment setup and builds against the wrong configuration.

Before you ship

Run the workspace checks locally first, since the pre-commit hook only covers linting:

Bash
pnpm lint
pnpm test
pnpm build

Services to provision

Have these ready before your first production deploy: Postgres and Redis per environment, MailerSend, an S3 bucket with access keys, Google and Apple OAuth clients, a Paddle product with a webhook, an OpenAI API key, a generated BETTER_AUTH_SECRET, and Cloudflare account and API tokens.