Self-hosting Firewatch
Current status
Section titled “Current status”Firewatch is under active development and is not yet a production-stable release. The core alert, schedule, escalation, incident, and notification paths exist, but operators must evaluate and test the current release against their own availability, security, and compliance requirements before relying on it for paging.
Quick start
Section titled “Quick start”Prerequisites are Docker Engine and Docker Compose v2. Clone the public repository, then run:
docker compose up --buildThe services are:
| Service | Default host address | Purpose |
|---|---|---|
web |
http://localhost:3000 |
React application and same-origin API proxy |
api |
http://localhost:8080 |
Versioned REST API, OpenAPI, health |
worker |
None | Outbox/queue processing in an independent process |
postgres |
127.0.0.1:5432 |
Durable state and default queue/outbox provider |
Confirm readiness:
docker compose pscurl --fail http://localhost:8080/health/livecurl --fail http://localhost:8080/health/readyThe first Compose boot sets Database__MigrateOnStartup=true on the single API
container. This is a local/simple-hosting convenience so
docker compose up --build works against a clean volume. It is not the managed
or multi-replica migration strategy.
Configuration and secrets
Section titled “Configuration and secrets”The initial boot needs no mail server. Self-hosted administrator registration
does not send a verification email, and the email notification channel remains
off until SMTP is configured. Before using the stack outside a disposable local
machine, copy .env.example to the ignored .env and change at least
POSTGRES_PASSWORD. Do not commit .env, cloud credentials, provider tokens,
connection strings, or database dumps.
The local HTTP stack uses FIREWATCH_ENVIRONMENT=Development so ASP.NET can
issue session and antiforgery cookies on http://localhost. Any deployment
reachable by other machines must terminate HTTPS, set
FIREWATCH_ENVIRONMENT=Production, and set FIREWATCH_PUBLIC_BASE_URL to its
public https:// origin. Production mode deliberately refuses to issue these
cookies over plain HTTP. It also refuses to start with the documented
development database password or an empty database password, and requires a
canonical HTTPS public origin.
The default FIREWATCH_QUEUE_PROVIDER=Postgres requires no AWS account. API and
worker use the same ConnectionStrings__Firewatch value assembled by Compose.
The SQS adapter is intended for a managed configuration and is not required for
self-hosting.
AWS SNS/SQS alert ingestion is independent of the internal queue provider.
Self-hosters can keep FIREWATCH_QUEUE_PROVIDER=Postgres and use the
Alert sources → AWS SNS / SQS setup generated in the UI. The generated
Lambda forwards SNS or SQS event batches to Firewatch using an organization API
key; no AWS credentials are stored by Firewatch.
Database UI
Section titled “Database UI”The default self-hosted stack does not include a database administration UI. For temporary troubleshooting, start the optional Adminer override:
docker compose -f docker-compose.yml -f docker-compose.adminer.yml up -d adminerOpen http://localhost:8081 and sign in with:
| Field | Value |
|---|---|
| System | PostgreSQL |
| Server | postgres |
| Username | the .env POSTGRES_USER value |
| Password | the .env POSTGRES_PASSWORD value |
| Database | the .env POSTGRES_DB value |
The override’s ADMINER_BIND_ADDRESS defaults to 127.0.0.1. Keep that
loopback binding:
Adminer has direct database access and must not be exposed to the public
internet or routed through the product UI. For a remote self-hosted server,
leave it on loopback and use an SSH tunnel when access is needed:
ssh -L 8081:127.0.0.1:8081 your-serverThen browse to http://localhost:8081 on the administrator’s machine. Change
ADMINER_PORT if port 8081 is already in use. Pin updates by changing
ADMINER_VERSION, reviewing the release, and recreating the service.
Compose defaults to FIREWATCH_REGISTRATION_MODE=Bootstrap, which permits
exactly the first self-hosted administrator to create an organization and then
closes registration automatically. No ownership email is sent in this mode.
The administrator chooses a discoverable passkey (recommended) or a password
of at least 12 characters, then follows the in-product setup guide. Additional
users join through administrator invitations. Open deliberately enables
ongoing public registration, while Disabled closes it; existing users can
still sign in.
Provisioned is reserved for the managed-cloud control-plane workflow and is not
a self-hosting bootstrap mechanism.
FIREWATCH_USER_PROFILE_EDITING_ENABLED=true allows signed-in users to update
their own first and last names and is the self-hosted default. Set it to false
when those identity attributes are managed centrally; the API will advertise
the restriction to the web app and reject profile-update requests directly.
Firewatch sends incident email and account-security notifications through a
configurable SMTP server. Both are disabled by default. The setup guide reports
whether SMTP is active and generates the required .env block. Configure the
FIREWATCH_SMTP_* values and use a sender domain your SMTP service is
authorized to send as. StartTls is the normal setting for port 587;
SslOnConnect supports implicit TLS, commonly on port 465. Store SMTP passwords
as deployment secrets rather than committing them.
After changing deployment variables, run docker compose up -d so the API and
worker receive the same configuration. During first-run setup, use Re-check
on the relevant step. After setup, verify provider status under the applicable
organization settings page.
Mailpit remains available only as an explicit development override; it is not
part of the self-hosted stack.
Phone delivery with Twilio
Section titled “Phone delivery with Twilio”Set FIREWATCH_TWILIO_ENABLED=true and supply the account SID, auth token, and
an SMS/voice-capable E.164 sender in FIREWATCH_TWILIO_*. Users can then verify
one primary E.164 phone number under Settings → Profile. Verified numbers
receive incident SMS by default. Automated voice calls are disabled unless
FIREWATCH_TWILIO_INCIDENT_VOICE_ENABLED=true because calls are more intrusive
and can have materially different provider costs. These credentials are global
deployment secrets and are never exposed to users.
Slack app
Section titled “Slack app”Set FIREWATCH_SLACK_ENABLED=true with a Slack app client ID, client secret,
and signing secret to make Organization → Integrations → Add to Slack
available. Register
<FIREWATCH_PUBLIC_BASE_URL>/api/v1/organization/integrations/slack/callback as
the OAuth redirect URL. Enable Interactivity & Shortcuts and register
<FIREWATCH_PUBLIC_BASE_URL>/api/v1/integrations/slack/interactions as its
request URL. Firewatch requests chat:write, im:write,
users:read, and users:read.email; synchronization looks up only existing,
verified Firewatch member emails and stores their immutable Slack user IDs. The
worker opens a bot DM and sends each incident escalation to the mapped on-call
responder. Responders can acknowledge or resolve from the DM. Firewatch verifies
Slack’s timestamped HMAC signature, updates every delivered alert when status
changes, and posts the lifecycle event in the message thread. It never guesses
from a display name and does not import the Slack directory. Unmatched members
continue to use their other verified notification methods.
Alert templates
Section titled “Alert templates”Organization administrators can customize email subject/body, SMS, voice,
Slack, and webhook wording under Organization → Alert templates. Templates
use a small allow-listed {{variable.name}} syntax; they cannot execute code,
read configuration or secrets, or emit raw HTML. Firewatch validates every
template before saving and keeps built-in defaults available through Restore
defaults. The settings are stored in PostgreSQL, so every API and worker
replica sees the same version without custom container images.
Signed outbound webhooks
Section titled “Signed outbound webhooks”Configure outbound alert endpoints under Organization → Integrations.
Firewatch creates a separate HMAC-SHA256 signing secret for each endpoint,
encrypts it at rest with the shared Data Protection key ring, and displays it
only on creation or rotation. Receivers validate the timestamp, delivery ID,
raw body, and X-Firewatch-Signature; see
Outbound alert webhooks for the exact payload and a
verification example.
Outbound destinations require HTTPS and public network addresses by default.
For a reviewed internal self-hosted receiver, set
FIREWATCH_WEBHOOK_ALLOW_PRIVATE_NETWORKS=true; set
FIREWATCH_WEBHOOK_ALLOW_HTTP=true only when plain HTTP is also explicitly
required. These values must be the same on the API (configuration validation)
and worker (delivery), which the Compose file handles.
Set FIREWATCH_PUBLIC_BASE_URL to the external origin through which users reach
the web application, including https:// in a real deployment and no trailing
path. Firewatch uses this value for verification and invitation links and infers
the passkey Relying Party ID from its host. Normally leave
FIREWATCH_PASSKEY_SERVER_DOMAIN empty. Set it only when deliberately sharing
the RP ID across trusted subdomains, and never serve untrusted content below that
domain. Do not set it to the API container name or rely on an arbitrary incoming
Host header.
WebAuthn requires HTTPS outside the browser’s special localhost development
case. Production must keep the public URL, TLS origin, reverse-proxy host, and RP
ID stable; changing the RP ID makes previously registered passkeys unusable.
Users can register up to ten named passkeys from Settings → Authentication. Firewatch prevents a passkey-only account from deleting its final credential; operators should encourage at least two credentials on separate devices. Email account recovery is not enabled yet. The intended self-hosted policy is a short-lived verified-email link that permits setting a password, followed by sign-in and passkey re-enrollment. Managed deployments may disable that route in favor of administrator or enterprise identity-provider recovery.
Account passwords, invitation tokens, recovery tokens, external-provider client
secrets, and Data Protection key-protection material are secrets. Do not place
them in .env under source control. Firewatch stores its shared Data Protection
key ring in PostgreSQL so logins survive API restarts and multiple replicas; a
production operator is responsible for database encryption, access control,
backups, and any configured certificate or key used to protect that key ring.
The web image sends same-origin API calls through its Nginx proxy. Compose sets
FIREWATCH_API_UPSTREAM=http://api:8080; operators can change that internal URL
without rebuilding the static assets.
Runbook image storage
Section titled “Runbook image storage”Images uploaded to Firewatch-authored runbooks are stored by the API through a
provider-neutral asset-store interface. The open-source deployment uses durable
filesystem storage at Runbooks__StoragePath; Compose mounts the
firewatch-runbooks named volume at /app/data/runbooks. This keeps local and
self-hosted installations lightweight—MinIO or another object-storage service is
not required—and survives ordinary container replacement.
Compose runs a short-lived runbook-storage-init container before the API to
give the API’s non-root account ownership of this volume. The official image
uses UID/GID 1654; FIREWATCH_CONTAINER_UID and
FIREWATCH_CONTAINER_GID exist only for operators who deliberately build a
custom image with a different runtime account.
Do not point this setting at a temporary container directory. A non-Compose
installation should mount a persistent filesystem at the configured path. The
public Firewatch.Storage.S3 adapter lets a managed deployment select S3
without changing the runbook API, editor, or database model. Self-hosting keeps
the filesystem provider by default so the one-command install does not require
an AWS account.
Data and backups
Section titled “Data and backups”PostgreSQL data lives in the firewatch-postgres named volume. Runbook image
objects live in the separate firewatch-runbooks named volume. Routine container
replacement does not remove either volume. Treat the database as the
authoritative metadata state, including outbox and idempotency records, and back
up the image volume alongside it so Markdown image references remain usable.
Create a logical backup to the host (the output file may contain sensitive data):
docker compose exec -T postgres \ pg_dump --format=custom --username=firewatch --dbname=firewatch \ > firewatch-backup.dumpProduction operators should additionally automate encrypted backups, point-in-time recovery, retention, off-host copies, and restore drills. A backup that has not been restored successfully is not a recovery plan.
Never run docker compose down --volumes unless permanent deletion of the local
database and uploaded runbook images is intended.
Migrations and upgrades
Section titled “Migrations and upgrades”For a controlled deployment, run the target API image’s migrations exactly once before starting new API or worker replicas:
docker compose run --rm api --migratedocker compose up --detach --no-buildFor a source checkout, rebuild with docker compose up --build. For eventual
published releases, set FIREWATCH_VERSION to a versioned image tag and pin an
image digest in higher-assurance environments.
Before upgrading:
- read release notes and minimum PostgreSQL requirements;
- take and verify a backup;
- run the one-off migration command;
- start API and worker at matching versions;
- wait for readiness and exercise the test-job smoke flow;
- review worker retries and structured logs.
Rolling an image back does not reverse its database migration. Follow the expand/contract compatibility policy in Cloud deployment contract.
Network and process boundaries
Section titled “Network and process boundaries”Only the web/API ports need ingress. PostgreSQL is published on loopback by the development Compose file for local tools; remove that host-port mapping on a remote server. The worker has no public port.
Before any real deployment exists, place web/API traffic behind a TLS-terminating reverse proxy, restrict database access, use a secrets manager, and configure resource limits and log retention. Forward request IDs and only trust forwarded headers from the known proxy.
Apply per-client rate limiting at that trusted ingress for authentication routes. Firewatch enforces per-account/invitation throttles and password lockout itself, but it does not trust unconfigured forwarded IP headers or impose one shared proxy-IP bucket that could lock out every user behind the web container.
API and worker can be scaled independently after migrations are separated from startup. Graceful shutdown windows should be at least 30 seconds for API and 45 seconds for worker. At-least-once delivery means a worker termination can cause a message retry; idempotent handlers make that safe.
Health and troubleshooting
Section titled “Health and troubleshooting”/health/liveanswers whether the API process is alive./health/readyincludes PostgreSQL and gates dependent Compose services./web-health/readychecks only the web container.
Inspect structured logs and service state:
docker compose psdocker compose logs --follow --tail=200 api worker postgresIf the API is live but not ready, start with PostgreSQL health and the connection string. If jobs stay pending, verify the worker is healthy, uses the same database and provider, and can claim outbox work. Stable message IDs in worker logs are the primary correlation key for retries.
What belongs elsewhere
Section titled “What belongs elsewhere”Self-hosting runs the complete public core. The private Firewatch Cloud repository adds AWS infrastructure, managed providers, billing, provisioning, private operations, and disaster recovery around these same images. It will not contain a second product implementation. See Cloud deployment contract for the exact boundary.
There is no Helm chart yet; the Helm packaging status records that deliberate placeholder.