Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

How to Self-Host Karakeep with AI Tagging on a VPS

Published

AIvpsDocker Management
karakeep-ai

Saved links are only useful if you can find them again. Karakeep is a self-hosted bookmark manager that saves links and notes, archives the page text, and asks an AI model to suggest tags. On your own VPS, the tagging model can run locally with Ollama instead of sending bookmark text to a hosted AI provider.

This setup runs Karakeep 0.33.2, its headless Chrome crawler, Meilisearch, and Ollama with Docker Compose on Ubuntu 24.04. Caddy serves it over HTTPS on your own domain.

Before you start

You need:

  • An Ubuntu 24.04 VPS with sudo access. Four CPU cores and 8 GB RAM are an evaluation starting point, not a measured minimum. Model, context length, concurrent jobs and crawler activity change memory demand.
  • A domain or subdomain, such as karakeep.example.com, with an A record pointing to the VPS IPv4. Replace that name everywhere below.
  • Ports 80 and 443 open to the internet so Caddy can obtain a certificate.

Use a fresh server or preserve existing web-server configuration deliberately. Measure your intended library and tagging load before choosing production capacity. CPU inference is possible, but responsiveness depends on the model, workload and available resources.

Install Docker and Caddy

Install the packages from Ubuntu’s repositories:

$ sudo apt-get update
$ sudo apt-get install -y docker.io docker-compose-v2 caddy openssl curl
$ sudo systemctl enable --now docker

If UFW is enabled, allow SSH, HTTP, and HTTPS. Use your real SSH port if it is not 22:

$ sudo ufw allow OpenSSH
$ sudo ufw allow 80/tcp
$ sudo ufw allow 443/tcp

Karakeep itself will listen only on 127.0.0.1, so there is no firewall rule for port 3000.

Before using the loopback Docker mapping as a security boundary, require Docker Engine 28 or newer and review Docker’s port-publishing limitations. Confirm that the public IP does not expose port 3000 directly; a Caddy restriction cannot protect a bypass route.

Create the Karakeep configuration

Create a project directory and a private environment file:

$ mkdir -p ~/karakeep
$ cd ~/karakeep
$ touch .env
$ chmod 600 .env
$ openssl rand -hex 32
$ openssl rand -hex 32

Create ~/karakeep/.env with the following contents. Put the first generated value in NEXTAUTH_SECRET and the second in MEILI_MASTER_KEY:

KARAKEEP_VERSION=0.33.2
NEXTAUTH_URL=https://karakeep.example.com
NEXTAUTH_SECRET=REPLACE_WITH_FIRST_VALUE
MEILI_MASTER_KEY=REPLACE_WITH_SECOND_VALUE
OPENAI_API_KEY=ollama
INFERENCE_TEXT_MODEL=qwen3:4b-instruct
INFERENCE_JOB_TIMEOUT_SEC=300

OPENAI_API_KEY=ollama is a placeholder, not a real key. Karakeep needs a value there to use Ollama’s OpenAI-compatible API. The longer job timeout gives a CPU-only model time to finish; the default of 30 seconds is too short.

Create ~/karakeep/compose.yaml:

services:
  web:
    image: ghcr.io/karakeep-app/karakeep:${KARAKEEP_VERSION}
    restart: unless-stopped
    env_file:
      - .env
    environment:
      DATA_DIR: /data
      MEILI_ADDR: http://meilisearch:7700
      BROWSER_WEB_URL: http://chrome:9222
      OPENAI_BASE_URL: http://ollama:11434/v1
    ports:
      - "127.0.0.1:3000:3000"
    volumes:
      - data:/data
    depends_on:
      - meilisearch
      - chrome
      - ollama

  chrome:
    image: ghcr.io/karakeep-app/karakeep-chrome:151.0.7922.47-r1
    restart: unless-stopped
    init: true
    command:
      - --disable-gpu
      - --disable-dev-shm-usage
      - --hide-scrollbars
      - --disable-blink-features=AutomationControlled
      - --window-size=1440,900

  meilisearch:
    image: getmeili/meilisearch:v1.41.0
    restart: unless-stopped
    environment:
      MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
      MEILI_NO_ANALYTICS: "true"
    volumes:
      - meilisearch:/meili_data

  ollama:
    image: ollama/ollama:0.35.1
    restart: unless-stopped
    volumes:
      - ollama:/root/.ollama

volumes:
  data:
  meilisearch:
  ollama:

This is the upstream Compose layout with pinned image versions and an Ollama service added. Ollama has no ports entry, so it is not published on the host. Containers on the same Compose network can reach it; the network is not an access boundary between those containers. Never publish port 11434 to the internet; Ollama has no authentication.

Start the stack:

$ sudo docker compose config --quiet
$ sudo docker compose up -d
$ sudo docker compose ps
$ curl -s http://127.0.0.1:3000/api/health

All four containers should be Up, and the health check should return {"status":"ok","message":"Web app is working"}.

Understand the four services

The web service holds the application and coordinates background work. Chrome fetches the linked pages, Meilisearch makes saved content searchable, and Ollama generates suggested tags. Caddy is the public HTTPS entry point; it forwards requests to the web service on the host’s loopback address. The browser never needs to connect directly to Meilisearch or Ollama.

