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
| 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 | depends 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:[email protected]:5432/next_wiki"
URL: "https://wiki.suaempresa.com"
BOOTSTRAP_ADMIN_EMAIL: "[email protected]"
# 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
| Variable | Required | Description |
|---|---|---|
SECRET_KEY | yes | Signs the session. 32 characters or more; without it the application won’t start. |
DATABASE_URL | yes | postgresql://usuario:senha@host:5432/next_wiki. |
URL | recommended | Public address, with https. Builds invitation and email links and sets the passkey origin. |
BOOTSTRAP_ADMIN_EMAIL | on 1st boot | Email that becomes the first admin on first sign-in. |
PORT | no | Internal port. Default 8080. If you change it, the right side of ports follows (5210:9000 with PORT=9000). |
FILE_STORAGE_LOCAL_ROOT_DIR | no | Local data folder. Default /app/data. |
| Variable | Description |
|---|---|
SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD | SMTP server. Default port 587. |
SMTP_FROM_EMAIL, SMTP_REPLY_EMAIL, SMTP_SECURE | Sender, 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)
| Variable | Description |
|---|---|
OIDC_ISSUER_URL | Realm issuer, e.g. https://auth.suaempresa.com/realms/empresa. |
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET | The wiki’s client in Keycloak. |
OIDC_AUTH_URI, OIDC_LOGOUT_URI | Alternative to the issuer (it is derived from the authorization address) and logout. |
KEYCLOAK_APPS_PERMITIDOS | Keycloak 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_MINUTO | Requests per minute for those apps: per person (default 30) and per app (default 300). |
MOSTRAR_LOGIN_SEM_SSO | true 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 fromappsettings.json, and the sign-in screen shows only the SSO button. If you don’t use SSO, declareOIDC_ISSUER_URL: ""andOIDC_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
| Variable | Description |
|---|---|
REDIS_URL | Distributed cache (tree, embeds, version check). Empty = in-memory cache. |
Embeddings__Provider | qdrant turns semantic search on. Default: off. |
Qdrant__Host, Qdrant__Port, Qdrant__Https, Qdrant__ApiKey | Initial Qdrant connection (the screen can change it later). |
Blob__Backend | local (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__Provider | Web search for agents: tavily, brave or searxng, with Ia__WebSearch__ApiKey or Ia__WebSearch__Url. Default: off. |
OTEL_EXPORTER_OTLP_ENDPOINT | Sends logs and traces via OpenTelemetry. |
RATE_LIMIT_GLOBAL, RATE_LIMIT_SENSIVEL | Requests per minute per IP: overall (default 240) and on the sign-in, passkey, POST /api/chat and MCP routes (default 12). |
TRUSTED_PROXIES | Proxies, 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: "[email protected]"
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
- Open the configured
URL. - Sign in with the
BOOTSTRAP_ADMIN_EMAILaddress. It receives the sign-in link (without SMTP, the link shows up indocker 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. - 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_KEYacross 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
| Symptom | Likely cause | What to do |
|---|---|---|
| Container doesn’t start | SECRET_KEY missing or short, or DATABASE_URL missing | Set both; the key with 32+ characters. |
password authentication failed | Wrong DATABASE_URL or database unreachable | Check host, port and credentials; from inside the container use host.docker.internal. |
| The sign-in link doesn’t arrive | No SMTP | The link is in docker compose logs app. |
| Sign-in bounces back to the start screen behind a proxy | URL without https or proxy without X-Forwarded-* | Use URL with https and check TRUSTED_PROXIES. |
| SSO button doesn’t show | Issuer or client empty | Configure it in Authentication → Keycloak and see where each value comes from. |
| Only the SSO button shows, and you didn’t configure SSO | OIDC_* variables absent: the factory Keycloak kicked in | Declare OIDC_ISSUER_URL: "" and OIDC_CLIENT_ID: "". |
| The AI doesn’t answer | No active provider | Activate one in Configurações → IA → LLM. |
| Semantic search is empty | Qdrant off or not indexed | Embeddings__Provider=qdrant and reindex on the screen. |
| Content vanished after updating | /app/data not on a volume | Restore the backup and use a named volume. |