Semaphor

Deployment

Deploy and manage Semaphor on your own infrastructure

Overview

Semaphor Self-Hosted Edition gives you complete control over your analytics infrastructure. Run the entire Semaphor stack on your own servers, maintain data sovereignty, and customize to your needs.

Why Self-Host?

  • Data Sovereignty: Keep all data within your infrastructure
  • Compliance: Meet regulatory requirements for data residency
  • Customization: Configure for your specific security and network needs
  • Performance: Optimize for your workload and geographic distribution
  • Cost Control: Predictable infrastructure costs at scale

Architecture

Semaphor's self-hosted edition consists of three core services:

  • API Service: Main application container running on port 3000, handles authentication, Semaphor Console UI, and API endpoints
  • Data Service: Responsible for connecting to your databases and executing queries
  • Data Service Sidecar: Runs custom code (Python) in a secure sandbox environment

Database Options:

  • Testing & Development: Use the embedded PostgreSQL and Redis that start automatically inside the API container - perfect for trying out Semaphor with zero database setup
  • Production Deployment: Connect to your existing dedicated PostgreSQL and Redis instances for better performance, scalability, and integration with your infrastructure

The embedded databases make it incredibly easy to test Semaphor, while the external database option ensures you can scale to production workloads using your existing database infrastructure.

Prerequisites

Before you begin, you'll need:

1. Docker

  • Docker 20.10+ and Docker Compose 2.0+
  • Verify installation: docker --version and docker-compose --version
  • Install Docker if needed

2. Semaphor License

  • Get your license key from support.semaphor.cloud
  • Format: SEMA-<payload>.<signature>
  • Note: You can request a free 60-day evaluation license to test Semaphor

3. Kinde Authentication

Semaphor requires Kinde for user authentication and access control.

Quick Setup:

Required Credentials:

  • Client ID
  • Client Secret
  • Issuer URL (format: https://your-subdomain.kinde.com)

→ See Kinde Authentication Setup for detailed instructions

Quick Start (Docker Compose)

1. Get Setup Files

First, create a new directory for your Semaphor deployment:

mkdir semaphor-deployment
cd semaphor-deployment

Create a docker-compose.yml file with the following content:

services:
  # API Service (with embedded PostgreSQL and Redis)
  api-service:
    image: semaphorstack/api-service:latest
    env_file:
      - ./semaphor.env # All configuration including LICENSE_KEY
    volumes:
      - ./semaphor.env:/app/.env
      - semaphor-data:/data # Persistent storage for embedded DB and cache
    ports:
      - '3000:3000'
    healthcheck:
      test:
        [
          'CMD-SHELL',
          'node -e "http=require(''http''); http.get(''http://localhost:3000/api/health'', r=>process.exit(r.statusCode===200?0:1)).on(''error'', ()=>process.exit(1))"',
        ]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s # Give embedded services time to start

  # Data Service (Main)
  data-service:
    image: semaphorstack/data-service:latest
    depends_on:
      api-service:
        condition: service_healthy
    environment:
      PORT: 80
      SIDECAR_URL: http://data-service-sidecar:8080
    ports:
      - '8080:80'
    healthcheck:
      test:
        [
          'CMD',
          'python',
          '-c',
          "import urllib.request,sys; r=urllib.request.urlopen('http://localhost:80/health', timeout=2); sys.exit(0 if r.getcode()==200 else 1)",
        ]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s

  # Data Service Sidecar
  data-service-sidecar:
    image: semaphorstack/data-service-sidecar:latest
    depends_on:
      data-service:
        condition: service_healthy
    environment:
      PORT: 8080
      # Add any sidecar-specific environment variables
    ports:
      - '8081:8080'
    healthcheck:
      test:
        [
          'CMD',
          'python',
          '-c',
          "import urllib.request,sys; r=urllib.request.urlopen('http://localhost:8080/health', timeout=2); sys.exit(0 if r.getcode()==200 else 1)",
        ]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s

volumes:
  semaphor-data: # Stores embedded PostgreSQL and Redis data

networks:
  default:
    name: semaphor-network
    driver: bridge

Then generate your configuration file:

docker run --rm semaphorstack/api-service:latest generate-env > semaphor.env

2. Configure

Edit semaphor.env and configure the required fields:

# ===== REQUIRED: License Token =====
# Provided by Semaphor (format: SEMA-<payload>.<sig>)
LICENSE_KEY=SEMA-<payload>.<sig>

# ===== REQUIRED: Authentication =====
KINDE_CLIENT_ID=<your_kinde_client_id>
KINDE_CLIENT_SECRET=<your_kinde_client_secret>
KINDE_ISSUER_URL=https://<your_kinde_subdomain>.kinde.com

Leave the rest of the configuration as-is. The default values are pre-configured for local testing.

3. Start

docker-compose up

Wait for all 3 services to start - api-service, data-service, and data-service-sidecar. You should see output similar to:

api-service-1           | Database connected
api-service-1           | Mode: DB=embedded, Redis=embedded
api-service-1           | App: starting on http://0.0.0.0:3000
api-service-1           |   ▲ Next.js 14.2.31
api-service-1           |   - Local:        http://localhost:3000
api-service-1           |  ✓ Ready in 47ms
data-service-1          | INFO:     Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit)
data-service-sidecar-1  | INFO:     Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)

