Clone. Init.
Compose up.
Three commands: clone the repo, run init.sh to create .env with random secrets, then docker compose up -d. MIT-licensed. No usage limits. Your data stays on your network.
- License
- MIT
- Telemetry
- Zero
- Pro
- Self-hosted
- Status
- Production ready
01 — Requirements
Anything that runs Docker.
- Docker
- 24+
- Docker Compose
- v2+
- RAM
- 2 GB
- Disk
- 5 GB
02 — Quick start
Zero to running in 4 steps.
- Step01
Get the self-host folder
The selfhost/ folder holds docker-compose.yml, a commented .env.example and the helper scripts. Nothing is built locally — Docker pulls prebuilt images (amd64 + arm64). - Step02
Create .env with random secrets
init.sh copies .env.example to .env, replaces every change-me value with a random secret and prints the admin password — save it. No bash? Copy the file to .env and replace the change-me values yourself; every variable is explained in comments. - Step03
Start the stack
Docker pulls the images, creates volumes, and brings up everything in order.First run takes ~1–2 minutes while images download.
- Step04
Open the web UI and log in
Log in as admin with the password init.sh printed. The login comes from ADMIN_EMAIL / ADMIN_PASSWORD in .env and is re-applied on every restart. To change it, edit .env and run docker compose up -d again.
Or let an AI coding agent do it
Paste this prompt into Claude Code, Cursor or a similar agent on the machine that should run the stack.
03 — What's inside
Five services. One compose file.
RustFS — default
Bundled S3 storage (Apache-2.0). Starts with plain docker compose up -d.
MinIO — keep v1 data
Uncomment in .env to run the bundled MinIO instead.
COMPOSE_FILE=docker-compose.yml:docker-compose.minio.ymlExternal S3
AWS S3, Cloudflare R2 or your own server — no bundled storage. Set STORAGE_* too.
COMPOSE_FILE=docker-compose.yml:docker-compose.external-s3.ymlApp
Web UI + .NET API in one container (port 8080)
PostgreSQL
Application database
RustFS (or MinIO)
S3 storage — one bucket for templates, reports & fonts
Font Syncer
Mirrors fonts into the render volume
Gotenberg
DOCX → PDF conversion (+ Excel previews)
04 — Going to production
Three things before going live.
Change these secrets
The defaults work on localhost but must be rotated before you expose the instance.
| Variable | Purpose |
|---|---|
| ADMIN_EMAIL | Admin login (re-applied on every restart) |
| ADMIN_PASSWORD | Admin password (re-applied on every restart) |
| DB_PASSWORD | PostgreSQL password |
| STORAGE_ACCESS_KEY | S3 storage access key (RustFS/MinIO root user) |
| STORAGE_SECRET_KEY | S3 storage secret key |
| JWT_KEY | JWT signing key — at least 32 characters |
| ENCRYPTION_KEY | AES key — exactly 32 characters |
| ENCRYPTION_IV | AES IV — exactly 16 characters |
Generate strong secrets
The admin login comes from ADMIN_EMAIL / ADMIN_PASSWORD and is re-applied on every restart, so a password changed in the app is reset on the next start. To rotate it, change both in .env and run docker compose up -d. Leave them blank after first login if you prefer to manage the password in the app.
Reach it from another device
When opening the UI from another device, localhost points to that device, not the server. Set PUBLIC_URL in .env to the address people open — the web UI and the API share it:
VPN / LAN
Domain + HTTPS
Use external PostgreSQL or S3
Already running your own database or object store? Point the stack at it and disable the bundled service.
Your own PostgreSQL
Then disable the postgres service in docker-compose.yml.
S3-compatible store
The external-s3 override skips the bundled RustFS (keep COMPOSE_PATH_SEPARATOR=: from .env.example). Works with AWS S3, Cloudflare R2 or your own MinIO/RustFS. Everything goes in the one bucket under template/, generated-report/ and fonts/. Storage needs no public address — downloads go through the API.
05 — Activate Pro
Already have a license?
Drop your license file next to docker-compose.yml and recreate the app container. (Feature comparison is at the bottom of this page.)
With a license file
- 01Place your license file (named like license-you_at_example_com.json) next to docker-compose.yml — keep its original name.
- 02Under the app service, uncomment Pro__LicenseFile and the license line in volumes:, with your file name on the left (see diff below).
- 03Run docker compose up -d — it recreates app with the new settings; docker compose restart would not apply them.
License is validated offline — no call to Qorstack.
Check active flags: curl http://localhost:8080/api/features
06 — Day-2 ops
Update, stop, wipe.
Update
Stop (keep data)
Stop + wipe data
Upgrading from v1 (buckets templates / reports / fonts)? Before starting v2, run bash migrate-storage.sh keep-minio (stay on MinIO) or to-rustfs (move to RustFS) once. It copies the old buckets into one bucket (default qorstack-report) with rclone and leaves them untouched. Your v1 MINIO_* settings keep working. v2 also accepts render requests as keyed arrays only — see the changelog on GitHub.
07 — Troubleshooting
Something not working?
Check the logs
| Symptom | Check |
|---|---|
| Login fails on first run | ADMIN_EMAIL, ADMIN_PASSWORD, docker compose logs app |
| Page loads but sign-in or data fails | docker compose logs app, /api/health |
| Previews / downloads fail | PUBLIC_URL reachable from the browser, docker compose logs rustfs (or minio) |
| PDF conversion fails | gotenberg logs |
| Custom fonts missing | font-syncer logs |
Free vs Pro
Run the document API on your own infrastructure. Pro adds PDF security, async renders with webhooks, ZIP output, project export & import and more template versions — a one-time licence, validated offline.