# 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 server on port 80 (HTTP)
3. Ensure the LB sets `X-Forwarded-Proto: https` 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.

***

## **\[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.

***

## 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) | Wire the GitHub App to your repos                           |
| [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: 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.
