Self-host (technical)
Run Next Wiki with Docker — environment variables, database, storage and AI.
Self-host with Docker
Next Wiki runs as a single image (the frontend is served by the .NET backend
itself, on the same port). You only need Docker and an accessible PostgreSQL.
Configuration is done through standardized environment variables (SECRET_KEY,
DATABASE_URL, URL, REDIS_URL, SMTP_*…).
Prerequisites
- Docker 24+ (and Docker Compose v2)
- PostgreSQL 14+ — can be an existing database; Next Wiki uses its own database
(e.g.
next_wiki) and never drops it (migrations useIF NOT EXISTS). - An OpenAI-compatible AI endpoint (optional, but recommended): OpenAI, a litellm gateway or local Ollama.
Hardware requirements
The image is lean and the app reuses a “warm” connection pool with the database — it runs comfortably on small machines.
| Scenario | CPU | RAM | Disk |
|---|---|---|---|
| Minimum (small team) | 1 vCPU | 1 GB | 10 GB |
| Recommended (app + Postgres on the same VM) | 2 vCPU | 2 GB | 20 GB+ |
| Large team / many attachments | 2–4 vCPU | 4 GB | as the content grows |
Keep backend and PostgreSQL in the same region/network. Each query pays the network latency to the database; with them together, queries stay in a few milliseconds.
Environment variables
| Variable | Required | Description |
|---|---|---|
SECRET_KEY | yes | Secret that signs the session JWT. ≥ 32 characters. |
DATABASE_URL | yes | Postgres connection: postgresql://user:password@host:5432/next_wiki. |
URL | yes | Public URL of the wiki (used in invite/email links). |
PORT | no | Port the app listens on. Default 8080. |
BOOTSTRAP_ADMIN_EMAIL | no | Email that becomes the first admin on first boot (via magic link). |
FILE_STORAGE_LOCAL_ROOT_DIR | no | Local storage root (markdown + icons). Default /app/data. |
REDIS_URL | no | Distributed cache. Empty = in-memory cache. E.g. redis://host:6379. |
SMTP_HOST … SMTP_SECURE | no | Email sending (magic link, invites). Without SMTP_HOST, email is a no-op. |
OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET | no | SSO via OIDC (Keycloak, etc). Empty = magic link only. |
OTEL_EXPORTER_OTLP_ENDPOINT | no | OpenTelemetry observability. Empty = disabled. |
The AI uses no environment variable — it’s configured inside the app, per workspace (see Artificial intelligence below).
docker-compose.yml
services:
app:
image: nexttag/next-wiki:latest
environment:
SECRET_KEY: "replace-with-a-secret-longer-than-32-chars"
DATABASE_URL: "postgresql://admin:password@host.docker.internal:5432/next_wiki"
URL: "https://wiki.yourcompany.com"
BOOTSTRAP_ADMIN_EMAIL: "you@yourcompany.com"
ports:
- "5210:8080" # host:container
volumes:
- nextwiki_data:/app/data # document markdown + icons
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
nextwiki_data:
Bring it up with:
docker compose up -d
Database (PostgreSQL)
Next Wiki stores the metadata (users, permissions, tree, versions) in
PostgreSQL. You point to it with the DATABASE_URL variable:
postgresql://user:password@host:5432/next_wiki
└ user └ password └ host └ port └ database
Migrations run automatically on boot and never drop the database
(IF NOT EXISTS / ADD COLUMN IF NOT EXISTS).
Option A — use a Postgres you already have
Point DATABASE_URL to your server and just create the database:
CREATE DATABASE next_wiki;
From inside the container, use host.docker.internal as the host (that’s the
case in the compose above).
Option B — run Postgres alongside (everything in one compose)
If you don’t have a Postgres, run one in the same docker-compose — the
database host becomes the service name (db):
services:
app:
image: nexttag/next-wiki:latest
environment:
SECRET_KEY: "replace-with-a-secret-longer-than-32-chars"
DATABASE_URL: "postgresql://nextwiki:password@db:5432/next_wiki"
URL: "https://wiki.yourcompany.com"
BOOTSTRAP_ADMIN_EMAIL: "you@yourcompany.com"
ports:
- "5210:8080"
volumes:
- nextwiki_data:/app/data
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: nextwiki
POSTGRES_PASSWORD: password
POSTGRES_DB: next_wiki # creates the database on first boot
volumes:
- nextwiki_pg:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nextwiki -d next_wiki"]
interval: 10s
timeout: 5s
retries: 5
volumes:
nextwiki_data:
nextwiki_pg:
The depends_on with healthcheck makes the app start only once the database
is ready. The nextwiki_pg volume preserves the database between restarts —
include it in your backup (or use pg_dump, see below).
First access
- Open the configured
URL. - Enter the email set in
BOOTSTRAP_ADMIN_EMAIL— it receives the magic link (or, without SMTP, the link appears in the container logs). - Once in, you are already the admin of the workspace. Invite the team and organize the collections.
Artificial intelligence
AI is configured inside the app, per workspace, under Settings → AI: each
workspace picks the provider (OpenAI, Anthropic, Google, litellm, Ollama…),
the model and the key. Everything is stored in the database — there is
no environment variable for it, and nothing needs to be set in docker run.
Advanced (optional): you can set a global default provider, used only when a workspace hasn’t configured its own, via
Ia__BaseUrl,Ia__ModeloandIa__ApiKey. The normal path, though, is to configure it from the menu.
HTTPS and reverse proxy
The image serves HTTP on the internal port — in production, put it behind a
TLS proxy. The app already honors the X-Forwarded-* headers, so the proxy just
needs to forward them; links and session cookies are issued with the https scheme
correctly.
Caddy (automatic TLS via Let’s Encrypt):
wiki.yourcompany.com {
reverse_proxy localhost:5210
}
Cloudflare Tunnel (no open ports):
ingress:
- hostname: wiki.yourcompany.com
service: http://localhost:5210
- service: http_status:404
Always set the
URLvariable with the public https address — it builds the invite/email links and validates the origin of requests.
Verify it’s up
# should return 200 (the root serves the app)
curl -I https://wiki.yourcompany.com
# follow the boot and migrations
docker compose logs -f app
For a Docker healthcheck, add to the app service:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/"]
interval: 30s
timeout: 5s
retries: 3
Storage and data
The body of the documents is stored as markdown in
FILE_STORAGE_LOCAL_ROOT_DIR (default /app/data), together with the icons. Keep
that directory in a volume so you don’t lose content between updates. The
metadata (users, permissions, tree) lives in PostgreSQL.
Production: scale and security
- Rate limiting is built in — the app limits requests by default, no extra configuration.
- Security headers and response compression are also applied automatically.
- Real-time editing: collaboration uses WebSocket (SignalR). For one
instance, it works with nothing extra. To run multiple instances behind a load
balancer, set
REDIS_URL— Redis becomes the backplane that synchronizes the connections across nodes (and also serves as a distributed cache). - Observability: set
OTEL_EXPORTER_OTLP_ENDPOINTto send logs/traces (OpenTelemetry) to your collector (Grafana/Loki, etc). - Session and keys: keep the same
SECRET_KEYacross restarts and instances — it signs the session; changing it logs everyone out. ASP.NET stores encryption keys (DataProtection) under/root/.aspnet, which are ephemeral in the container; to avoid regenerating them on every deploy (or when scaling), persist that directory to a volume.
Update
# 1. back up the database and the volume (see below)
docker compose pull
docker compose up -d # migrations run automatically on boot
Backup
# Database (metadata)
pg_dump "postgresql://admin:password@localhost:5432/next_wiki" > next_wiki.sql
# Content (markdown + icons)
docker run --rm -v nextwiki_data:/data -v "$PWD":/backup alpine \
tar czf /backup/nextwiki_data.tgz -C /data .
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Container won’t start and exits | SECRET_KEY missing/short | Set SECRET_KEY with ≥ 32 characters. |
password authentication failed / no connection | wrong DATABASE_URL or unreachable DB | Check host/port/credentials; from inside the container use host.docker.internal. |
| Magic link doesn’t arrive | SMTP not configured | Without SMTP_HOST, the link appears in the logs (docker compose logs app). |
| Login fails/redirects wrong behind a proxy | URL or X-Forwarded-* | Use URL with public https and make sure the proxy forwards the headers. |
| Content gone after updating | volume not persisted | /app/data must be in a named volume. |
| AI doesn’t respond | provider not configured | Set the provider under Settings → AI (provider, model and key) in the workspace. |
That’s it — an instance of your own, with your data under your control and the AI pointing to the provider you choose.