Install Sentry Self-Hosted on EC2 with Docker, Forge & SSL
TL;DR (What we’ll build)
By the end, you’ll have:
- A
m7i.xlargeEC2 instance (Ubuntu 24.04) provisioned through Forge with Nginx + Let’s Encrypt. - Sentry Self-Hosted 25.x running via Docker Compose, correctly bound behind your Forge Nginx reverse proxy with SSL.
- Swap (16 GB) configured to avoid Docker “cannot allocate memory” and similar OCI runtime issues under load.
- MaxMind GeoIP working, system.url-prefix set, and Docker bound to 127.0.0.1:9000 (not the public internet).
- Slack and GitHub integrations fully wired (with GitHub App and correct Setup URL so the install dialog actually completes).
- Mailgun SMTP for invites and alerts.
- Laravel server SDK + Browser JS tracing snippet (performance & optional replay).
- GitHub Copilot MCP connected to your self-hosted Sentry so Copilot can query your issues/projects with an auth token.
Why this setup?
- EC2 (x86_64 m7i) avoids container/runtime constraints seen on some VPS virtualization stacks.
- Forge gives you quick Nginx + SSL and a tidy place to manage the reverse proxy.
- Docker Compose matches Sentry’s official self-hosted approach.
- Slack/GitHub integrations bring actionability (alerts, linking code, issue automation).
- Mailgun enables invites, password resets, and alert emails.
- MCP lets GitHub Copilot query your Sentry directly (super useful for triage).
Prerequisites
- A domain you control (we’ll use sentry.example.com below; replace with your real domain).
- An AWS EC2 instance (we used m7i.xlarge, 4 vCPU / 16 GB RAM) running Ubuntu 24.04.
- Provision the instance using Forge as a Load Balancer (Forge installs Nginx + cert tooling).
- SSH access as a sudo-enabled user (we’ll use forge) and root as needed.
1) System prep + Swap (16 GB)
Run as root on the EC2 box.
# See current disks and mounts
lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS
# If a small swapfile exists, turn it off (optional)
swapoff -a
# Create a 16 GB swapfile
dd if=/dev/zero of=/swapfile bs=1M count=16384 status=progress
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# Persist across reboots
grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
# Verify
free -h
swapon --show
Expected output (example):
Swap: 15Gi 0B 15Gi
NAME TYPE SIZE USED PRIO
/swapfile file 16G 0B -2
2) Install Docker & Docker Compose
Run as root:
apt-get update -y
apt-get install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" \
| tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
Give your non-root user (forge) permission to run Docker:
usermod -aG docker forge
loginctl terminate-user forge || true
Re-SSH as forge and verify:
docker run --rm hello-world
docker info --format 'Hello {{.Name}}; Server={{.ServerVersion}}; CgroupDriver={{.CgroupDriver}}'
Expected output snippet:
Hello Sentry; Server=28.x; CgroupDriver=systemd
3) Fetch Sentry Self-Hosted and Install
Run as forge:
cd ~
git clone https://github.com/getsentry/self-hosted.git
cd self-hosted
# Pin to latest release tag
LATEST_TAG=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/getsentry/self-hosted/releases/latest)
LATEST_TAG=${LATEST_TAG##*/}
git checkout "${LATEST_TAG}"
git describe --tags # should print something like 25.8.0
(Optional) Configure MaxMind GeoIP (recommended)
cat > geoip/GeoIP.conf <<'CFG'
AccountID YOUR_MAXMIND_ACCOUNT_ID
LicenseKey YOUR_MAXMIND_LICENSE_KEY
EditionIDs GeoLite2-City
CFG
Run the helper (or it runs during install):
./install/geoip.sh
Expected output (condensed):
Setting up IP address geolocation ...
... ghcr.io/maxmind/geoipupdate:v6.x ...
Done updating IP address geolocation database.
Install Sentry
./install.sh
# After it completes:
docker compose up --wait
Expected: dozens of containers, and web, nginx, worker show (healthy).
4) Bind Sentry to localhost & configure Nginx (Forge)
We’ll keep Sentry’s internal Nginx bound to 127.0.0.1:9000 and use Forge Nginx as the public SSL proxy.
A) Bind the Sentry container port to localhost
Run as forge in ~/self-hosted:
cat > docker-compose.override.yml <<'YML'
services:
nginx:
ports:
- "127.0.0.1:9000:80"
YML
docker compose up -d nginx
docker compose ps nginx
Expected PORTS: 127.0.0.1:9000->80/tcp
B) Set Sentry URL prefix
DOMAIN="sentry.example.com" # <-- change me
test -f sentry/config.yml || touch sentry/config.yml
if grep -q '^system.url-prefix:' sentry/config.yml; then
sed -i.bak -E "s|^system\.url-prefix:.*|system.url-prefix: https://${DOMAIN}|" sentry/config.yml
else
printf "\nsystem.url-prefix: https://${DOMAIN}\n" >> sentry/config.yml
fi
C) Forge Nginx upstream → 127.0.0.1:9000
On Forge servers, the upstream lives under /etc/nginx/upstreams/<yourdomain>. Run as root:
cat >/etc/nginx/upstreams/sentry.example.com <<'NGX'
upstream 2844682_app {
server 127.0.0.1:9000;
keepalive 64;
}
NGX
# Add some sane proxy limits (Forge includes these directories)
mkdir -p /etc/nginx/forge-conf/sentry.example.com/server
cat >/etc/nginx/forge-conf/sentry.example.com/server/sentry-proxy.conf <<'NGX'
client_max_body_size 100m;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
NGX
nginx -t && systemctl reload nginx
Note: Check the name of the upstream app (here 2844682_app) matches your Forge server’s upstream file.
D) Health check (host + domain)
curl -I http://127.0.0.1:9000/_health/
curl -I https://sentry.example.com/_health/
Expected: HTTP/1.1 200 OK and HTTP/2 200
5) First-run admin + login
The installer asks for an admin user; if you missed it, create via:
# inside the web container
docker compose exec web sentry createuser
Then login at https://sentry.example.com with that user.
6) Email (Mailgun SMTP)
Add these to ~/self-hosted/sentry/config.yml (forge user):
# Email via Mailgun SMTP
mail.backend: 'smtp'
mail.host: 'smtp.mailgun.org' # or 'smtp.eu.mailgun.org' for EU region
mail.port: 587
mail.username: '[email protected]'
mail.password: 'YOUR_MAILGUN_SMTP_PASSWORD'
mail.use-tls: true
mail.use-ssl: false
mail.from: '[email protected]'
Apply:
docker compose restart web worker cron
Test email by inviting a new user via the Sentry UI.
7) Slack Integration (self-hosted)
Create a Slack App (api.slack.com/apps → Create New App → From scratch):
- Redirect URL: https://sentry.example.com/extensions/slack/setup/
- Interactivity Request URL: https://sentry.example.com/extensions/slack/actions/
- Bot Token Scopes (minimum useful):
chat:write
chat:write.public
channels:read
groups:read
im:read
mpim:read
links:read
links:write
commands
Add credentials to Sentry sentry/config.yml:
slack.client-id: "YOUR_SLACK_CLIENT_ID"
slack.client-secret: "YOUR_SLACK_CLIENT_SECRET"
slack.signing-secret: "YOUR_SLACK_SIGNING_SECRET"
Restart & install:
docker compose restart web worker
# In Sentry: Settings → Integrations → Slack → Add → select workspace
Create an alert rule and choose Send a Slack notification to test.
8) GitHub Integration (GitHub App)
Create a GitHub App (Org Settings → Developer settings → GitHub Apps → New):
- Homepage URL: https://sentry.example.com/
- Callback URL: https://sentry.example.com/extensions/github/setup/
- Setup URL: https://sentry.example.com/extensions/github/setup/ ← don’t skip this
- Webhook → Active
- Webhook URL: https://sentry.example.com/extensions/github/webhook/
- Webhook secret: (generate one)
Permissions (common set):
- Repository Contents: Read & write
- Pull requests: Read & write
- Issues: Read & write
- Metadata: Read-only
Save, then Generate a private key (.pem). Note:
- App ID (integer)
- Client ID and Client Secret
- Add credentials to Sentry
sentry/config.yml:
github-app.id: 123456 # <-- integer, no quotes
github-app.client-id: "Iv1_xxx"
github-app.client-secret: "ghs_xxx"
github-app.webhook-secret: "random-string-you-set"
github-app.private-key: |
-----BEGIN RSA PRIVATE KEY-----
(paste the full PEM here)
-----END RSA PRIVATE KEY-----
Apply & install:
docker compose restart web worker
# In Sentry: Settings → Integrations → GitHub → Add Integration → pick org → Install
If the popup doesn’t close after installing on GitHub, you most likely forgot the Setup URL.
Add repositories in Sentry → GitHub → Configure.
Code Mappings (per project): Project → Settings → Integrations → GitHub → Configure → Add Code Mapping
Choose repository, default branch (e.g., main), and Source Root matching your stack trace paths (e.g., src/).
9) Laravel Backend SDK + Browser JS Tracing
A) Laravel Backend
Install the sentry/sentry-laravel package:
composer require sentry/sentry-laravel
Enable capturing unhandled exception to report to Sentry by making the following change to your bootstrap/app.php:
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Sentry\Laravel\Integration;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
Integration::handles($exceptions);
})->create();
Configure SDK
When you create a project in Sentry, you’ll get a DSN. Like the follwowing:
php artisan sentry:publish --dsn=https://[email protected]/1
It creates the config file (config/sentry.php) and adds the DSN to your .env file where you can add further configuration options:
SENTRY_LARAVEL_DSN=https://UNIQUE_PROJECT_KEY@sentry.example.com/1
Browser JS (frontend tracing + optional Replay)
Similar to backend setup, inside Sentry project settings, retrieve the Loader Script snippet and add it to your main HTML template (e.g., resources/views/layouts/app.blade.php):
<script src="https://sentry.example.com/js-sdk-loader/UNIQUE_PROJECT_FILE.min.js" crossorigin="anonymous"></script>
10) GitHub Copilot MCP → Your Self-Hosted Sentry
This lets Copilot query your self-hosted Sentry via MCP.
Create a user auth token in Sentry: Avatar → User Settings → Auth Tokens → Create Token
Scopes you likely want:
org:read, project:read, event:write (optionally project:write if you want to create releases via MCP).
Add the MCP server to Copilot config (e.g., VS Code → Settings JSON):
{
"mcpServers": {
"sentry": {
"command": "npx",
"args": ["-y", "@sentry/mcp-server@latest"],
"env": {
"SENTRY_HOST": "https://sentry.example.com",
"SENTRY_ACCESS_TOKEN": "${secrets.COPILOT_MCP_SENTRY_ACCESS_TOKEN}",
"SENTRY_ORG": "your-org-slug", // optional
"SENTRY_PROJECT": "your-project-slug" // optional
}
}
}
}
Store the token as a Copilot secret:
Command Palette → Copilot: Manage Secrets →
COPILOT_MCP_SENTRY_ACCESS_TOKEN = <your token>
Local quick test (optional):
npx @sentry/mcp-server@latest \
--host=https://sentry.example.com \
--access-token=YOUR_TOKEN
Now ask Copilot:
“Use the sentry MCP server to list my projects”
“Find issue by ID …”
11) Security & hardening tips
- Keep Sentry’s Docker Nginx bound to 127.0.0.1:9000 only (we did).
- Use Let’s Encrypt in Forge (already done) and consider enabling HSTS (Forge SSL settings).
- Regularly
git checkoutthe latest self-hosted tag and run the upgrade:
cd ~/self-hosted
git fetch --tags
NEW=$(git tag | sort -V | tail -n1); echo $NEW && git checkout $NEW
./install.sh && docker compose up --wait
12) Troubleshooting notes
502 Bad Gateway after editing config
docker compose ps web nginx → if web is “restarting,” run docker logs --tail=200 sentry-self-hosted-web-1.
YAML type errors are common (e.g., github-app.id must be an integer, not "123456").
GitHub popup never closes
You forgot Setup URL (/extensions/github/setup/) in the app config.
Mail not sending on EC2
Use 587/TLS, not 25. Verify from server:
nc -vz smtp.mailgun.org 587
OpenVZ / nested virt errors
If you previously saw Docker “OCI runtime create failed” on some VPSs: moving to EC2 (KVM/metal) fixes it.
Seer/Autofix
Sentry’s AI features are SaaS-only. Don’t rely on the UI flag in self-hosted.
Conclusion
You now have a production-ready Sentry Self-Hosted stack on AWS EC2 with Forge managing SSL + Nginx, Slack and GitHub integrations working end-to-end, Mailgun for email, Laravel + Browser tracing sending data, and MCP wired so Copilot can query your self-hosted Sentry.
If you found this useful, share it with your team, this exact recipe saves hours for anyone bootstrapping Sentry self-hosted with real-world integrations and developer tooling.
Once Sentry is live, two things will make your error signal cleaner and more actionable. First, blocking malicious 404s with NGINX before they reach Laravel stops bot traffic from inflating your error counts and burning through monitoring quota. Second, for custom application-level metrics that go beyond exception tracking, building custom Laravel Pulse recorders lets you surface domain events — payment rates, API quota consumption — in a dashboard alongside Sentry's error feed. Sentry and Pulse complement each other: Sentry for exceptions and user impact, Pulse for application health metrics. For the deployment pipeline that puts your Forge server into production, see Zero-Downtime Laravel Deployments with GitHub Actions and Forge.
Steven is a software engineer with a passion for building scalable web applications. He enjoys sharing his knowledge through articles and tutorials.