Updating sfp server
Update a self-hosted sfp server with sfp server update — the drain, backup, collation reconcile, migration, and recovery steps the command runs.
sfp server update moves a tenant to a new server image. It runs as a maintenance-window update: the command backs up the tenant's configuration, waits for in-flight workflows to finish, swaps the running services for the new image, and re-applies database migrations. It does not do a zero-downtime rolling swap — the application services are stopped and recreated, and Caddy serves a maintenance page while they are down.
Run it on the server, or from your workstation over SSH with --ssh-connection and --identity-file. Every invocation takes --tenant.
sfp server update --tenant <tenant> --base-dir /opt/sfp-serverWhat the command does
The update runs a fixed sequence for the named tenant. Each step is described below in the order it happens.
Back up critical files. Unless
--skip-backupis passed, the command copies.env,config/Caddyfile, and thesecrets/directory intotenants/<tenant>/backups/update-<timestamp>/before anything changes. A backup failure is logged but does not stop the update.Rewrite config and
.env. The compose file,Caddyfile, and supporting config are regenerated from the current template, and the new image tag is written into.envasIMAGE_TAG(see Selecting the version). The API gateway configuration underconfig/envoy/is reconciled to the installed release — the route map can change between releases — and legacy Kong files (kong.yml,kong-entrypoint.sh) plus the retiredconfig.yaml/config.<env>.yamlsecret stashes are removed.Drain active workflows and pre-pull the image, in parallel. The command waits for in-flight Hatchet workflows to complete (see Workflow drain) while pulling the new image in the background, so the pull is already done when services stop. If the drain times out with workflows still active, the update aborts here, before any service is stopped.
Recreate Caddy. Caddy is brought up with the new config so it serves the maintenance page while the application is down.
Stop the application services. The application services — server, workers, file storage, and the Supabase API gateway — are stopped and removed. The remaining infrastructure (the Hatchet engine and databases, the Supabase database, the registry, metrics and logs stores) stays up; if the selective stop fails, the command falls back to stopping the whole stack.
Rebuild indexes when the postgres image changed the collation version. If the supabase/postgres base image in this release carries a new glibc, the command detects the resulting collation-version mismatch and reindexes each affected database before any migration runs. It extends the maintenance window once per affected database and is a no-op on every other update. See Rebuild indexes after a postgres image bump. A failure here is logged and does not stop the update; it is retried on the next update.
Start the new services.
docker compose up -drecreates the stopped services on the new image and re-runs the migration init container.Wait for migrations. The command polls the migration container until it exits, then verifies the exit code (see Database migrations).
Selecting the version
With no version flag, the update keeps the tenant's current IMAGE_TAG and re-runs the update sequence against it. Select a different version explicitly:
--cadence <cadence>
Follows a named release cadence
--docker-tag <tag>
Deploys a specific image tag, overriding the cadence
The resolved value is written into the tenant's .env as IMAGE_TAG, which the compose file interpolates into every service image. Check the sfp release notes for breaking changes before pinning a new version.
Workflow drain
Stopping the workers mid-run aborts the workflows they are executing, so the update drains before it stops them. The drain polls the server's /sfp/api health endpoint, which reports the active-workflow count as activeWorkflows: { running, queued, total }, every 10 seconds.
The update proceeds in these cases:
A confirmed count of zero — no workflows are running or queued.
The server is unreachable after the initial retries — the server is most likely already down, so there is nothing to protect. This is reported explicitly, not as "no active workflows".
--forceis passed, or no domain is configured for the tenant (the drain needs a reachable server URL). This stops the workers immediately and aborts any workflow still running.
If the drain timeout elapses with workflows still active — after --drain-timeout seconds (default 3600) — the update aborts. It stops before touching the running services, so nothing is interrupted; re-run with --force to update anyway (which interrupts the in-flight work), or retry once the workflows have finished.
An unknown count — the health probe is degraded but the server is up — or a transient error during the wait never short-circuits to "proceed". The drain keeps waiting for a confirmed count or the timeout instead.
On a tenant with continuous scheduled work — recurring jobs, long pool preparations that overlap across time zones — the active-workflow count may never reach zero. The drain then waits out the full timeout and the update aborts. Before updating such a tenant, pause scheduled jobs and let in-flight pool operations finish, or schedule the update in a quiet window.
--force stops the workers immediately. Any workflow that is running at that moment — a build, a release, a scratch-org provisioning task — is aborted. Use it only when you know nothing is in flight, or when the drain cannot reach the server.
Backup and recovery
The pre-update backup captures the files that carry the tenant's configuration and secrets:
.env
Image tag, worker counts, ports, and all runtime configuration
config/Caddyfile
The reverse-proxy and TLS configuration
secrets/
The tenant's secret material
Each backup lands in tenants/<tenant>/backups/update-<timestamp>/. It does not include the database or the Docker volumes — those are covered by your own volume-level backups (the critical data volumes are declared external in the compose file so they survive docker compose down).
To recover the configuration from a backup, copy the files back into the tenant directory and start the server:
Database migrations
Migrations are applied by the supabase-migrations init container when the services start, not by the CLI directly. The update copies the new migration SQL into place, and when services restart the init container applies any migrations the new image adds.
Migrations are forward-only — the schema changes a newer version applies cannot be automatically reversed. After the services start, the command polls the migration container for up to two minutes:
Exit code 0 — migrations applied. The command reloads the PostgREST schema cache and reports success.
Non-zero exit — the command prints the migration container's logs and stops. Timeout — the command fails without printing them; read the migration container's logs directly. Either way the services are left on the new image with migrations unapplied — fix the cause before continuing.
Pass --continue-on-db-migration-failure to let the update finish even when a migration fails. The services are left restarted on the new image, and the failure is logged rather than raised. Use this only when you intend to resolve the migration state manually afterwards.
If a migration fails because of a manual schema change that conflicts with the migration (for example, a column the migration adds already exists), reconcile the schema against the migration's expectation, or restore the database from your volume backup, before retrying.
Rebuild indexes after a postgres image bump
A supabase/postgres base-image bump can carry a new glibc. A change from older builds to 15.14.x, for example, moved glibc 2.39 → 2.40. Postgres then warns that every database "has a collation version mismatch": btree indexes on text were built under the old ordering and may no longer agree with the collation the server now computes, which can drop rows from index scans and weaken unique constraints.
To correct this, sfp server update detects the mismatch and rebuilds the affected indexes before migrations run — a migration that depends on a unique index gets a sound one. This runs only for a self-hosted stack (a cloud Supabase project has no supabase-db container and is not reindexed).
For each affected database the command runs, as the supabase_admin superuser:
REINDEX DATABASE <db>ALTER DATABASE <db> REFRESH COLLATION VERSION
The two statements run as separate invocations: REINDEX DATABASE (and REINDEX CONCURRENTLY) refuse to run inside a transaction block, and the Supabase CLI applies migrations in one. That is why this step is part of the update and not a migration.
Operator-visible effect:
It extends the update's maintenance window once per affected database. The application is already down behind the Caddy maintenance page when this runs, and
REINDEXholds the database — the added downtime is the reindex time.It runs at most once per database. After the indexes are rebuilt and the recorded version refreshed, subsequent updates detect no mismatch (one cheap detection query per update) and add no delay. Fresh installs record the current version at init and are unaffected.
It is non-fatal. If the reconcile fails, the update logs a warning and continues; the mismatch keeps warning on later updates and is retried then.
template0is left untouched: it accepts no connections, holds no user data, and cannot be reindexed without alteringpg_database.
Flags
--tenant, -t
(required)
Tenant to update
--base-dir
./sfp-server
Directory that holds the tenant tree
--cadence, -r
latest
Release cadence to follow
--docker-tag
(unset)
Specific image tag, overrides --cadence
--skip-backup
off
Skip the pre-update backup
--force
off
Skip the drain and update even if workflows are in flight
--drain-timeout
3600
Seconds to wait for the drain before the update aborts
--continue-on-db-migration-failure
off
Finish the update even if a migration fails
--ssh-connection
(unset)
Run on a remote host: user@host[:port]
--identity-file
(unset)
SSH private key for --ssh-connection
--config-file
(unset)
JSON file with server configuration values
Secrets during an update
The update reads the tenant's secrets the same way start does, through the configured secrets provider. Pass --secrets-provider to select it:
infisical— a dedicated Infisical secrets manager.aws-secretsmanager— AWS Secrets Manager.custom— read from environment variables. Inject them before running the command using your secrets tooling; consult its documentation.
Running the update from a pipeline
The same command can run from a CI/CD pipeline instead of an operator shell: the pipeline installs the sfp CLI and runs sfp server update (over SSH with --ssh-connection when the runner is not the host), so updates leave an audit trail in the pipeline history. The requirements are the same as the manual path — network access to the host and the registry credentials — provided through your CI platform's runners and secrets; consult its documentation.
Related pages
OperationsScaling workersSelf Hosted Supabase ConfigurationLast updated
Was this helpful?