Technical deploy (self-host)

Run Next Wiki with Docker — environment variables, database, storage, sign-in and AI.

Self-host with Docker

Next Wiki runs as a single image (nexttag/next-wiki on Docker Hub): the frontend is served by the .NET backend itself, on the same port. You need Docker and a PostgreSQL. Redis, Qdrant, SMTP and MinIO are optional.

Prerequisites

  • Docker 24+ (and Docker Compose v2).
  • PostgreSQL 14+. It can be a database server you already have: Next Wiki uses its own database (e.g. next_wiki), runs migrations on its own at boot and never drops the database.
  • An AI provider (optional, but it’s what makes the product worthwhile): OpenAI, Google Gemini, OpenRouter, a litellm gateway or a local Ollama. It is configured inside the app, not on the server.

Hardware requirements

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 GBdepends on content

The AI runs on the provider you choose, not on this machine — unless you run Ollama on the same server, in which case the model is what weighs.

Keep the application and PostgreSQL on the same network. Every query pays the latency to the database; with both close together, that round trip takes a few milliseconds.

docker-compose.yml

services:
  app:
    image: nexttag/next-wiki:latest
    restart: unless-stopped
    environment:
      SECRET_KEY: "troque-por-um-segredo-com-mais-de-32-caracteres"
      DATABASE_URL: "postgresql://admin:senha@host.docker.internal:5432/next_wiki"
      URL: "https://wiki.suaempresa.com"
      BOOTSTRAP_ADMIN_EMAIL: "voce@suaempresa.com"
      # No SSO? Keep these two declared and EMPTY (see "Single sign-on" below).
      OIDC_ISSUER_URL: ""
      OIDC_CLIENT_ID: ""
    ports:
      - "5210:8080"                   # host:container
    volumes:
      - nextwiki_data:/app/data       # documents, attachments, icons and session keys
    extra_hosts:
      - "host.docker.internal:host-gateway"

volumes:
  nextwiki_data:
docker compose up -d

To pin a version instead of latest, use its tag (e.g. nexttag/next-wiki:1.4.0).

Environment variables

Essentials

VariableRequiredDescription
SECRET_KEYyesSigns the session. 32 characters or more; without it the application won’t start.
DATABASE_URLyespostgresql://usuario:senha@host:5432/next_wiki.
URLrecommendedPublic address, with https. Builds invitation and email links and sets the passkey origin.
BOOTSTRAP_ADMIN_EMAILon 1st bootEmail that becomes the first admin on first sign-in.
PORTnoInternal port. Default 8080. If you change it, the right side of ports follows (5210:9000 with PORT=9000).
FILE_STORAGE_LOCAL_ROOT_DIRnoLocal data folder. Default /app/data.

Email

VariableDescription
SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORDSMTP server. Default port 587.
SMTP_FROM_EMAIL, SMTP_REPLY_EMAIL, SMTP_SECURESender, reply-to and TLS.

Without SMTP_HOST, no email is sent. The sign-in link shows up in the container log (with URL set; without it, the log only has the token), and you copy the invitation link from the Members screen. The sign-in link lasts 15 minutes, with up to 5 requests per hour. Any provider with SMTP works.

Single sign-on (OIDC / Keycloak)

VariableDescription
OIDC_ISSUER_URLRealm issuer, e.g. https://auth.suaempresa.com/realms/empresa.
OIDC_CLIENT_ID, OIDC_CLIENT_SECRETThe wiki’s client in Keycloak.
OIDC_AUTH_URI, OIDC_LOGOUT_URIAlternative to the issuer (it is derived from the authorization address) and logout.
KEYCLOAK_APPS_PERMITIDOSKeycloak clients, comma-separated, that may use their own token on the chat API (e.g. the desktop app).
KEYCLOAK_LIMITE_PESSOA_MINUTO, KEYCLOAK_LIMITE_APP_MINUTORequests per minute for those apps: per person (default 30) and per app (default 300).
MOSTRAR_LOGIN_SEM_SSOtrue shows email and passkey on the sign-in screen even with SSO. By default, with SSO, only SSO shows.

