Setting up sfp server
This page walks you through initializing and starting a self-hosted sfp server. Complete the Prerequisites and Setting up Docker on the server or Podman Support pages before you begin. You run two lifecycle commands: sfp server init writes the tenant configuration (it does not start containers), then sfp server start brings the stack up and runs migrations.
[1] Create a server.json on your local machine
Save this file in the directory where you run sfp server init and pass it with --config-file. Every field is explained in the Reference → Properties page; defaults handle the most common install path. The two Docker registry secrets are required.

cat{
"domain": "sfp.yourcompany.com",
"workers": 2,
"secrets": {
"DOCKER_REGISTRY": "source.flxbl.io",
"DOCKER_REGISTRY_TOKEN": "<paste-token-from-step-1.1>"
}
}image_fqdn and image_tag are optional overrides. By default the production cadence resolves to source.flxbl.io/flxbl/sfp-pro-v3/sfp-server:v3-latest (see Reference → Properties); set them only to mirror images to a private registry or pin a specific tag at init.
[1.1] Generate DOCKER_REGISTRY_TOKEN (required)
sfp server init validates registry credentials and fails if DOCKER_REGISTRY or DOCKER_REGISTRY_TOKEN is missing, in both dev and prod mode. Get a token at source.flxbl.io → User Settings → Applications — Generate New Token, All repos + Org access, package: Read scope. Contact flxbl for source.flxbl.io access if you don't have it yet.
Provide the credentials via one of:
server.jsonsecretsblock (shown above) — simplest for prod installs- Environment variables:
export DOCKER_REGISTRY=source.flxbl.io+export DOCKER_REGISTRY_TOKEN=<token>before running init - Secrets provider:
--secrets-provider infisicaloraws-secretsmanager(see Reference)
The registry username defaults to USERNAME (token-only registries); set DOCKER_REGISTRY_USERNAME when a named user is required.
[2] Configure TLS
DNS prerequisite. All options assume your domain (e.g.
sfp.yourcompany.com) already resolves to the server's public IP with anArecord. See Prerequisites → Network & DNS for the exact Cloudflare / Route 53 / GoDaddy table (TTL, proxy mode,digverification). Incloudflaremode keep the main record DNS-only / grey cloud; the orange-cloud proxy intercepts the ACME challenge used for the admin subdomain.
SFP Server uses Caddy as its reverse proxy. Caddy runs in every mode (it is required for auth routing); --no-caddy is deprecated and ignored. The mode selects which Caddyfile is rendered and is set during sfp server init via --tls-mode:
| Mode | --tls-mode value | What is rendered | What you provide |
|---|---|---|---|
| Bring Your Own Cert | cloudflare (default) | Origin CA cert pair + Cloudflare trusted_proxies | Base64-encoded ORIGIN_CERT + ORIGIN_KEY (or place files manually) |
| Let's Encrypt | letsencrypt | Automatic ACME certificates | Ports 80 + 443 open, public DNS |
| Custom / On-demand | custom | Base prod Caddyfile with Caddy on-demand TLS | Nothing — Caddy requests each host's cert on demand |
| Behind Load Balancer | none | HTTP only (no TLS termination) | External proxy terminates TLS, forwarded headers |
Admin surfaces (Hatchet dashboard, npm registry, Supabase Studio) are IP-restricted. In cloudflare mode they are served on a dedicated admin subdomain — <first-label>-admin.<rest> of your domain (e.g. sfp.yourcompany.com → sfp-admin.yourcompany.com), which must be DNS-only so Caddy can issue Let's Encrypt certs for it. The allowed IPs come from ALLOWED_IPS in .env (default 88.216.59.233). In all other modes admin surfaces stay on the main domain's 8080 / 4873 / 3100 ports behind the same ALLOWED_IPS gate.
[2.a] Bring Your Own Certificate (--tls-mode cloudflare, default)
Use this when the server is on a private network or you own the certificate chain (internal CA, commercial CA, or Cloudflare Origin CA). The CLI base64-decodes the two secrets into PEM files in the tenant's certs/ directory.
- Obtain a TLS certificate + private key for your domain (full chain recommended).
- Base64-encode both PEM files:

