Self-Hosting Prerequisites
Prepare a host for sfp-server before you run sfp server init — sizing, operating system, the container engine, network, DNS, TLS, firewall, and the registry secrets required at init time.
This page covers what you prepare before running sfp server init on the host that will run sfp-server. It lists what sfp server init checks, what the full container stack runs, and the inputs you must gather.
Download the sfp CLI from source.flxbl.io/flxbl/sfp-pro/releases and install it on the machine that runs the init command — your workstation (init can be run remote over SSH) or the server itself (local mode).
[1] Server Requirements
[1.a] Hardware sizing
sfp-server ships as a single self-contained Docker Compose stack. On the host it runs these services, so dimension the host for the whole stack, not just the app:
- Application:
server(NestJS API + SPA) and the Hatchet workers (default + long-running). - Workflow orchestration:
hatchet-db(Postgres 15),hatchet-migrate,hatchet-setup-config,hatchet-engine,hatchet-init,hatchet-dashboard. - Telemetry:
victoriametrics(metrics),victorialogs(logs), and the optionalotel-collector(only with themonitoringCompose profile). - Private npm registry:
verdaccio(+ one-shotverdaccio-init). - Self-hosted Supabase (the default
--supabase-mode self-hosted):supabase-db(Postgres 15),supabase-auth(GoTrue),supabase-rest(PostgREST),supabase-storage,supabase-envoy(API gateway),supabase-meta, and the opt-insupabase-studio(only with thestudioprofile). - Reverse proxy:
caddy— the only service that publishes ports to the host.
The exact pinned images live in resources/docker/docker-compose.yml on the sfp-pro branch (supabase/postgres:15.14.1.162, supabase/gotrue:v2.195.0, postgrest/postgrest:v14.17, supabase/storage-api:v1.69.9, supabase/postgres-meta:v0.98.0, supabase/studio:2026.08.03-sha-022b374, envoyproxy/envoy:v1.39.0, victoriametrics v1.144.0 / v1.50.0, hatchet-* v0.98.9).
Use these footprints as a starting point and measure against the worker count you configure (--workers, and HATCHET_WORKER_COUNT / HATCHET_LONGRUNNING_WORKER_COUNT):
| Tier | vCPU | RAM | Disk | Typical workload |
|---|---|---|---|---|
| Minimum | 4 | 16 GB | 100 GB SSD | Small team (< 10 developers) |
| Recommended | 8 | 32 GB | 250 GB SSD | Medium team (10–50 developers) |
| Production | 16 | 64 GB | 500 GB SSD | Large team (50+), heavy parallel workflows |
Disk should be SSD; workflow orchestration and database queries are I/O intensive. Keep headroom on the host CPU/RAM: every container in the stack (Supabase, Hatchet, telemetry, Verdaccio) runs alongside the app. The one hard floor the CLI actually enforces is on the PostgreSQL data volumes: hatchet-db sets shm_size: 1g, and VictoriaMetrics refuses to write below 5 GB of free disk (-storage.minFreeDiskSpaceBytes=5368709120), so keep at least several GB free on the data volume for telemetry.
The CLI does not check CPU, memory, or disk during init.
sfp server initonly verifies that the container engine and Compose are present. These floors are operational guidance, not enforced gates.
[1.b] Operating system
- Linux x86_64 is the deployment target. Production and CI builds ship
linux/amd64images. - ARM64 hosts are supported but run the whole stack under x86 emulation. Every service in the self-hosted stack pins
platform: linux/amd64indocker-compose.yml, so an arm64 host (for example AWS Graviton) pulls the amd64 child image and emulates it rather than running it natively. This adds a constant CPU tax. See decision recorddecision records/server/009-arm64-multi-arch-images.md. The exception is sfp-pro's internal dev stack (npm run compose:up), which can select a native arm64 image viaSFP_DEV_PLATFORM=linux/arm64— that opt-in does not change the deployed self-hosted stack.
[1.c] Software to install
sfp server init runs its prerequisite check at the start (see packages/cli/impl/server/lifecycle/ServerInitImpl.ts → ContainerEngine). It confirms the engine binary and the Compose command exist, and — for Podman — that the Compose provider is Docker Compose v2, not podman-compose.

