Install Sentry Self-Hosted on EC2 with Docker, Forge & SSL

8 min read

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):

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):

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.

Sentry
Self Hosted
AWS EC2
Laravel Forge
Error Tracking
Performance Monitoring
Docker
MaxMind GeoIP
Slack
GitHub
Mailgun
Observability
Steven Richardson
Steven is a software engineer with a passion for building scalable web applications. He enjoys sharing his knowledge through articles and tutorials.