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
To update to a new version:
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