Qorstack ReportQorstack Report
Documentation
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

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 5s...';
73        while true; do sleep 5; 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:latest
109    container_name: qorstack-report-api
110    restart: unless-stopped
111    ports:
112      - "8080:8080"
113    environment:
114      - ASPNETCORE_ENVIRONMENT=Production
115      - License=${LICENSE:-free}
116      - Admin__Email=${ADMIN_EMAIL}
117      - Admin__Password=${ADMIN_PASSWORD}
118      - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-qorstack-postgres};Port=${DB_PORT:-5432};Database=${DB_NAME:-qorstack_report};Username=${DB_USER:-postgres};Password=${DB_PASSWORD}
119      - Database__MigrationPaths__0=database
120      - Minio__Endpoint=${MINIO_ENDPOINT:-qorstack-minio:9000}
121      - Minio__PublicEndpoint=${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}
122      - Minio__AccessKey=${MINIO_ACCESS_KEY}
123      - Minio__SecretKey=${MINIO_SECRET_KEY}
124      - Minio__UseSsl=${MINIO_USE_SSL:-false}
125      - Minio__TemplateBucket=templates
126      - Minio__ReportBucket=reports
127      - Gotenberg__BaseUrl=http://qorstack-gotenberg:3000
128      - Jwt__Key=${JWT_KEY}
129      - Jwt__Issuer=qorstack-report
130      - Jwt__Audience=qorstack-report
131      - Encryption__Key=${ENCRYPTION_KEY}
132      - Encryption__IV=${ENCRYPTION_IV}
133      - Cors__AllowedOriginsString=${CORS_ORIGINS:-http://localhost:3000}
134      # --- Pro license (optional) — uncomment both this line and the volumes block below ---
135      # - Pro__LicenseFile=/app/license.json
136    # volumes:
137    #   - ./license.json:/app/license.json:ro
138    depends_on:
139      postgres:
140        condition: service_healthy
141      minio:
142        condition: service_healthy
143    networks:
144      - qorstack-network
145
146  frontend:
147    image: qorstack/report-web:latest
148    container_name: qorstack-report-web
149    restart: unless-stopped
150    ports:
151      - "${FRONTEND_PORT:-3000}:3000"
152    environment:
153      # URL that your browser uses to reach the API.
154      # VPN/LAN: http://<vpn-ip>:8080
155      # Local:   http://localhost:8080
156      - NEXT_PUBLIC_SERVICE=${API_URL:-http://localhost:8080}
157    depends_on:
158      - backend
159    networks:
160      - qorstack-network
161
162volumes:
163  postgres_data:
164  minio_data:
165  fonts_cache:
166
167networks:
168  qorstack-network:
169    driver: bridge
170
.env.example
Download
1ADMIN_EMAIL=admin
2ADMIN_PASSWORD=admin
3
4LICENSE=free
5
6DB_HOST=qorstack-postgres
7DB_PORT=5432
8DB_NAME=qorstack_report
9DB_USER=postgres
10DB_PASSWORD=change-me-db-password
11
12MINIO_ENDPOINT=qorstack-minio:9000
13MINIO_PUBLIC_ENDPOINT=http://localhost:9000
14MINIO_USE_SSL=false
15MINIO_ACCESS_KEY=minioadmin
16MINIO_SECRET_KEY=change-me-minio-secret
17
18JWT_KEY=change-me-jwt-signing-key-at-least-32-chars
19ENCRYPTION_KEY=change-me-encryption-key-32char!
20ENCRYPTION_IV=change-me-iv-16!
21
22API_URL=http://localhost:8080
23CORS_ORIGINS=http://localhost:3000
24FRONTEND_PORT=3000
25
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