Qorstack ReportQorstack Report
Documentation
Self-Host
GitHub
Self-Host Guide

Two files.
One command.

Download docker-compose.yml and .env, run docker compose up, done.MIT-licensed. No usage limits. Your data stays on your network.

Start in 4 stepsView on GitHub
MIT LicensedZero TelemetrySelf-Hosted Pro AvailableProduction Ready

Requirements

Anything that can run Docker.

Docker

24+

Docker Compose

v2+

RAM

2 GB

Disk

5 GB

Quick Start

In a hurry? The whole thing:

git clone https://github.com/qorstack/qorstack-report.git
cd qorstack-report/selfhost
cp .env.example .env
docker compose up -d

Or let your AI agent do it — paste this prompt:

Deploy Qorstack Report on this machine with Docker and confirm it's running. Use the prebuilt images — do NOT build images, edit the Dockerfile, or change project files (the only allowed edit is remapping a clashing port, below). If anything else is blocked, STOP and report it; don't work around it.

  git clone https://github.com/qorstack/qorstack-report.git
  cd qorstack-report/selfhost
  cp .env.example .env

Before starting, make sure these host ports are free: 3000 (web), 8080 (API), 9000/9001 (MinIO), 5432 (Postgres). If one is in use, remap the host side in docker-compose.yml first (e.g. "8081:8080") and say what you changed — for the web UI set FRONTEND_PORT in .env; if you move the API port, also set API_URL in .env so the web UI can reach it.

  docker compose up -d   # first run pulls images (~2 min); multi-arch, Apple Silicon OK

Then verify and report — that's the whole job: run "bash selfhost/verify.sh" (expect "All checks passed") and report the web UI URL, admin login (admin / admin), the verify output, and any ports you remapped.

Optional, in the WEB UI (not via scripts): upload examples/invoice/invoice.docx, create an API key (Settings -> API Keys), and render examples/invoice/data.json (see examples/invoice/README.md).

From zero to running in 4 steps.

01

Download the two files