Once you see "Ready" from all services, Semaphor is running. You can now stop the services with Ctrl+C and restart them in the background:

docker-compose up -d

To shut down the services later:

docker-compose down

Your data remains safe in the semaphor-data volume even after shutdown.

4. Access

Open http://localhost:3000/project

Deploy as Independent Containers

To run Semaphor services as individual containers instead of using docker-compose:

1. API Service

docker run -it --rm \
  --name semaphor-api \
  --network semaphor-network \
  -p 3000:3000 \
  -v $(pwd)/semaphor.env:/app/.env:ro \
  -v semaphor-data:/data \
  semaphorstack/api-service:latest

2. Data Service

docker run -it --rm \
  --name semaphor-data \
  --network semaphor-network \
  -p 8080:80 \
  -e PORT=80 \
  -e SIDECAR_URL=http://semaphor-sidecar:8080 \
  semaphorstack/data-service:latest

3. Data Service Sidecar

docker run -it --rm \
  --name semaphor-sidecar \
  --network semaphor-network \
  -p 8081:8080 \
  -e PORT=8080 \
  semaphorstack/data-service-sidecar:latest

Monitoring

# Health check
curl http://localhost:3000/api/health

# View logs
docker logs -f semaphor

# Resource usage
docker stats semaphor

# License info (shown in logs on startup)
docker logs semaphor | grep -i license

Optional: Report Generation and Scheduling

Semaphor supports automated report generation and email delivery through an AWS-based scheduling service that you deploy in your own AWS account.

Architecture

The service consists of three Lambda functions:

  • Schedule Processor: Triggered by CloudWatch Events every 60 minutes (configurable), polls Semaphor APIs to check for active schedules
  • PDF Generator: Creates PDFs of dashboards and paginated reports
  • Email Sender: Handles email delivery through AWS SES

What It Enables

  • Manual PDF export of visuals and dashboards
  • Automated report generation on schedules
  • Email delivery of generated reports

Prerequisites

  • AWS account with appropriate permissions
  • AWS CLI and SAM CLI installed locally
  • AWS SES configured for sending emails

Deployment

  1. Deploy the report scheduler to your AWS account:

    Follow the instructions at: github.com/semaphor-analytics/report-scheduler

  2. Get your Lambda function URL after deployment completes

  3. Add the Lambda URL to your semaphor.env:

    # PDF generation Lambda URL (required for PDF export and report scheduling)
    PDF_FUNCTION_URL='https://your-lambda-url.lambda-url.region.on.aws/'

    Example:

    PDF_FUNCTION_URL='https://3hyxrgytvhjyobd3hi2tlpbepe0vhqkd.lambda-url.us-east-1.on.aws/'

    Note: This environment variable is required for users to export visuals as PDFs or generate reports.

  4. Restart Semaphor to apply the configuration:

    docker-compose restart

Once configured, users can create and manage scheduled reports directly from the Semaphor console.

Optional: Observability

Semaphor emits structured JSON logs to stdout and stderr for all server-side failure events. These logs are always active and require no configuration -- pipe them into your existing logging infrastructure (Datadog, ELK, CloudWatch, Fluent Bit) using standard Docker or Kubernetes log collection.

Enable Webhook Destinations

To receive failure events as HTTP POST webhooks in addition to console logs, add the following to your semaphor.env:

ENTERPRISE_TELEMETRY_ENABLED=true

Then restart Semaphor:

docker-compose restart

Once enabled, organization administrators can configure webhook endpoints from Organization Settings > Telemetry Webhook in the admin console.

See Observability for full documentation on event types, webhook setup, and payload reference.

Optional: Custom Visuals (Plugins)

Semaphor supports custom React visualizations (called plugins) that extend the built-in chart types. In self-hosted deployments, Semaphor serves plugin assets directly from the app domain — no external CDN required. Plugin storage uses a private S3-compatible bucket; Semaphor proxies assets to the browser at request time.

This is opt-in: Semaphor works without plugin storage configured. Enable it only when you're ready to publish custom visuals. For building custom visual components, see Custom Visuals.

Architecture

Developer                     Self-Hosted Semaphor              S3 Bucket
                                                                (private)
semaphor publish ──────→  POST /api/v1/upload-url  ──────→  presigned URL
      │                                                          │
      └──── direct upload to S3 ────────────────────────────────→│

