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 --versionanddocker-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:
- Sign up at kinde.com
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-deploymentCreate 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: bridgeThen generate your configuration file:
docker run --rm semaphorstack/api-service:latest generate-env > semaphor.env2. 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.comLeave the rest of the configuration as-is. The default values are pre-configured for local testing.
3. Start
docker-compose upWait 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 -dTo shut down the services later:
docker-compose downYour 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:latest2. 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:latest3. Data Service Sidecar
docker run -it --rm \
--name semaphor-sidecar \
--network semaphor-network \
-p 8081:8080 \
-e PORT=8080 \
semaphorstack/data-service-sidecar:latestMonitoring
# 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 licenseOptional: 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
-
Deploy the report scheduler to your AWS account:
Follow the instructions at: github.com/semaphor-analytics/report-scheduler
-
Get your Lambda function URL after deployment completes
-
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.
-
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=trueThen restart Semaphor:
docker-compose restartOnce 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-clinpm 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:
| Variable | Required | Default | Description |
|---|---|---|---|
APPS_BUCKET_NAME | Yes | — | S3 bucket name for storing plugin assets |
APPS_BUCKET_REGION | No | us-east-1 | AWS region of the bucket |
AWS_ACCESS_KEY_ID | No* | — | AWS access key (*required if not using an IAM role) |
AWS_SECRET_ACCESS_KEY | No* | — | AWS secret key (*required if not using an IAM role) |
AWS_SESSION_TOKEN | No | — | Temporary 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 -dOn 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 pluginStep 3: Publish a Plugin
Install the CLI:
npm install -g semaphor-cliInitialize your plugin project:
semaphor initDuring 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 publishYou 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 publishHost resolution order:
| Priority | Source | Example |
|---|---|---|
| 1 (highest) | --host CLI flag | semaphor publish --host https://analytics.acme.com |
| 2 | apiBaseUrl in .semaphor.config.js | Set during semaphor init |
| 3 | SEMAPHOR_API_URL environment variable | export SEMAPHOR_API_URL=https://... |
| 4 (default) | Semaphor Cloud | https://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
| Problem | Cause | Solution |
|---|---|---|
503 — Custom plugin storage is not configured | APPS_BUCKET_NAME not set | Add APPS_BUCKET_NAME to your semaphor.env and restart |
| Plugin publish fails with connection error | CLI cannot reach the self-hosted instance | Verify the --host URL is correct and reachable. Check firewall and DNS. |
AccessDenied errors in container logs | Insufficient AWS permissions | Ensure the IAM policy grants s3:GetObject and s3:PutObject on the plugin bucket |
| Plugin assets return 404 | Wrong bucket region or the plugin was not published | Verify APPS_BUCKET_REGION matches the actual bucket region |
| Plugin does not appear after publishing | Browser cache | Hard 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
- Email: support@semaphor.cloud