What the stack needs
| Item | Figure | Stated in |
|---|---|---|
| CPU | 4 cores minimum | Self-Hosted Sentry page |
| Memory | 16 GB RAM + 16 GB swap minimum; 32 GB recommended | Self-Hosted Sentry page |
| Disk | 20 GB free minimum; a high-IOPS disk helps most | Self-Hosted Sentry page |
| Docker | Engine 19.03.6+, Compose 2.32.2+ | Self-Hosted Sentry page, install/_min-requirements.sh |
| Installer refusal | fewer than 4 CPUs or 14,000 MB RAM visible to Docker | install/_min-requirements.sh, check-minimum-requirements.sh |
| x86-64 CPU flag | SSE 4.2, needed by ClickHouse; the installer checks it | install/check-minimum-requirements.sh |
| Operating system | Debian or Ubuntu preferred; RHEL family has known issues; Alpine unsupported | Self-Hosted Sentry page |
The docs warn that every database and message broker shares one machine, so disk latency decides how it feels: if iowait stays above about 10% of CPU time, the system cannot keep up. They also note that resource use stays roughly flat whatever your event volume. A lighter profile exists: set COMPOSE_PROFILES=errors-only (available since 24.8.0) and you keep issues, alerts, integrations, dashboards, releases and errors, but lose traces, profiles, replays, crons and user feedback. For that profile install/_min-requirements.sh lowers the installer's floor to 2 CPUs and 7,000 MB.
Sizing the machine
Size memory first, then disk speed, then cores. Swap on a fast disk makes 16 GB workable by the docs' own account, but 32 GB of physical memory removes the question. In the dedicated server range three classes fit:
- Xeon D-1587 (Moonshot cartridge): 16 cores, 16 to 128 GB, a 1.92 TB NVMe SSD, 1 Gbps unmetered. Take 32 GB or more.
- Intel Xeon E-2286M (Bucharest): 8 cores, 32 to 64 GB, two 960 GB NVMe SSDs.
- Dual-socket rack servers (Xeon Gold 6130, Xeon E5-2630L v3): 16 to 32 cores, memory far past 32 GB, SATA SSDs, and an out-of-band console (iLO, iDRAC or BMC) like every racked single-, dual- and quad-socket server. See enterprise servers.
Skip the 8 GB cartridges and anything on spinning SATA. This page covers the x86-64 lines only: leave out the ARM-based X-Gene cartridge (it boots nothing past Ubuntu 18.04) and the Ampere Altra line. Delivery: about 30 minutes once payment is approved for a racked machine, 4 to 24 hours for a build to order. Inbound traffic, which is what event ingestion is, is not billed at all.
Installing with the port on loopback
Install Docker Engine and the Compose plugin from Docker's own Ubuntu repository first, as the Sentry docs advise. The official recipe clones the latest release tag and runs ./install.sh, then docker compose up --wait. The block below does exactly that, with three changes of ours: ufw goes on before anything starts, --skip-user-creation and --no-report-self-hosted-issues remove both interactive prompts, and a .env.custom file moves the published port to 127.0.0.1.
# As root (sudo -i) on Ubuntu 24.04 with Docker Engine 28+ and the Compose plugin
# from Docker's apt repository. Firewall first (SSH only), then the latest release
# (or the tag in $t, if set). Refuses to run if /opt/sentry already exists.
v=$(docker version --format '{{.Server.Version}}')
if [ "${v%%.*}" -ge 28 ] 2>/dev/null && [ ! -e /opt/sentry ]; then
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw &&
ufw allow OpenSSH && ufw --force enable &&
if [ -z "${t:-}" ]; then
t=$(curl -Ls -o /dev/null -w '%{url_effective}' https://github.com/getsentry/self-hosted/releases/latest)
t=${t##*/}
fi &&
git clone https://github.com/getsentry/self-hosted.git /opt/sentry &&
cd /opt/sentry && git checkout "$t" &&
s=$(openssl rand -hex 32) && [ ${#s} -eq 64 ] &&
install -m 600 /dev/null .env.custom &&
printf 'SENTRY_BIND=127.0.0.1:9000\nLAUNCHPAD_RPC_SHARED_SECRET=%s\n' "$s" > .env.custom &&
./install.sh --skip-user-creation --no-report-self-hosted-issues &&
docker compose --env-file .env --env-file .env.custom up --wait &&
curl -fsS http://127.0.0.1:9000/_health/ && echo " - up on 127.0.0.1:9000"
else
echo "stopped: Docker '$v' is older than 28, or /opt/sentry already exists"
fi
Why the .env.custom: the stock .env sets SENTRY_BIND=9000, a mapping with no address, which Docker publishes on every host address (0.0.0.0 and [::]). On IPv4 that traffic skips ufw's rules entirely. Binding to 127.0.0.1 keeps it local, and Docker Engine 28 or later is what makes that binding local-only, hence the version check. The same file replaces LAUNCHPAD_RPC_SHARED_SECRET, whose shipped default the Configuration page calls too weak for production. Once .env.custom exists the docs say plain .env is ignored, so every compose command on this page passes both files: docker compose --env-file .env --env-file .env.custom.
The first superuser, before anything is public
- Still as root:
cd /opt/sentry && docker compose --env-file .env --env-file .env.custom run --rm web createuser --email you@example.com --superuser. It asks for the password twice. - From your workstation:
ssh -L 9000:127.0.0.1:9000 you@SERVER_IP, through your own login. - Open
http://localhost:9000, sign in, and on the welcome screen enter the address the instance will have,https://sentry.example.com, as its root URL. - Turn on two-factor authentication under your account settings, then close the tunnel.
TLS proxy and firewall
The docs strongly recommend a reverse proxy that terminates TLS outside the bundled nginx, plus system.url-prefix in sentry/config.yml and the SSL/TLS lines in sentry/sentry.conf.py; without them you may hit CSRF errors on some actions, such as configuring integrations. The block sets all three, re-runs ./install.sh (the docs prefer that to a restart after config changes) and installs Caddy (in Ubuntu 24.04's universe repository).
# As root, after the superuser exists. Set h first; its DNS must point here.
h=sentry.example.com; k='SECURE_PROXY_SSL_HEADER|USE_X_FORWARDED_HOST|SESSION_COOKIE_SECURE|CSRF_COOKIE_SECURE|SOCIAL_AUTH_REDIRECT_IS_HTTPS'; cd /opt/sentry &&
sed -i "s|^# system.url-prefix:.*|system.url-prefix: 'https://$h'|" sentry/config.yml &&
grep -qx "system.url-prefix: 'https://$h'" sentry/config.yml &&
sed -i -E "s/^# ($k) /\\1 /" sentry/sentry.conf.py &&
[ "$(grep -cE "^($k) " sentry/sentry.conf.py)" = 5 ] &&
sed -i "s|^# CSRF_TRUSTED_ORIGINS = .*|CSRF_TRUSTED_ORIGINS = [\"https://$h\"]|" sentry/sentry.conf.py &&
grep -q "^CSRF_TRUSTED_ORIGINS = \[\"https://$h\"\]" sentry/sentry.conf.py &&
./install.sh --skip-user-creation --no-report-self-hosted-issues && docker compose --env-file .env --env-file .env.custom up --wait &&
apt-get install -y caddy && cat > /etc/caddy/Caddyfile <<EOF &&
$h {
@ingest_endpoint path_regexp ^/api/[0-9]+/(envelope|minidump|security|store|unreal)/
handle @ingest_endpoint {
header {
Access-Control-Allow-Origin *
Access-Control-Allow-Credentials false
Access-Control-Allow-Methods GET,POST,PUT
Access-Control-Allow-Headers sentry-trace,baggage
Access-Control-Expose-Headers sentry-trace,headers
}
reverse_proxy 127.0.0.1:9000
}
handle {
reverse_proxy 127.0.0.1:9000
}
}
EOF
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile &&
ufw allow 80,443/tcp && systemctl reload caddy && echo "live: https://$h"
Our changes, in full: we also set CSRF_TRUSTED_ORIGINS, which sentry.conf.example.py documents for proxied installs. The Caddyfile is the docs' Caddy example with your hostname and 127.0.0.1:9000 as upstream; we dropped its comments, the tls contact line, the -Server header removal and, inside both reverse_proxy blocks, health_uri, health_status and the header_up Host rewrite. Caddy runs on the host, so ufw filters it on IPv4 and IPv6 alike, hence the 80/443 rule.
Self-hosted has no spike protection; the docs advise a rate limiter on the proxy, especially on a public instance. Caddy's rate limiting needs the separate caddy-ratelimit module. If only apps outside your network must reach Sentry, the Reverse Proxy page lists the ingest paths you can expose while keeping the dashboard internal.
Backups and restoring
The supported backup is a JSON export of users, organisations, projects, teams, alerts and global settings. It holds no events or issues. scripts/backup.sh writes it to sentry/backup.json after making that file world-writable, so the script below moves it into a root-only folder at mode 600, with the release tag and a config archive beside it. The --no-report-self-hosted-issues flag matters in cron: without it the script asks a yes/no question cron cannot answer, and the backup fails.
# As root. Nightly at 03:30: the supported JSON export plus the config files,
# kept 14 days in /var/backups/sentry. Copy that folder off the machine yourself.
install -m 700 /dev/null /usr/local/sbin/sentry-backup.sh &&
cat > /usr/local/sbin/sentry-backup.sh <<'EOF' &&
set -euo pipefail
umask 077
d=/var/backups/sentry; t=$(date +%F)
mkdir -p -m 700 "$d"
cd /opt/sentry
./scripts/backup.sh global --no-report-self-hosted-issues
python3 -m json.tool sentry/backup.json > /dev/null
install -m 600 sentry/backup.json "$d/sentry-export-$t.json" && rm sentry/backup.json
git describe --tags > "$d/sentry-release-$t.txt"
tar -czf "$d/sentry-config-$t.tar.gz" -C / opt/sentry/.env.custom \
opt/sentry/sentry/config.yml opt/sentry/sentry/sentry.conf.py etc/caddy/Caddyfile
tar -tzf "$d/sentry-config-$t.tar.gz" > /dev/null
find "$d" -name 'sentry-*' -mtime +14 -delete
find /opt/sentry -maxdepth 1 -name 'sentry_backup_log-*' -mtime +14 -delete
echo "backup ok $t"
EOF
printf '30 3 * * * root bash /usr/local/sbin/sentry-backup.sh > /var/log/sentry-backup.log 2>&1\n' > /etc/cron.d/sentry-backup &&
bash /usr/local/sbin/sentry-backup.sh
Event history lives in Docker volumes. install/create-docker-volumes.sh creates sentry-clickhouse, sentry-data, sentry-kafka, sentry-postgres, sentry-redis and sentry-seaweedfs; the Backup & Restore page calls archiving volumes unsupported, at your own risk. If you want it anyway, stop the stack first.
Restoring overwrites the tables it touches, and the docs strongly recommend a fresh install of the same version, so restore onto a new machine and leave the old one untouched as your rollback:
- Copy the three files from one night to /root on the new server. Read the tag:
cat /root/sentry-release-DATE.txt. - Set
t=to that tag in the root shell, then paste the install block. cd /opt/sentry && tar -xzf /root/sentry-config-DATE.tar.gz -C / --exclude=etc/caddy/Caddyfile && install -m 644 /root/sentry-export-DATE.json sentry/backup.json && ./scripts/restore.sh global --no-report-self-hosted-issues. It must exit with code 0. The Caddyfile is left out: the TLS block writes a new one, and dpkg would otherwise halt the caddy install with a config-file question.rm sentry/backup.json && ./install.sh --skip-user-creation --no-report-self-hosted-issues && docker compose --env-file .env --env-file .env.custom up --wait. The restore script stops the containers when it exits, hence the restart. Point DNS at the new address, then sethand paste the TLS block.
Upgrades, retention, email and licence
Releases are monthly, CalVer, due on the 15th, and old versions get no patches. The docs suggest upgrading monthly: take a backup, then cd /opt/sentry && git fetch --tags && git checkout NEW_TAG, read git diff HEAD@{1} -- sentry .env for changed example configs, and run ./install.sh --skip-user-creation --no-report-self-hosted-issues && docker compose --env-file .env --env-file .env.custom up --wait. Expect downtime: the installer stops everything to migrate. Upgrade through every hard stop in order; the four most recent are 24.8.0, 25.5.1, 26.5.0 and 26.7.0. The Releases page also lists versions to skip.
A cleanup job prunes events older than 90 days. Change SENTRY_EVENT_RETENTION_DAYS in .env.custom and re-run the installer. For mail, the built-in SMTP container is fine for evaluation, but the docs advise a relay for production: point the mail.* settings in sentry/config.yml at your provider. SENTRY_BEACON = False in sentry/sentry.conf.py turns off the beacon (version checks, system notices, anonymous usage statistics); for no outbound calls at all, the docs suggest SENTRY_AIR_GAP = True in the same file.
Licence (a general reading on our part, not legal advice): Sentry is under the Functional Source License (FSL-1.1-Apache-2.0 in LICENSE.md). You may run and modify it for your own organisation, including inside a company; you may not sell a deployed instance as a service or use the code to compete with Sentry. Each version becomes Apache 2.0 two years after release. It is not OSI open source.
When this is the wrong place
Sentry's team states it gives self-hosted no guarantees or dedicated support beyond best effort, and no scaling guidance: every service runs as a single node, Kafka included. Seer and the other AI features, spike protection and spend allocation do not exist outside sentry.io, and iOS symbolication and PlayStation crash ingestion are limited. If those matter more than owning the data, self-hosting is the wrong trade. For one database tier on its own box, see ClickHouse on a dedicated server; for uptime checks beside it, Uptime Kuma.
Questions
What hardware does self-hosted Sentry need at minimum?
4 CPU cores, 16 GB of RAM with 16 GB of swap, and 20 GB of free disk, with Docker 19.03.6 and Compose 2.32.2 or newer. Sentry recommends 32 GB of RAM. The installer stops if Docker sees fewer than 4 CPUs or 14,000 MB of memory, or if an x86-64 CPU lacks SSE 4.2.
Can it run on less memory?
Only in the errors-only profile, set with COMPOSE_PROFILES=errors-only in the env file. The installer then accepts 2 CPUs and 7,000 MB. You give up traces, profiles, replays, crons and user feedback.
What does an upgrade involve?
Back up, check out the new tag in the self-hosted folder, compare the example configs, re-run install.sh, then bring the stack up with docker compose up --wait. Pass through every hard stop listed on the Releases page, and skip the versions it says to avoid. Expect downtime: the installer stops everything while migrations run.
Does the JSON backup include my events?
No. It covers users, organisations, projects, teams, alerts and settings. Events and issues live in the Docker volumes, and the docs call volume backups unsupported, at your own risk.
Can I offer self-hosted Sentry to my own customers?
Not under the FSL, on our reading of the docs' licence section: selling a deployed instance as an offering is prohibited until a version converts to Apache 2.0 two years after its release. Running it for your own organisation is allowed. Check LICENSE.md, or a lawyer, for your case.