All of this can also be done from the settings screen, in Configurações → Authentication → Keycloak, and the screen’s value takes priority over the environment’s. See the administration manual.

⚠️ Absent is not off. Without the OIDC_* variables, the image uses the factory Keycloak from appsettings.json, and the sign-in screen shows only the SSO button. If you don’t use SSO, declare OIDC_ISSUER_URL: "" and OIDC_CLIENT_ID: "" — a variable that is present and empty turns it off. Check in Authentication → Keycloak: each field says where its value came from.

Optional infrastructure

VariableDescription
REDIS_URLDistributed cache (tree, embeds, version check). Empty = in-memory cache.
Embeddings__Providerqdrant turns semantic search on. Default: off.
Qdrant__Host, Qdrant__Port, Qdrant__Https, Qdrant__ApiKeyInitial Qdrant connection (the screen can change it later).
Blob__Backendlocal (default) or minio to store attachments and icons in S3-compatible storage. With minio: Blob__Endpoint, Blob__AccessKey, Blob__SecretKey, Blob__Bucket, Blob__UseSSL.
Ia__WebSearch__ProviderWeb search for agents: tavily, brave or searxng, with Ia__WebSearch__ApiKey or Ia__WebSearch__Url. Default: off.
OTEL_EXPORTER_OTLP_ENDPOINTSends logs and traces via OpenTelemetry.
RATE_LIMIT_GLOBAL, RATE_LIMIT_SENSIVELRequests per minute per IP: overall (default 240) and on the sign-in, passkey, POST /api/chat and MCP routes (default 12).
TRUSTED_PROXIESProxies, besides loopback, whose X-Forwarded-For is accepted.

Database (PostgreSQL)

Next Wiki stores users, permissions, the tree, versions, conversations and settings in PostgreSQL. Migrations run on their own at boot.

Option A — use a Postgres you already have

Create just the database and point DATABASE_URL at it:

CREATE DATABASE next_wiki;

From inside the container, use host.docker.internal as the host (as in the compose above).

Option B — run Postgres alongside

services:
  app:
    image: nexttag/next-wiki:latest
    restart: unless-stopped
    environment:
      SECRET_KEY: "troque-por-um-segredo-com-mais-de-32-caracteres"
      DATABASE_URL: "postgresql://nextwiki:senha@db:5432/next_wiki"
      URL: "https://wiki.suaempresa.com"
      BOOTSTRAP_ADMIN_EMAIL: "voce@suaempresa.com"
      OIDC_ISSUER_URL: ""             # no SSO: declared and empty
      OIDC_CLIENT_ID: ""
    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: senha
      POSTGRES_DB: next_wiki
    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:

First access

  1. Open the configured URL.
  2. Sign in with the BOOTSTRAP_ADMIN_EMAIL address. It receives the sign-in link (without SMTP, the link shows up in docker compose logs app). If SSO is configured, signing in through SSO with that email also works. If the screen only shows the SSO button and you haven’t configured one, see the warning under Single sign-on.
  3. You come in as admin. Set up the AI in Configurações → IA → LLM (Settings → AI → LLM) and invite the team — see Getting started.

Artificial intelligence

AI is configured per workspace, inside the app, in Configurações → IA → LLM: provider (OpenAI, Google Gemini, OpenRouter, litellm or Ollama), key and model. It’s all stored in the database; there is no environment variable for it. To use Anthropic models (Claude), go through OpenRouter or a litellm gateway.

Semantic search (Qdrant)

For the AI to find documents by meaning, run a Qdrant in the same compose and turn it on in the application:

services:
  app:
    # ...the rest of the service, as above
    environment:
      Embeddings__Provider: "qdrant"
      Qdrant__Host: "qdrant"        # the service name below
      Qdrant__Port: "6334"          # gRPC

  qdrant:
    image: qdrant/qdrant
    restart: unless-stopped
    volumes:
      - nextwiki_qdrant:/qdrant/storage

