Install Sentry Self-Hosted on EC2 with Docker, Forge & SSL
TL;DR (What we’ll build)
By the end, you’ll have:
- A
m7i.xlarge
EC2 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://UNIQUE_PROJECT_KEY@sentry.example.com/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 checkout
the 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.
