Java 25 on PostgreSQL, four deployment modes that live in the database rather than a properties file, a cluster that tells you exactly what is wrong with it, streaming backups, and updates that apply themselves and hand the restart back to systemd.
Self-hosting is a division of labour. Here is where the line sits.
A short list, deliberately. Fewer moving parts is an operational feature.
A node's role lives in cluster_nodes, not in a properties file — so you change a topology from the admin screen rather than from a config-management run.
The default and, for most installs, the last topology you will ever need. One process serves the Thymeleaf interface, answers the REST API and runs the background work.
Start here. Split only when you have a reason — an isolation requirement, a network boundary, or a scheduler you want on its own hardware.
Serves the Thymeleaf UI and nothing else, pointing at an external backend URL. Useful when the presentation tier belongs in a DMZ and the API does not.
Answers the API and skips the interface entirely, with CORS configured for the frontend nodes that call it. The cluster health check verifies that CORS correctness itself, so a mismatch shows up as a verdict rather than as a browser console error somebody eventually reports.
Mail polling, SLA sweeps, garbage collection, report delivery, materialised-view refresh and OAuth token refresh — with no user traffic competing for the same heap.
Scheduler leadership means it does not matter how many nodes could run this work: it runs once. A node checks whether the scheduler is held elsewhere before it starts.
Not a green tick and a shrug. Every health verdict comes with the remedial action attached.
A node registers itself on startup and heartbeats from then on. Three missed beats and it is treated as dead. From the admin screen you can forget a node, probe it, or enable and disable it, and a cross-node liveness probe endpoint at /api/system/cluster/ping lets nodes check each other directly.
Assign a deployment mode to any node from the interface. An optional autoApply flag lets the node restart itself to adopt the new mode, so promoting a spare node to backend duty is a click rather than an SSH session.
One node can be designated the backup node, and the health check will tell you if none is.
The thing that goes wrong first in a naively clustered tracker is two nodes both polling the same mailbox, both sweeping the same SLA timers, and both emailing the same scheduled report.
Trakr resolves it with explicit leadership: a node asks whether the scheduler is held elsewhere before it takes the work. Duplicated mail, duplicated escalations and duplicated report sends simply do not arise.
The cluster health check evaluates node registration, per-node liveness, frontend coverage, backend and API coverage, scheduler coverage, whether a backup node is present, version skew across nodes, CORS correctness, and the source of each node's mode.
Each verdict carries a remedial action. Version skew after a partial rollout is the classic silent failure in a clustered deployment; here it is a named finding on a screen, not something you deduce from an odd stack trace three days later.
Six steps at /setup, ending with a systemd unit installed and started.
The wizard is reachable before anyone has authenticated — that is the whole point of a first-run wizard — and it is designed around that fact rather than in spite of it.
application.properties: Application (app.name, server.port), Security (jwt.secret, minimum 64 characters; app.encryption.key, a base64 256-bit key auto-generated if you leave it blank; and jwt.expiration), and API documentation. Secret fields are never echoed back, because the endpoint is unauthenticated by design. Database credentials and the deployment mode are deliberately excluded, and there is no email step — email is configured per organisation, tenant or platform afterwards.A curated allow-list, masked secrets, and a restart mechanism that gets the HTTP response out before it kills the process.
The configuration editor exposes a superadmin-editable allow-list of application.properties keys, grouped: General (app.name, app.base-url, app.api.base-url), Server (server.port), Database (datasource URL, username, password), Security (jwt.secret, app.encryption.key), email-to-ticket polling, translation (DeepL enabled, key, URL), storage paths for attachments, backups and translations, and software update and restart (enabled, JAR path, restart command, maximum file size).
Each key carries a label, a group, a type, a danger flag and a description. Handing an administrator a text area over the whole properties file is how installs get bricked; this is not that.
Secret values are masked. The API reports whether a value is set, never what it is. Changes are validated and written to an override file rather than trampling the shipped defaults, and saving requires confirming with your password.
A public-URL helper checks that the base URL you have configured is the one your users will actually reach — the cause of most broken email links in a self-hosted deployment.
Restarting is either an allow-listed external command, giving a clean full restart, or an in-process restart. Either way Trakr writes a short detached script and lets it do the work.
That detail matters: a process that kills itself inside a request handler leaves the administrator staring at a dead connection with no idea whether the restart was accepted. Detaching means the HTTP response flushes first, and then the process goes down.
Beyond the properties file there is a cached, typed key/value store covering subscriptions, garbage collection, the git server, Stripe keys, the base URL, agentic coding (enabled, default agent, maximum budget per run) and the whole branding family.
Two of those have a properties-level hard kill-switch that overrides the database toggle: billing and the internal git server. A feature you have disabled at the filesystem level cannot be re-enabled from a web form.
Data and attachment files together, because a database dump without the attachments is not a backup of an issue tracker.
Two routes in, one route through. Both end with the service manager restarting a swapped JAR.
SFTP settings — host, username, password or private key, and known hosts — are stored in the database and testable from the interface, so you find out that the credentials are wrong before you need them, not during an upgrade window.
Migrations, garbage collection, logs and demo data — the unglamorous half of running software.
The migrations screen lists every bundled migration file with its applied status. You can view the SQL — capped at 5 MiB so a monster migration cannot take the page down — execute a migration, mark one as applied without executing it, and unmark it to make it pending again.
Mark and unmark are the escape hatch every Flyway operator eventually needs when a migration was applied out of band. Having it in the product beats connecting to the database and editing the schema history table by hand.
Application GC runs in five categories on a deliberate stagger: sessions at T, notifications at T+10 minutes, activity logs at T+20, orphaned attachments at T+30 and audit logs at T+40. You can run everything or one category, with statistics on what was collected.
Separately, a JVM garbage collection runs on a cron stored in the database and is dynamically rescheduled without a restart, with an immediate run available on demand. Both mechanisms enable, disable and toggle independently.
Read the application log, search it with pattern and level filters, inspect metadata — files, sizes, levels — and download or clear it.
There is also a live viewer with a level filter from TRACE to ERROR, a configurable line count, a pause control and an application-versus-system switch. Useful when the person diagnosing the problem has an admin account but not an SSH key.
Populate a chosen organisation, or add a batch of tickets to an existing project across a month span. The generator produces domain-specific project blueprints, components, coloured labels, member names, customer companies, generated summaries, descriptions, comments in both internal and customer variants, and work-log notes.
It is written by a raw-JDBC bulk writer with a pollable progress job covering projects, users, tickets, comments, work logs, links and history — and a cleanup path, so a demo environment can be reset rather than rebuilt.
Self-hosting is not a feature you switch on; it is a job you take on. It is a short job, but it is not an empty one.
PostgreSQL. Trakr manages its own schema through Flyway, but it does not run your database. Provisioning, patching, tuning, replication and the database's own backups are yours. PostgreSQL is the only supported engine, which at least means there is exactly one runbook to write.
Verifying that backups restore. Trakr will take an organisation or tenant backup on a cron, stream it, package it, email you on completion and delete it when retention expires. It cannot tell you that the archive restores into a working system. An untested backup is a hypothesis. Restore one into a scratch environment on a schedule you set, and find out while it does not matter.
TLS termination. Put a reverse proxy in front of the application and terminate TLS there. Trakr sets its session cookie Secure and SameSite=Strict and emits a strict Content-Security-Policy with frame-ancestors 'none' — none of which helps if the traffic reaching it is plaintext.
The encryption key. app.encryption.key is a base64 256-bit key, auto-generated at setup if you leave it blank. AES-256-GCM field encryption uses it for OAuth client secrets, LDAP bind passwords, SMTP credentials, MFA secrets and integration tokens. Lose that key and the encrypted data is unrecoverable — not by you, and not by us. Store it the way you store any other root secret, and store it somewhere that is not the machine it protects.
A JVM and a PostgreSQL database. Trakr is Java 25 on Spring Boot 4.0.6, with a PostgreSQL-only persistence layer managed by Flyway and pooled with HikariCP. The build is GraalVM native-image capable. The user interface is server-rendered Thymeleaf with vanilla ES modules — there is no separate SPA build to deploy — and the theme ships self-hosted webfonts, so the browser makes no external font request.
Each node registers itself on startup and sends a heartbeat; a node is considered dead after three missed beats. A node's deployment mode — ALL_IN_ONE, FRONTEND_ONLY, BACKEND_ONLY or SCHEDULER_ONLY — lives in the cluster_nodes table rather than a properties file, so you assign a role from the admin screen and optionally let the node restart itself to adopt it. Scheduler leadership ensures background work runs on exactly one node.
Backups come in two scopes: organisation, covering all of an organisation's data plus its attachments, and tenant, covering every organisation in the tenant with a top-level manifest. Execution is asynchronous — it streams a JSON export element by element, copies the attachment files, packages a ZIP and emails the requester on completion. Downloads are path-traversal-safe and access-scoped. Per tenant you can set enabled, a cron expression and a retention period, with an hourly scheduler deciding whether a backup is due and a daily cleanup of expired ones.
Two paths. Trakr can check an SFTP manifest for a newer release and download the JAR and SQL migrations asynchronously with checksum verification, or you can upload an update ZIP by hand — exactly one root JAR plus its migrations. Applying copies the SQL migrations into the Flyway directory and exits the JVM, so the swap script replaces the JAR and your service manager restarts the process. Update history is paginated, and the version string is checked against an allow-list because it is interpolated into file paths.
The wizard is designed on the assumption that it is reachable before authentication, which is why the application settings step is an allow-list rather than a free-form properties editor, and why secret fields are never echoed back in a response. Database credentials and the deployment mode are deliberately excluded from it, and scheduling stays gated until setup is marked complete.
Four things: the PostgreSQL instance and its own backups; verifying that Trakr's backups actually restore, because an untested backup is a hypothesis; TLS termination in front of the application; and the encryption key. AES-256-GCM field encryption protects OAuth client secrets, LDAP bind passwords, SMTP credentials, MFA secrets and integration tokens — if you lose app.encryption.key, that data is gone, and no one can recover it for you.
No user minimum, no cloud-only tier, no separate SPA to deploy. A JAR, a PostgreSQL database, and a setup wizard that installs the service unit for you.