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 use IF 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.

ScenarioCPURAMDisk
Minimum (small team)1 vCPU1 GB10 GB
Recommended (app + Postgres on the same VM)2 vCPU2 GB20 GB+
Large team / many attachments2–4 vCPU4 GBas 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

VariableRequiredDescription
SECRET_KEYyesSecret that signs the session JWT. ≥ 32 characters.
DATABASE_URLyesPostgres connection: postgresql://user:password@host:5432/next_wiki.
URLyesPublic URL of the wiki (used in invite/email links).
PORTnoPort the app listens on. Default 8080.
BOOTSTRAP_ADMIN_EMAILnoEmail that becomes the first admin on first boot (via magic link).
FILE_STORAGE_LOCAL_ROOT_DIRnoLocal storage root (markdown + icons). Default /app/data.
REDIS_URLnoDistributed cache. Empty = in-memory cache. E.g. redis://host:6379.
SMTP_HOSTSMTP_SECUREnoEmail sending (magic link, invites). Without SMTP_HOST, email is a no-op.
OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRETnoSSO via OIDC (Keycloak, etc). Empty = magic link only.
OTEL_EXPORTER_OTLP_ENDPOINTnoOpenTelemetry 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

  1. Open the configured URL.
  2. Enter the email set in BOOTSTRAP_ADMIN_EMAIL — it receives the magic link (or, without SMTP, the link appears in the container logs).
  3. 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__Modelo and Ia__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 URL variable 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_ENDPOINT to send logs/traces (OpenTelemetry) to your collector (Grafana/Loki, etc).
  • Session and keys: keep the same SECRET_KEY across 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

SymptomLikely causeWhat to do
Container won’t start and exitsSECRET_KEY missing/shortSet SECRET_KEY with ≥ 32 characters.
password authentication failed / no connectionwrong DATABASE_URL or unreachable DBCheck host/port/credentials; from inside the container use host.docker.internal.
Magic link doesn’t arriveSMTP not configuredWithout SMTP_HOST, the link appears in the logs (docker compose logs app).
Login fails/redirects wrong behind a proxyURL or X-Forwarded-*Use URL with public https and make sure the proxy forwards the headers.
Content gone after updatingvolume not persisted/app/data must be in a named volume.
AI doesn’t respondprovider not configuredSet 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.