Iskue overview
Iskue is a self-hosted, Jira-class issue-tracking platform. It covers tickets (epics, stories, tasks and bugs with sub-tickets, custom fields, labels and full change history), a per-project workflow engine, agile boards with sprints and backlog ordering, TQL search (project = CORE AND assignee = me AND status != Done), a service desk (knowledge base, CSAT, SLA policies), per-project automation rules, and a REST API with OpenAPI documentation at /swagger-ui.html. Authentication is JWT-based, with optional OIDC single sign-on (Google, Microsoft) and optional TOTP two-factor authentication.
This page describes the moving parts, the two supported deployment shapes, the system requirements, and where persistent data lives. Everything runs in Docker on a Linux host.
Moving parts
| Component | Technology | Role |
|---|---|---|
| Backend | Java 21, Spring Boot 3.3, Spring Data JPA, Flyway | Modular-monolith API server. Listens on port 8080 inside its container; exposes health at /actuator/health and OpenAPI at /swagger-ui.html. Live board updates are pushed to the browser over SSE. |
| Frontend | React + TypeScript, built with Vite | Single-page application. Shipped as a static bundle served by the frontend (single host) or load-balancer (cluster) container. |
| Database — single host | PostgreSQL 16 (postgres:16-alpine) | One Postgres instance; Flyway migrations are applied by the backend at start-up. |
| Database — cluster | Citus 14 on PostgreSQL 17 (citusdata/citus:14.0.0-pg17) | One coordinator plus two workers. Migrations run once via a one-shot Flyway container; the ticket table and its child tables are sharded, everything else is replicated as reference tables. |
The backend schema is managed exclusively by Flyway migrations shipped in the backend image (backend/src/main/resources/db/migration). In the single-host shape the backend applies them automatically on boot; in the cluster shape a dedicated one-shot container applies them and the application boots with SPRING_FLYWAY_ENABLED: "false". Never apply schema changes by hand.
Deployment shapes
Three Compose files ship in the repository and do not interfere with each other: the root docker-compose.yml (development and single-host full stack), deploy/docker-compose.yml (self-contained distributed Citus cluster, dev/e2e) and deploy/docker-compose.prod.yml (the cluster's production variant, covered on the cluster install page).
Single host — http://localhost:8088
The root docker-compose.yml defines three services: postgres (container issuehub-postgres — PostgreSQL 16, published on host port 5432), backend (container issuehub-backend — built from ./backend, internal only) and frontend (container issuehub-frontend — built from ./frontend, published as 8088:80). The backend and frontend sit behind the full profile, so a bare docker compose up -d postgres starts only the database for local development. Only the frontend publishes an application port; the backend is reachable solely on the internal Docker network.
cp .env.example .env # set JWT_SECRET
docker compose --profile full up -d --build
# http://localhost:8088JWT_SECRET is mandatory: the Compose file declares it as ${JWT_SECRET:?Set JWT_SECRET (32+ chars) in .env}, so the stack refuses to start until you set a random string of at least 32 characters in .env.
Distributed Citus cluster — http://localhost:8090
deploy/docker-compose.yml runs Iskue on a Citus distributed Postgres cluster behind two clustered application servers and an nginx load balancer. It is self-contained and does not touch the root Compose file.
docker compose -f deploy/docker-compose.yml up -d --build| Service(s) | Container name(s) | Role |
|---|---|---|
| citus-coordinator, citus-worker1, citus-worker2 | issuehub-cl-coordinator, issuehub-cl-worker1, issuehub-cl-worker2 | Citus 14 / PG 17. Coordinator published on host :5432. |
| citus-register | issuehub-cl-register | One-shot: registers the workers with the coordinator (citus/register.sql). |
| flyway | issuehub-cl-flyway | One-shot: flyway/flyway:11 applies all migrations, exactly once, on the coordinator. |
| citus-distribute | issuehub-cl-distribute | One-shot: applies the sharding scheme from deploy/citus/distribute.sql after migrations. |
| backend1, backend2 | issuehub-cl-backend1, issuehub-cl-backend2 | Two Spring Boot replicas (multi-stage Maven build, JDK 21), Flyway disabled, health-checked on /actuator/health. |
| lb | issuehub-cl-lb | nginx: serves the SPA bundle and round-robins /api/* across the backends. Published as 8090:80. |
Bring-up order is enforced by depends_on conditions: the three Citus nodes become healthy, then citus-register, flyway and citus-distribute run to completion in sequence, then both backends start and pass their health checks, and finally the load balancer comes up on :8090. Sharding follows Scheme B: ticket is distributed by id; ticket-child tables (ticket_comment, ticket_label, ticket_change, ticket_watcher, attachment, git_link, csat_rating, ticket_sla and the other ticket-child tables) are distributed by ticket_id and colocated with ticket; every non-ticket table is a reference table replicated to every node.
The cluster Compose file is a development and test topology, not a hardened production deployment as shipped: it sets APP_DEV_LOGIN_ENABLED: "true" (the login page gains a passwordless Dev login button that issues a JWT for a seeded dev-admin@issuehub.local admin), uses a fixed JWT_SECRET, and configures POSTGRES_HOST_AUTH_METHOD: trust between nodes. Review and change these before exposing it beyond a trusted network.
docker compose -f deploy/docker-compose.yml down -vSystem requirements
- A Linux host with Docker Engine and the Docker Compose v2 plugin (
docker compose). All builds happen inside Docker: the backend image is a multi-stage Maven build on JDK 21, so no local Java, Maven or Node installation is required to deploy. - Free host ports:
8088and5432for the single-host shape;8090and5432for the cluster. The two shapes both claim5432, so do not run them simultaneously on one host. - Disk space for the Postgres data volume, the attachments volume and the built images; the first cluster build additionally downloads Maven dependencies and base images.
Where data lives
| Shape | Volume | Mounted at | Contents |
|---|---|---|---|
| Single host | pgdata (named) | /var/lib/postgresql/data in issuehub-postgres | The entire PostgreSQL database: tickets, users, workflows, audit log — everything except file attachments. |
| Single host | attachments (named) | /data/attachments in issuehub-backend (STORAGE_DIR) | Uploaded file attachments. |
| Cluster | attachments (named) | /data/attachments in backend1 and backend2 (shared) | Uploaded file attachments, shared by both backend replicas. |
| Cluster | (none declared for the database) | Citus node data directories | Database state lives in the Citus containers' anonymous volumes in the dev/e2e cluster file; the production variant (deploy/docker-compose.prod.yml) declares the named volumes coordinator-data, worker1-data and worker2-data for it. |
Back up both stores: dump the database from Postgres on port 5432 and copy the attachments volume. In the dev/e2e cluster file, treat database state as disposable — no named volume protects it and down -v destroys it by design. The production variant (deploy/docker-compose.prod.yml) keeps it in the coordinator-data, worker1-data and worker2-data named volumes (though down -v still destroys those too).
Key environment variables (single-host stack)
Copy .env.example to .env and adjust before docker compose --profile full up. The Compose file passes these through to the backend container; values shown are the defaults applied when a variable is unset.
| Variable | Default | Purpose |
|---|---|---|
| JWT_SECRET | (required, no default) | Signing secret for issued JWTs. Random string of at least 32 characters; the stack refuses to start without it. |
| DB_PASSWORD | issuehub | Password for the issuehub Postgres user, used by both the database and backend containers. |
| CORS_ALLOWED_ORIGINS | http://localhost:8088 | Origins allowed by the API. Set to the URL your users browse to. |
| MAIL_ENABLED | false | Optional SMTP email notification channel. When true, also set SPRING_MAIL_HOST, SPRING_MAIL_PORT, SPRING_MAIL_USERNAME, SPRING_MAIL_PASSWORD and MAIL_FROM. |
| APP_DEV_LOGIN_ENABLED | false | Passwordless dev login that seeds a dev ADMIN. Keep false in any real deployment; when false the endpoint returns 404. |
| STORAGE_DIR | /data/attachments (set by Compose) | Attachment storage path inside the backend container, backed by the attachments volume. |
| OAUTH2_GOOGLE_CLIENT_ID / OAUTH2_GOOGLE_CLIENT_SECRET | (unset) | Google OIDC single sign-on. A provider activates only when its client-id is set. Redirect URI to register: <backend>/login/oauth2/code/google. |
| OAUTH2_MICROSOFT_CLIENT_ID / OAUTH2_MICROSOFT_CLIENT_SECRET / OAUTH2_MICROSOFT_TENANT | (unset; tenant defaults to common) | Microsoft (Azure AD) OIDC single sign-on. Redirect URI to register: <backend>/login/oauth2/code/microsoft. |
| OAUTH2_SUCCESS_REDIRECT / OAUTH2_FAILURE_REDIRECT | http://localhost:8088/oauth2/callback / http://localhost:8088/login?error=sso | Where the browser is sent after the backend mints (or fails to mint) the token. Must match the SPA origin. |
On a fresh installation, the first user to register becomes the administrator. Register your admin account immediately after first start. SSO buttons appear on the login page automatically when a provider is configured; with no OAUTH2_* variables set, the application stays password-only.