base64 -w 0 origin.pem # → ORIGIN_CERT
base64 -w 0 origin-key.pem # → ORIGIN_KEY- Provide
ORIGIN_CERTandORIGIN_KEYat init via one of: thesecretsblock of--config-file, environment variables (export ORIGIN_CERT=...), or a secrets provider. - The CLI writes
origin.pem/origin-key.peminto{tenantDir}/certs/automatically (see the decoded path in the figure above).
If you have
.crt+.keyfiles instead of.pem, they are the same format — rename before encoding:cp your-domain.crt origin.pemandcp your-domain.key origin-key.pem.
Fallback: if you skip the secrets, the CLI warns and you can manually place
origin.pemandorigin-key.pemin{tenantDir}/certs/before starting services.
[2.b] Automatic TLS via Let's Encrypt (--tls-mode letsencrypt)
- Ensure your domain's DNS resolves to the server's public IP.
- Open ports 80 and 443 inbound:
sudo ufw allow 80/tcp # ACME challenge sudo ufw allow 443/tcp # HTTPS - No certificate files are needed — Caddy obtains and renews certificates via ACME.
Caddy fails to obtain a cert if DNS hasn't propagated or port 80 is blocked by an upstream firewall. Verify both before running
sfp server init.
[2.c] Behind a Load Balancer (--tls-mode none)
If existing infrastructure (AWS ALB, Azure Application Gateway, F5, NGINX) already terminates TLS:
- Terminate TLS on the load balancer.
- Forward traffic to Caddy's HTTP listener (port 443 is the published HTTP port; dev uses 3029).
- Preserve
X-Forwarded-Proto: https,X-Forwarded-Host, andX-Forwarded-For; setTRUST_PROXY_HOPSto your proxy-chain depth (see Proxy / Client IP). - Pass
--tls-mode noneat init.
Caddy still runs and routes auth, but serves HTTP only.
For GitHub.com, Azure DevOps, or other public SaaS webhook providers, the webhook URL must be reachable over public HTTPS — reachability via SSH, VPN, a bastion, or ProxyJump is not webhook reachability. Keep the backend private and expose only the public webhook payload URL unless you intentionally publish the whole UI/API:
https://<public-webhook-host>/sfp/api/repository/webhookSee Webhook ingress for private servers for the security model and provider-specific checks. In all patterns, preserve the raw request body and the provider's signature headers.
Do not use SSH reachability as the webhook test. ssh -J <bastion> <server> only proves an operator can administer the server, not that GitHub.com can deliver a webhook. Test the exact configured payload URL with the provider's delivery tooling (GitHub Recent deliveries / Redeliver).
[3] Initialize, then start
You can run init and start remotely from your workstation (via SSH flags) or locally on the server. init writes configuration only; start creates volumes, pulls images, boots the stack, and runs database migrations.
[3.a] Initialize and start remotely (from your workstation)

sfp server init success output: tenant details and where credentials are savedsfp server init \
--base-dir /opt/sfp-server \ # install path on the target box — init creates this directory
--tenant your-company \ # one tenant = one isolated install. Lives at <base-dir>/tenants/<tenant>/
--mode prod \ # prod for real installs; dev for local tinkering
--domain sfp.yourcompany.com \ # FQDN used in TLS and auth callbacks
--config-file ./server.json \ # the file from step 1
--ssh-connection ubuntu@your-server-ip \
--identity-file ~/.ssh/your-key.pem \
--tls-mode cloudflare # see step 2 for options
sfp server start \
--base-dir /opt/sfp-server \
--tenant your-company \
--ssh-connection ubuntu@your-server-ip \
--identity-file ~/.ssh/your-key.pemstart pulls images from the registry, so export DOCKER_REGISTRY and DOCKER_REGISTRY_TOKEN in the same shell session (or pass --secrets-provider) before start on a fresh install. Verify after each boot:

