What you will set up
GlitchTip collects the errors your apps throw, groups them into issues and shows each one with its stack trace, the release it came from and how often it happens. It also measures how long requests and jobs take, checks that your sites are up, and sends alerts by email or to a chat. Your apps talk to it with Sentry's official SDKs, for Python, JavaScript, PHP, Go, Java, Ruby and many more, so an app that already reports to Sentry only needs a new address.
GlitchTip is made by Burke Software and Consulting, a small software company that also sells it as a hosted service, and developed in the open on GitLab. It is open source under the MIT licence. Sentry, which it is compatible with, can also be run on your own server, but it is a different kind of project. Its code is under the Functional Source License, which Sentry itself calls Fair Source rather than open source: you may run it for your own use, each release becomes Apache 2.0 after two years, and you may not offer it as a competing service. Its self-hosted version runs Kafka, ClickHouse, Redis, PostgreSQL and many other containers, and Sentry's documentation asks for at least 4 CPU cores, 16 GB of memory plus 16 GB of swap, and 20 GB of free disk (checked on 14 October 2026). It also has features GlitchTip lacks, such as session replay, profiling and release health. For a small team that wants errors, performance and uptime, GlitchTip does it in one container and a database.
Here GlitchTip runs as a pod under a user of its own called glitchtip, with PostgreSQL for its data, behind Caddy from the Podman guide.
Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:
- GlitchTip 6.2.6 started with PostgreSQL 18 and no Valkey or Redis. The first account and the organisation were created over an SSH tunnel before Caddy was set up. After that, the sign-up page said "Registration is currently unavailable", and a second user could not create an organisation.
- A team, a Python project and a Node.js project were created, and a second person joined through an email invitation.
- A small Python app sent an error with the official Sentry SDK. GlitchTip showed it with the stack trace, the source lines and the local variables, and its 200 ms job under Performance. The issue was resolved, came back as unresolved when the same error arrived from a new release, and set off an alert by email and by webhook.
- A minified Node.js app sent an error, and with its source maps uploaded by
sentry-cli, GlitchTip showed the original file, function names and lines. - Uptime monitors checked an app on the same server and a heartbeat from a scheduled job, and sent alerts when each went down and came back.
- A capture of the server's traffic showed no connections that we had not set up ourselves, and the browser loaded nothing from other sites.
- An update from GlitchTip 6.2.3 to 6.2.6 went through
podman auto-updateand ran its database migrations by itself. GlitchTip was restored from a backup into empty volumes, and everything came back by itself after a reboot.
The pod used about 250 MB of memory: about 200 MB for GlitchTip and the rest for PostgreSQL. Its interface is in English only.
Before you start
You need:
- a server set up as in the Podman guide, with Caddy running;
- an A record and an AAAA record for
errors.example.compointing at your server; - an email account that GlitchTip can send from, for invitations, password resets and alerts (step 7).
The examples use errors.example.com for GlitchTip, 203.0.113.10 for your server's IPv4 address, Example for the organisation, anna@example.com for you and bob@example.com for a colleague. Replace them throughout.
1. Create the user
As root:
useradd -m -s /bin/bash glitchtip
loginctl enable-linger glitchtip
machinectl shell glitchtip@
Everything up to step 6 runs as glitchtip.
2. Let GlitchTip reach this server
A rootless container normally shares the server's own addresses, so an uptime check or a webhook aimed at an app on this server, such as https://wiki.example.com, would reach the container itself instead of Caddy. Give this user's containers addresses of their own instead. Create ~/.config/containers/containers.conf:
[network]
# Give containers their own address, so that the server's public address, and the apps behind Caddy, can be reached from them.
pasta_options = ["-a", "10.0.2.0", "-n", "24", "-g", "10.0.2.2", "--dns-forward", "10.0.2.3", "-a", "fd00::2", "-g", "fd00::1"]
The same setting lets the test apps in steps 9 and 10 send their errors to https://errors.example.com from this server. It is the second fix in step 8 of the Podman guide, and neither of the changes listed there affects GlitchTip.
3. Create the secrets
openssl rand -hex 32 | tr -d '\n' | podman secret create glitchtip-secret-key -
openssl rand -hex 24 | tr -d '\n' | podman secret create glitchtip-db-password -
The first is GlitchTip's secret key, which signs logins and the links in its emails. The second is the database password. Both are stored by Podman, not in the files below.
4. Define the pod
mkdir -p ~/.config/containers/systemd
cd ~/.config/containers/systemd
Create glitchtip.pod:
[Pod]
PodName=glitchtip
# Only Caddy, on this server, can reach GlitchTip: the port is not open to the internet.
PublishPort=127.0.0.1:8191:8000
[Install]
WantedBy=default.target
Create glitchtip-db.container:
[Container]
ContainerName=glitchtip-db
Image=docker.io/library/postgres:18
Pod=glitchtip.pod
Volume=glitchtip-db:/var/lib/postgresql
Environment=POSTGRES_USER=glitchtip POSTGRES_DB=glitchtip
Secret=glitchtip-db-password,type=env,target=POSTGRES_PASSWORD
HealthCmd=pg_isready -h 127.0.0.1 -U glitchtip -d glitchtip
HealthInterval=10s
Notify=healthy
AutoUpdate=registry
[Service]
Restart=always
And glitchtip-web.container:
[Unit]
Requires=glitchtip-db.service
After=glitchtip-db.service
[Container]
ContainerName=glitchtip-web
Image=docker.io/glitchtip/glitchtip:6
Pod=glitchtip.pod
Volume=glitchtip-uploads:/code/uploads
Environment=SERVER_ROLE=all_in_one VALKEY_URL=
Environment=GLITCHTIP_DOMAIN=https://errors.example.com ALLOWED_HOSTS=errors.example.com,localhost
Environment=DATABASE_HOST=127.0.0.1 DATABASE_NAME=glitchtip DATABASE_USER=glitchtip
Environment=ENABLE_USER_REGISTRATION=false ENABLE_ORGANIZATION_CREATION=false
Environment=GLITCHTIP_RETENTION_DAYS=90
Secret=glitchtip-db-password,type=env,target=DATABASE_PASSWORD
Secret=glitchtip-secret-key,type=env,target=SECRET_KEY
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=300
What the settings do:
SERVER_ROLE=all_in_oneruns the web server and the background worker, which processes events and sends alerts, in one container. GlitchTip can split them into separatewebandworkercontainers to handle more traffic; a small team does not need that.VALKEY_URL=left empty tells GlitchTip not to use Valkey or Redis. Since GlitchTip 6, PostgreSQL holds the task queue and the cache, and older guides that start a Redis container are out of date. GlitchTip's sample setup still includes Valkey as an option for busier servers.GLITCHTIP_DOMAINis the public address GlitchTip puts in links, emails and the DSNs of your projects.ALLOWED_HOSTSmakes it answer only to that name, and tolocalhostfor step 5.ENABLE_USER_REGISTRATION=falsecloses the sign-up page, andENABLE_ORGANIZATION_CREATION=falsestops users from creating organisations of their own. GlitchTip still lets the very first account sign up, which is why step 5 creates it before the site is online.GLITCHTIP_RETENTION_DAYSis how long events, performance data and uptime checks are kept. Step 14 has more.- The database password reaches GlitchTip as
DATABASE_PASSWORD, separate from the other database settings, so it stays in the secret. Source maps and other uploaded files go in theglitchtip-uploadsvolume; everything else is in the database.
Start it:
systemctl --user daemon-reload
systemctl --user start glitchtip-pod
systemctl --user enable --now podman-auto-update.timer
The first start downloads about 1.2 GB of images, and GlitchTip takes a minute to set up its database. It is ready when this prints 200:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8191/
podman logs glitchtip-web starts with a summary of the settings, such as Email: disabled (no transport configured), Cache/Queue: Database and Event retention: 90 days.
5. Create your account
The first account on a new GlitchTip can always sign up, and so can the first organisation, whoever creates them. Create both before the site is online, through an SSH tunnel. On your own computer:
ssh -L 8191:localhost:8191 root@203.0.113.10
Keep that connection open, and open http://localhost:8191 in your browser. Choose Sign Up, enter your email address and a password, and choose Register. GlitchTip logs you in and asks for an organisation: enter its Name, such as Example, and choose Create Organization. The name becomes part of addresses, such as /example/issues.
Close the tunnel with exit.
6. Put Caddy in front
Go back to root with exit, switch to machinectl shell caddy@, and add this block at the end of ~/Caddyfile:
errors.example.com {
reverse_proxy 127.0.0.1:8191
}
Restart Caddy with systemctl --user restart caddy, open https://errors.example.com, and log in. https://errors.example.com/register now says "Registration is currently unavailable. Contact your server administrator."
Under Profile > MFA you can add an authenticator app or a security key to your account. GlitchTip asks you to confirm your email address first, which needs email from step 7.
7. Email
GlitchTip sends email for invitations, password resets, confirming email addresses and alerts. Without it, GlitchTip starts and works, but:
- an invitation says "An email invite has been sent", and nothing is sent, so the person cannot join;
- Reset Password on the login page sends nothing;
- multi-factor authentication cannot be set up, since the email address cannot be confirmed;
- alerts can only go to webhooks.
Send through your email provider's SMTP server. As glitchtip, store the password as a secret:
printf %s 'the-smtp-password' | podman secret create glitchtip-mail-password -
Then add these lines to glitchtip-web.container, after the other Environment= lines, with your own server, account and sender address:
Environment=EMAIL_HOST=smtp.example.com EMAIL_PORT=587 EMAIL_USE_TLS=true
Environment=EMAIL_HOST_USER=errors@example.com DEFAULT_FROM_EMAIL=errors@example.com
Secret=glitchtip-mail-password,type=env,target=EMAIL_HOST_PASSWORD
And restart:
systemctl --user daemon-reload
systemctl --user restart glitchtip-web
The log now shows Email: SMTP. Port 587 with EMAIL_USE_TLS=true uses STARTTLS; for port 465, which uses TLS from the start, set EMAIL_PORT=465 and EMAIL_USE_SSL=true instead of EMAIL_USE_TLS. To check it, use Reset Password on the login page with your own address. We tested sending with a mail catcher on the server, where the invitation, the password reset, the address confirmation and every alert arrived; we did not send through a real provider.
8. Teams, projects and people
In GlitchTip, an organisation has teams, and each project, usually one app, belongs to a team. Alerts by email go to the members of the project's team.
Choose Projects, then Create new project in Example. Pick the Platform, such as Python, and give it a Name, such as shop-api. Under the name, Create a team makes a team, such as backend, and adds you to it. Choose Create Project. GlitchTip then shows how to install the SDK, with the project's DSN filled in, an address like https://4f1c…@errors.example.com/1. The DSN tells the SDK where to send events; it is not a password, and it ends up in front-end code that anyone can read. You find it again under Settings for the project, in Client Keys (DSN).
With registration closed, GlitchTip only invites people who already have an account. Create Bob's account as glitchtip, without a password:
podman exec glitchtip-web ./manage.py shell -c 'from apps.users.models import User; User.objects.create_user("bob@example.com", None)'
Then choose Organization > Members > Invite Member, enter his address, pick a Role and a Team, and choose Add Member(s). Bob gets an invitation by email. He sets his password with Reset Password on the login page, logs in, and opens the link in the invitation to join. Member can see and handle issues; Admin, Manager and Owner can also change projects, teams and members.
9. Send your first error from Python
To see it work before you change a real app, send an error from a small script in a throwaway Python container on the server. As glitchtip:
mkdir -p ~/sdk-test && cd ~/sdk-test
Create app.py:
import os, time
import sentry_sdk
sentry_sdk.init(
dsn=os.environ["SENTRY_DSN"],
release=os.environ.get("RELEASE", "shop-api@1.0.0"),
environment="production",
traces_sample_rate=1.0,
)
def average(values):
return sum(values) / len(values)
def nightly_report(orders):
return {"average": average([o["total"] for o in orders])}
with sentry_sdk.start_transaction(op="task", name="nightly-report"):
with sentry_sdk.start_span(op="db.query", name="load orders"):
time.sleep(0.2)
orders = []
nightly_report(orders)
Run it with the DSN of your Python project:
podman run --rm -v ./app.py:/app.py:ro -e RELEASE -e SENTRY_DSN='https://4f1c…@errors.example.com/1' \
docker.io/library/python:3.13-slim sh -c 'pip install -q --root-user-action=ignore sentry-sdk && python /app.py'
The script ends with ZeroDivisionError: division by zero, and the SDK sends it as it exits. Under Issues, GlitchTip shows ZeroDivisionError: division by zero. Open it to see the stack trace, from <module> through nightly_report to average, with the source lines around each and the value of values, which was [], plus the release, the environment and the Python version. Performance lists the nightly-report job and how long it took.
In a real app, the sentry_sdk.init() call goes where the app starts. traces_sample_rate is the share of requests and jobs whose timing is sent: 1.0 sends all of them, which is fine for a test; use something like 0.1 in production, as each one is stored. GlitchTip shows its own setup instructions for each platform when you create a project.
10. JavaScript and source maps
Minified JavaScript gives stack traces full of one-letter names. Upload the source maps from your build, and GlitchTip shows the original code. This works with Sentry's sentry-cli, the same way as with Sentry.
First, create a token for uploads: Profile > Auth Tokens > Create New Token, give it a Label, tick project:read, project:releases and org:read, and choose Create Token. Then create a project with the platform Node.js, named shop-web, as in step 8.
As glitchtip, make a small app:
mkdir -p ~/js-test/src && cd ~/js-test
Create src/app.js:
const Sentry = require("@sentry/node");
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: "shop-web@1.0.0",
environment: "production",
tracesSampleRate: 1.0,
});
function parsePrice(text) {
return Number(text.trim().replace(",", "."));
}
function cartTotal(items) {
return items.reduce((sum, item) => sum + parsePrice(item.price) * item.qty, 0);
}
Sentry.startSpan({ name: "checkout", op: "task" }, () => {
cartTotal([{ price: "12,50", qty: 2 }, { price: undefined, qty: 1 }]);
});
Paste the token when this waits for it, so it does not end up in your shell history:
read -rs SENTRY_AUTH_TOKEN && export SENTRY_AUTH_TOKEN
Then build it minified, with source maps, and upload them:
podman run --rm -v .:/app -w /app -e SENTRY_AUTH_TOKEN -e SENTRY_URL=https://errors.example.com \
-e SENTRY_ORG=example -e SENTRY_PROJECT=shop-web docker.io/library/node:22-slim sh -c '
npm init -y >/dev/null
npm install --silent @sentry/node
npm install --silent --save-dev esbuild @sentry/cli
npx esbuild src/app.js --bundle --minify --sourcemap --platform=node --packages=external --outfile=dist/app.js
npx sentry-cli sourcemaps inject dist
npx sentry-cli sourcemaps upload --release shop-web@1.0.0 dist'
sourcemaps inject writes a debug ID into the built file and its map, and sourcemaps upload sends both to GlitchTip, ending with a Source Map Upload Report. Now run the minified app with the project's DSN:
podman run --rm -v .:/app -w /app -e SENTRY_DSN='https://9a2e…@errors.example.com/2' \
docker.io/library/node:22-slim node dist/app.js
On the server, the error says at c (/app/dist/app.js:3:167). In GlitchTip, the same error shows ../src/app.js in parsePrice at line 11, with the original source. In a real project, run the two sentry-cli commands in your build after each release, with the token as a secret in your CI. For browser apps, use @sentry/browser or the SDK for your framework instead of @sentry/node.
11. Resolve, regressions and releases
Open an issue, and use the Unresolved menu at the top:
- Resolved closes it. If the same error comes back, GlitchTip opens it again. We resolved the Python issue, ran the script again with
RELEASE=shop-api@1.0.1in front of thepodman runfrom step 9, and the issue was unresolved again, with two events andshop-api@1.0.1as its latest release. - Resolve in next release closes it until an event arrives from a newer release.
- Ignored hides it, for errors you have decided to live with.
Releases lists every release that has sent events or source maps. Setting release in the SDK, as in the examples, is what lets you see which version brought an error back.
12. Uptime monitors
Choose Uptime Monitors > Add monitor. The types:
- GET and POST send a request to a Site URL and expect a success code; you can also require a word in the response.
- Ping sends an HTTP
HEADrequest, not an ICMP ping. - TCP Port checks that a port answers.
- Heartbeat works the other way: your job calls GlitchTip, and the monitor goes down when the call does not arrive in time.
Each monitor belongs to a project, under Associated Project, and alerts go to that project's team. Interval (seconds) is how often it checks.
GlitchTip refuses monitors for private addresses, such as http://127.0.0.1:8191/, with "There was an error saving your monitor details", so that users cannot probe your internal network. Point monitors at the public address of a site instead; step 2 is what makes that work for sites on this server.
A heartbeat suits backups and other scheduled jobs. Create a Heartbeat monitor with an interval a little longer than the job's, and copy the Endpoint URL from the monitor's page. Then make the job call it when it succeeds, for example in a crontab:
30 2 * * * /usr/local/bin/backup.sh && curl -fsS -X POST https://errors.example.com/api/0/organizations/example/heartbeat_check/160bc2c5-…/
In our tests, a new monitor sent a "back up" email after its first check, a stopped app on this server sent "Test site is down" within a minute, and a heartbeat that did not arrive sent "Nightly backup is down".
13. Alerts
Alerts are set per project, under Settings for the project > Project Alerts > Create New Alert:
- Tick If an event happens, and set Quantity and Timespan, such as
1time in1minute to hear about every new error, or higher to hear only about errors that happen often. - Tick If an uptime monitor is triggered to get the project's uptime alerts too.
- Recipients starts with email, which goes to the members of the project's team. Add An Alert Recipient adds a webhook: General (slack-compatible) Webhook, which also suits Mattermost and Rocket.Chat, Discord, Google Chat, ntfy, Microsoft Teams, Zulip or Feishu (Lark) Webhook, with its Webhook URL.
Choose submit. With one event in one minute, the regression in step 11 sent an email, "Error in shop-api: ZeroDivisionError: division by zero", and posted GlitchTip Alert with the issue's title, link, project, environment and release to the webhook, about a minute after the event. Uptime alerts post GlitchTip Uptime Alert. For push notifications on your phone, ntfy works as a recipient.
Webhooks cannot point at private addresses either, and GlitchTip does not follow redirects for them. Each user can also turn alert emails off for a project under Profile > Notifications.
14. Retention and limits
GlitchTip deletes events, performance data and uptime checks older than GLITCHTIP_RETENTION_DAYS, 90 days by default. Older guides call it GLITCHTIP_MAX_EVENT_LIFE_DAYS, which still works. To keep 30 days, change the line in glitchtip-web.container, then daemon-reload and restart glitchtip-web as in step 7. The log then shows Event retention: 30 days. GLITCHTIP_EVENT_RETENTION_DAYS, GLITCHTIP_TRANSACTION_RETENTION_DAYS and GLITCHTIP_UPTIME_RETENTION_DAYS set each kind separately.
Each event is checked for size when it arrives. In our tests, a compressed event that grew past 5 MB when unpacked was refused with Cannot parse request body, and a request body over 15 MB got 413. The first limit is GLITCHTIP_MAX_UNZIPPED_PAYLOAD_SIZE. The SDKs compress what they send and trim large values before they do.
GlitchTip itself does not limit how many events a project may send on your own server: 60 events sent at once were all accepted. A loop that throws errors will fill the database. Keep the number down in the SDK, with sample_rate for errors and traces_sample_rate for performance, and watch the disk with df -h and podman system df.
15. Outside connections
GlitchTip makes no connections of its own. We captured the server's DNS lookups and outgoing connections while GlitchTip started, received events, took source maps and sent alerts. The only ones were those we had set up: the uptime monitors, the webhooks, the email server, and the test apps sending to errors.example.com. There was no update check and no usage data. In the browser, every page loaded only from errors.example.com.
The SDKs in your apps send their events only to the address in the DSN. Each event can contain request data, local variables and user details. The Python SDK leaves out personal data unless you set send_default_pii=True, and GlitchTip can remove secrets from events as they arrive; see GLITCHTIP_PII_SCRUB_DEFAULT in its documentation, which we did not test.
16. Back up
As glitchtip:
umask 077
mkdir -p ~/backup
podman exec glitchtip-db pg_dump -U glitchtip -Fc glitchtip > ~/backup/glitchtip-db.dump
podman volume export glitchtip-uploads > ~/backup/glitchtip-uploads.tar
podman secret inspect --showsecret --format '{{.SecretData}}' glitchtip-secret-key > ~/backup/secret-key.txt
The database holds the users, organisations, projects, issues, events, monitors and alerts. The volume holds the uploaded source maps. The dump runs while GlitchTip keeps working. Copy ~/backup to another machine, for example with restic, and keep it private: the events can contain your users' data.
To restore into an empty pod, as glitchtip, with the backup in ~/backup:
systemctl --user stop glitchtip-pod
podman volume rm glitchtip-db glitchtip-uploads
podman volume create glitchtip-uploads
podman volume import glitchtip-uploads ~/backup/glitchtip-uploads.tar
systemctl --user start glitchtip-db
podman exec -i glitchtip-db pg_restore -U glitchtip -d glitchtip --no-owner < ~/backup/glitchtip-db.dump
systemctl --user start glitchtip-web
Starting only the database first loads the backup before GlitchTip sets up an empty one. On a new server, set it up as in steps 1 to 4, but before the first start create the secret key from the backup instead of a new one in step 3:
tr -d '\n' < ~/backup/secret-key.txt | podman secret create glitchtip-secret-key -
17. Update
podman auto-update checks once a day for new images of GlitchTip and PostgreSQL, and restarts the pod when one has changed. GlitchTip runs its database migrations when it starts, which shows as Applying … lines in its log. To update now:
podman auto-update
The 6 tag follows every GlitchTip 6 release, and 18 every PostgreSQL 18 release. A new major version, such as GlitchTip 7, needs a change of tag, which GlitchTip announces on its blog. Read the changelog before you change it, and back up first. Some updates change stored data: the update from 6.2.3 to 6.2.6 emptied the uptime check history, as its changelog said, while monitors and alerts kept working.
Troubleshooting
curl http://localhost:8191/ prints 400. The name you used is not in ALLOWED_HOSTS. http://127.0.0.1:8191/ gets 400 too; use localhost.
"Only existing users may be invited". Registration is closed, so the person needs an account first. Create it as in step 8.
An invitation says it was sent, and nothing arrives. Email is not set up, or the settings are wrong. The first lines of podman logs glitchtip-web show Email: disabled or Email: SMTP, and errors from the mail server appear further down.
An uptime monitor or a webhook for an app on this server fails, while the app works in your browser. The containers.conf from step 2 is missing, or was created after the pod started: restart it with systemctl --user restart glitchtip-pod.
The SDK runs, and no issue appears. The DSN must be the https:// address from the project's settings, and the app must be able to reach errors.example.com. Short scripts must give the SDK time to send before they exit; the Python SDK does that by itself, and Sentry.flush() does it in JavaScript.
The stack trace still shows minified names. The source maps were uploaded after the error, or for another file. Check that sentry-cli sourcemaps inject ran before the build was deployed, and that the upload report lists the same debug ID as the file you run.