The named volumes keep application data, the search index, and downloaded models outside the containers’ writable layers. Recreating a container is therefore different from removing its volume. Keep the volume declarations and project directory consistent when applying the configuration; do not remove data volumes to troubleshoot a slow tagging job.

Local inference does not make page collection an offline operation. The crawler still contacts the sites whose URLs you save, and installing the stack and model requires downloads. The privacy benefit here is narrower: the configured AI tagging request goes to your own Ollama service. Consider who can use your Karakeep account and server as well as where inference runs.

Download the tagging model

Pull the model into the Ollama container:

$ sudo docker compose exec ollama ollama pull qwen3:4b-instruct
$ sudo docker compose exec ollama ollama list

Inspect the selected model’s current metadata, download size and license before use. A model that returns structured tags can still produce irrelevant labels. Keep one model loaded initially and measure actual memory use before adding concurrency.

Enable HTTPS with Caddy

Keep a copy of the package’s default Caddyfile:

$ sudo cp -a /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak

Replace /etc/caddy/Caddyfile with:

karakeep.example.com {
    @setup_admin remote_ip 203.0.113.10
    handle @setup_admin {
        reverse_proxy 127.0.0.1:3000
    }
    handle {
        respond "Setup restricted" 403
    }
}

Validate and reload Caddy:

$ sudo caddy validate --config /etc/caddy/Caddyfile
$ sudo systemctl reload caddy
$ curl -sI https://karakeep.example.com/api/health

Before reloading, replace 203.0.113.10 with your administrator workstation’s public IP. This temporary gate assumes clients connect directly to Caddy, not through a CDN or another proxy. From that workstation the HTTPS health request should succeed; from another public IP it must return 403. A curl request made on the VPS itself may be denied. Keep this restriction until registration is disabled and verified.

Create your account and close sign-ups

Open https://karakeep.example.com and choose Sign up. The first account becomes the administrator.

While the temporary IP gate is still active, create only the intended administrator. Then add this line to ~/karakeep/.env:

DISABLE_SIGNUPS=true

Apply the change:

$ sudo docker compose up -d

Sign out and verify that registration is unavailable. Only then replace the temporary Caddy gate with the normal reverse_proxy 127.0.0.1:3000 route, validate and reload Caddy. Check again from a signed-out browser on a different network: ordinary access should work, but registration must remain disabled. Keep the gate if either check fails. Use the administrator’s user-management controls to add colleagues.

Save one public test article with New Item. Check collection, search indexing and the tagging job separately. CPU model startup and queued work can delay tags; there is no fixed one-to-two-minute completion guarantee.

Review the generated labels against the saved page. Remove unrelated tags and standardize names; a fluent or plausible label is not proof that the model understood the article.

Text notes are tagged the same way. Image bookmarks need a separate vision-capable model, which this setup does not configure.

Search for a word from the article body to confirm Meilisearch indexed it. To steer future tags, open the profile menu, choose User Settings → AI Settings, and add instructions, such as a preferred tag style or language.

For the browser extensions and mobile apps, enter https://karakeep.example.com as the server address and sign in with your account.

Start with one publicly accessible page containing ordinary article text. Check that its title and content appear, that a body-word search finds it, and that tags are added. These checks answer different questions: a saved URL confirms storage, searchable text confirms collection and indexing, and tags confirm that the model completed its work. A successful health response alone does not prove all three stages.

Review the first few tag suggestions before filling the library. A model can produce valid tags that are too broad or only loosely related to the page. Treat tags as editable organizational hints, not a reliable summary of everything the source says. Consistent naming is more useful than accepting a large collection of near-duplicate labels.

After a quiet period, allow time for the model to load before treating a pending tag job as a fault. Adding many bookmarks at once also creates queued work; it does not give each item a dedicated model instance. Avoid switching models repeatedly while diagnosing one item, because that makes memory use and model-name mismatches harder to distinguish from a crawl problem.

Troubleshooting

Check the application log first:

$ cd ~/karakeep
$ sudo docker compose logs --tail=50 web
  • No tags appear: look for [inference] lines. 404 model '...' not found means INFERENCE_TEXT_MODEL does not exactly match a name in ollama list. inference job failed: Error: Timeout means the model needed longer than INFERENCE_JOB_TIMEOUT_SEC; raise the value and apply it with sudo docker compose up -d.
  • A link shows only its URL, with no title or tags: the crawl failed, so there was nothing to tag. Look for Crawling job failed in the log and check that the server can reach the site. Then open the bookmark’s menu and choose More → Refresh to crawl and tag it again.
  • Failed to find potentially relevant tags ... Document not found: this line can appear for new bookmarks before the search index catches up. It does not stop tagging.
  • Ollama stops or the server freezes: the model does not fit in memory. Check free -h and sudo dmesg | grep -i oom. Add RAM, keep only one model in use, or choose a smaller model.
  • Changes to .env have no effect: run sudo docker compose up -d again. A restart alone does not reload the file.
  • Certificate errors: check that the A record points to this server and that ports 80 and 443 are reachable. Inspect sudo journalctl -u caddy -n 50 --no-pager.

Your bookmarks, archived pages, and search index now stay on your VPS, and new links are tagged by a model running on the same server.

Reference: Karakeep environment settings. Check the documentation for your installed version before applying changes.

Related guides

Account services are unavailable until the production backend is configured.