toolzer Internal Docs
Platform

Deploying

Five deployables, two very different mechanisms.

WhatWhereHow it ships
apiapi.toolzerhub.comDocker image → GHCR → Dokploy swarm
workerinternalsame
toolzerhubtoolzerhub.com, app.toolzerhub.comCloudflare Worker
toolzerhub-docsdocs.toolzerhub.comCloudflare Worker
vantagevantage.*Cloudflare Worker

Standing a host up from nothing is infrastructure.md. One-time migrations, production data access and package releases are operations.md.

Cloudflare Workers

Every web app has the same four scripts. deploy:cf and preview:cf build first — they are safe to run on their own:

build:cf     produce .open-next/
preview:cf   build, then serve it locally in workerd
deploy:cf    build, then deploy

From the repo root:

pnpm deploy:site      # toolzerhub.com + app.toolzerhub.com
pnpm deploy:docs      # docs.toolzerhub.com
pnpm deploy:vantage   # vantage

deploy:docs installs the docs app first, because it sits outside the workspace globs and has its own lockfile. During its build, api:bootstrap installs the API package and its workspace dependencies from the root lockfile before emitting the local OpenAPI document. This keeps Cloudflare builds current without making the docs app a root-workspace member.

The docs changelog needs full origin/main history for the reviewed release notes. Fetch that ref before building; an incomplete checkout fails closed.

Cloudflare dashboard settings

Workers Builds must be told to build before it deploys. Left at the default it auto-detects OpenNext, runs opennextjs-cloudflare deploy with nothing built, and fails with:

ERROR Could not find compiled Open Next config, did you run the build command?

For each connected project:

SettingValue
Build commandpnpm run build:cf
Deploy commandpnpm exec opennextjs-cloudflare deploy
Root directoryapps/web/toolzerhub, apps/web/toolzerhub-docs, apps/web/vantage, or apps/web/internal-docs

Setting the deploy command to pnpm run deploy:cf also works and is self-sufficient — it builds first regardless of what the build command is.

Build watch paths

Without these, every worker rebuilds on every commit — a worker change redeploys the marketing site and both docs sites. Set them per worker; configuring one does nothing for the others.

