Deploy on AWS EC2
Step-by-step guide to deploy Semaphor on a single EC2 instance with nginx and SSL
Deploy Semaphor on a single AWS EC2 instance with nginx as a reverse proxy and Let's Encrypt SSL certificates. This setup is suitable for small to medium teams.
Architecture
Prerequisites
Before you begin, ensure you have:
- AWS Account with permissions to create EC2 instances, security groups, and Elastic IPs
- Domain name with access to DNS settings (e.g.,
semaphor.yourdomain.com) - SSH key pair created in your target AWS region
- Kinde account with Client ID, Client Secret, and Issuer URL (setup guide)
- Semaphor license key from support.semaphor.cloud
Request a free 60-day evaluation license to test Semaphor before purchasing.
Semaphor uses Kinde for authenticating console users only. Before deploying, you'll need to set up a Kinde account and configure an application. See the Kinde setup guide for instructions.
Step 1: Create Security Group
First, create a security group that allows SSH, HTTP, and HTTPS traffic.
Using AWS CLI:
Using AWS Console:
- Go to EC2 > Security Groups > Create security group
- Add inbound rules:
| Port | Protocol | Source | Purpose |
|---|---|---|---|
| 22 | TCP | Your IP | SSH access |
| 80 | TCP | 0.0.0.0/0 | HTTP (Let's Encrypt) |
| 443 | TCP | 0.0.0.0/0 | HTTPS |
For production, restrict SSH access to your IP address or VPN range.
Step 2: Launch EC2 Instance
Using AWS CLI:
Save the Instance ID from the output - you'll need it for the next steps.
Using AWS Console:
- Open the EC2 Console and click Launch Instance
- Configure:
- Name:
Semaphor - AMI: Amazon Linux 2023
- Instance type:
t3.large(2 vCPU, 8GB RAM) - Key pair: Select your existing key pair
- Security group: Select
semaphor-sg - Storage: 50 GB gp3
- Name:
For production workloads with many concurrent users, consider t3.xlarge or
m5.large instances.
Step 3: Allocate Elastic IP
An Elastic IP ensures your instance keeps the same public IP address even after restarts.
Using AWS CLI:
Using AWS Console:
- Go to EC2 > Elastic IPs
- Click Allocate Elastic IP address
- Click Allocate
- Select the new Elastic IP, click Actions > Associate Elastic IP address
- Choose your Semaphor instance and click Associate
Note the Elastic IP address - you'll need it for DNS configuration.
Step 4: Configure DNS
Your domain must point to your EC2 instance's Elastic IP address. This is required for:
- Accessing Semaphor via your domain name
- SSL certificate generation (Let's Encrypt validates domain ownership)
- Kinde OAuth callbacks (must match your configured domain exactly)
DNS misconfiguration is a common source of issues. The domain must resolve to your Elastic IP before you can set up SSL in Step 10.
Option A: AWS Route 53
If your domain is hosted in Route 53:
Using AWS CLI:
Using AWS Console:
- Go to Route 53 > Hosted zones
- Click on your domain (e.g.,
yourdomain.com) - Click Create record
- Configure:
- Record name:
semaphor(or your chosen subdomain) - Record type:
A - Value: Your Elastic IP address
- TTL:
300(5 minutes)
- Record name:
- Click Create records
Option B: Other DNS Providers
For Cloudflare, GoDaddy, Namecheap, or other providers:
- Log into your DNS provider's dashboard
- Navigate to DNS management for your domain
- Create a new A record:
| Setting | Value |
|---|---|
| Type | A |
| Name/Host | semaphor (subdomain) or @ (root domain) |
| Value/Points to | Your Elastic IP (e.g., 54.82.8.111) |
| TTL | 300 or "Auto" |
If using Cloudflare, disable the orange cloud (proxy) initially. You can enable it later after Semaphor is working.
Verify DNS Configuration
Wait for propagation, then verify the DNS record is working:
If you get no output or a different IP, the DNS hasn't propagated yet. You can also check from multiple locations:
DNS Troubleshooting
| Problem | Solution |
|---|---|
dig returns nothing | DNS not propagated yet - wait 5-15 minutes |
| Wrong IP returned | Check for conflicting A records in your DNS provider |
| SSL setup fails later | Ensure DNS is resolving before running Certbot |
| Works locally but not elsewhere | Local DNS cache - try dig @8.8.8.8 to test |
Do not proceed to Step 10 (SSL setup) until DNS is working. Certbot will fail if the domain doesn't resolve to your server.
Step 5: Connect via SSH
Replace your-key.pem with your key file path and <your-elastic-ip> with your Elastic IP.
Step 6: Install Dependencies
Run these commands to install Docker, Docker Compose, nginx, and Certbot.
Amazon Linux 2023 uses dnf instead of yum. The commands below are for AL2023 - if using Amazon Linux 2, replace dnf with yum and use amazon-linux-extras for nginx.
Log out and back in for Docker group membership to take effect:
Verify installations:
Step 7: Set Up Semaphor
Create the Semaphor directory and configuration files:
Create docker-compose.yml:
Create semaphor.env with your credentials:
Edit the file with your actual values:
Replace:
SEMA-your-license-key-herewith your Semaphor license keyyour_kinde_client_id,your_kinde_client_secret,your-subdomainwith your Kinde credentialssemaphor.yourdomain.comwith your actual domain name- Generate random 32-character strings for the encryption keys
Generate random strings with: openssl rand -hex 16
Understanding the URL variables:
| Variable | Type | What to set |
|---|---|---|
APP_URL | External | Your domain with https:// (e.g., https://semaphor.yourdomain.com) |
DATA_API_URL | Internal | Keep as http://data-service - this is Docker's internal DNS |
KINDE_*_URL | External | Same domain as APP_URL |
The APP_URL is critical - it gets embedded in authentication tokens at container startup. If set incorrectly, your browser will try to call localhost:3000 instead of your actual domain.
Step 8: Configure Kinde Callback URLs
In your Kinde dashboard, update your application settings:
- Go to Settings > Applications > Your App > Authentication
- Add these callback URLs:
| Setting | Value |
|---|---|
| Allowed callback URLs | https://semaphor.yourdomain.com/api/auth/kinde_callback |
| Allowed logout redirect URLs | https://semaphor.yourdomain.com |
Replace semaphor.yourdomain.com with your actual domain.
Step 9: Configure nginx
Create the initial nginx configuration:
The proxy_buffer_size settings are critical. Without them, you'll get 502 Bad Gateway errors when Kinde authentication sends large cookies/headers.
Edit the file and replace semaphor.yourdomain.com with your domain:
Create the Certbot webroot and start nginx:
Step 10: Set Up SSL with Certbot
Verify your DNS is resolving correctly:
This should return your Elastic IP. If not, wait for DNS propagation.
Request the SSL certificate:
Replace:
semaphor.yourdomain.comwith your domainyour-email@example.comwith your email for certificate notifications
Set up automatic certificate renewal:
Step 11: Start Semaphor
Pull the Docker images and start Semaphor:
Wait about 60 seconds for all services to start. Check the status:
All three services should show as "healthy" or "running".
Step 12: Verify Deployment
Check that all components are working:
Open your browser and navigate to:
You should see the Semaphor login page.
Maintenance
Update Semaphor
Before upgrading, back up your data and review the Upgrade Guide for important information about database schema changes.
To update to a new version:
For the full upgrade procedure including backup, rollback, and production database considerations, see the Upgrade Guide.
View Logs
Backup Data
Create a backup of the Semaphor data volume:
Restore from Backup
Replace YYYYMMDD with your backup date.
SSL Certificate Renewal
Certificates renew automatically via cron. To test renewal:
To manually renew:
Troubleshooting
502 Bad Gateway Error
This is the most common issue and is usually caused by one of these:
1. nginx buffer sizes too small (most common after login)
Kinde authentication sends large cookies/headers that exceed nginx's default buffer size. You'll see this in /var/log/nginx/error.log:
Fix: Ensure your nginx config includes these buffer settings:
Add these lines inside the location / block:
Then reload nginx:
2. Semaphor containers not running
3. nginx not running
Container Keeps Restarting (Exit Code 92)
This usually indicates a license key issue:
If you see License check failed: License expired or License invalid:
- Get a new license key from support.semaphor.cloud
- Update the license in
semaphor.env:
- Restart the containers:
SSL Certificate Fails to Issue
Check DNS first:
If DNS isn't resolving:
- Wait 5-15 minutes for propagation
- Verify the A record in your DNS provider
- Try with Google's DNS:
dig +short semaphor.yourdomain.com @8.8.8.8
Check port 80 is accessible:
If connection refused:
- Verify security group has port 80 open
- Check nginx is running:
sudo systemctl status nginx
Check nginx error logs:
Cannot Access Semaphor After Setup
Run this diagnostic script:
Browser Making API Calls to localhost:3000
Symptoms: After login, the browser makes API calls to http://localhost:3000/api/... instead of your domain. You'll see errors in the browser console.
Cause: The APP_URL environment variable is not set correctly, or the containers weren't restarted after changing it.
How it works: At container startup, APP_URL is injected into the application. The server embeds this URL into JWT tokens. The browser decodes the token and uses that URL for all API calls.
Fix:
- Verify the env var is set correctly:
- Restart containers (required after any env var change):
- Clear browser cache and reload the page (or use incognito mode)
Advanced debugging: Decode the JWT token to verify the URL:
Authentication Redirect Fails
Symptoms: Login redirects to Kinde but returns with an error, or you get stuck in a redirect loop.
Check Kinde callback URLs:
In your Kinde dashboard, verify:
- Callback URL:
https://semaphor.yourdomain.com/api/auth/kinde_callback(exact match!) - Logout redirect:
https://semaphor.yourdomain.com
Check semaphor.env URLs:
All URLs must:
- Use
https://(nothttp://) - Match your actual domain exactly
- Have no trailing slashes
Common mistakes:
http://instead ofhttps://- Typo in domain name
- Missing
/api/auth/kinde_callbackin Kinde callback URL - Trailing slash mismatch
Services Start But Health Checks Fail
Wait longer: The API service needs ~60 seconds to fully initialize (embedded PostgreSQL and Redis start inside the container).
Look for:
Check resource usage:
Important Notes
Amazon Linux 2023 vs Amazon Linux 2
| Task | Amazon Linux 2023 | Amazon Linux 2 |
|---|---|---|
| Package manager | dnf | yum |
| Install nginx | sudo dnf install nginx | sudo amazon-linux-extras install nginx1 |
| Install Certbot | sudo pip3 install certbot certbot-nginx | sudo amazon-linux-extras install epel && sudo yum install certbot |
Critical nginx Configuration
The proxy_buffer_size settings are required for Kinde authentication to work. Without them, you'll get 502 errors after login:
After Certbot Runs
Certbot modifies your nginx config to add SSL settings. You must re-add the proxy buffer settings if they get removed:
License Key Expiration
License keys have expiration dates. If your containers keep restarting with exit code 92, check the logs:
Docker Ports
The docker-compose.yml binds ports to 127.0.0.1 only (not 0.0.0.0). This is intentional for security - all external traffic goes through nginx.
Environment Variable URLs
All URLs in semaphor.env must:
- Use
https://in production - Match your domain exactly
- Be consistent across all
*_URLvariables
Container Restart Required After Env Changes
Environment variables are read when containers start. If you change semaphor.env, you must restart the containers for changes to take effect:
This is especially important for APP_URL since it's injected into the application at container startup and embedded in JWT tokens.
Internal vs External URLs
| URL Type | Example | Purpose |
|---|---|---|
| Internal | http://data-service | Docker container-to-container communication |
| External | https://semaphor.yourdomain.com | Browser-to-server, OAuth callbacks, email links |
DATA_API_URLshould stay ashttp://data-service(Docker DNS resolves this internally)- All other URLs should use your external domain with
https://
Useful Commands Reference
Container Management
Health Checks
nginx Commands
SSL/Certbot Commands
System Diagnostics
Next Steps
- Configure data sources to connect your databases
- Set up report scheduling for automated reports
- Enable AI features by adding your OpenAI API key