Contents

Karakeep vs Linkwarden: Self-Hosted Bookmarking Compared

Two answers to the same 4,000-tab problem, and they disagree about almost everything

Contents

A quick note before anything else, because the naming will confuse you if you go searching: Karakeep is Hoarder. Same project, same maintainer, same container images moved to a new namespace — it was renamed earlier this year after a trademark problem made the old name untenable. Every blog post, Reddit thread and YouTube video from before the rename says “Hoarder”, and all of them are describing the software I am about to describe. If you have Hoarder running, you have Karakeep running, and the upgrade path is a namespace change in your compose file.

With that out of the way: I have been running both of these against the same set of about 1,800 links for two months, and they are far more different than the category suggests.

The Problem Nobody Admits To

Advertisement

Browser bookmarks fail for a reason that has nothing to do with the browser. They store a URL and a title, and both of those are pointers to something on someone else’s computer that they can delete at any moment. I ran a link check across a decade of my own bookmarks and roughly a fifth of them were dead. Gone entirely, with no redirect left behind. The interesting half of those were exactly the kind of thing you bookmark: a personal blog post explaining something obscure, hosted by someone who stopped paying for hosting.

So the actual requirement is an archive, and the actual feature is that the copy lives on your disk. Everything else — tags, search, mobile apps — is convenience layered on top of that one non-negotiable.

Both of these tools understand this. They then solve it in opposite ways.

Karakeep: The Ingestion Machine

Karakeep’s mental model is a firehose. You throw things at it — links, plain notes, images — and it does the filing. That is the entire personality of the project, and everything in the architecture serves it.

The stack is four containers: the app itself, a headless Chrome for rendering, Meilisearch for full-text search, and a worker process. Data lives in SQLite in a data directory, which is a decision I like more every year I spend in this hobby.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
services:
  karakeep:
    image: ghcr.io/karakeep-app/karakeep:release
    restart: unless-stopped
    env_file: .env
    environment:
      MEILI_ADDR: http://meilisearch:7700
      BROWSER_WEB_URL: http://chrome:9222
      DATA_DIR: /data
    ports:
      - 3000:3000
    volumes:
      - karakeep_data:/data

  chrome:
    image: gcr.io/zenika-hub/alpine-chrome:123
    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.13
    restart: unless-stopped
    env_file: .env
    environment:
      MEILI_NO_ANALYTICS: "true"
    volumes:
      - meili_data:/meili_data

volumes:
  karakeep_data:
  meili_data:

The .env needs three values, and Karakeep will refuse to start without them, which is the correct behaviour:

1
2
3
4
5
{
  printf 'NEXTAUTH_SECRET=%s\n' "$(openssl rand -base64 36)"
  printf 'MEILI_MASTER_KEY=%s\n' "$(openssl rand -base64 36)"
  printf 'NEXTAUTH_URL=%s\n' "https://keep.mylab.local"
} > .env

That headless Chrome container is the point of the whole design. When you save a link, a worker loads the page in a real browser, waits for the JavaScript to settle, extracts readable text, grabs a screenshot, and optionally stores a full-page snapshot. It also fires a request at the Internet Archive as a belt-and-braces second copy. Pages that are hostile to naive scrapers — the modern web, in other words — come through intact.

The other half of the pitch is the tagging. Point it at a model and it reads each page and assigns tags on its own. It speaks the OpenAI API, which means it also speaks Ollama:

1
2
3
4
OLLAMA_BASE_URL=http://ollama.mylab.local:11434
INFERENCE_TEXT_MODEL=llama3.1:8b
INFERENCE_IMAGE_MODEL=llava
INFERENCE_CONTEXT_LENGTH=4096

I ran this against a local 8B model on a spare GPU. The tags are decent — roughly what a bored intern would produce, which is genuinely useful at 1,800 links. It occasionally invents a tag like “productivity” for an article about a soldering iron, and about one in fifteen is confidently wrong. If you have already got a local inference box running from the small-model comparison, this is the cheapest possible way to get value out of it. Cloud models do this better, and paying a per-token bill to tag your own bookmarks is a strange place to spend money.

A word on the context length: the default is small, and a long article gets truncated before the model sees the part that says what it is about. Raising it costs VRAM and improves the tags noticeably.

Linkwarden: The Librarian

Advertisement

Linkwarden’s mental model is a filing cabinet you curate deliberately. Collections are first-class, nested, and shareable — and that sharing is real, with public collection pages you can hand to someone who has no account. Karakeep has nothing equivalent.

The stack is two containers: the Next.js app and Postgres. Archiving is done with Playwright inside the app container rather than a separate browser service.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
services:
  linkwarden:
    image: ghcr.io/linkwarden/linkwarden:latest
    restart: unless-stopped
    environment:
      DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
      NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
      NEXTAUTH_URL: https://links.mylab.local/api/v1/auth
      NEXT_PUBLIC_DISABLE_REGISTRATION: "true"
    ports:
      - 3000:3000
    volumes:
      - ./data:/data/data
    depends_on:
      - postgres

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

The archiving is where Linkwarden earns its keep. Every link gets four artefacts: a screenshot, a PDF, a single-file HTML archive, and a readable text extraction. That PDF matters more than it sounds. It is the format that will still open in twenty years on hardware that has not been designed yet, and it prints. Karakeep’s snapshot is a better representation of the live page; Linkwarden’s PDF is a better representation of the page in 2045.