That is the entire deploy package. No clone, no build step. Skim each file below, then grab the download from the top-right.
docker-compose.yml
Download
1services:
2  postgres:
3    image: postgres:16.13-alpine3.23
4    container_name: qorstack-postgres
5    restart: unless-stopped
6    environment:
7      - POSTGRES_DB=qorstack_report
8      - POSTGRES_USER=postgres
9      - POSTGRES_PASSWORD=${DB_PASSWORD}
10    ports:
11      - "5432:5432"
12    volumes:
13      - postgres_data:/var/lib/postgresql/data
14    networks:
15      - qorstack-network
16    healthcheck:
17      test: ["CMD-SHELL", "pg_isready -U postgres"]
18      interval: 10s
19      timeout: 5s
20      retries: 5
21
22  minio:
23    image: minio/minio:RELEASE.2025-09-07T16-13-09Z
24    container_name: qorstack-minio
25    restart: unless-stopped
26    command: server /data --console-address ":9001"
27    environment:
28      - MINIO_ROOT_USER=${MINIO_ACCESS_KEY}
29      - MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY}
30    ports:
31      - "9000:9000"
32      - "9001:9001"
33    volumes:
34      - minio_data:/data
35    networks:
36      - qorstack-network
37    healthcheck:
38      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
39      interval: 10s
40      timeout: 5s
41      retries: 5
42
43  # Mirrors fonts from MinIO into the shared volume used by Gotenberg.
44  font-syncer:
45    image: minio/mc:RELEASE.2025-08-13T08-35-41Z
46    container_name: qorstack-font-syncer
47    restart: unless-stopped
48    entrypoint: >
49      /bin/sh -c "
50        until mc alias set minio http://${MINIO_ENDPOINT:-qorstack-minio:9000} $$MINIO_ACCESS_KEY $$MINIO_SECRET_KEY 2>/dev/null; do
51          echo '[font-syncer] Waiting for MinIO...'; sleep 3;
52        done;
53        mc mb --ignore-existing minio/fonts;
54        sync_fonts() {
55          for ext in ttf otf woff woff2; do
56            mc find minio/fonts --name \"*.$$ext\" 2>/dev/null | while read -r obj; do
57              fname=$$(basename $$obj);
58              test ! -f /fonts-cache/$$fname && mc cp $$obj /fonts-cache/$$fname 2>/dev/null && echo \"[font-syncer] + $$fname\";
59            done;
60          done;
61          for ext in ttf otf woff woff2; do
62            for f in /fonts-cache/*.$$ext; do
63              test -f $$f || continue;
64              fname=$$(basename $$f);
65              in_usr=$$(mc find minio/fonts --name $$fname 2>/dev/null);
66              test -z \"$$in_usr\" && rm -f $$f && echo \"[font-syncer] - $$fname\";
67            done;
68          done;
69        };
70        echo '[font-syncer] Initial sync...';
71        sync_fonts;
72        echo '[font-syncer] Polling every 30s...';
73        while true; do sleep 30; sync_fonts; done
74      "
75    environment:
76      - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
77      - MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
78    volumes:
79      - fonts_cache:/fonts-cache
80    depends_on:
81      minio:
82        condition: service_healthy
83    networks:
84      - qorstack-network
85
86  gotenberg:
87    image: qorstack/gotenberg:8
88    container_name: qorstack-gotenberg
89    restart: unless-stopped
90    command:
91      - "gotenberg"
92      - "--api-port=3000"
93      - "--log-level=warn"
94      - "--libreoffice-auto-start=true"
95      - "--libreoffice-restart-after=100"
96      - "--api-disable-health-check-logging=true"
97    volumes:
98      - fonts_cache:/usr/local/share/fonts/custom
99    networks:
100      - qorstack-network
101    healthcheck:
102      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
103      interval: 30s
104      timeout: 10s
105      retries: 3
106
107  backend:
108    image: qorstack/report-api:${REPORT_API_TAG:-latest}
109    container_name: qorstack-report-api
110    restart: unless-stopped
111    ports:
112      - "8080:8080"
113    environment:
114      - ASPNETCORE_ENVIRONMENT=Production
115      - Admin__Email=${ADMIN_EMAIL}
116      - Admin__Password=${ADMIN_PASSWORD}
117      - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-qorstack-postgres};Port=${DB_PORT:-5432};Database=${DB_NAME:-qorstack_report};Username=${DB_USER:-postgres};Password=${DB_PASSWORD}
118      - Database__MigrationPaths__0=database
119      - Minio__Endpoint=${MINIO_ENDPOINT:-qorstack-minio:9000}
120      - Minio__PublicEndpoint=${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}
121      - Minio__AccessKey=${MINIO_ACCESS_KEY}
122      - Minio__SecretKey=${MINIO_SECRET_KEY}
123      - Minio__UseSsl=${MINIO_USE_SSL:-false}
124      - Minio__TemplateBucket=templates
125      - Minio__ReportBucket=reports
126      - Gotenberg__BaseUrl=http://qorstack-gotenberg:3000
127      - Jwt__Key=${JWT_KEY}
128      - Jwt__Issuer=qorstack-report
129      - Jwt__Audience=qorstack-report
130      - Encryption__Key=${ENCRYPTION_KEY}
131      - Encryption__IV=${ENCRYPTION_IV}
132      - Cors__AllowedOriginsString=${CORS_ORIGINS:-http://localhost:3000}
133      # --- Pro license (optional) ---
134      # Have a Pro license? You received a file named like:  license-you_at_example_com.json
135      # 1) Put that file next to this docker-compose.yml (keep its original name).
136      # 2) Uncomment the line below AND the volumes block, then run: docker compose up -d
137      #    left  = your downloaded file   |   right = /app/license.json (fixed path the backend reads — don't change)
138      # - Pro__LicenseFile=/app/license.json
139    # volumes:
140    #   - ./license-you_at_example_com.json:/app/license.json:ro
141    depends_on:
142      postgres:
143        condition: service_healthy
144      minio:
145        condition: service_healthy
146    networks:
147      - qorstack-network
148
149  frontend:
150    image: qorstack/report-web:${REPORT_WEB_TAG:-latest}
151    container_name: qorstack-report-web
152    restart: unless-stopped
153    ports:
154      - "${FRONTEND_PORT:-3000}:3000"
155    environment:
156      # URL that your browser uses to reach the API.
157      # VPN/LAN: http://<vpn-ip>:8080
158      # Local:   http://localhost:8080
159      - NEXT_PUBLIC_SERVICE=${API_URL:-http://localhost:8080}
160    depends_on:
161      backend:
162        condition: service_healthy
163    networks:
164      - qorstack-network
165
166volumes:
167  postgres_data:
168  minio_data:
169  fonts_cache:
170
171networks:
172  qorstack-network:
173    driver: bridge
174
.env.example
Download
1ADMIN_EMAIL=admin
2ADMIN_PASSWORD=admin
3
4DB_HOST=qorstack-postgres
5DB_PORT=5432
6DB_NAME=qorstack_report
7DB_USER=postgres
8DB_PASSWORD=change-me-db-password
9
10MINIO_ENDPOINT=qorstack-minio:9000
11MINIO_PUBLIC_ENDPOINT=http://localhost:9000
12MINIO_USE_SSL=false
13MINIO_ACCESS_KEY=minioadmin
14MINIO_SECRET_KEY=change-me-minio-secret
15
16JWT_KEY=change-me-jwt-signing-key-at-least-32-chars
17ENCRYPTION_KEY=change-me-encryption-key-32char!
18ENCRYPTION_IV=change-me-iv-16!
19
20API_URL=http://localhost:8080
21CORS_ORIGINS=http://localhost:3000
22FRONTEND_PORT=3000
23
24# Image versions — leave unset to track :latest, or pin for reproducible deploys.
25# REPORT_API_TAG=latest
26# REPORT_WEB_TAG=latest
27
02

Rename .env.example → .env

Place both files in the same folder, then rename. The defaults are safe for localhost — you can change secrets later before going to production.
mv .env.example .env
03

Start the stack

Docker pulls the images, creates volumes, and brings up everything in order.
docker compose up -d

First run takes ~1–2 minutes while images download.

04

Open the web UI and log in

Sign in with admin / admin. Then change the password from Settings.

Web UI

http://localhost:3000

Login: admin / admin

API

http://localhost:8080

Login: —

MinIO console

http://localhost:9001

Login: from .env

What's inside

Six services. One compose file.

Frontend

Next.js web UI

Backend

.NET API + render engine

PostgreSQL

Application database

MinIO

Template & report storage

Font Syncer

Mirrors fonts into the render volume

Gotenberg

DOCX/XLSX → PDF conversion

Going to production

Three things to handle before going live.

A

Change these secrets

The defaults work on localhost but must be rotated before you expose the instance.

VariablePurpose
ADMIN_EMAILInitial admin login
ADMIN_PASSWORDInitial admin password
DB_PASSWORDPostgreSQL password
MINIO_ACCESS_KEYMinIO root/access key
MINIO_SECRET_KEYMinIO root/secret key
JWT_KEYJWT signing key — at least 32 characters
ENCRYPTION_KEYAES key — exactly 32 characters
ENCRYPTION_IVAES IV — exactly 16 characters

Generate strong secrets

1openssl rand -hex 32   # JWT_KEY
2openssl rand -hex 16   # ENCRYPTION_KEY  (32 hex chars)
3openssl rand -hex 8    # ENCRYPTION_IV   (16 hex chars)

The admin user is re-applied on every restart — blank out ADMIN_PASSWORD after first login so the seeder skips on subsequent boots.

B

Reach it from another device

When opening the UI from another device, localhost points to that device, not the server. Update .env with browser-reachable URLs:

VPN / LAN

API_URL=http://192.168.1.10:8080
CORS_ORIGINS=http://192.168.1.10:3000
MINIO_PUBLIC_ENDPOINT=http://192.168.1.10:9000

Domain + HTTPS

API_URL=https://api.your-domain.com
CORS_ORIGINS=https://your-domain.com
MINIO_PUBLIC_ENDPOINT=https://minio.your-domain.com
C

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

DB_HOST=your-postgres-host
DB_PORT=5432
DB_NAME=qorstack_report
DB_USER=postgres
DB_PASSWORD=your-db-password

Then disable the postgres service in docker-compose.yml.

S3-compatible store

MINIO_ENDPOINT=your-minio-host:9000
MINIO_PUBLIC_ENDPOINT=https://minio.your-domain.com
MINIO_USE_SSL=false
MINIO_ACCESS_KEY=...
MINIO_SECRET_KEY=...

Then disable the minio service.

Activate Pro

Already have a license?

Drop your license file next to docker-compose.yml and restart the backend. (Feature comparison is at the bottom of this page.)

With a license file

  1. Place license.json next to docker-compose.yml.
  2. Uncomment Pro__LicenseFile env and the volumes: block under backend (see diff below).
  3. Run docker compose restart backend.
BeforeFree — license lines commented out
backend:
  environment:
    # - Pro__LicenseFile=/app/license.json
  # volumes:
  #   - ./license.json:/app/license.json:ro
AfterPro — both blocks uncommented
backend:
  environment:
    - Pro__LicenseFile=/app/license.json
  volumes:
    - ./license.json:/app/license.json:ro

License is validated offline — no call to Qorstack.

Check active flags: curl http://localhost:8080/features

Day-2 ops

Update, stop, wipe.

Update

docker compose pull
docker compose up -d

Stop (keep data)

docker compose down

Stop + wipe data

docker compose down -v

Troubleshooting

Something not working?

Check the logs

docker compose logs backend
docker compose logs frontend
docker compose logs gotenberg
docker compose logs font-syncer
SymptomCheck
Login fails on first runADMIN_EMAIL, ADMIN_PASSWORD, backend logs
Frontend cannot call APIAPI_URL, CORS_ORIGINS
Previews / downloads failMINIO_PUBLIC_ENDPOINT reachable from the browser
PDF conversion failsgotenberg logs
Custom fonts missingfont-syncer logs
View on GitHubReport an issue

Free vs Pro

Run the document API on your own infrastructure. Pro adds more template versions, PDF security controls, and team collaboration.

Feature
FreeMIT
ProLicense
PDF, DOCX & Excel generation
REST API access
Unlimited projects
Google Fonts + custom font upload
QR code & barcode support
Auto-detected template variables
Template versions per template
1
10
PDF Password Protection
PDF Watermark
Project Members
License
MIT
Commercial
Qorstack ReportQorstack Report

© 2026 Qorstack Report.
Built for the developer era.

Product

Company

  • GitHub

Legal

© 2026 Qorstack Report. All rights reserved.

Made withfor developers