Browser  ←── GET /plugins/{projectId}/{pluginId}/index.js ←───── reads from S3
             (same-origin, served by Semaphor)

Prerequisites

  • An S3-compatible bucket (private ACL is fine)
  • AWS credentials with the required permissions, or an IAM role attached to the container
  • The semaphor-cli npm package installed on developer machines

Minimum IAM Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::your-plugin-bucket",
        "arn:aws:s3:::your-plugin-bucket/*"
      ]
    }
  ]
}

Step 1: Configure Environment Variables

Add these variables to your semaphor.env file:

VariableRequiredDefaultDescription
APPS_BUCKET_NAMEYesS3 bucket name for storing plugin assets
APPS_BUCKET_REGIONNous-east-1AWS region of the bucket
AWS_ACCESS_KEY_IDNo*AWS access key (*required if not using an IAM role)
AWS_SECRET_ACCESS_KEYNo*AWS secret key (*required if not using an IAM role)
AWS_SESSION_TOKENNoTemporary session token for STS assume-role
# ===== Custom Plugin Storage =====
APPS_BUCKET_NAME=my-semaphor-plugins
APPS_BUCKET_REGION=us-east-1

# AWS credentials (leave blank if credentials are available via the AWS SDK credential chain)
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...

When AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are left blank, the AWS SDK falls back to its default credential chain. This works automatically in AWS-managed environments:

  • EC2 — instance profile credentials via the metadata service
  • ECS — task role credentials injected by the container agent
  • EKS — pod-level credentials via IAM Roles for Service Accounts (IRSA)

If you run Semaphor in plain Docker outside AWS (e.g., on a local machine or a non-AWS VM), no implicit credentials are available. You must set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY explicitly, or S3 calls will fail with authentication errors.

Step 2: Restart Semaphor

docker-compose down
docker-compose up -d

On startup, Semaphor checks for APPS_BUCKET_NAME. If it is not set, you will see a warning in the logs:

Warning: custom plugin storage is not fully configured.
Set APPS_BUCKET_NAME to enable custom plugin publishing and same-origin plugin asset serving.

If configured correctly, no warning appears. Verify with:

docker logs semaphor | grep -i plugin

Step 3: Publish a Plugin

Install the CLI:

npm install -g semaphor-cli

Initialize your plugin project:

semaphor init

During init, the CLI prompts for a Semaphor host URL. Enter the URL of your self-hosted instance (e.g., https://analytics.acme.com). This gets saved in .semaphor.config.js as apiBaseUrl.

Build and publish:

npm run build
semaphor publish

You can also specify the host with the --host flag or an environment variable:

# Using the --host flag
semaphor publish --host https://analytics.acme.com

# Using an environment variable
export SEMAPHOR_API_URL=https://analytics.acme.com
semaphor publish

Host resolution order:

PrioritySourceExample
1 (highest)--host CLI flagsemaphor publish --host https://analytics.acme.com
2apiBaseUrl in .semaphor.config.jsSet during semaphor init
3SEMAPHOR_API_URL environment variableexport SEMAPHOR_API_URL=https://...
4 (default)Semaphor Cloudhttps://semaphor.cloud

How Same-Origin Serving Works

In cloud Semaphor, plugins are served from a CDN. In self-hosted deployments, plugins are served through the app domain at /plugins/{projectId}/{pluginId}/.... Semaphor reads plugin files from your private S3 bucket and streams them to the browser. No public bucket or separate CDN is needed.

Files with content hashes in their names (e.g., index.a1b2c3d4.js) receive long-lived immutable cache headers. Other files receive no-cache headers for rapid iteration during development.

Troubleshooting

ProblemCauseSolution
503 — Custom plugin storage is not configuredAPPS_BUCKET_NAME not setAdd APPS_BUCKET_NAME to your semaphor.env and restart
Plugin publish fails with connection errorCLI cannot reach the self-hosted instanceVerify the --host URL is correct and reachable. Check firewall and DNS.
AccessDenied errors in container logsInsufficient AWS permissionsEnsure the IAM policy grants s3:GetObject and s3:PutObject on the plugin bucket
Plugin assets return 404Wrong bucket region or the plugin was not publishedVerify APPS_BUCKET_REGION matches the actual bucket region
Plugin does not appear after publishingBrowser cacheHard refresh (Ctrl+Shift+R / Cmd+Shift+R)

If you see AccessDenied errors, double-check that the IAM policy includes both the bucket ARN (arn:aws:s3:::your-bucket) and the object ARN (arn:aws:s3:::your-bucket/*). Missing either one causes permission failures.

Upgrading

For instructions on safely upgrading your Semaphor deployment — including backup procedures, schema migration details, and rollback steps — see the Upgrade Guide.

Support

On this page