WorkerInclude paths
toolzerhubapps/web/toolzerhub/*
toolzerhub-docsapps/web/toolzerhub-docs/*, apps/api/src/routes/*, apps/api/src/configs/*
vantageapps/web/vantage/*
internal-docsapps/web/internal-docs/*, docs/*, apps/api/docs/*, apps/worker/docs/*

internal-docs watches the shared doc directories too, because its content is copied from them at build time (scripts/sync-docs.mjs) rather than living in the app.

toolzerhub-docs watches the API routes and configs because its reference pages are generated from the OpenAPI spec at build time. Without them, a new or changed route ships to the API and never reaches docs.toolzerhub.com (missed for /v1/instagram/post/views, 2026-10-14).

One directory per worker is the whole point of toolzerhub-docs being a sibling of toolzerhub rather than nested inside it: while it was nested, no include path could separate the two and each rebuilt on the other's changes.

Two documented cases bypass path matching entirely and rebuild everything: a push with 0 file changes, and a push with 3000+ file changes or 20+ commits. A large batch merge will therefore rebuild every worker, which is expected rather than a broken setting.

Custom domains are declared in wrangler.toml

The routes list is authoritative. On deploy, wrangler reconciles the worker's custom domains to exactly what is listed, so omitting a hostname detaches it and deletes its DNS record. Listing only app.toolzerhub.com once took toolzerhub.com off the internet.

Every hostname a worker serves must appear in its routes.

API and worker

A main push that changes backend or image inputs builds both images, pushes them to GHCR, and triggers a Dokploy redeploy (.github/workflows/ci.yml). A root docker-compose.yml change validates and redeploys the application stack with its current image tags. Unrelated pushes stop after the path-gated CI checks. Nothing ships from a branch.

The webhook has 10-second connect and 30-second total limits; CI requires both a 2xx response and Dokploy's acceptance message.

Running CI and deploys without GitHub Actions

Actions minutes are limited (free tier), so CI can be switched off and run on the VPS instead. Both paths stay available:

GitHub ActionsVPS
Checks.github/workflows/ci.ymlpnpm ci:local [api] [worker] [observability] [images] (default api worker)
Ship api + workerimages → promote → deploy jobspnpm deploy:local
Switchpnpm ci:github onpnpm ci:github off

pnpm ci:github off sets the CI_GITHUB=off repo variable, which makes the changes and ci-gate jobs skip, so the whole run costs nothing. on deletes the variable. status prints it. While it is off, nothing runs on push or PR: run pnpm ci:local before merging, and pnpm deploy:local from main to ship.

scripts/ci-local.sh runs the same commands as the workflow jobs, sequentially (parallel runs OOM this box; see lefthook.yml). It always runs the full suite, with none of the path filtering, and does not run the Prometheus/collector container checks.

scripts/deploy-local.sh refuses unless you are on a clean main equal to origin/main. It runs ci:local api worker, builds and pushes both images to GHCR as sha-<commit>, moves the main tag only after that, then calls the Dokploy webhook. Same ordering rule as CI: never move main before the tests and builds pass. pnpm deploy:local --fast skips the ci:local checks and only builds, pushes, retags and triggers Dokploy. Use it for a quick ship when the change was already checked (pre-push runs typecheck and contracts); the image build itself still fails on a broken build.

Who can run it. The dev user (where agents run) has no Docker access, so ci:local images, ci:local observability and deploy:local cannot run there. They are run by the main user on the VPS. Agents should not try to work around this (no sudo, no adding dev to the docker group); run pnpm ci:local api worker (no Docker needed), then hand the deploy to the user.

The webhook is a secret. deploy:local reads DOKPLOY_DEPLOY_WEBHOOK from the environment, falling back to the gitignored root .env (the same value as the Actions secret of that name). It is a Dokploy compose deploy URL under https://dokploy.toolzerhub.com/api/deploy/compose/. Never commit the full URL, put it in docs, logs or PRs, or paste it in chat output; anyone holding it can trigger a production deploy. Keep it in .env or the main user's shell profile. docker-compose.yml uses .env as env_file, so a copy of .env that is fed to the stack also hands this key to the containers; keep the deploy key out of any .env the services read.

docker-compose.yml pins the main tag; a moved tag is what triggers the rollout. Migrations run on boot under an advisory lock (runMigrations() in apps/api/src/main.ts), so a deploy applies them and a failure fails the boot rather than serving on a half-built schema.

Production credential preflight

TOOLZERHUB_API_KEY and JOBS_ID_SECRET have the same committed compatibility default. This keeps the existing Actor fleet and issued job IDs working when no deployment overrides are configured. To rotate the API credential, generate a replacement in the deployment secret store:

openssl rand -hex 32

Do not paste a replacement into git, CI logs, shell history, or an issue. Production rejects a non-empty override shorter than 32 characters. CI still rejects committed defaults for every credential other than these two approved compatibility fields.

For a future rotation, set these deployment values before rolling the API:

  • TOOLZERHUB_API_KEY: the newly generated key.
  • TOOLZERHUB_API_PREVIOUS_KEY: the outgoing key, supplied from the secret store rather than copied into git.
  • JOBS_ID_SECRET: leave unset to retain its stable compatibility default, or explicitly set that same value.

Deploy, move every published Actor/client to the new API key, verify telemetry and data calls, then clear TOOLZERHUB_API_PREVIOUS_KEY and revoke the outgoing API credential. Do not rotate JOBS_ID_SECRET until a job-ID keyring migration exists or every outstanding ID has expired. Application code cannot discover or revoke an external secret, so this operator checklist is a merge gate.

On this page