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.
The compose stack
Section titled “The compose stack”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.
Upgrading
Section titled “Upgrading”git pull # newest compose files and .env.examplemake docker-pull # newest imagesmake docker-up # restart — migrations run automaticallyWithout 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.
Scaling out
Section titled “Scaling out”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:
CACHE_TYPE=redisBROKER_TYPE=redisTASK_RUNNER_LEADER_PROVIDER=redisSEMAPHORE_PROVIDER=redisTASK_RUNNER_MODE=queue # hand execution to task-workerCompose 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.
If something doesn’t start
Section titled “If something doesn’t start”- 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*_PORTvalues in.env. - A service is unhealthy —
docker compose -f docker-compose.prod.yml logs -fshows every container’s output; the APIs report readiness on/readyz. - Broken after an upgrade — diff your
.envagainst 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/callbackderived from yourPLATFORM_API_BASE_URL(see Configuration).
Health & operations
Section titled “Health & operations”- Every API exposes
/livezand/readyzprobes — wire these into your orchestrator or uptime monitoring. - Prometheus metrics endpoints are available behind the commented
*_METRICS_PORToptions in.env.example. - The UI reads its environment at container start (runtime
env.jsinjection), so the same image serves any environment — no rebuild needed to change URLs.
Building from source
Section titled “Building from source”docker-compose.dev.yml builds every image from the working tree instead of pulling — useful for patched deployments or development. See Contributing & architecture.