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 optional otel-collector (only with the monitoring Compose profile).
  • Private npm registry: verdaccio (+ one-shot verdaccio-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-in supabase-studio (only with the studio profile).
  • 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):

TiervCPURAMDiskTypical workload
Minimum416 GB100 GB SSDSmall team (< 10 developers)
Recommended832 GB250 GB SSDMedium team (10–50 developers)
Production1664 GB500 GB SSDLarge 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 init only 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/amd64 images.
  • ARM64 hosts are supported but run the whole stack under x86 emulation. Every service in the self-hosted stack pins platform: linux/amd64 in docker-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 record decision 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 via SFP_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.tsContainerEngine). It confirms the engine binary and the Compose command exist, and — for Podman — that the Compose provider is Docker Compose v2, not podman-compose.

sfp server init prerequisite check output showing the container engine and Compose being verified
sfp server init verifies the container engine and Compose before collecting configuration.
SoftwareRequirementHow it is checked
Docker Engine (default) or PodmanBinary present and docker compose (Podman: podman compose) workssfp server init runs docker --version and docker compose version (or the Podman equivalents)
Docker Compose v2Required as the Compose backenddocker compose version must succeed. For Podman, the provider must report Docker Compose v2podman-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 labelsfp server init runs getenforce, checks podman info --format '{{.Store.GraphRoot}}', and runs restorecon; it fails if the label is still user_home_t
sfp CLIThe build that matches your server releasesfp --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 standalone docker-compose. See Setting up Docker on the server for Docker installation, or Podman Support for the SELinux/graphroot detail and the podman-compose rejection.


[2] Network & DNS

[2.a] Choose your domain

Pick the hostname that will serve the codev UI and the sfp API. Examples:

  • flxbl.yourcompany.com
  • codev.yourcompany.com
  • devops.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:

FieldValue
TypeA
NameYour chosen subdomain (for example sfp)
ValueYour server's IP address
ProxyOff / DNS-only (for Cloudflare, grey cloud — bypass the proxy for the DNS-only admin domain)
TTLAuto 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
dig output resolving the sfp subdomain to the server IP address
Confirm the A record resolves before you run init.

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 valueWhat it doesReady before init
Cloudflare (default)cloudflareServes the Origin CA certificate and trusts Cloudflare's proxy IP rangesORIGIN_CERT + ORIGIN_KEY (base64-encoded PEM)
Let's EncryptletsencryptCaddy auto-obtains a certificate via ACMEPublic DNS resolving to the server, inbound TCP 443 open, usually 80 for the ACME fallback
CustomcustomCaddy serves user-provided certificate filesYour cert files in the host certs/ directory
HTTP onlynoneCaddy serves HTTP on port 3029; an external reverse proxy (ALB, F5, NGINX, API gateway, tunnel) terminates TLSA proxy that forwards HTTP to the server and passes the origin protocol

Full configuration steps live in Setting up sfp server → Configure TLS.

In cloudflare mode, if ADMIN_DOMAIN is not set it is derived from the main domain by replacing the first label with <tenant>-admin (for example acme.flxbl.ioacme-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):

PortEnv var (default)PurposeRequired when
443HTTP_PORT (443)HTTPS — codev UI + sfp APIAlways for the HTTPS modes (except none)
80Let's Encrypt ACME HTTP fallbackOnly --tls-mode letsencrypt (TLS-ALPN runs on 443; 80 is the HTTP-01 fallback)
8080HATCHET_DASHBOARD_PORT (8080)Hatchet dashboardWhen you use it — IP-restricted by Caddy
4873VERDACCIO_PORT (4873)Verdaccio npm registryWhen you use it — IP-restricted by Caddy
3100STUDIO_PORT (3100)Supabase StudioWhen 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 docker commands (member of the docker group, or root).

[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 is source.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-migrations container 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:

ValueExampleSource
Tenant nameacmeCLI arg, lower-case letters, numbers, hyphens
Domainsfp.yourcompany.com[2.a]
TLS modecloudflare, letsencrypt, custom, or none[2.d]
ORIGIN_CERT + ORIGIN_KEY (cloudflare mode only)Base64-encoded PEM cert + key[2.d]
DOCKER_REGISTRY + DOCKER_REGISTRY_TOKENThe registry hosting the server image + a token with package read accessSection [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:

HeaderPurpose
X-Forwarded-ForClient IP — used for IP attribution, rate limiting, monitoring allowlists, and audit logs. Controlled by trustProxyHops (Express trust proxy, default 2, range 010).
X-Forwarded-ProtoOriginal protocol (https) — used for request-origin detection and redirect URLs. Read separately, not gated by trustProxyHops.
X-Forwarded-HostOriginal 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:

WhatWhereGuide
Git provider (GitHub App / Azure DevOps service principal) + repository integrationSettings → 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 connectionSetting 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:

SecretWhenDescription
DOCKER_REGISTRYAlwaysThe container registry that hosts the sfp-server image (for flxbl-hosted tenants, source.flxbl.io)
DOCKER_REGISTRY_TOKENAlwaysToken with package read access for that registry
ORIGIN_CERT + ORIGIN_KEY--tls-mode cloudflareBase64-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 secrets block of server.json and 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 version works)
  • 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_REGISTRY and DOCKER_REGISTRY_TOKEN obtained
  • 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.

On this page