Skip to main content

Enterprise self-hosting onboarding

This guide is for teams deploying FeatureSignals on their own infrastructure using the official on-premises bundle (deploy/onprem/). It covers prerequisites, first boot, configuration, sizing, TLS fronting, upgrades, backups, monitoring, and common failures.

Prerequisites

RequirementNotes
Docker24 or newer (Engine + CLI).
Docker Composev2 (docker compose, not legacy docker-compose).
Host memoryAt least 4 GB RAM available to containers for the default Compose limits (see Sizing guide).
DatabasePostgreSQL-compatible: the stack ships PostgreSQL 16 in Compose. You may use an external Postgres 14+ if you adapt DATABASE_URL and remove or replace the postgres service (not covered by the default compose file).
Network ports8080 (API), 3000 (dashboard), and 5432 (database, bound to 127.0.0.1:5432 by default). Ensure these are free or override API_PORT, DASHBOARD_PORT, and DB_PORT in .env.
Shell utilitiesbash, curl, and gzip for bundled scripts.

Optional: a reverse proxy (Nginx, Caddy, or your platform ingress) terminating TLS in front of the API and dashboard.

Quick start (about five minutes)

  1. Copy the on-premises bundle from the FeatureSignals repository to a directory on the server (example: /opt/featuresignals):

    sudo mkdir -p /opt/featuresignals
    sudo cp -r /path/to/featuresignals/deploy/onprem/* /opt/featuresignals/
  2. Copy SQL migrations next to the compose file (the migrate service mounts ./migrations read-only):

    sudo cp -r /path/to/featuresignals/server/migrations /opt/featuresignals/migrations
  3. Create environment file:

    cd /opt/featuresignals
    cp .env.onprem.example .env
  4. Set secrets in .env (required):

    openssl rand -hex 32   # use for JWT_SECRET
    openssl rand -hex 24 # use for POSTGRES_PASSWORD
  5. Start the stack:

    docker compose -f docker-compose.onprem.yml up -d
  6. Verify:

    • API: http://<host>:8080/health (or your API_PORT)
    • Dashboard: http://<host>:3000 (or your DASHBOARD_PORT)

Alternatively, after step 3, run ./setup.sh from the install directory; it checks Docker, pulls images, runs docker compose up -d, and probes /health.

Configuration reference

Variables below are defined in deploy/onprem/.env.onprem.example. Values shown are typical defaults unless noted.

Required

VariableDescription
JWT_SECRETSigning key for management API JWTs. Required. Use a long random value (e.g. openssl rand -hex 32). Do not reuse across environments.
POSTGRES_PASSWORDPassword for the POSTGRES_USER database role. Required.

URLs and CORS

VariableDescription
APP_BASE_URLPublic base URL of the API as seen by clients (e.g. https://api.flags.example.com). Used by the server for links and callbacks.
DASHBOARD_URLPublic URL of the dashboard (e.g. https://flags.example.com).
CORS_ORIGINAllowed browser origin for the management API. Must match the scheme/host/port your users use to open the dashboard (comma-separated list if multiple).
NEXT_PUBLIC_API_URLDashboard build-time/public API base URL; set to the same origin users use to reach the API (often your HTTPS API URL).

Ports

VariableDescription
API_PORTHost port mapped to the API container (8080 inside the container).
DASHBOARD_PORTHost port mapped to the dashboard.
DB_PORTHost bind for Postgres (default 127.0.0.1:5432 so the DB is not exposed on all interfaces).

Database

VariableDescription
POSTGRES_DBDatabase name (default featuresignals).
POSTGRES_USERDatabase user (default fs).

Operations

VariableDescription
LOG_LEVELServer log verbosity (e.g. info, debug).
AUDIT_RETENTION_DAYSHow long the server retains audit data before pruning (default 90).
VERSIONImage tag for server and dashboard (e.g. latest or a release tag). Used by Compose and upgrade.sh.
REGISTRYContainer registry prefix for images (default ghcr.io/dinesh-g1).

Resource limits (Compose deploy.resources.limits.memory)

VariableDefaultDescription
DB_MEMORY_LIMIT2GCap for the PostgreSQL container.
API_MEMORY_LIMIT1GCap for the API server container.
DASHBOARD_MEMORY_LIMIT512MCap for the dashboard container.

Enterprise license

VariableDescription
LICENSE_KEYOptional. Enterprise license string when provided by FeatureSignals.

OpenTelemetry (optional)

VariableDescription
OTEL_ENABLEDtrue / false — master switch for telemetry export.
OTEL_EXPORTER_OTLP_ENDPOINTOTLP gRPC/HTTP endpoint for your collector (e.g. otel-collector:4317).
OTEL_INGESTION_KEYOptional header/ingestion key if your backend requires it.
OTEL_SERVICE_NAMELogical service name (default in compose: featuresignals-onprem).
OTEL_SERVICE_REGIONRegion or site label (default onprem).
OTEL_TRACES_ENABLEDExport traces when true.
OTEL_METRICS_ENABLEDExport metrics when true.

The API server also supports additional OTEL tuning via environment (for example trace sampling and log export); see server/internal/config/config.go for the full set when you need fine-grained control.

Script helpers (environment only, not in .env.onprem.example)

VariableDescription
INSTALL_DIRDirectory containing docker-compose.onprem.yml (default /opt/featuresignals in setup.sh, upgrade.sh, backup.sh, health-check.sh).
BACKUP_DIRWhere backup.sh writes dumps (default $INSTALL_DIR/backups).
BACKUP_RETENTION_DAYSHow long to keep featuresignals_*.sql.gz files (default 7).

Ensure API_PORT is exported in your shell when running upgrade.sh or health-check.sh if you use a non-default API port (scripts default to 8080).

Sizing guide

Default Compose memory limits total roughly 3.5 GB (2G + 1G + 512M) before OS and Docker overhead. Treat 4 GB RAM as the practical minimum for the bundle as shipped.

Small (up to ~10 users)

  • RAM: 4 GB host minimum; keep default limits or slightly lower DB_MEMORY_LIMIT only if the database is lightly used.
  • vCPU: 2 cores.
  • Disk: 20 GB+ SSD for images, volumes, and logs.
  • Use case: pilots, single team, low evaluation traffic.

Medium (up to ~50 users)

  • RAM: 8 GB recommended (DB_MEMORY_LIMIT=3G, API_MEMORY_LIMIT=2G, DASHBOARD_MEMORY_LIMIT=512M is a reasonable starting point).
  • vCPU: 4 cores.
  • Disk: 50 GB+ SSD; monitor PostgreSQL volume growth.
  • Use case: several teams, moderate dashboard and API load.

Large (100+ users or heavy evaluation volume)

  • RAM: 16 GB+ on the application host; run PostgreSQL on dedicated hardware or a managed service with appropriate max_connections, IOPS, and backups.
  • vCPU: 8+ cores for the API tier; consider multiple API instances behind a load balancer (requires splitting the default single-compose topology).
  • Disk: Sized for retention, audit volume, and backup storage.
  • Use case: organization-wide adoption, strict isolation, or high request rates.

Scale evaluation traffic separately: the hot path is designed for low latency; if you outgrow a single node, plan for horizontal API replicas and a shared Postgres with connection pooling.

Reverse proxy setup

Place TLS termination in front of the host ports (API_PORT / DASHBOARD_PORT). Update .env so APP_BASE_URL, DASHBOARD_URL, CORS_ORIGIN, and NEXT_PUBLIC_API_URL use HTTPS and the public hostnames.

Nginx (example)

# API
server {
listen 443 ssl http2;
server_name api.flags.example.com;

ssl_certificate /etc/ssl/certs/flags.example.com.crt;
ssl_certificate_key /etc/ssl/private/flags.example.com.key;

location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

# Dashboard
server {
listen 443 ssl http2;
server_name flags.example.com;

ssl_certificate /etc/ssl/certs/flags.example.com.crt;
ssl_certificate_key /etc/ssl/private/flags.example.com.key;

location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

Caddy (example)

api.flags.example.com {
reverse_proxy 127.0.0.1:8080
}

flags.example.com {
reverse_proxy 127.0.0.1:3000
}

After changing URLs, restart the dashboard and API containers so configuration and the dashboard’s NEXT_PUBLIC_API_URL match what browsers use.

Upgrade procedure

The repository includes deploy/onprem/upgrade.sh.

  1. Install directory should contain docker-compose.onprem.yml, .env, and migrations/.

  2. Run the upgrade (from the install directory or with INSTALL_DIR set):

    cd /opt/featuresignals
    export API_PORT=8080 # if not using default
    ./upgrade.sh v1.2.3 # or: ./upgrade.sh latest

What the script does:

  • Runs backup.sh (continues with a warning if backup fails).
  • docker compose pull with VERSION set to the target tag.
  • docker compose down, then up -d with the new images.
  • Waits briefly, prints docker compose ps, and curls http://localhost:${API_PORT}/health.

Rollback

  1. Identify the previous working image tag (VERSION).

  2. Bring the stack up with that tag:

    cd /opt/featuresignals
    VERSION=<previous-tag> docker compose -f docker-compose.onprem.yml pull
    VERSION=<previous-tag> docker compose -f docker-compose.onprem.yml up -d
  3. If the database migrated forward and is incompatible with the old binary, restore from backup (see below) or contact support for migration guidance—always take backups before upgrading production.

Backup and restore

Backup (backup.sh)

From the install directory:

cd /opt/featuresignals
./backup.sh

Produces a gzipped plain SQL dump: backups/featuresignals_YYYYMMDD_HHMMSS.sql.gz (via pg_dump --clean --if-exists). Old files are pruned according to BACKUP_RETENTION_DAYS (default 7 days).

Schedule with cron (example daily at 02:30 UTC):

30 2 * * * INSTALL_DIR=/opt/featuresignals /opt/featuresignals/backup.sh >> /var/log/featuresignals-backup.log 2>&1

Restore (from backup.sh output)

The bundled backups are SQL text, not the custom/binary format. Restore with psql, not pg_restore:

  1. Stop traffic to the stack (maintenance window).

  2. Stop application containers (keep Postgres running, or start only Postgres):

    cd /opt/featuresignals
    docker compose -f docker-compose.onprem.yml stop server dashboard
  3. Restore (replace the file name with your backup):

    gunzip -c backups/featuresignals_20260115_020000.sql.gz | \
    docker compose -f docker-compose.onprem.yml exec -T postgres \
    psql -U "${POSTGRES_USER:-fs}" -d "${POSTGRES_DB:-featuresignals}"
  4. Start the stack:

    docker compose -f docker-compose.onprem.yml up -d
  5. Verify /health and a dashboard login.

pg_restore is used for custom-format archives (pg_dump -Fc). If you take custom-format dumps separately, restore with:

pg_restore --clean --if-exists -h <host> -U <user> -d <database> backup.dump

Adapt host/user/database to your deployment; for Docker, use docker compose exec with pg_restore inside the image if available.

Monitoring

Health script (health-check.sh)

cd /opt/featuresignals
export API_PORT=8080 # match .env if customized
./health-check.sh

The script prints:

  • docker compose ps
  • HTTP check against http://localhost:${API_PORT}/health
  • pg_isready inside the Postgres container
  • docker stats for running compose containers
  • Docker volume disk summary (when available)
  • Latest backup file in BACKUP_DIR, if present

OpenTelemetry

Set OTEL_ENABLED=true and point OTEL_EXPORTER_OTLP_ENDPOINT at your collector. Enable OTEL_TRACES_ENABLED / OTEL_METRICS_ENABLED as needed. Use resource attributes (OTEL_SERVICE_NAME, OTEL_SERVICE_REGION) to separate environments in your observability backend.

Troubleshooting

Container will not start

  • Run docker compose -f docker-compose.onprem.yml logs server migrate dashboard postgres and look for the first error.
  • Confirm .env defines JWT_SECRET and POSTGRES_PASSWORD (Compose treats missing required variables as fatal).
  • Confirm the migrations directory exists beside the compose file and is readable.

Migration fails

  • Ensure Postgres is healthy (docker compose ps, postgres healthcheck).
  • Check migrate logs: docker compose -f docker-compose.onprem.yml logs migrate.
  • Do not delete partial migration state without guidance; restore from backup if the database is in a bad state.

Cannot connect to the database

  • From the host: docker compose -f docker-compose.onprem.yml exec postgres pg_isready -U fs -d featuresignals.
  • Verify DATABASE_URL components match POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB.
  • If using a custom DB_PORT, confirm nothing else is bound to that address.

CORS errors in the browser

  • CORS_ORIGIN must exactly match the dashboard origin (scheme + host + port), e.g. https://flags.example.com with no trailing slash mismatch.
  • The API must trust the same URL you put in NEXT_PUBLIC_API_URL for the dashboard build/runtime.

Dashboard blank page or endless loading

  • Open dev tools → Network: failed calls to the wrong host indicate NEXT_PUBLIC_API_URL not updated for your public API URL; rebuild/restart the dashboard container after changing it.
  • Confirm the API is reachable from the browser (same network, firewall, and TLS certificates valid).
  • Check dashboard logs: docker compose -f docker-compose.onprem.yml logs dashboard.

For licensing, support escalation, and architecture review for large deployments, use your enterprise support channel or sales contact.