/health response used as an availability checkcurl https://sfp.yourcompany.com/health
startcleans up unused images older than 24h after booting (image prune -af --filter "until=24h").
[3.b] Run locally (on the server itself)
SSH into the server and install the sfp CLI. Replace <version> with the tag from source.flxbl.io/flxbl/sfp-pro-v3/releases (e.g. v3.5.1):
VERSION=<version> # e.g. v3.5.1
TOKEN=<your-source-flxbl-pat>
# Ubuntu / Debian
curl -sL -H "Authorization: token $TOKEN" \
"https://source.flxbl.io/flxbl/sfp-pro-v3/releases/download/$VERSION/sfp-pro_${VERSION#v}_linux_amd64.deb" \
-o /tmp/sfp-pro.deb
sudo dpkg -i /tmp/sfp-pro.deb
# RHEL / Fedora
curl -sL -H "Authorization: token $TOKEN" \
"https://source.flxbl.io/flxbl/sfp-pro-v3/releases/download/$VERSION/sfp-pro_${VERSION#v}_linux_amd64.rpm" \
-o /tmp/sfp-pro.rpm
sudo rpm -i /tmp/sfp-pro.rpmGitea release-asset URLs require a pinned tag (
/releases/download/<tag>/<asset>) because the.deb/.rpmfilenames embed the version (sfp-pro_3.5.1_linux_amd64.deb). Browse the releases page for the current version.
Then run init and start without --ssh-connection / --identity-file:
sfp server init \
--base-dir /opt/sfp-server \
--tenant your-company \
--mode prod \
--domain sfp.yourcompany.com \
--config-file ./server.json \
--tls-mode cloudflare
sfp server start \
--base-dir /opt/sfp-server \
--tenant your-company
curl https://sfp.yourcompany.com/healthAll subsequent lifecycle commands (
start,stop,status,logs,update,scale) take--tenantand--base-dir; drop--ssh-*flags when running locally.
Quick evaluation without a domain? Use
--mode devto skip TLS and domain requirements. Caddy serves HTTP on port 3029 and the server is reachable athttp://<server-ip>:3029. Pass the server IP as--domainwhen a domain is desired.
[3.i] What init does (in order):
- Checks the container engine (Docker or Podman, per
--container-engine) and compose are installed. - Creates
<base-dir>/tenants/<tenant>/plusconfig/andsecrets/(secrets dir chmod700). With--force, it first stops the stack, removes that tenant's named volumes, and deletes the existing tenant directory. - Collects config (cicd provider, worker count, domain, release cadence, supabase mode).
- Collects secrets:
server.json→ environment → secrets provider → existing remote.env→ self-hosted auto-generation. - Self-hosted Supabase: generates
POSTGRES_PASSWORD,JWT_SECRET, anon/service keys,PG_META_CRYPTO_KEY, a PKCS#1 SAML private key, Studio basic-auth, and object-storage S3 credentials. Cloud Supabase: tests DB connectivity (viasupabase db dumpon the local workstation — this needs the Supabase CLI locally). - Validates registry login against
DOCKER_REGISTRY+DOCKER_REGISTRY_TOKEN. - Writes
{tenantDir}/.env(chmod600), rendersdocker-compose.yml,config/Caddyfile(per TLS mode), the Envoy gateway config, andcerts/origin.pem/origin-key.pemwhen provided. Copies seed SQL, migrations, and postgres init scripts. - Writes the admin
credentials.json(self-hosted) or creates the admin user directly against Supabase (cloud). See Step 4. - Writes
sfp-server-init-<tenant>.jsonin the working directory with the resulting config. - Migrations are not run at init. They run when the stack starts, in the
supabase-migrationscontainer (self-hosted and cloud).startwaits up to 2 minutes for them.
[3.c] Enable auto-restart on the server
ssh ubuntu@your-server-ip
sudo systemctl enable docker
docker update --restart=unless-stopped $(docker ps -q)[4] Log in
Open https://sfp.yourcompany.com in a browser and sign in with the admin email and password. The default admin email is admin@<tenant>.local (override at init with SFP_DEFAULT_ADMIN_EMAIL). On a self-hosted install the user is created on first startup via seed.sql; the password uses the form XXXX-XXXX-XXXX.
Credentials land in {baseDir}/tenants/{tenant}/credentials.json (chmod 600), containing adminEmail, adminPassword, studioUser, and studioPassword. --print-credentials additionally prints the email and password to stdout — do not use it in CI/CD, where it appears in logs.

