Install Iskue on a single host
This page installs the full Iskue stack on one Linux host with Docker Compose: PostgreSQL 16, the Spring Boot backend, and the nginx-served frontend. The frontend container publishes the whole application on port 8088 and proxies /api/ (including Server-Sent Events) to the backend internally, so the host only ever talks to one port. A reverse proxy with TLS in front of it makes the installation production-ready.
Prerequisites
- A Linux host with Docker Engine and the Docker Compose plugin (
docker compose, not the legacydocker-compose). gitto fetch the source.- For public access: a DNS name pointing at the host, a TLS certificate for it, and Apache (or an equivalent proxy) on the host.
- Free ports:
8088(application) and5432(PostgreSQL — see the warning below).
docker-compose.yml publishes PostgreSQL on the host with "5432:5432". On an internet-facing host, block port 5432 in your firewall — nothing outside the Compose network needs it.
1. Get the source
git clone <repository-url> iskue
cd iskue2. Configure the environment
All runtime configuration is read from a .env file next to docker-compose.yml. Start from the committed template:
cp .env.example .envSet JWT_SECRET before anything else. It signs every session token and has no usable default: the Compose file declares it as ${JWT_SECRET:?Set JWT_SECRET (32+ chars) in .env}, so docker compose refuses to start and prints that message if the variable is unset. Generate a strong value and put it in .env:
sed -i "s|^JWT_SECRET=.*|JWT_SECRET=$(openssl rand -base64 48 | tr -d '\n')|" .envTreat JWT_SECRET like a password: never commit .env, and note that changing the secret later invalidates every active login session.
Also review DB_PASSWORD. It defaults to issuehub and is applied to PostgreSQL when the database volume is first initialised — set a strong value now, before the first start, rather than after. If the site will be served over TLS on a public domain, set CORS_ALLOWED_ORIGINS to that origin (you can do this later, in the reverse-proxy step).
| Variable | Default | Purpose |
|---|---|---|
| JWT_SECRET | none — required | Signs authentication tokens. Compose aborts with Set JWT_SECRET (32+ chars) in .env when missing. |
| DB_PASSWORD | issuehub | PostgreSQL password. Applied on first initialisation of the database volume. |
| CORS_ALLOWED_ORIGINS | http://localhost:8088 | Browser origin(s) allowed to call the API. Set to your public https:// origin behind a proxy. |
| MAIL_ENABLED | false | Optional email notification channel. When true, also set SPRING_MAIL_HOST, SPRING_MAIL_PORT, SPRING_MAIL_USERNAME, SPRING_MAIL_PASSWORD and MAIL_FROM (see .env.example). |
| APP_DEV_LOGIN_ENABLED | false | Passwordless development login. Must stay false — see below. |
| STORAGE_DIR | /data/attachments (fixed in Compose) | Attachment storage path inside the backend container, backed by the attachments volume. |
| OAUTH2_GOOGLE_CLIENT_ID / OAUTH2_MICROSOFT_CLIENT_ID | unset | Optional single sign-on. A provider activates only when its client ID is set; leave unset for password-only login. |
Email and single sign-on are optional and can be added at any time by editing .env and re-running the start command below. Nothing in this page depends on them.
3. Start the stack
docker compose --profile full up -d --buildThe --profile full flag matters: the backend and frontend services carry profiles: ["full"], so a plain docker compose up -d postgres starts only the database (the development default). The first run builds both images and applies all database migrations; allow a few minutes.
docker compose --profile full ps
curl -fsS http://localhost:8088/actuator/healthThe containers are named issuehub-postgres, issuehub-backend and issuehub-frontend. If the health check fails, inspect the backend log with docker compose --profile full logs -f backend.
4. Register the first administrator — immediately
Iskue allows open self-service registration, and the first account registered becomes the global ADMIN. Do not expose the application publicly before that account exists — anyone who registers first owns the instance.
Once the admin exists, close sign-up entirely: add APP_REGISTRATION_ENABLED: "false" to the backend service's environment block (the shipped docker-compose.yml does not pass it through from .env) and re-run docker compose --profile full up -d. The sign-up link disappears and POST /api/v1/auth/register returns 403. With it off there is currently no other way to add a user — no invite or password-reset flow, and SSO provisions accounts only when an OIDC provider is configured — so re-enable it briefly when onboarding someone new.
Open http://localhost:8088 (from the host, or through an SSH tunnel: ssh -L 8088:localhost:8088 user@host), choose register, and create your admin account. Only after this account exists should you open the site to the world in the reverse-proxy step.
Keep dev login disabled
The backend has a passwordless development login (APP_DEV_LOGIN_ENABLED) that seeds a dev ADMIN account and issues tokens without a password. It defaults to false and the endpoint returns 404 when disabled. The Compose file carries the comment verbatim: *"Passwordless dev login (seeds a dev ADMIN). Keep false in any real deployment."*
Never set APP_DEV_LOGIN_ENABLED=true on this installation. It exists only for throwaway demo environments and grants admin access to anyone who can reach the login page.
Reverse proxy with TLS (Apache)
The repository ships a working reference vhost at deploy/apache-iskue.conf.example. It was written for a different topology (a clustered deployment whose load balancer binds 127.0.0.1:8092 and listens publicly on 8443), so adapt two things for this single-host stack: proxy to 127.0.0.1:8088, and use your own domain, certificate paths and port. Everything else — ProxyPreserveHost, the explicit X-Forwarded-Proto, the SSE timeout, the temporary IP allow-list — carries over unchanged:
sudo a2enmod ssl proxy proxy_http headers
sudo systemctl reload apache2<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName iskue.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/iskue.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/iskue.example.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
# Pass the real hostname through to the stack.
ProxyPreserveHost On
# Apache sets X-Forwarded-For/Host itself; Proto has to be explicit, and the
# backend relies on it (forward-headers-strategy: framework) to build correct
# https URLs for OAuth redirects and portal links.
RequestHeader set X-Forwarded-Proto "https"
# Server-Sent Events (boards, tickets) hold connections open for a long time.
ProxyTimeout 3600
ProxyPass / http://127.0.0.1:8088/
ProxyPassReverse / http://127.0.0.1:8088/
# Remove this block once the first (admin) account has been registered.
<Location />
Require ip 127.0.0.1
Require ip 203.0.113.10
</Location>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
ErrorLog ${APACHE_LOG_DIR}/iskue-error.log
CustomLog ${APACHE_LOG_DIR}/iskue-access.log combined
</VirtualHost>
</IfModule>X-Forwarded-Protois mandatory. The backend runs withforward-headers-strategy: frameworkand uses this header to build correcthttps://URLs; without it, redirects and generated links fall back tohttp.- Long proxy timeouts are mandatory. Live board and ticket updates use Server-Sent Events, which hold connections open;
ProxyTimeout 3600prevents Apache cutting them off. (Inside the stack, the frontend's nginx already setsproxy_buffering offandproxy_read_timeout 1hfor/api/.) - The
Require ipallow-list is the safety net for the first-user-becomes-admin window. Keep it in place until your admin account exists, then remove the<Location />block and reload Apache.
Finally, point the application at its public origin. In .env, set CORS_ALLOWED_ORIGINS=https://iskue.example.com, and if you enable single sign-on later, also set OAUTH2_SUCCESS_REDIRECT and OAUTH2_FAILURE_REDIRECT to the same origin (their defaults point at http://localhost:8088). Then apply the change:
docker compose --profile full up -dWhere your data lives
All state is kept in two named Docker volumes declared in docker-compose.yml, so containers can be rebuilt and upgraded freely without data loss:
| Volume | Mounted at | Contents |
|---|---|---|
| pgdata | /var/lib/postgresql/data (issuehub-postgres) | The PostgreSQL database: tickets, users, projects, configuration. |
| attachments | /data/attachments (issuehub-backend) | Uploaded file attachments. The backend reads the path from STORAGE_DIR, which Compose fixes to /data/attachments. |
# Database dump
docker compose exec postgres pg_dump -U issuehub issuehub > iskue-db-$(date +%F).sql
# Attachment files
docker cp issuehub-backend:/data/attachments ./iskue-attachments-$(date +%F)Uploads are limited to 10 MB per file (12 MB per request) by the backend's multipart configuration.
Upgrading
git pull
docker compose --profile full up -d --build