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.

server.json sample displayed with cat
The sample server.json shown with 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.json secrets block (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 infisical or aws-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 an A record. See Prerequisites → Network & DNS for the exact Cloudflare / Route 53 / GoDaddy table (TTL, proxy mode, dig verification). In cloudflare mode 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 valueWhat is renderedWhat you provide
Bring Your Own Certcloudflare (default)Origin CA cert pair + Cloudflare trusted_proxiesBase64-encoded ORIGIN_CERT + ORIGIN_KEY (or place files manually)
Let's EncryptletsencryptAutomatic ACME certificatesPorts 80 + 443 open, public DNS
Custom / On-demandcustomBase prod Caddyfile with Caddy on-demand TLSNothing — Caddy requests each host's cert on demand
Behind Load BalancernoneHTTP 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.comsfp-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.

  1. Obtain a TLS certificate + private key for your domain (full chain recommended).
  2. Base64-encode both PEM files:
base64 encoding origin.pem and origin-key.pem
Encoding the Origin CA certificate and key
base64 -w 0 origin.pem      # → ORIGIN_CERT
base64 -w 0 origin-key.pem  # → ORIGIN_KEY
  1. Provide ORIGIN_CERT and ORIGIN_KEY at init via one of: the secrets block of --config-file, environment variables (export ORIGIN_CERT=...), or a secrets provider.
  2. The CLI writes origin.pem / origin-key.pem into {tenantDir}/certs/ automatically (see the decoded path in the figure above).

If you have .crt + .key files instead of .pem, they are the same format — rename before encoding: cp your-domain.crt origin.pem and cp your-domain.key origin-key.pem.

Fallback: if you skip the secrets, the CLI warns and you can manually place origin.pem and origin-key.pem in {tenantDir}/certs/ before starting services.

[2.b] Automatic TLS via Let's Encrypt (--tls-mode letsencrypt)

  1. Ensure your domain's DNS resolves to the server's public IP.
  2. Open ports 80 and 443 inbound:
    sudo ufw allow 80/tcp    # ACME challenge
    sudo ufw allow 443/tcp   # HTTPS
  3. 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:

  1. Terminate TLS on the load balancer.
  2. Forward traffic to Caddy's HTTP listener (port 443 is the published HTTP port; dev uses 3029).
  3. Preserve X-Forwarded-Proto: https, X-Forwarded-Host, and X-Forwarded-For; set TRUST_PROXY_HOPS to your proxy-chain depth (see Proxy / Client IP).
  4. Pass --tls-mode none at 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/webhook

See 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 with tenant details and credentials path
sfp server init success output: tenant details and where credentials are saved
sfp 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.pem

start 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:

curl health endpoint response
The /health response used as an availability check
curl https://sfp.yourcompany.com/health

start cleans 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.rpm

Gitea release-asset URLs require a pinned tag (/releases/download/<tag>/<asset>) because the .deb/.rpm filenames 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/health

All subsequent lifecycle commands (start, stop, status, logs, update, scale) take --tenant and --base-dir; drop --ssh-* flags when running locally.

Quick evaluation without a domain? Use --mode dev to skip TLS and domain requirements. Caddy serves HTTP on port 3029 and the server is reachable at http://<server-ip>:3029. Pass the server IP as --domain when a domain is desired.

[3.i] What init does (in order):

  1. Checks the container engine (Docker or Podman, per --container-engine) and compose are installed.
  2. Creates <base-dir>/tenants/<tenant>/ plus config/ and secrets/ (secrets dir chmod 700). With --force, it first stops the stack, removes that tenant's named volumes, and deletes the existing tenant directory.
  3. Collects config (cicd provider, worker count, domain, release cadence, supabase mode).
  4. Collects secrets: server.json → environment → secrets provider → existing remote .env → self-hosted auto-generation.
  5. 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 (via supabase db dump on the local workstation — this needs the Supabase CLI locally).
  6. Validates registry login against DOCKER_REGISTRY + DOCKER_REGISTRY_TOKEN.
  7. Writes {tenantDir}/.env (chmod 600), renders docker-compose.yml, config/Caddyfile (per TLS mode), the Envoy gateway config, and certs/origin.pem / origin-key.pem when provided. Copies seed SQL, migrations, and postgres init scripts.
  8. Writes the admin credentials.json (self-hosted) or creates the admin user directly against Supabase (cloud). See Step 4.
  9. Writes sfp-server-init-<tenant>.json in the working directory with the resulting config.
  10. Migrations are not run at init. They run when the stack starts, in the supabase-migrations container (self-hosted and cloud). start waits 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.

The sfp server web login page
Signing in with the admin email and password

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.
Settings → Integrations with source control providers
Settings → Integrations, where git providers are added

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

Settings → Webhooks list
Settings → Webhooks, where payload URLs are reviewed and re-synced

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).

ProviderHow to enable
GitHub OAuthCreate 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 OAuthRegister a Microsoft OAuth app, set AZURE_OAUTH_CLIENT_ID / _SECRET / _TENANT_ID (and AZURE_OAUTH_ENABLED=true) in .env, restart.
SAML SSOOkta, 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 .env edit 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

PageWhat it covers
OperationsStart, stop, status, logs, scale, and other lifecycle commands
Updating sfp serverImage bumps, drains, migrations via sfp server update
ReferenceProperties, secrets, CLI flags, cloud Supabase setup
TroubleshootingCommon issues, fixes, and hardening recommendations
Connecting GitHub as a CI/CD providerGitHub App registration + permissions reference (deep dive for [5.b] GHES)
SAML AuthenticationConfigure SAML SSO with Entra ID, Okta, or any SAML 2.0 IdP

On this page