Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
Updated Oct 2026
verified on Ubuntu 26.04 · Oct 2026Self-host Sentry, the full error tracking and performance monitoring suite, on a 4 vCPU / 16 GB VPS with the official installer pinned to a release, swap, a loopback port behind Caddy HTTPS and tested backups.
Sentry is the error tracker most teams already know from the hosted product: it collects exceptions with stack traces from your applications, groups them into issues, links them to releases and commits, alerts you, and adds performance tracing, session replay and profiling on top. The self-hosted edition is the same software and uses the same SDKs, so pointing an app at your own server is a one-line DSN change. It is licensed under the Functional Source License (FSL-1.1-Apache-2.0), which is source-available and converts to Apache 2.0 after two years, and it is rated 4 / 5 to deploy.
That difficulty is not about any single step. It is that the upstream
getsentry/self-hosted repository is a Docker Compose blueprint of dozens of
containers: Kafka, ClickHouse, PostgreSQL, Redis, Snuba, Relay, Symbolicator and
more. This guide installs that blueprint pinned to a release, keeps it off the
public internet except through HTTPS, and covers backups and upgrades.
If you only need error tracking and your server is small, look first at GlitchTip, which speaks the Sentry SDK protocol and runs in a fraction of the memory (see GlitchTip vs Sentry and its deploy guide). If you are weighing this against the hosted product, see Sentry alternatives.
Sentry's documented minimum is 4 CPU cores, 16 GB of RAM plus 16 GB of swap, and 20 GB of free disk, and it recommends 32 GB of RAM. The installer enforces part of that for you: it fails outright if Docker sees fewer than 4 cores or about 14 GB of RAM.
We measured the installed stack on a 4 vCPU / 16 GB Ubuntu 26.04 server: about 13 GB of RAM in use once every container was healthy, with no events yet, and about 15 GB of disk for images and volumes. That leaves roughly 2 GB of headroom, which is why the swap file below is not optional decoration. Everything in Sentry's footprint grows with traffic: ClickHouse and Kafka hold event data, and the default retention is 90 days.
A 16 GB plan works for a small team's staging and production errors. If you send high event volume, or turn on replay and profiling for many projects, size up to 32 GB rather than adding more swap. For help choosing a provider, see Best VPS for Monitoring.
Install Docker and the Compose plugin first by following
Docker and Compose on Ubuntu, and make
sure your user can run docker without sudo. Then confirm the versions:
docker --version
docker compose version
Open SSH plus the two ports Caddy will use. Sentry itself gets no public port:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Docker publishes container ports by editing the firewall directly, which
bypasses ufw rules. That is why the install below binds Sentry's own port to
127.0.0.1 instead of relying on ufw to block it.
Now add the swap file Sentry's documentation asks for:
sudo fallocate -l 16G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -m
Clone the official repository and pin it to a release tag. A tag is what makes this reproducible; the default branch moves.
cd ~
git clone https://github.com/getsentry/self-hosted.git
cd self-hosted
git checkout 26.9.0
The repository's .env file binds Sentry's web port on every interface. Instead
of editing .env (which an upgrade would conflict with), put your overrides in
.env.custom, which the installer reads first:
echo 'SENTRY_BIND=127.0.0.1:9000' > .env.custom
Run the installer. --skip-user-creation avoids an interactive prompt, and
--no-report-self-hosted-issues stops it from sending install telemetry to
Sentry:
./install.sh --skip-user-creation --no-report-self-hosted-issues
This is the slow step: it pulls many gigabytes of images, builds a local Sentry image and runs the database migrations for PostgreSQL, ClickHouse and Snuba. Plan on a quarter of an hour or more on a fresh server. It ends with a note about IP address geolocation because no MaxMind key is configured; the install still completes.
Tell Sentry its public address before first start. Create an A record for
your hostname (say sentry.example.com) pointing at the server. With no domain
yet, an sslip.io name resolves to your server's IP with no DNS setup, which is
what this guide uses so every command runs as written:
DOMAIN="sentry.$(curl -4s https://ifconfig.me).sslip.io"
echo "$DOMAIN"
Keep this shell session open (or re-run the line) for the rest of the guide, and replace the value with your real hostname when you have one. Now edit the two config files the installer generated. Set the URL prefix, and enable the reverse-proxy settings that Sentry ships commented out:
sed -i "s|^# system.url-prefix:.*|system.url-prefix: https://$DOMAIN|" sentry/config.yml
sed -i \
-e 's|^# SECURE_PROXY_SSL_HEADER|SECURE_PROXY_SSL_HEADER|' \
-e 's|^# USE_X_FORWARDED_HOST|USE_X_FORWARDED_HOST|' \
-e 's|^# SESSION_COOKIE_SECURE|SESSION_COOKIE_SECURE|' \
-e 's|^# CSRF_COOKIE_SECURE|CSRF_COOKIE_SECURE|' \
-e 's|^# SOCIAL_AUTH_REDIRECT_IS_HTTPS|SOCIAL_AUTH_REDIRECT_IS_HTTPS|' \
sentry/sentry.conf.py
echo "CSRF_TRUSTED_ORIGINS = [\"https://$DOMAIN\"]" >> sentry/sentry.conf.py
grep -n '^system.url-prefix' sentry/config.yml
grep -n '^SECURE_PROXY_SSL_HEADER\|^CSRF_TRUSTED_ORIGINS' sentry/sentry.conf.py
Sentry's documentation says to update system.url-prefix and the SSL/TLS section
of sentry.conf.py when you put a proxy in front, or you can hit CSRF errors at
login. Because the installer reads .env.custom, every later Compose command
needs both env files. A small shell function saves retyping them:
sc() { docker compose --env-file .env --env-file .env.custom "$@"; }
sc up --wait
up --wait returns once every container reports healthy, which takes several
minutes on a first start. Memory is the constraint to watch:
free -m
sc ps | wc -l
Run Caddy as a container on the host network. It obtains and renews a Let's Encrypt certificate on its own (see Automatic HTTPS with Caddy) and proxies to the loopback port:
mkdir -p ~/caddy
cat > ~/caddy/Caddyfile <<EOF
$DOMAIN {
reverse_proxy 127.0.0.1:9000
}
EOF
docker run -d --name caddy --restart unless-stopped --network host \
-v ~/caddy/Caddyfile:/etc/caddy/Caddyfile:ro -v caddy_data:/data caddy:2
Check that HTTPS answers with a trusted certificate, retrying while the certificate is issued:
out=000
for i in $(seq 1 24); do
out=$(curl -s -o /dev/null -w '%{http_code} %{ssl_verify_result}' "https://$DOMAIN/_health/") && [ "${out%% *}" = "200" ] && break
sleep 5
done
echo "health endpoint and certificate check: $out"
[ "${out%% *}" = "200" ] || false
A second number of 0 means the certificate verified.
Create the first superuser from the command line (the installer was told to skip this). The password is generated and printed once, so copy it:
PW=$(openssl rand -hex 12)
sc run --rm -T web createuser --email you@example.com --password "$PW" --superuser
echo "password: $PW"
Replace you@example.com with your own address. Then open
https://sentry.example.com (your hostname) and sign in.
Read the setup screen before clicking Continue. The first superuser login lands on a "Welcome to Sentry" page that asks for the admin email, outbound SMTP settings, whether to allow open registration, and two telemetry choices. In our run the CPU/RAM usage option was pre-selected as "Yes", which sends usage statistics about your instance to Sentry. Pick "Please keep my usage information anonymous" and "No, I would prefer to keep CPU/RAM usage private" if you do not want that, and leave "Allow Registration" off so strangers cannot create accounts on your server. Without SMTP configured, invitations and password-reset emails will not arrive.
Then send a test event. Every project has a key; the built-in internal
project's key is enough for a smoke test:
cat > /tmp/dsn.py <<'EOF'
from sentry.models.projectkey import ProjectKey
print("KEY=" + ProjectKey.objects.get(project__slug="internal").public_key)
EOF
KEY=$(sc run --rm -T web exec < /tmp/dsn.py 2>/dev/null | sed -n 's/^KEY=//p')
curl -s -X POST "https://$DOMAIN/api/1/store/" \
-H "X-Sentry-Auth: Sentry sentry_key=$KEY, sentry_version=7" \
-H "Content-Type: application/json" \
-d '{"message":"guide smoke test","level":"error"}'
A JSON body with an event id means Relay accepted it. Confirm it reached
ClickHouse, which is where Snuba stores errors:
for i in $(seq 1 24); do
n=$(sc exec -T clickhouse clickhouse-client -q "select count() from errors_local" 2>/dev/null)
[ "${n:-0}" -ge 1 ] && break
sleep 5
done
echo "events stored: $n"
[ "${n:-0}" -ge 1 ] || false
For a real project, create it in the web UI and copy the DSN it shows into your
SDK configuration. The DSN will use your https:// address because of the
system.url-prefix setting above.
Two things need protecting, and neither is covered by the usual "dump the database" habit:
sentry-* volume, not just PostgreSQL, because
events live in ClickHouse and Kafka..env.custom, sentry/config.yml (it holds the system
secret key) and sentry/sentry.conf.py are not in any volume.Stop the stack for a consistent copy, archive each volume, then start it again:
mkdir -p ~/sentry-backup
sc stop
for v in $(docker volume ls -q | grep '^sentry'); do
docker run --rm -v "$v":/v:ro -v ~/sentry-backup:/b alpine tar czf "/b/$v.tgz" -C /v .
done
tar czf ~/sentry-backup/config.tgz .env.custom sentry/config.yml sentry/sentry.conf.py
sc up --wait
ls -lh ~/sentry-backup
Sentry also ships a JSON export for users, organizations and configuration. It leaves out high-volume data such as events and issues, so treat it as a supplement, and restore it onto the same Sentry version it came from:
Create the output folder yourself first and make it writable. The script mounts
~/.sentry/sentry-admin into the container, and if the folder does not exist
Docker creates it owned by root; the export then fails with Could not open file '/sentry-admin/export.json': Permission denied, which is what happened on our
first run.
mkdir -p ~/.sentry/sentry-admin
chmod 777 ~/.sentry/sentry-admin
./sentry-admin.sh export global /sentry-admin/export.json
ls -lh ~/.sentry/sentry-admin/export.json
Copy the archives off the server to object storage or another machine on a schedule. An untested backup is a guess, so restore into a throwaway server now and then.
Sentry upgrades are not docker compose pull. Each release ships migrations, and
the project defines hard stops that you must upgrade through one at a time;
skipping them can break the database. Read the list on the
Sentry releases page before
you jump versions, and upgrade often, because a long gap makes it less likely
that going straight to the latest works. Take the backup above first, then:
cd ~/self-hosted
git fetch --tags
git checkout NEW_VERSION
./install.sh --skip-user-creation --no-report-self-hosted-issues
sc up --wait
Expect downtime while services stop and migrations run. If .env gained new
variables in the new release, copy them into .env.custom before running the
installer, as the upgrade documentation says. Re-check your edits to
sentry/config.yml and sentry/sentry.conf.py against the new
config.example.yml and sentry.conf.example.py too.
docker compose up --wait ends with symbolicator exited (137). On our first
start with no swap on a 16 GB server, up --wait reported the Symbolicator
container had exited with code 137, which is a SIGKILL. Re-running up --wait
after adding the 16 GB swap file brought everything healthy. If you see 137 or
containers restarting, check free -m and add swap before anything else.
The installer stops with a minimum-requirements failure. It needs at least 4 cores and about 14 GB of RAM visible to Docker. A "16 GB" plan that reserves memory for a hypervisor can report less; choose the next size up.
Login shows a CSRF error or a redirect loop behind the proxy. The
system.url-prefix or the sentry.conf.py SSL/TLS settings do not match the
address you are browsing. Fix them, then restart the Sentry containers with
sc restart web.
Port 9000 is reachable from the internet. SENTRY_BIND was left at its
default. Confirm .env.custom exists and that you start the stack with both
--env-file flags, then check ss -tln | grep 9000 shows 127.0.0.1.
Certificate never issues. That is a Caddy and DNS problem rather than a Sentry
one: confirm the A record resolves (dig +short sentry.example.com) and that
ports 80 and 443 are reachable. Look at docker logs caddy.
Disk keeps filling. Retention defaults to 90 days. Set
SENTRY_EVENT_RETENTION_DAYS in .env.custom to a lower number and recreate the
stack.
You are done when https://sentry.example.com loads with a valid certificate, you
can sign in as the superuser, a test event shows up in the issues list, port 9000
is bound only to 127.0.0.1, and a volume backup is landing off the server.
From here, create a project per application, add the DSN to its SDK, configure outgoing email for alerts and invitations, and set retention to what your disk can hold. If the memory footprint is more than you want to run, look at SigNoz for traces and logs, or GlitchTip for errors alone.