| Software | Requirement | How it is checked |
|---|---|---|
| Docker Engine (default) or Podman | Binary present and docker compose (Podman: podman compose) works | sfp server init runs docker --version and docker compose version (or the Podman equivalents) |
| Docker Compose v2 | Required as the Compose backend | docker compose version must succeed. For Podman, the provider must report Docker Compose v2 — podman-compose (Python) is rejected because it mishandles the stack's depends_on graph (see the error in PodmanContainerEngineAdapter.ts) |
| Podman on an SELinux-enforcing host (RHEL/Fedora) | Graphroot must not carry the user_home_t label | sfp server init runs getenforce, checks podman info --format '{{.Store.GraphRoot}}', and runs restorecon; it fails if the label is still user_home_t |
| sfp CLI | The build that matches your server release | sfp --version |
The check is presence-only: it confirms the engine binary and the Compose command exist, it does not enforce a specific engine version. Compose must be v2 (
docker compose), not the legacy standalonedocker-compose. See Setting up Docker on the server for Docker installation, or Podman Support for the SELinux/graphroot detail and thepodman-composerejection.
[2] Network & DNS
[2.a] Choose your domain
Pick the hostname that will serve the codev UI and the sfp API. Examples:
flxbl.yourcompany.comcodev.yourcompany.comdevops.internal.yourcompany.com(private/internal domains work for interactive use)
In --tls-mode cloudflare (the default), sfp-server also derives an admin hostname from the domain — the public "admin" dashboards (Hatchet, Verdaccio, Studio) live there. You configure this in server.json / the init prompt; the mode decides the template that generates the Caddyfile.
[2.b] Create a DNS record
In your DNS provider, create an A record:
| Field | Value |
|---|---|
| Type | A |
| Name | Your chosen subdomain (for example sfp) |
| Value | Your server's IP address |
| Proxy | Off / DNS-only (for Cloudflare, grey cloud — bypass the proxy for the DNS-only admin domain) |
| TTL | Auto or 300 |
For the cloudflare mode the admin domain is typically DNS-only; the main domain is proxied. The exact split depends on the Caddy template the mode selects (see [2.d]).
[2.c] Verify DNS resolves
dig sfp.yourcompany.com +short
# Should return your server IP
If using a private/internal domain, verify from a machine on the same network.
[2.d] Choose your TLS mode
--tls-mode selects how Caddy terminates TLS, and the default is cloudflare (see packages/cli/commands/server/init.ts):
| Mode | --tls-mode value | What it does | Ready before init |
|---|---|---|---|
| Cloudflare (default) | cloudflare | Serves the Origin CA certificate and trusts Cloudflare's proxy IP ranges | ORIGIN_CERT + ORIGIN_KEY (base64-encoded PEM) |
| Let's Encrypt | letsencrypt | Caddy auto-obtains a certificate via ACME | Public DNS resolving to the server, inbound TCP 443 open, usually 80 for the ACME fallback |
| Custom | custom | Caddy serves user-provided certificate files | Your cert files in the host certs/ directory |
| HTTP only | none | Caddy serves HTTP on port 3029; an external reverse proxy (ALB, F5, NGINX, API gateway, tunnel) terminates TLS | A proxy that forwards HTTP to the server and passes the origin protocol |
Full configuration steps live in Setting up sfp server → Configure TLS.
In
cloudflaremode, ifADMIN_DOMAINis not set it is derived from the main domain by replacing the first label with<tenant>-admin(for exampleacme.flxbl.io→acme-admin.flxbl.io).
[2.e] Firewall / security group
Only caddy publishes ports to the host. The API gateway (supabase-envoy, internal port 8000) and the sfp server (server, internal port 3029) are expose-only on the Docker network — they are never published to the host, so they need no inbound firewall rule and are not directly reachable from outside.
Caddy publishes four ports, all configurable in .env (defaults in docker-compose.yml):
| Port | Env var (default) | Purpose | Required when |
|---|---|---|---|
| 443 | HTTP_PORT (443) | HTTPS — codev UI + sfp API | Always for the HTTPS modes (except none) |
| 80 | — | Let's Encrypt ACME HTTP fallback | Only --tls-mode letsencrypt (TLS-ALPN runs on 443; 80 is the HTTP-01 fallback) |
| 8080 | HATCHET_DASHBOARD_PORT (8080) | Hatchet dashboard | When you use it — IP-restricted by Caddy |
| 4873 | VERDACCIO_PORT (4873) | Verdaccio npm registry | When you use it — IP-restricted by Caddy |
| 3100 | STUDIO_PORT (3100) | Supabase Studio | When you opt in to Studio — IP-restricted by Caddy, plus basic auth |
The dashboards on ports 8080, 4873 and 3100 are IP-restricted by Caddy: only addresses matching 127.0.0.1, RFC 1918 ranges, and the ALLOWED_IPS value reach them. Set ALLOWED_IPS in .env (comma-separated IPs or CIDR ranges) if the defaults do not fit. These ports should not be open to all users.
For GitHub.com, Azure DevOps, and other SaaS webhook providers, the HTTPS entry point must be reachable from the public internet. Private-only DNS names, bastion-only hosts, VPN-only hosts, and SSH ProxyJump paths are valid for operators, but not for inbound webhooks. If the server is private, front it with public HTTPS ingress and keep the backend host private behind that ingress.
For GitHub.com webhooks, do not depend on source-IP allowlists as the primary control: IP ranges change and source IP does not prove authenticity. Let sfp-server validate the provider signature. Expose only the public HTTPS ingress and keep the backend privately reachable to that ingress.
See Webhook ingress for private servers and Setting up sfp server → Behind a Load Balancer.
[2.f] SSH access
sfp server init can run remotely from your workstation over SSH, or locally on the server. If running remotely:
- The server must be reachable over SSH from the machine running sfp.
- The SSH user must be able to run
dockercommands (member of thedockergroup, orroot).
[2.g] Outbound access
The server needs outbound internet access for image pulls at init and update time:
- The private registry that hosts the sfp-server image — the configured
DOCKER_REGISTRY(the default image issource.flxbl.io/flxbl/sfp-server). - Docker Hub — the Supabase, Caddy, VictoriaMetrics, Verdaccio, and WireMock images.
- GitHub Container Registry — the Hatchet images.
- github.com — the Supabase CLI is downloaded once during init by the one-shot
supabase-migrationscontainer and verified by checksum before it applies migrations. - GitHub API / the provider API — for OAuth and repository operations at runtime.
If using --tls-mode letsencrypt, keep the ACME ports (443, and 80 for the fallback) open outbound so Caddy can complete the challenge.
[2.h] What you provide to sfp server init
After this section you should have:
| Value | Example | Source |
|---|---|---|
| Tenant name | acme | CLI arg, lower-case letters, numbers, hyphens |
| Domain | sfp.yourcompany.com | [2.a] |
| TLS mode | cloudflare, letsencrypt, custom, or none | [2.d] |
ORIGIN_CERT + ORIGIN_KEY (cloudflare mode only) | Base64-encoded PEM cert + key | [2.d] |
DOCKER_REGISTRY + DOCKER_REGISTRY_TOKEN | The registry hosting the server image + a token with package read access | Section [5] |
[2.i] Reverse-proxy headers
If sfp-server sits behind one or more reverse proxies (Cloudflare, a customer load balancer, NGINX), every proxy in the chain must preserve or set these headers:
| Header | Purpose |
|---|---|
X-Forwarded-For | Client IP — used for IP attribution, rate limiting, monitoring allowlists, and audit logs. Controlled by trustProxyHops (Express trust proxy, default 2, range 0–10). |
X-Forwarded-Proto | Original protocol (https) — used for request-origin detection and redirect URLs. Read separately, not gated by trustProxyHops. |
X-Forwarded-Host | Original hostname — used for auth callbacks and link generation. Read separately, not gated by trustProxyHops. |
In cloudflare mode, Caddy additionally restricts remote_ip to Cloudflare's published proxy ranges via trusted_proxies. See Setting up sfp server → Properties → trustProxyHops and Troubleshooting → Proxy / Client IP if client IPs look wrong.
[3] Git provider & login — configured after the server is up
You do not set up GitHub, Azure DevOps, or login auth here, and none of it is needed for sfp server init. Once the server is running, connect the git provider and configure authentication from the codev UI:
| What | Where | Guide |
|---|---|---|
| Git provider (GitHub App / Azure DevOps service principal) + repository integration | Settings → Integrations (and the onboarding wizard) | Setting up sfp server → Configure your git provider |
| Login auth (GitHub OAuth / Azure OAuth / SAML SSO) | .env + restart, or SAML connection | Setting up sfp server → Configure login |
You can prepare a GitHub App or Azure service principal in advance, but you enter it post-boot.
[4] Admin IP addresses
Collect the public (or, on a corporate network, private) IP addresses of anyone who needs the administrative dashboards — Hatchet dashboard (8080), Supabase Studio (3100), Verdaccio (4873). Caddy restricts these by source IP:
- Single IP:
ALLOWED_IPS=203.0.113.10 - Multiple IPs:
ALLOWED_IPS=203.0.113.10,198.51.100.5 - CIDR ranges:
ALLOWED_IPS=10.0.0.0/8,172.16.0.0/12
Set ALLOWED_IPS in .env after init. After changing it, restart Caddy for the change to take effect: docker compose restart caddy.
[5] Secrets / configuration
sfp server init reads its inputs from --config-file (recommended), environment variables, or a --secrets-provider (Infisical, AWS Secrets Manager). Database credentials and Supabase API keys are auto-generated — you do not create them.
Three things are genuine init-time inputs:
| Secret | When | Description |
|---|---|---|
DOCKER_REGISTRY | Always | The container registry that hosts the sfp-server image (for flxbl-hosted tenants, source.flxbl.io) |
DOCKER_REGISTRY_TOKEN | Always | Token with package read access for that registry |
ORIGIN_CERT + ORIGIN_KEY | --tls-mode cloudflare | Base64-encoded PEM cert + key |
DOCKER_REGISTRY_USERNAME is optional and defaults to USERNAME for token-only registries. sfp server init validates the pair at secret-collection time by logging into the registry; if either is missing it fails with "Docker registry credentials not found. Please set DOCKER_REGISTRY and DOCKER_REGISTRY_TOKEN." (see packages/cli/impl/server/lifecycle/SecretsManager.ts).
Git provider credentials, login OAuth, and the GitHub Packages token are not init secrets — you configure them after the server is up (Section [3]). Do not put them in
server.json; the GitHub App deploy key is configured via the integration API post-boot.
Place the init inputs in the
secretsblock ofserver.jsonand pass it via--config-file ./server.json— the canonical path for repeatable deployments. See Setting up sfp server for the file shape.
[6] Pre-installation checklist
Run through this before sfp server init:
- Server provisioned with adequate resources (Section [1])
- Container engine installed: Docker Engine or Podman, with Docker Compose v2 (
docker compose versionworks) - Podman rootless on RHEL/Fedora: graphroot label is not
user_home_t(see Podman Support) - SSH access to the server (if running init remotely)
- sfp CLI installed on the machine that runs init
- Domain DNS record configured and resolving to the server IP
- Origin CA certificate and key ready as base64-encoded PEM (if using
--tls-mode cloudflare) - Firewall ports open: 443 (always for HTTPS modes) + 80 (only if using
--tls-mode letsencrypt); 8080/4873/3100 only to admin IPs - Server has outbound internet access (private registry, Docker Hub, GitHub Container Registry, provider API)
-
DOCKER_REGISTRYandDOCKER_REGISTRY_TOKENobtained - Admin IP addresses collected for the dashboards (configured post-init via
ALLOWED_IPS) - (After the server is up — Section [3]) git provider connected in the codev UI; login provider configured
Next Steps
Once the prerequisites are in place, proceed to Setting up sfp server for installation, configuration, and first-run.