> For the complete documentation index, see [llms.txt](https://docs.flxbl.io/flxbl/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.flxbl.io/flxbl/sfp-server/setting-up/setting-up-sfp-server.md).

# Setting up sfp server

This page walks you through initializing and starting a self-hosted sfp server. Complete the [Prerequisites](/flxbl/sfp-server/setting-up/prerequisites.md) before you begin.

***

## **\[1]** Create a `server.json` on your local machine

Save this file as `./server.json` in the directory where you'll run `sfp server init`. Every field is explained in the [Reference → Properties](/flxbl/sfp-server/setting-up/setting-up-sfp-server/reference.md#properties) page; defaults handle the most common install path.

```json
{
  "domain": "sfp.yourcompany.com",
  "workers": 2,
  "image_fqdn": "source.flxbl.io/flxbl/sfp-pro-v3/sfp-server",
  "image_tag":  "v3-latest",
  "secrets": {
    "DOCKER_REGISTRY_TOKEN": "<paste-token-from-step-1.1>"
  }
}
```

{% hint style="info" %}
`image_fqdn` and `image_tag` are explicit overrides today. They will fold into the `production` cadence default in a future release; drop them from `server.json` at that point.
{% endhint %}

### **\[1.1]** Generate `DOCKER_REGISTRY_TOKEN` (required)

`sfp server init` will fail if this token isn't provided. Get one at [source.flxbl.io → User Settings → Applications](https://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 token to `sfp server init` via one of:

* **`server.json`** **`secrets` block** (shown above) — simplest for prod installs
* **Environment variable**: `export DOCKER_REGISTRY_TOKEN=<token>` before running init
* **Secrets provider**: `--secrets-provider infisical` or `aws-secretsmanager` (see [Reference](/flxbl/sfp-server/setting-up/setting-up-sfp-server/reference.md))

***

## **\[2]** Configure TLS

> **DNS prerequisite.** All three options below assume your domain (e.g. `sfp.yourcompany.com`) already resolves to the server's public IP. If you haven't set that up — point an A record at the server's IP — see [Prerequisites → Network & DNS](/flxbl/sfp-server/setting-up/prerequisites.md#2-network--dns) for the exact Cloudflare / Route 53 / GoDaddy table (TTL, proxy mode, `dig` verification). Cloudflare users: keep the record **DNS-only / grey cloud**; the orange-cloud proxy intercepts the ACME challenge and breaks Let's Encrypt.

SFP Server uses **Caddy** as its reverse proxy. Caddy handles TLS termination and supports four modes, configured via the `--tls-mode` flag during `sfp server init`:

| Mode                     | `--tls-mode` value     | Best for                                                             | What you provide                      |
| ------------------------ | ---------------------- | -------------------------------------------------------------------- | ------------------------------------- |
| **Bring Your Own Cert**  | `cloudflare` (default) | Enterprise / private networks, corporate CA, Cloudflare Origin CA    | Base64-encoded PEM cert + key         |
| **Let's Encrypt**        | `letsencrypt`          | Public-facing servers, quick evaluation                              | Ports 80 + 443 open, public DNS       |
| **Custom / On-demand**   | `custom`               | On-demand TLS via Caddy                                              | Nothing — Caddy manages certs         |
| **Behind Load Balancer** | `none`                 | Existing infrastructure (ALB, F5, NGINX) that already terminates TLS | Nothing — server runs HTTP internally |

### **\[2.a]** Bring Your Own Certificate (`--tls-mode cloudflare`, default)

If the server is on a **private network** or you manage certificates through your own PKI / corporate CA:

1. Obtain a TLS certificate and private key for your domain (from your internal CA, a commercial CA, or Cloudflare Origin CA)
2. You need two files in PEM format:
   * Certificate (full chain recommended): `origin.pem`
   * Private key: `origin-key.pem`
3. **Base64-encode** both files:

   ```bash
   base64 -w 0 origin.pem      # → use as ORIGIN_CERT
   base64 -w 0 origin-key.pem  # → use as ORIGIN_KEY
   ```
4. During `sfp server init`, provide them as the `ORIGIN_CERT` and `ORIGIN_KEY` secrets via one of:
   * **JSON config file** (`--config-file`) — under the `secrets` block, same as `DOCKER_REGISTRY_TOKEN`
   * **Environment variables**: export `ORIGIN_CERT` and `ORIGIN_KEY` before running init
   * **Secret provider** (Infisical, AWS Secrets Manager)
5. The CLI decodes the base64 values and writes `origin.pem` / `origin-key.pem` into the tenant's `certs/` directory automatically

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

**Advantages:** No inbound ports required. Works on private networks and air-gapped environments. Fits into existing corporate certificate management workflows.

> **Fallback:** If you skip providing the secrets, the CLI will warn you and you can manually place `origin.pem` and `origin-key.pem` in the `{tenantDir}/certs/` directory before starting services.

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

If the server is **internet-accessible** and you prefer automated certificate management:

1. Ensure your domain's DNS resolves to the server's public IP
2. Open ports 80 and 443 inbound:

   ```bash
   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 will fail to obtain a cert if (a) DNS hasn't propagated to the server's public IP, or (b) 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 your existing infrastructure (AWS ALB, Azure Application Gateway, F5, NGINX) already handles TLS:

1. Configure your load balancer to terminate TLS on port 443
2. Forward traffic to the Caddy HTTP listener (normally port 80 in production; use the tenant's published HTTP port for dev/smoke environments)
3. Ensure the LB sets or preserves `X-Forwarded-Proto: https`, `X-Forwarded-Host`, and `X-Forwarded-For` headers
4. During `sfp server init`, pass `--tls-mode none`

> Caddy still runs (it is required for auth routing), but serves HTTP only — no TLS termination.

For GitHub.com, Azure DevOps, or other public SaaS webhook providers, the webhook URL must be reachable from the public internet over HTTPS. A server that is reachable only through SSH, VPN, a bastion host, or `ProxyJump` is not reachable by GitHub.com webhooks.

This does not mean exposing the sfp server host directly. Keep the server backend private, and expose only the public webhook payload URL unless you intentionally want to publish the full sfp UI/API:

```
https://<public-webhook-host>/sfp/api/repository/webhook
```

For private servers, use one of these ingress patterns:

| Pattern                       | How it works                                                                                                                                                     | Required checks                                                            |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Public ALB / load balancer    | SaaS webhook provider calls the public HTTPS listener; the load balancer forwards privately to Caddy                                                             | Recommended default on AWS or when a customer load balancer already exists |
| Public reverse proxy / tunnel | SaaS webhook provider calls a public HTTPS proxy URL; the proxy opens an outbound tunnel or forwards to the private server                                       | Use when the server cannot accept inbound internet traffic                 |
| API Gateway                   | SaaS webhook provider calls a public API Gateway HTTPS endpoint; API Gateway routes through VPC Link / private integration to an internal ALB/NLB, then to Caddy | Use only when API Gateway features are required                            |

See [Webhook ingress for private servers](/flxbl/sfp-server/setting-up/webhook-ingress-for-private-servers.md) for the security model, provider-specific checks, and troubleshooting guidance. In all patterns, preserve the raw request body and provider signature headers.

{% hint style="warning" %}
Do not use SSH reachability as the webhook test. `ssh -J <bastion> <server>` only proves that an operator can administer the server. It does not prove that GitHub.com can deliver a webhook. Test the exact configured payload URL with the provider's delivery tooling, such as GitHub **Recent deliveries** / **Redeliver**. A manual `POST` to the exact webhook path can prove routing, but only a real provider delivery proves signature validation end to end.
{% endhint %}

***

## **\[3]** Initialize and start

You can run init and start **remotely** from your workstation (via SSH) or **locally** on the server itself. Both options produce the same result.

### **\[3.a]** Remote (from your workstation)

```bash
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 (TLS + --domain required); dev for local tinkering
  --domain sfp.yourcompany.com \       # FQDN that goes into the TLS cert and the 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

curl https://sfp.yourcompany.com/health
```

### **\[3.b]** Local (on the server itself)

SSH into the server and install the sfp CLI. Replace `<version>` with the tag you want from [source.flxbl.io/flxbl/sfp-pro-v3/releases](https://source.flxbl.io/flxbl/sfp-pro-v3/releases) (e.g. `v3.5.1`):

```bash
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 either a pinned tag (`/releases/download/<tag>/<asset>`) or a literal asset name (`/releases/latest/download/<literal>`). Because the `.deb` / `.rpm` filenames embed the version (`sfp-pro_3.5.1_linux_amd64.deb`), pin the tag rather than using `latest`. Browse the releases page above for the current version.

Then run init and start without `--ssh-connection` / `--identity-file`:

```bash
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 commands (`start`, `stop`, `logs`, `update`) work the same way — drop the `--ssh-*` flags when running locally.

> **Quick evaluation without a domain?** Use `--mode dev` to skip TLS and domain requirements. The server will be accessible at `http://<server-ip>:3029`. Pass the server IP as `--domain`.

**\[3.i] What init does:**

1. Checks Docker + Docker Compose are installed
2. Creates `<base-dir>/tenants/<tenant>/`
3. Collects secrets (from `--config-file`, env, or `--secrets-provider`)
4. **Self-hosted Supabase:** generates Postgres password + JWT secret + anon/service keys
5. **Cloud Supabase:** tests DB connectivity (fail-fast before writing state)
6. Writes `.env`, renders `docker-compose.yml`, configures Caddy
7. Runs schema migrations
8. Creates default admin user
9. Persists admin credentials to a permissioned file (or stdout if `--print-credentials`)

**Health response:** HTTP 200 with `status: healthy` plus a `components` map (`api`, `database`, `metrics`, `logs`, `flows`, `registry`). HTTP 503 when `database` or `flows` (Hatchet) is down — wire that into your LB probe.

### **\[3.c]** Enable auto-restart on the server

```bash
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 printed during init (or from the `credentials.json` file on the server). Use the **Login via Email** option.

***

## **\[5]** Configure your git provider <a href="#id-5-configure-your-git-provider" id="id-5-configure-your-git-provider"></a>

With the server up and you logged in, connect your git provider from the UI. Nothing here was needed at init. There are two entry points to the same configuration:

* **Onboarding wizard** — the **8/8 Onboarding** panel walks you 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**. Create the App and grant the required permissions per [Connecting GitHub as a CI/CD provider](/flxbl/sfp-server/setting-up/connecting-github-as-a-ci-cd-provider.md).

### **\[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 the 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](/flxbl/sfp-server/setting-up/webhook-ingress-for-private-servers.md).

### **\[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 <a href="#id-6-configure-login" id="id-6-configure-login"></a>

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` in `.env`, restart.                                                                                                         |
| **SAML SSO**     | Okta, Entra ID, or any SAML 2.0 IdP — follow [SAML Authentication](/flxbl/sfp-server/setting-up/saml-authentication.md).                                                                                           |

> Login OAuth is wired into the auth layer (GoTrue) at container start, so it needs an `.env` edit and a 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](/flxbl/sfp-server/setting-up/setting-up-sfp-server/operations.md)                                 | Start, stop, logs, and other lifecycle commands                             |
| [Updating sfp server](/flxbl/sfp-server/setting-up/updating-sfp-server.md)                                     | Image bumps, drains, migrations via `sfp server update`                     |
| [Reference](/flxbl/sfp-server/setting-up/setting-up-sfp-server/reference.md)                                   | Properties, secrets, CLI flags, cloud Supabase setup                        |
| [Troubleshooting & security](/flxbl/sfp-server/setting-up/setting-up-sfp-server/troubleshooting.md)            | Common issues, fixes, and hardening recommendations                         |
| [Connecting GitHub as a CI/CD provider](/flxbl/sfp-server/setting-up/connecting-github-as-a-ci-cd-provider.md) | GitHub App registration + permissions reference (deep dive for \[5.b] GHES) |
| [SAML Authentication](/flxbl/sfp-server/setting-up/saml-authentication.md)                                     | Configure SAML SSO with Entra ID, Okta, or any SAML 2.0 IdP                 |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.flxbl.io/flxbl/sfp-server/setting-up/setting-up-sfp-server.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
