Use case · Bookmark archive

Karakeep self hosted on a dedicated server: Compose, AI tagging, backups

Self-hosting Karakeep means running three containers from Karakeep's published compose file (the web app, a headless Chrome crawler and Meilisearch) with a four-line .env, then creating the admin account privately before anything faces the internet. Karakeep is the app formerly called Hoarder. It saves links, notes, images and PDFs, fetches each page with that Chrome instance, and tags the content with an LLM if you give it one: OpenAI or a local Ollama.

See the machines

What the stack is made of

The compose file in the karakeep-app/karakeep repository defines three services. Only web publishes a port; chrome and meilisearch are reached by name on the Compose network (BROWSER_WEB_URL: http://chrome:9222, MEILI_ADDR: http://meilisearch:7700).

ServiceImageJobPersistent state
webghcr.io/karakeep-app/karakeepUI, API, background workersvolume data at /data: the database file db.db and crawled assets
chromeghcr.io/karakeep-app/karakeep-chromeLoads each saved URL for text, images and screenshotsnone
meilisearchgetmeili/meilisearch (pinned version)Full-text search indexvolume meilisearch at /meili_data

The docs publish no minimum hardware, so there is no official sizing table to quote. What grows is disk. Screenshots are stored by default; full-page screenshots (CRAWLER_FULL_PAGE_SCREENSHOT) and full local page copies (CRAWLER_FULL_PAGE_ARCHIVE) are off by default because, in the docs' words, they can lead to much higher disk usage. Uploads are capped by MAX_ASSET_SIZE_MB, default 50. If you turn archiving on, size the disk for it.

Choosing the machine

With OpenAI doing the tagging, Karakeep is a one-job box, which is what the Moonshot cartridges on our dedicated server range are for: one small server per slot, a whole machine each. Pick an m510 or m710x cartridge, or any racked single- or dual-socket server, if you want an out-of-band console to recover from a firewall mistake; the remaining cartridges offer remote power control and no screen. Skip the X-Gene ARM line: it boots only Ubuntu 16.04 and 18.04, and our own view is that those releases are too old to start a new Docker stack on.

Tagging locally with Ollama changes the sizing, because the model wants RAM or a GPU of its own. The desktop line (Core i5-9500 plus a 6 GB RTX 3050) is the one listed for small AI models; our Ollama page covers the runtime itself. Racked machines are handed over in about 30 minutes once payment is approved; built-to-order lines take 4 to 24 hours. On every line the port is unmetered and the login is root.

Installing it: firewall first, app on loopback

Install Docker Engine and the Compose plugin first (the docs require both). We ask for Docker Engine 28 or later, because from that version a port published on 127.0.0.1 is reachable only from the host itself (moby 45610); that requirement is ours, not the docs'. The block below turns ufw on with SSH only, fetches the official compose file, changes its 3000:3000 mapping to 127.0.0.1:3000:3000, writes the .env the docs ask for with generated secrets, and starts the stack. The loopback binding is our change too: a mapping with no address is published on every host address (0.0.0.0 and [::]), and Docker's published ports skip ufw's rules on IPv4 and, once Docker's own IPv6 support is enabled, on IPv6 too.

sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw --force enable
mkdir -p ~/karakeep-app && cd ~/karakeep-app
wget https://raw.githubusercontent.com/karakeep-app/karakeep/main/docker/docker-compose.yml
sed -i 's/- 3000:3000/- 127.0.0.1:3000:3000/' docker-compose.yml
grep -n '3000:3000' docker-compose.yml
umask 077
cat > .env <<EOF
KARAKEEP_VERSION=release
NEXTAUTH_SECRET=$(openssl rand -base64 36)
MEILI_MASTER_KEY=$(openssl rand -base64 36 | tr -dc 'A-Za-z0-9')
NEXTAUTH_URL=http://localhost:3000
EOF
sudo docker compose up -d

The grep line must print 127.0.0.1:3000:3000. If it prints the bare mapping, the upstream file has changed: edit the ports entry by hand before going on. The .env holds both secrets; umask 077 makes it readable by your user only. The Meilisearch key is generated the way the configuration page suggests, letters and digits only.

The admin account, before exposure

Karakeep promotes the first account that signs up to administrator, so create it while the app is still on loopback. The first line of the next block runs on your own computer and forwards port 3000 through your own SSH login. With it open, browse to http://localhost:3000, sign up, and confirm that Admin Settings appears. The remaining lines run on the server afterwards.

Then close sign-ups and point the app at its public name. DISABLE_SIGNUPS=true disables new sign-ups and the button for them; an administrator can still create users from the Users List in Admin Settings. NEXTAUTH_URL must be the address people will use: Karakeep builds email links and the OAuth callback from it, and an https value makes the session cookies secure-only. The docs say to re-run docker compose up after every .env change.

# on your own computer; leave it open while you sign up
ssh -N -L 3000:127.0.0.1:3000 you@your-server
# then on the server
cd ~/karakeep-app
echo 'DISABLE_SIGNUPS=true' >> .env
sed -i 's|^NEXTAUTH_URL=.*|NEXTAUTH_URL=https://karakeep.example.com|' .env
sudo docker compose up -d
sudo ufw allow 80,443/tcp

The reverse proxy is not covered by the Karakeep docs, so this part is our arrangement. First point an A record (and an AAAA record, if the server has IPv6) for karakeep.example.com at the server and wait until it resolves; with that done and 80 and 443 open, a certificate can be issued. Run the proxy on the host, terminating TLS and forwarding to 127.0.0.1:3000. With Caddy that is a site block for the hostname holding one reverse_proxy 127.0.0.1:3000 line, and Caddy obtains the certificate itself. With nginx, get the certificate yourself once DNS resolves, with certbot for example, and raise client_max_body_size to at least MAX_ASSET_SIZE_MB, or larger uploads fail at the proxy. A proxy on the host is an ordinary process, which is why the ufw rule above opens 80 and 443 on IPv4 and IPv6.

Keep the account list closed. The security page explains why: every saved URL is fetched by the Chrome container from your server's address, so a stranger with an account could make your IP visit any site or probe internal endpoints. Karakeep has basic SSRF protections, and the docs say they cover only some of the risk.

AI tagging with OpenAI or Ollama

Tagging needs either OPENAI_API_KEY or OLLAMA_BASE_URL; with neither, the docs say tagging is simply skipped. For OpenAI, add the key to .env; sudo docker compose up -d applies it. The tagging-costs page names the default models and gives rough cost estimates; check them there rather than here.

  • Ollama through its OpenAI-compatible endpoint (the docs' recommended option): OPENAI_API_KEY=ollama, OPENAI_BASE_URL=http://OLLAMA_HOST:11434/v1, plus INFERENCE_TEXT_MODEL and INFERENCE_IMAGE_MODEL set to models you have pulled (the docs' examples are gemma3 and llava).
  • Ollama's native API: OLLAMA_BASE_URL=http://OLLAMA_HOST:11434 with OPENAI_API_KEY unset, since the OpenAI key takes precedence.
  • The address must be reachable from inside the web container. localhost there means the container itself; the docs list a different Docker network as a common cause of failed tagging.
  • INFERENCE_CONTEXT_LENGTH defaults to 2048 tokens, which the docs call quite small; content past it is truncated before tagging. Raising it gives better tags at a higher cost per bookmark, in money on OpenAI and in compute on Ollama.
  • INFERENCE_JOB_TIMEOUT_SEC defaults to 30 seconds. The docs suggest raising it when Ollama runs without a powerful GPU.

Whichever endpoint you set receives bookmark content for tagging. If nothing happens, sudo docker compose logs web usually names the cause, such as a mistyped variable name.

Extensions and phone apps

The docs link an official Chrome extension, a Firefox add-on, an iOS app and an Android app; the quick-sharing page also lists a community-built Safari extension for macOS and iOS. The docs say no more about them than where to get them. Our reading is that they need your instance's public https address, so set them up after the proxy works.

Backups, restore checks and updates

The data volume is the one to keep: it holds db.db and the stored assets. Stopping the stack during the copy is our choice, so the database file is not caught mid-write. Compose names the volume after the directory; sudo docker volume ls shows the exact name.

cd ~/karakeep-app
mkdir -p -m 700 ~/karakeep-backups
sudo docker compose stop
sudo docker run --rm -v karakeep-app_data:/data:ro -v ~/karakeep-backups:/backup alpine tar czf /backup/data-$(date +%F).tar.gz -C /data .
sudo docker compose start
tar tzf ~/karakeep-backups/data-$(date +%F).tar.gz | grep db.db

That last line proves only that the archive reads and contains the database. It does not prove Karakeep will open it. The real test is restoring onto a spare machine, as follows. Copy the archives off the server yourself, for example with scp from your own computer.

  1. On the spare machine, put the compose file (with the 127.0.0.1 edit) and your original .env in ~/karakeep-app, without starting anything, and copy the archive into ~/karakeep-backups.
  2. Name the archive and check it is there: ARCHIVE=~/karakeep-backups/data-2026-10-23.tar.gz (your file's date), then sudo test -s "$ARCHIVE" && echo found || echo MISSING. Continue only on found.
  3. cd ~/karakeep-app && sudo docker compose down
  4. VOL=karakeep-app_data (the name docker volume ls showed on the old server), then sudo docker volume create "$VOL". Compose may warn later that it did not create this volume; it still uses it.
  5. sudo docker run --rm -v "$VOL":/data alpine ls -A /data must print nothing; if it lists files, stop, because the volume is not empty.
  6. sudo docker run --rm -v "$VOL":/data -v "$ARCHIVE":/archive.tar.gz:ro alpine tar xzf /archive.tar.gz -C /data
  7. sudo docker compose up -d; then sign in and run Reindex All Bookmarks so search matches the restored library.

With KARAKEEP_VERSION=release, updating is sudo docker compose up --pull always -d. With a pinned version, edit the number in .env, then sudo docker compose up -d. Updating Karakeep does not move the pinned Meilisearch image, and the troubleshooting page advises against changing it without good reason. If a Meilisearch version change leaves the index unreadable, with a message that the database version is incompatible, that page's fix is: stop the Meilisearch container, erase or rename data.ms in its volume, start it again, then click Reindex All Bookmarks under Admin Settings, Background Jobs. A Hoarder install still pulling ghcr.io/hoarder-app/hoarder may not get updates; the migration page swaps it for ghcr.io/karakeep-app/karakeep.

Licence and limits

Karakeep is published under AGPL-3.0. Our general reading of the licence (not legal advice): you may run and modify it. If you pass copies to anyone, changed or not, you must make the corresponding source available to them. And if you let other people use a modified copy over a network, the licence expects you to offer them that modified source.

One machine is a single point of failure for your library, and nothing here replicates it. If the archive matters, keep the off-machine backups current and practise the restore. For several self-hosted tools on one box, the Linkwarden write-up and the Meilisearch page cover neighbouring setups.

Questions

Is Hoarder the same software as Karakeep?

Yes. The docs describe Karakeep as previously Hoarder. Existing installs should change their image from ghcr.io/hoarder-app/hoarder to ghcr.io/karakeep-app/karakeep, because the old image may stop receiving updates. Installs made with the Debian/Ubuntu script have a migrate option.

Does Karakeep need a GPU?

Not for itself. With OpenAI handling the tags, no local model runs at all. A GPU only matters if you run Ollama for local tagging, and how much depends on the models you choose.

Which .env values are required?

The minimal file in the docs has KARAKEEP_VERSION, NEXTAUTH_SECRET, MEILI_MASTER_KEY and NEXTAUTH_URL. Replace both secrets with random strings and set NEXTAUTH_URL to the address users will type. After editing it, bring the stack up again with Compose.

How do I reset the admin password?

If SMTP is configured, the sign-in page has a reset link. Otherwise the FAQ describes stopping Karakeep and updating the password hash in db.db with sqlite3. That is another argument for copying the data volume before you start.

Can I let friends sign up?

You can, but the docs warn that every user can make your server fetch arbitrary URLs. Keep DISABLE_SIGNUPS=true and create accounts for people you trust from Admin Settings.

What do I get with the server?

Root on the entire machine, an unmetered network port, and an out-of-band console where the model has one: racked single-, dual- and quad-socket servers, plus m510 and m710x cartridges. The dedicated range lists every line with its current price.

A whole machine for your bookmark archive

Pick a line, take root, and run the Compose stack on hardware nobody else uses.

See the machines

Related on Server Room

Other use cases