Bring your own orchestrator
Run Hogsend on Kubernetes, Nomad, ECS, or any platform you already operate. One image, three run modes, one env contract — you provide Postgres, Redis, and a Hatchet endpoint.
If you already run a container platform — Kubernetes, Nomad, ECS, a fleet of VMs — Hogsend drops straight in. There is no Railway lock-in and nothing platform-specific in the engine: it is the same image the Docker self-host and Railway paths use, the same three run modes, and the same env contract. You bring the infrastructure and the scheduler.
What you provide
| Dependency | What Hogsend needs |
|---|---|
| Postgres (TimescaleDB recommended) | A connection string in DATABASE_URL. The primary datastore. |
| Redis | A connection string in REDIS_URL. Required in production — Better Auth sessions, reset tokens, and cross-replica auth rate limiting (gated on a raw REDIS_URL), plus PostHog property caching. |
| A Hatchet engine | A token + gRPC endpoint — Hatchet Cloud, your own hatchet-lite, or any Hatchet you run. See Get a Hatchet token. |
The image and its three run modes
Build the repo-root Dockerfile (or pull your built image) and run it three ways via command override:
| Run mode | Command | As |
|---|---|---|
| api | node apps/api/dist/index.js (default CMD) | a long-running Deployment / Service on port 3002, healthcheck GET /v1/health |
| worker | node apps/api/dist/worker.js | a long-running Deployment, no port, no healthcheck |
| migrate | tsx packages/db/src/migrate.ts | a one-shot Job / init step that must complete before api and worker start |
docker build -t your-registry/hogsend:TAG .
docker push your-registry/hogsend:TAGOrdering you must enforce
The platform's scheduler is responsible for the ordering the compose file otherwise handles for you:
- Postgres reachable before anything else.
migrateruns and completes — engine track then client track — before api and worker start. The worker has no schema guard of its own, so it must not start against an unmigrated database. On Kubernetes this is aJobgated by an init step or aninitContainer; on ECS a one-off task; on a VM a step in your deploy script.- api and worker start only after migrate succeeds. The api also self-guards at boot (
exit(1)if the engine schema is behind), but the worker relies entirely on your ordering.
The env contract
Inject the full annotated contract as environment variables (or secrets) on all three run modes. The minimum to boot:
# Required — no defaults; the app fails fast without them.
DATABASE_URL=postgresql://USER:PASS@HOST:PORT/DB
BETTER_AUTH_SECRET=<openssl rand -base64 32>
HATCHET_CLIENT_TOKEN=<from your Hatchet>
# Hatchet endpoint + transport (match your engine).
HATCHET_CLIENT_HOST_PORT=<your-hatchet-host:7077>
HATCHET_CLIENT_TLS_STRATEGY=tls # tls for Hatchet Cloud; none for an
# insecure internal hatchet-lite
# Email provider — swappable, Resend by default. RESEND_API_KEY is OPTIONAL
# (a Postmark-only deploy boots without it). For Postmark instead:
# EMAIL_PROVIDER=postmark + POSTMARK_SERVER_TOKEN (+ POSTMARK_WEBHOOK_USER/PASS).
RESEND_API_KEY=re_…
RESEND_FROM_EMAIL=noreply@yourdomain.com
# First Studio admin — public sign-up is disabled, so mint it from your server.
# Either set this (minted on boot into an empty user table)…
STUDIO_ADMIN_EMAIL=admin@example.com
STUDIO_ADMIN_PASSWORD=... # optional; omit and one is printed once to the log
# …or run `hogsend studio admin create` from a shell with DATABASE_URL + BETTER_AUTH_SECRET loaded.
# Production hygiene. Redis is REQUIRED — auth secondaryStorage gates on a raw REDIS_URL.
NODE_ENV=production
REDIS_URL=redis://HOST:PORT
API_PUBLIC_URL=https://api.yourdomain.com
BETTER_AUTH_URL=https://api.yourdomain.comSecure by default. HATCHET_CLIENT_TLS_STRATEGY defaults to tls, so Hatchet Cloud and any TLS-fronted engine work without thinking about it. Only override to none when you are dialing an explicitly insecure internal endpoint (e.g. a hatchet-lite on a private network).
Healthchecks and scaling
- api — liveness/readiness on
GET /v1/health. It returnsstatus: "migration_pending"(non-200 semantics for your probe) when a track is behind, and"healthy"when both are in sync. Stateless; scale horizontally behind a load balancer. - worker — no HTTP endpoint, so use a process-liveness probe (the container exits on fatal errors; rely on your scheduler's restart policy). Hatchet distributes tasks across worker replicas with no duplicate execution — scale on queue depth. Workers handle graceful shutdown on
SIGTERM/SIGINT.
Future-cloud note
Nothing in the engine bakes in single-tenancy. The DI container (createHogsendClient) is the single composition root, HATCHET_CLIENT_NAMESPACE provides per-tenant isolation on a shared engine, and the domain tables carry a nullable tenant column. A multi-tenant control plane is additive on top of this contract — see the deployment plan §6 — but it is not built today, and self-host behaves identically with the default sentinel.
Next steps
Deploy on Railway (one paved option)
Railway is one paved on-ramp for Hogsend — managed Postgres + Redis, self-hosted hatchet-lite, GitHub auto-deploy. Two services from one repo, same build, different entry points.
Production email on a fresh domain
A domain registered this morning to DKIM-signed lifecycle email in about thirty minutes — no mailbox provider needed.