Installation¶
doction is two containers: the app, and a Postgres it talks to. There is no third service, no broker, and no external API. The published image is multi-arch, so the same tag runs on amd64 and arm64 hardware — from a cloud VM to a small self-hosted board.
Requirements¶
| Minimum | Comfortable | |
|---|---|---|
| Architecture | amd64 or arm64 | — |
| RAM for the app container | 256 MB | 768 MB |
RAM extra, with SEMANTIC_SEARCH=1 |
+150 MB | — |
RAM extra, with RERANK=1 |
+100 MB | — |
| Disk | the git repo of your pages, plus uploads, plus the Postgres volume | — |
| Postgres | 16 | 16 |
doction runs comfortably on modest self-hosted hardware — for example, a Raspberry Pi 4 with 2 GB runs it with semantic search on. The reference deployment caps the app container at 768 MB.
Option 1: Docker Compose¶
The compose.yaml in the repo root wires both containers and is the fastest path to a
working instance.
git clone https://github.com/dny1020/doction.git
cd doction
cp .env.example .env
# Edit .env: POSTGRES_PASSWORD and SECRET_KEY are the two that matter.
# openssl rand -hex 24 → POSTGRES_PASSWORD
# openssl rand -hex 32 → SECRET_KEY
mkdir -p data/logs # so the bind mounts get your uid; the container runs as uid 1000
docker compose up -d
Open http://localhost:8000 and register. The first account is the one that matters —
registration is open by default, so create yours immediately, then set
DISABLE_REGISTRATION=1 and restart.
Postgres is published on 127.0.0.1:5432 only, so psql works locally without exposing
the database to the network.
Option 2: Plain docker run, behind a proxy¶
For a host where you manage the reverse proxy yourself. This is the shape the reference deployment uses.
docker network create doction-net
docker run -d --name doction-postgres --restart unless-stopped --network doction-net \
-e POSTGRES_USER=doction \
-e POSTGRES_PASSWORD=$(openssl rand -hex 24) \
-e POSTGRES_DB=doction \
-v /srv/doction/postgres:/var/lib/postgresql/data \
postgres:16-alpine
docker run -d --name doction --restart unless-stopped --network doction-net \
-p 127.0.0.1:8000:8000 \
-e SECRET_KEY=$(openssl rand -hex 32) \
-e DATABASE_URL=postgresql://doction:<the-password-above>@doction-postgres:5432/doction \
-e SECURE_COOKIES=1 \
-e DISABLE_REGISTRATION=1 \
-e SEMANTIC_SEARCH=1 \
-v /srv/doction:/data \
-v /srv/doction/logs:/logs \
ghcr.io/dny1020/doction:latest
Two details that are easy to get wrong:
- Publish on loopback,
-p 127.0.0.1:8000:8000, not-p 8000:8000. Otherwise the app is reachable directly, bypassing the proxy and its TLS. - Put Postgres on an internal network only. It needs no published port at all. The
reference deployment gives it a separate
db_netso the database stays unreachable even if the proxy is misconfigured.
Then terminate TLS in nginx, Caddy, or Traefik pointing at http://127.0.0.1:8000.
nginx, minimal¶
server {
listen 443 ssl http2;
server_name wiki.example.com;
ssl_certificate /etc/letsencrypt/live/wiki.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/wiki.example.com/privkey.pem;
# Uploaded images and PDFs. Raise it if you paste large screenshots.
client_max_body_size 25m;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
client_max_body_size is the one people forget. nginx defaults to 1 MB, so an upload
larger than that fails at the proxy with a 413 before doction ever sees it.
Option 3: From source¶
For development, or to run without Docker. You still need a Postgres somewhere.
uv sync --dev
cd frontend && npm install && npm run build && cd ..
export DATABASE_URL=postgresql://doction:doction@localhost:5432/doction
export SECRET_KEY=$(openssl rand -hex 32)
export DATA_DIR=./data LOG_DIR=./data/logs
uv run uvicorn app.main:app --reload
The frontend build step is not optional. app/static/app/ is gitignored — the Docker
build produces it — so from a fresh clone the /app route serves nothing until you run
npm run build (or make build-web).
Verifying the install¶
curl -s http://localhost:8000/health | jq
# {"status":"ok","db":"ok","version":"0.31.3"}
db: "ok" means the schema was created and is reachable. The schema is built with
CREATE TABLE IF NOT EXISTS on every boot, so first start and every start after it run
the same code path — there is no migration step to remember and no migration ladder to
keep in order.
To check the agent surface without authenticating:
curl -s -X POST http://localhost:8000/api/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26"}}' | jq .result.serverInfo
# {"name":"doction","version":"0.31.3"}
That is also the way to confirm which version a remote deployment is actually running, which matters because deploys are manual.
What to do next¶
- Register the first user, then set
DISABLE_REGISTRATION=1. - Create a personal access token for your agent: see Agents and MCP.
- Set up backups before you rely on it: see Operations. Both the volume and the database, or the restore does not work.