Deployment
qnop is one container plus two backing services. This page covers what to run and how to put it behind a proxy; Configuration covers every setting it reads.
The image
Section titled “The image”The released image is published to two registries:
| Registry | Image | Published |
|---|---|---|
| GHCR | ghcr.io/qnophq/qnop-ce | always — this is the guaranteed one |
| Docker Hub | qnophq/qnop-ce | when the release workflow has its credentials |
Both are multi-arch (linux/amd64, linux/arm64), built on
eclipse-temurin:21 and run as the non-root user qnop. They include a headless
LibreOffice, which converts DOCX uploads and renders annotation exports — see
Document conversion.
Each release publishes three tags: the exact version, the major.minor line,
and latest. Production deployments pin the exact version. Rolling builds of
main go to GHCR only, as :main and immutable :sha-… tags — useful for
evaluation, not for production.
What it needs
Section titled “What it needs”| Component | Purpose |
|---|---|
| qnop | REST API and the embedded web UI, one port |
| PostgreSQL | all relational data |
| S3-compatible object storage | the document binaries |
The reference deploy/docker-compose.yml in the main repository wires these
together with PostgreSQL and MinIO for a single host. Any S3-compatible endpoint
works — the storage layer is an SPI implementation, see
Extending qnop.
The container serves everything on port 8080: /api/v1/** is the REST API,
/actuator/** is the operational surface, and every other path falls through to
the single-page UI embedded in the same jar. There is no separate frontend to
deploy.
One-click templates
Section titled “One-click templates”The repository also ships starting points for two self-hosting dashboards:
- Portainer — add the raw URL of
deploy/portainer-templates.jsonas an app-template source under Settings → App Templates. The template deploys the reference compose stack and prompts for the required secrets. - Coolify — paste
deploy/coolify/docker-compose.ymlas a Docker Compose service. Coolify generates the passwords and the auth secrets itself; onlyQNOP_AUTH_ENCRYPTION_SALThas to be set by hand, hex-encoded (openssl rand -hex 32).
Both target a single host, and the environment contract on Configuration stays authoritative.
The qnop container itself is stateless and needs no volume. State lives entirely in PostgreSQL and the bucket.
Behind a reverse proxy
Section titled “Behind a reverse proxy”Terminate TLS at a proxy in front of qnop. Two settings matter, and missing the second one is the common mistake:
# Trust X-Forwarded-* from the proxy, so redirects and cookies use the public URLQNOP_HTTP_FORWARD_HEADERS_STRATEGY=framework
# Tell the rate limiter which hop to trust for the client IPQNOP_AUTH_RATE_LIMIT_TRUSTED_PROXY_CIDRS=10.0.0.0/8The refresh-token cookie is issued Secure by default, so the browser will not
return it over plain HTTP. Plain HTTP is for evaluation only; a real
deployment terminates TLS.
CORS usually needs nothing: the UI is served from the same origin as the API.
QNOP_CORS_ALLOWED_ORIGINS exists for the split dev-server setup and defaults
to a localhost value that production should not keep.
Health checks
Section titled “Health checks”GET /actuator/health is anonymous and safe to point a load balancer at. The
liveness and readiness groups are enabled:
| Endpoint | Use |
|---|---|
/actuator/health | overall status |
/actuator/health/liveness | restart-if-failing probe |
/actuator/health/readiness | route-traffic-to-me probe |
Health details — and every other actuator endpoint, metrics included — require
an authenticated ADMIN. Only the bare status is public.
The reference compose file probes every 15 s with a 60 s start period, which accommodates schema migration on first boot.
First start
Section titled “First start”On first start qnop bootstraps the admin account. Unless you set
QNOP_ADMIN_PASSWORD, it generates a one-time password, writes it to the
container’s stderr and to /tmp/qnop-admin-password.txt (mode 0600) inside
the container, and flags the account so the password must be changed at first
login:
docker compose logs qnop | tail -5Setting QNOP_ADMIN_PASSWORD skips both the generated password and the forced
change — convenient for automated provisioning, and one more secret to manage.
Schema migration
Section titled “Schema migration”Liquibase migrates the schema forward automatically on startup; Hibernate never generates DDL. This means a new image can be started against an existing database and will bring it up to date on its own.
JVM tuning
Section titled “JVM tuning”The entrypoint already sets -XX:MaxRAMPercentage=75.0 and
-XX:+ExitOnOutOfMemoryError, so the container sizes its heap from the memory
limit you give it and dies rather than thrashing. Add further flags through
JDK_JAVA_OPTS.