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 legacy docker-compose).
  • git to 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) and 5432 (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

Clone the repository and enter it — use the repository URL supplied with your licence
git clone <repository-url> iskue
cd iskue

2. 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 .env

Set 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:

Generate a random 64-character secret and write it into .env
sed -i "s|^JWT_SECRET=.*|JWT_SECRET=$(openssl rand -base64 48 | tr -d '\n')|" .env

Treat 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).

VariableDefaultPurpose
JWT_SECRETnone — requiredSigns authentication tokens. Compose aborts with Set JWT_SECRET (32+ chars) in .env when missing.
DB_PASSWORDissuehubPostgreSQL password. Applied on first initialisation of the database volume.
CORS_ALLOWED_ORIGINShttp://localhost:8088Browser origin(s) allowed to call the API. Set to your public https:// origin behind a proxy.
MAIL_ENABLEDfalseOptional 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_ENABLEDfalsePasswordless 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_IDunsetOptional 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

Build and start PostgreSQL, backend and frontend
docker compose --profile full up -d --build

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

Verify: three containers running, health endpoint returns {"status":"UP"}
docker compose --profile full ps
curl -fsS http://localhost:8088/actuator/health

The 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:

Enable the required Apache modules
sudo a2enmod ssl proxy proxy_http headers
sudo systemctl reload apache2
Adapted from deploy/apache-iskue.conf.example — replace iskue.example.com and 203.0.113.10 with your domain and IP
<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-Proto is mandatory. The backend runs with forward-headers-strategy: framework and uses this header to build correct https:// URLs; without it, redirects and generated links fall back to http.
  • Long proxy timeouts are mandatory. Live board and ticket updates use Server-Sent Events, which hold connections open; ProxyTimeout 3600 prevents Apache cutting them off. (Inside the stack, the frontend's nginx already sets proxy_buffering off and proxy_read_timeout 1h for /api/.)
  • The Require ip allow-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:

Recreates only the containers whose environment changed
docker compose --profile full up -d

Where 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:

VolumeMounted atContents
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.
Minimal backup of both volumes
# 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

Rebuild images and recreate containers; volumes are untouched. Database migrations run automatically on backend start and are one-way — take a backup first.
git pull
docker compose --profile full up -d --build