Linkwarden gained AI tagging earlier this year and it also talks to Ollama. It is more conservative than Karakeep’s — you can give it a fixed tag vocabulary and tell it to pick from that list, which produces a tag set a human would recognise. Karakeep’s free-form tagging produces a long tail of near-duplicates (“homelab”, “home-lab”, “home lab”) that you will eventually go and merge by hand.

Both do RSS subscriptions now, so a feed can pour straight into your archive. That is a lovely idea and a fast route to 10,000 links you have never read, and I turned it off in both.

The Things That Actually Decided It

Two months in, here is what separated them, in the order that mattered to me.

Editing what the machine got wrong. Both let you fix a tag. Karakeep makes it a per-item chore, which at 1,800 items means the wrong tags stay wrong forever. Linkwarden’s fixed-vocabulary approach means fewer errors reach you in the first place. This is the quiet advantage of constraining a model instead of trusting it.

Mobile. Karakeep has proper native apps on both platforms with a share sheet target. Linkwarden has a PWA and a mobile app that is younger and thinner. Ninety per cent of my saves happen on a phone while reading something. This is not a close contest and it is the single biggest practical difference between the two.

Search. Meilisearch is a real search engine and it feels like one — typo-tolerant, instant, ranked sensibly. Linkwarden’s search is competent Postgres querying. At 1,800 items you notice; at 200 you would not. Karakeep pays for this with an extra container and about 300 MB of RAM.

Resource cost. Karakeep’s four containers idle at roughly 900 MB on my box, and the Chrome container spikes hard while crawling — give it a memory limit or a burst of RSS imports will make it the noisy neighbour on a small machine. Linkwarden sits around 500 MB and its Playwright work is more contained. Neither is heavy by 2025 standards; both are heavier than a bookmarks file.

Sharing. If you want to hand someone a curated reading list, Linkwarden does it and Karakeep does not. If you never do this, ignore the point entirely.

Bulk import. Both eat a browser HTML export. Both then queue every link for archiving, and both will hammer your network and your CPU for hours doing it. Import in batches of a few hundred unless you enjoy watching a fan.

Troubleshooting

Karakeep saves links with no content. The crawler cannot reach Chrome. Check that BROWSER_WEB_URL matches the service name and that the Chrome container survived — it OOMs quietly on small hosts and Karakeep just records an empty snapshot rather than shouting.

1
2
3
docker compose ps
docker compose logs --tail=50 chrome
curl -sS http://localhost:9222/json/version

Meilisearch refuses to start after an upgrade. Meilisearch changes its on-disk index format between minor versions and will not migrate silently. The fix is to dump and reindex, or to delete the index volume and let Karakeep repopulate it — your bookmarks live in SQLite, so the search index is disposable. Pinning the Meilisearch tag rather than tracking latest avoids the surprise entirely, which is the same lesson every latest tag eventually teaches.

Linkwarden archives are blank white PDFs. Playwright is being blocked, usually by a site behind a bot-detection layer. There is no clean fix. This is the failure mode I hit most, and it is worth knowing that it is roughly one link in twenty-five on my mix.

Neither one archives anything behind a login. Both browse anonymously. Every paywalled article you save becomes a permanent archive of a paywall notice. Check what you actually saved before you trust the archive to be there later.

Postgres connection refused on first boot. Linkwarden starts faster than Postgres and the retry logic is thin. A depends_on with a health check fixes it; a restart also fixes it, which is why people never notice the bug.

Getting Back Out Again

The question I ask of anything that holds my data is how I leave, and both answers here are acceptable with an asterisk.

Karakeep exports JSON through the API and its database is SQLite, so worst case you open the file and read your own rows:

1
2
docker compose exec karakeep sqlite3 /data/db.db \
  "SELECT url, title, createdAt FROM bookmarkLinks LIMIT 5;"

Linkwarden exports JSON from the settings page and its database is Postgres, so worst case is a pg_dump and an afternoon.

The asterisk is that neither export carries the archives. You get your URLs, titles and tags in a portable file, and the screenshots and PDFs — the entire reason you installed the thing — stay in a volume you have to copy separately. Back up the volume alongside the database dump or your “export” is a bookmarks list with extra steps. Both projects treat the archive directory as data rather than cache, which is correct, and both let you forget that on the way out.

Worth saying plainly: the archive volume grows faster than you expect. Four artefacts per link at Linkwarden’s settings, or a screenshot plus a full-page snapshot at Karakeep’s, works out around 1–3 MB per link on my mix. At 1,800 links that is a few gigabytes, and the RSS firehose would have made it tens. Watch it for a month before you decide where it lives.

Verdict

These tools have different customers and the labels on the tin are wrong.

Karakeep is for hoarders. You save fast, you save from your phone, you save more than you read, and you want a machine to impose retroactive order on the pile. The AI tagging is the whole product and the native apps make it a habit rather than a project. This is the one I kept.

Linkwarden is for curators. You save deliberately, you organise as you go, you share collections with other people, and you want the PDF because you have been burned by link rot and you intend to still have this in a decade. If any of those describe you, the mobile gap will not bother you, because you save from a desktop anyway.

Neither is bad. I ran both for two months looking for the flaw that would decide it and never found one — the decision came down to where my thumb was when I found the link.

The honest caveat for both: this is a 500-to-900 MB service and an hour of setup to solve a problem that a text file solves for free. What you are buying is the archive, and the archive is worth it only if you check it. Mine has rescued four dead pages in two months, which is roughly one useful thing a fortnight. Consider whether that clears your bar. If you already run a private search stack and a local model, the marginal cost is small and the answer is probably yes. If this would be your first container, start with the single-container Linkwarden setup and see whether the habit sticks before you build the machine.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.