Use the Sign in with email option. By default password sign-in requires TOTP: you enroll an authenticator app on first login (AUTH_PASSWORD_REQUIRE_MFA=true). If you lose the authenticator, reset it with sfp server user mfa reset --target-email <email>.
[5] Configure your git provider
With the server up and you logged in, connect your git provider from the UI. Nothing here was needed at init. Two entry points reach the same configuration:
- Onboarding wizard — the 8/8 Onboarding panel walks through App Integration, Dev Hub, Project Readiness, and Configure Webhooks per feature.
- Settings → Integrations (
/settings/integrations) — the durable surface to add, view, and re-scope integrations. Source control providers (GitHub, Azure DevOps, GitLab) live under SOURCE CONTROL.

Credentials are stored encrypted at rest and can be global or per-project.
[5.a] GitHub (github.com)
In Settings → Integrations → GitHub, install the prebuilt flxbl-cloud GitHub App (github.com/apps/flxbl-cloud). You authorize it on GitHub and pick the repositories; no keys to copy. This is the whole setup for cloud GitHub.
[5.b] GitHub Enterprise Server
GHES has no prebuilt app, so you register your own GitHub App and enter its App ID + private key in Settings → Integrations → GitHub. Grant the permissions per Connecting GitHub as a CI/CD provider.
[5.c] Azure DevOps
In Settings → Integrations → Azure DevOps, enter a service principal (Microsoft Entra app registration): organization_url, client_id, client_secret, tenant_id (optional entra_authority_url for sovereign clouds). Once connected it shows as Service Principal. sfp-server subscribes these service hooks: git.push, git.pullrequest.created, git.pullrequest.updated, git.pullrequest.merged, ms.vss-code.git-pullrequest-comment-event.
[5.d] Webhooks

The git integration creates repository webhooks (or Azure DevOps service hooks) automatically. Review and re-sync them under Settings → Webhooks. The server's webhook endpoint must be reachable from the provider — see Webhook ingress for private servers.
[5.e] GitHub Packages (npm)
Only if your pipelines pull or publish npm packages from npm.pkg.github.com: add a token with read:packages + write:packages under Settings → Integrations → npm Registry. GitHub Apps cannot operate on GitHub Packages, so this is a separate registry-auth token.
[6] Configure login
The admin email/password login works immediately after init. To let your team sign in with their own accounts, enable one login provider. This is set by AUTH_PROVIDER (github / azure / saml, default github).
| Provider | How to enable |
|---|---|
| GitHub OAuth | Create a GitHub OAuth App (callback https://<your-domain>/auth/v1/callback), set GITHUB_OAUTH_CLIENT_ID + GITHUB_OAUTH_CLIENT_SECRET and GITHUB_OAUTH_ENABLED=true in the server .env, then restart. |
| Azure OAuth | Register a Microsoft OAuth app, set AZURE_OAUTH_CLIENT_ID / _SECRET / _TENANT_ID (and AZURE_OAUTH_ENABLED=true) in .env, restart. |
| SAML SSO | Okta, Entra ID, or any SAML 2.0 IdP — see SAML Authentication. In self-hosted mode the lifecycle generates the GoTrue SAML key and enables SAML automatically. |
Login OAuth is wired into the auth layer (GoTrue) at container start, so it needs an
.envedit and a stack restart — it is not a pure UI toggle. The login provider is independent of the git provider in Section 5; even if both are GitHub, they are two separate registrations (OAuth App for login, GitHub App for repository work).
Note on attribution: issues, release requests, and PRs raised on a user's behalf run under that signed-in user's OAuth token, so they appear under the user's name. Machine-driven work runs under the git provider's app / service principal from Section 5.
Next steps
| Page | What it covers |
|---|---|
| Operations | Start, stop, status, logs, scale, and other lifecycle commands |
| Updating sfp server | Image bumps, drains, migrations via sfp server update |
| Reference | Properties, secrets, CLI flags, cloud Supabase setup |
| Troubleshooting | Common issues, fixes, and hardening recommendations |
| Connecting GitHub as a CI/CD provider | GitHub App registration + permissions reference (deep dive for [5.b] GHES) |
| SAML Authentication | Configure SAML SSO with Entra ID, Okta, or any SAML 2.0 IdP |