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

ComponentTechnologyRole
BackendJava 21, Spring Boot 3.3, Spring Data JPA, FlywayModular-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.
FrontendReact + TypeScript, built with ViteSingle-page application. Shipped as a static bundle served by the frontend (single host) or load-balancer (cluster) container.
Database — single hostPostgreSQL 16 (postgres:16-alpine)One Postgres instance; Flyway migrations are applied by the backend at start-up.
Database — clusterCitus 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.

Full single-host stack
cp .env.example .env                      # set JWT_SECRET
docker compose --profile full up -d --build
# http://localhost:8088

JWT_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.

Bring up the cluster; then open http://localhost:8090. The first build is long (Maven build + image pulls).
docker compose -f deploy/docker-compose.yml up -d --build
Service(s)Container name(s)Role
citus-coordinator, citus-worker1, citus-worker2issuehub-cl-coordinator, issuehub-cl-worker1, issuehub-cl-worker2Citus 14 / PG 17. Coordinator published on host :5432.
citus-registerissuehub-cl-registerOne-shot: registers the workers with the coordinator (citus/register.sql).
flywayissuehub-cl-flywayOne-shot: flyway/flyway:11 applies all migrations, exactly once, on the coordinator.
citus-distributeissuehub-cl-distributeOne-shot: applies the sharding scheme from deploy/citus/distribute.sql after migrations.
backend1, backend2issuehub-cl-backend1, issuehub-cl-backend2Two Spring Boot replicas (multi-stage Maven build, JDK 21), Flyway disabled, health-checked on /actuator/health.
lbissuehub-cl-lbnginx: 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.

Reset the cluster completely, including all database state
docker compose -f deploy/docker-compose.yml down -v

System 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: 8088 and 5432 for the single-host shape; 8090 and 5432 for the cluster. The two shapes both claim 5432, 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

ShapeVolumeMounted atContents
Single hostpgdata (named)/var/lib/postgresql/data in issuehub-postgresThe entire PostgreSQL database: tickets, users, workflows, audit log — everything except file attachments.
Single hostattachments (named)/data/attachments in issuehub-backend (STORAGE_DIR)Uploaded file attachments.
Clusterattachments (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 directoriesDatabase 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.

VariableDefaultPurpose
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_PASSWORDissuehubPassword for the issuehub Postgres user, used by both the database and backend containers.
CORS_ALLOWED_ORIGINShttp://localhost:8088Origins allowed by the API. Set to the URL your users browse to.
MAIL_ENABLEDfalseOptional 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_ENABLEDfalsePasswordless 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_REDIRECThttp://localhost:8088/oauth2/callback / http://localhost:8088/login?error=ssoWhere 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.