Skip to content
Self-hosted only — Cloud is operated for you.

Deployment

BlockNext ships as prebuilt containers: Go binaries on distroless images, the UI on nginx-unprivileged — all published to ghcr.io/blocknextai as multi-arch (amd64/arm64) images on every release, tagged latest plus the release version.

docker-compose.prod.yml runs the published images and is what the Quickstart starts. It brings up:

  • the 5 HTTP services and the event relay worker (full list),
  • PostgreSQL 18 as the single database,
  • no Redis by default — the cache, realtime broker, leader election, concurrency semaphore and task runner all start in-process; see Scaling out for when to add it,
  • a one-shot migration container that brings the schema up to date before the services start.

Configuration comes entirely from the root .env file — see Configuration. For anything internet-facing, put a reverse proxy with TLS in front and update the *_BASE_URL values to your public URLs.

Terminal window
git pull # newest compose files and .env.example
make docker-pull # newest images
make docker-up # restart — migrations run automatically

Without make, the last two lines are docker compose -f docker-compose.prod.yml pull and ... up -d.

The compose file defaults to the latest tag; pin a specific release tag if you prefer explicit upgrades. After upgrading, diff your .env against .env.example for newly added options.

A fresh install runs a single instance of each service, so the cache, realtime broker, leader election and concurrency semaphore all keep their state in-process — 7 containers idling around 100 MB of RAM, with no Redis to operate.

That state is per-process, so the moment you run more than one instance of a service it has to move somewhere shared. Point each subsystem at Redis in .env:

Terminal window
CACHE_TYPE=redis
BROKER_TYPE=redis
TASK_RUNNER_LEADER_PROVIDER=redis
SEMAPHORE_PROVIDER=redis
TASK_RUNNER_MODE=queue # hand execution to task-worker

Compose derives its profiles from these settings, so each backing service starts as soon as something is configured to use it — no separate switch to remember. The full Redis-backed stack is 10 containers and roughly 160 MB.

You can mix: a shared cache with in-process everything else is fine. What is never safe is running two instances with a subsystem left on memory — they would each keep their own cache, their own realtime subscribers and their own concurrency counter, and both would consider themselves the scheduling leader.

  • Port already in use — the stack binds 3000/3100/3200/3300/4000 (plus PostgreSQL, and Redis if you enabled it) on the host; stop whatever holds the port or change the *_PORT values in .env.
  • A service is unhealthydocker compose -f docker-compose.prod.yml logs -f shows every container’s output; the APIs report readiness on /readyz.
  • Broken after an upgrade — diff your .env against the new .env.example; a newly required option is the usual cause.
  • OAuth logins fail — the provider’s callback URL must exactly match .../credential-oauth/oauth2/callback derived from your PLATFORM_API_BASE_URL (see Configuration).
  • Every API exposes /livez and /readyz probes — wire these into your orchestrator or uptime monitoring.
  • Prometheus metrics endpoints are available behind the commented *_METRICS_PORT options in .env.example.
  • The UI reads its environment at container start (runtime env.js injection), so the same image serves any environment — no rebuild needed to change URLs.

docker-compose.dev.yml builds every image from the working tree instead of pulling — useful for patched deployments or development. See Contributing & architecture.