Skip to content

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:

  1. Open the web UI and sign up (first user becomes admin)
  2. Navigate to Settings > API Keys and generate an API key
  3. 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
  • sqlite3 CLI (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_URL must 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, password and confirmPassword must match.
  • To make this idempotent, check if users exist first (see Step 4).

Important: When DISABLE_SIGNUPS=true is set, this endpoint returns a 403. You must temporarily set DISABLE_SIGNUPS=false (and DISABLE_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 custom is 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_KEY is 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.com and http://192.168.1.x:3000, OIDC will only work on the NEXTAUTH_URL domain

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:

  1. Create LXC / provision host
  2. Install Docker and sqlite3
  3. Fetch secrets from Vault (admin creds, OIDC, Meili key, NextAuth secret)
  4. Deploy docker-compose with signups temporarily enabled
  5. Wait for health check (GET / returns 200 or 302)
  6. Check if users exist (sqlite3 "SELECT COUNT(*) FROM user")
  7. If first run: create admin via POST /api/trpc/users.create
  8. Generate API key components, insert into SQLite apiKey table
  9. Store API key back in Vault
  10. Rewrite .env with DISABLE_SIGNUPS=true, DISABLE_PASSWORD_AUTH=true, OAUTH_AUTO_REDIRECT=true
  11. 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

References