volumes:
  nextwiki_qdrant:

Then, in Configurações → IA → Busca semântica (semantic search), choose the embedding model (Ollama, Gemini, OpenAI or another OpenAI-compatible one) and run reindex. Without Qdrant, the wiki works normally with text search.

HTTPS and reverse proxy

The image serves HTTP. In production, put it behind a proxy with TLS. The application honors X-Forwarded-* headers coming from trusted proxies (loopback, the Docker network and whatever you list in TRUSTED_PROXIES).

Caddy (automatic TLS):

wiki.suaempresa.com {
    reverse_proxy localhost:5210
}

Cloudflare Tunnel (no open ports):

ingress:
  - hostname: wiki.suaempresa.com
    service: http://localhost:5210
  - service: http_status:404

Chat and presence use WebSocket: the proxy has to let it through (Caddy and Cloudflare Tunnel already do).

Check that it’s up

# 200 with the available sign-in methods
curl -s https://wiki.suaempresa.com/api/auth/metodos

# follow the boot and migrations
docker compose logs -f app

The image ships without curl or wget, and the application has no healthcheck route of its own. Monitor from outside, with the call above.

Storage and data

/app/data (the volume) holds the document text in Markdown, the attachments, the icons and the data-protection keys (dp-keys). Keep that directory on a volume: without it, recreating the container loses the content. Everything else lives in PostgreSQL.

The session doesn’t depend on the volume: it stays valid as long as SECRET_KEY is the same. Losing dp-keys only interrupts whoever was in the middle of an SSO or passkey sign-in.

Production: scale and security

  • Rate limiting, security headers (CSP, HSTS) and compression are on out of the box.
  • One instance is the recommendation. Chat, presence, the agent queue, indexing and webhooks live in the memory of the instance that received them; Redis only shares the cache. To run several, use the same SECRET_KEY, the same volume (or MinIO) and session affinity on the load balancer — and be aware that a conversation or an agent doesn’t cross from one instance to another.
  • Session: keep the same SECRET_KEY across restarts and instances. If it changes, everyone is signed out.
  • Blocked accounts lose their session and API keys immediately.

Update

# 1. take the backup (below)
docker compose pull
docker compose up -d        # migrations run at boot

The Configurações → Instalação (Installation) screen tells you when a new version is out.

Backup

# Database — use the user, password and host from your DATABASE_URL
pg_dump "postgresql://admin:senha@localhost:5432/next_wiki" > next_wiki.sql

# Database, with option B (Postgres in the same compose)
docker compose exec db pg_dump -U nextwiki next_wiki > next_wiki.sql

# Files (documents, attachments, icons, keys)
docker run --rm -v nextwiki_data:/data -v "$PWD":/backup alpine \
  tar czf /backup/nextwiki_data.tgz -C /data .

Both need to come from the same moment: the database points to the files.

Troubleshooting

SymptomLikely causeWhat to do
Container doesn’t startSECRET_KEY missing or short, or DATABASE_URL missingSet both; the key with 32+ characters.
password authentication failedWrong DATABASE_URL or database unreachableCheck host, port and credentials; from inside the container use host.docker.internal.
The sign-in link doesn’t arriveNo SMTPThe link is in docker compose logs app.
Sign-in bounces back to the start screen behind a proxyURL without https or proxy without X-Forwarded-*Use URL with https and check TRUSTED_PROXIES.
SSO button doesn’t showIssuer or client emptyConfigure it in Authentication → Keycloak and see where each value comes from.
Only the SSO button shows, and you didn’t configure SSOOIDC_* variables absent: the factory Keycloak kicked inDeclare OIDC_ISSUER_URL: "" and OIDC_CLIENT_ID: "".
The AI doesn’t answerNo active providerActivate one in Configurações → IA → LLM.
Semantic search is emptyQdrant off or not indexedEmbeddings__Provider=qdrant and reindex on the screen.
Content vanished after updating/app/data not on a volumeRestore the backup and use a named volume.