Automating Karakeep Setup with Ansible (No GUI Required)
How to fully automate Karakeep deployment — including first-run admin creation, API key generation, PocketID OIDC, and AI tagging via Ollama — without ever opening the web UI. Applies to Karakeep v0.29+ (formerly Hoarder).
The Problem
Karakeep's first-run experience requires you to:
- Open the web UI and sign up (first user becomes admin)
- Navigate to Settings > API Keys and generate an API key
- Manually configure OIDC if desired
For IaC deployments, all of this should happen automatically. Karakeep exposes a tRPC endpoint for signup and uses SQLite for storage, which means we can automate everything — including API key creation via direct database insert.
Prerequisites
- Docker and Docker Compose
sqlite3CLI (for API key injection)openssl(for key generation)- (Optional) A PocketID instance for OIDC SSO
- (Optional) An Ollama instance for AI auto-tagging
- (Optional) Ansible for full automation
Step 1: Deploy the Container
Karakeep runs three containers: the web app, a headless Chrome instance (for page archiving), and Meilisearch (for full-text search).
services:
karakeep:
image: ghcr.io/karakeep-app/karakeep:${KARAKEEP_VERSION:-release}
restart: unless-stopped
volumes:
- data:/data
ports:
- 3000:3000
env_file:
- .env
environment:
MEILI_ADDR: http://meilisearch:7700
BROWSER_WEB_URL: http://chrome:9222
DATA_DIR: /data
chrome:
image: gcr.io/zenika-hub/alpine-chrome:124
restart: unless-stopped
command:
- --no-sandbox
- --disable-gpu
- --disable-dev-shm-usage
- --remote-debugging-address=0.0.0.0
- --remote-debugging-port=9222
- --hide-scrollbars
meilisearch:
image: getmeili/meilisearch:v1.37.0
restart: unless-stopped
env_file:
- .env
environment:
MEILI_NO_ANALYTICS: "true"
volumes:
- meilisearch:/meili_data
volumes:
data:
meilisearch:
Required .env variables
KARAKEEP_VERSION=release
NEXTAUTH_SECRET=<random-string> # openssl rand -base64 36
NEXTAUTH_URL=https://karakeep.example.com
MEILI_MASTER_KEY=<random-string> # openssl rand -base64 36
MEILISEARCH_MASTER_KEY=<same-as-above> # must match MEILI_MASTER_KEY
DB_WAL_MODE=true # improves SQLite performance
Critical:
NEXTAUTH_URLmust exactly match the URL users access Karakeep through. If behind a reverse proxy, use the external URL (e.g.,https://karakeep.example.com), not the internal address. This is the #1 source of OIDC failures.
Step 2: Create Admin User via tRPC
Karakeep uses tRPC for its internal API. The signup endpoint is a public procedure (no auth required) and is available at /api/trpc/users.create:
curl -sf -X POST "http://localhost:3000/api/trpc/users.create" \
-H "Content-Type: application/json" \
-d '{
"json": {
"name": "admin",
"email": "[email protected]",
"password": "your-secure-password",
"confirmPassword": "your-secure-password"
}
}'
Response:
{
"result": {
"data": {
"json": {
"id": "clxyz123abc",
"name": "admin",
"email": "[email protected]",
"role": "admin"
}
}
}
}
Key details:
- The first user automatically becomes admin. No role assignment needed.
- Rate limited: 3 requests per 60 seconds.
- Turnstile CAPTCHA is disabled by default for self-hosted instances.
- Password requirements: 8-100 characters,
passwordandconfirmPasswordmust match. - To make this idempotent, check if users exist first (see Step 4).
Important: When
DISABLE_SIGNUPS=trueis set, this endpoint returns a 403. You must temporarily setDISABLE_SIGNUPS=false(andDISABLE_PASSWORD_AUTH=false) for the initial signup, then lock it down after.
Step 3: Generate API Key via SQLite
Karakeep stores API keys as SHA-256 hashes in SQLite. There is no API endpoint to create API keys — it can only be done through the web UI. However, we can insert directly into the database.
API key format
Karakeep API keys follow this format: ak2_{keyId}_{secret}
keyId: 10 random bytes as hex (20 characters)secret: 20 random bytes as hex (40 characters)keyHash: SHA-256 of the secret, base64-encoded
Generate and insert
# Generate key components
KEY_ID=$(openssl rand -hex 10)
SECRET=$(openssl rand -hex 20)
SECRET_HASH=$(echo -n "$SECRET" | openssl dgst -sha256 -binary | base64)
PLAIN_KEY="ak2_${KEY_ID}_${SECRET}"
CUID=$(openssl rand -hex 12)
# Get the admin user ID (from Step 2 response, or query the DB)
USER_ID=$(docker exec karakeep-karakeep-1 sh -c \
"sqlite3 /data/db.db \"SELECT id FROM user WHERE role='admin' LIMIT 1;\"")
# Insert the API key
docker exec karakeep-karakeep-1 sh -c \
"sqlite3 /data/db.db \"INSERT INTO apiKey (id, name, createdAt, keyId, keyHash, userId) \
VALUES ('${CUID}', 'automation', strftime('%s','now'), '${KEY_ID}', '${SECRET_HASH}', '${USER_ID}');\""
# The usable API key is:
echo "API Key: ${PLAIN_KEY}"
Verify the key works
curl -s "http://localhost:3000/api/v1/bookmarks" \
-H "Authorization: Bearer ${PLAIN_KEY}" | jq .
SQLite schema reference
The apiKey table:
| Column | Type | Description |
|---|---|---|
id |
text (PK) | CUID-style unique ID |
name |
text | Human-readable key name (unique per user) |
createdAt |
integer | Unix timestamp |
lastUsedAt |
integer | Unix timestamp (nullable) |
keyId |
text (unique) | Public identifier portion |
keyHash |
text | SHA-256 hash of secret, base64-encoded |
userId |
text (FK) | References user.id |
Step 4: Configure PocketID OIDC
Unlike Grimmory, Karakeep configures OIDC entirely via environment variables — no API calls or database settings needed.
PocketID client setup
Create an OIDC client in PocketID with:
- Name: Karakeep
- Callback URL:
https://karakeep.example.com/api/auth/callback/custom
The word
customis literal, not a placeholder. This is the hardcoded NextAuth callback path.
Environment variables
OAUTH_WELLKNOWN_URL=https://auth.example.com/.well-known/openid-configuration
OAUTH_CLIENT_ID=<client-id>
OAUTH_CLIENT_SECRET=<client-secret>
OAUTH_PROVIDER_NAME=Pocket-ID
OAUTH_SCOPE=openid email profile
OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING=true
Lock down after setup
After the admin user is created and OIDC is working:
DISABLE_SIGNUPS=true
DISABLE_PASSWORD_AUTH=true
OAUTH_AUTO_REDIRECT=true
With OAUTH_AUTO_REDIRECT=true, users hitting the Karakeep URL are immediately redirected to PocketID — no login page shown.
Step 5: Configure AI Tagging (Ollama)
Karakeep can automatically tag bookmarks using a local Ollama instance:
OLLAMA_BASE_URL=http://your-ollama-host:11434
INFERENCE_TEXT_MODEL=llama3.1
INFERENCE_IMAGE_MODEL=llava
INFERENCE_LANG=english
This enables:
- Auto-tagging: AI generates tags when you save a bookmark
- Auto-summarization: AI summarizes article content
- OCR: Extracts text from images using the vision model
If
OPENAI_API_KEYis also set, it takes precedence over Ollama. To use Ollama, do not set an OpenAI key.
Step 6: Force Clean (Fresh Start)
To wipe all data and start fresh:
# Stop containers
docker compose down
# Remove Docker volumes (data + search index)
docker volume rm karakeep_data karakeep_meilisearch
# Restart
docker compose up -d
Since Karakeep uses SQLite stored in the data volume, removing the volume wipes everything: users, bookmarks, API keys, and settings.
Gotchas and Known Issues
NEXTAUTH_URL must be exact
This is the single most common failure. The value must:
- Match the exact URL users access (protocol + domain + port)
- Use the external/public URL when behind a reverse proxy
- Have no trailing slash
- Only support a single domain — if you access Karakeep via both
https://karakeep.example.comandhttp://192.168.1.x:3000, OIDC will only work on theNEXTAUTH_URLdomain
OAUTH_WELLKNOWN_URL must be fully qualified
Must end in /.well-known/openid-configuration, not just the base domain. Without this, Karakeep v0.29+ throws:
id_token detected in the response, you must use client.callback() instead of client.oauthCallback()
Email case sensitivity in account linking
OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING performs case-sensitive email comparison. If PocketID returns [email protected] but the Karakeep admin was created with [email protected], linking fails silently and a new account is created instead. Ensure email case matches exactly.
tRPC request format
The signup endpoint expects a {"json": {...}} wrapper, not a bare JSON body. This is standard tRPC v10 format.
MEILI_MASTER_KEY vs MEILISEARCH_MASTER_KEY
Both must be set to the same value. MEILI_MASTER_KEY is used by the Karakeep web app to connect. MEILISEARCH_MASTER_KEY is used by the Meilisearch container itself.
Ansible Automation Summary
The full automated sequence:
- Create LXC / provision host
- Install Docker and sqlite3
- Fetch secrets from Vault (admin creds, OIDC, Meili key, NextAuth secret)
- Deploy docker-compose with signups temporarily enabled
- Wait for health check (
GET /returns 200 or 302) - Check if users exist (
sqlite3 "SELECT COUNT(*) FROM user") - If first run: create admin via
POST /api/trpc/users.create - Generate API key components, insert into SQLite
apiKeytable - Store API key back in Vault
- Rewrite
.envwithDISABLE_SIGNUPS=true,DISABLE_PASSWORD_AUTH=true,OAUTH_AUTO_REDIRECT=true - Restart containers with locked-down config
API Reference
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/api/trpc/users.create |
POST | None (public) | Create user (first = admin) |
/api/v1/bookmarks |
GET/POST | Bearer token | List/create bookmarks |
/api/v1/bookmarks/{id} |
GET/PATCH/DELETE | Bearer token | Manage bookmark |
/api/v1/lists |
GET/POST | Bearer token | List/create lists |
/api/v1/tags |
GET | Bearer token | List tags |
/api/v1/users/me |
GET | Bearer token | Get current user |
Full API docs: docs.karakeep.app/api/karakeep-api