Contents

Linkwarden: Self-Hosted Bookmarking for the Tab Hoarder

Save the page, not just the link — before it 404s forever

Contents

I have a confession that will surprise nobody who has ever borrowed my laptop: my browser at any given moment is holding somewhere north of two hundred open tabs. Each one is a promise to myself — “I’ll read this later” — and each one is a lie. Worse, the tabs I do eventually bookmark have a nasty habit of rotting. I click through six months later and find a 404, a parked domain, or a “this article has been removed” notice. The thing I wanted to keep is gone, and all my bookmark preserved was the gravestone.

Link rot is the quiet tax on a life lived online, and it’s getting steeper as more of what we read lives on platforms that reorganise, paywall, or simply delete their back catalogues. The fix I landed on is Linkwarden, and it has done more for my digital hoarding than any number of New Year’s resolutions to “tab less”.

What Linkwarden actually does

Advertisement

Most bookmark managers store a URL and a title. That is the bit that rots. Linkwarden stores the URL and a copy of the page — it archives whatever you save as a screenshot, a PDF, a full HTML capture, and a stripped-down readable version. So when the original inevitably vanishes, you still have the content. The link points at a tombstone; Linkwarden hands you the body.

On top of the archiving it does all the sensible organisational things you’d expect: collections (nested folders, basically), tags, and full-text search across the archived content — not just titles, but the actual words inside the saved pages. There’s a browser extension for one-click saving, and you can share collections or individual links publicly if you want to.

It’s open source, written by a small team, and — crucially for me — runs perfectly well on a modest home server.

Getting it running

Linkwarden ships as a Docker image and wants a PostgreSQL database alongside it. A docker-compose.yml is the path of least resistance. Here’s a trimmed-down version of what I run:

 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
services:
  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_USER: linkwarden
      POSTGRES_PASSWORD: change-me-to-something-long
      POSTGRES_DB: linkwarden
    volumes:
      - ./pgdata:/var/lib/postgresql/data

  linkwarden:
    image: ghcr.io/linkwarden/linkwarden:latest
    restart: always
    depends_on:
      - postgres
    environment:
      # Generate with: openssl rand -base64 32
      NEXTAUTH_SECRET: paste-a-real-random-secret-here
      NEXTAUTH_URL: https://links.example.com/api/v1/auth
      DATABASE_URL: postgresql://linkwarden:change-me-to-something-long@postgres:5432/linkwarden
    ports:
      - "3000:3000"
    volumes:
      - ./data:/data/data

A few things worth saying out loud, because the docs assume you already know them. NEXTAUTH_SECRET is not optional and not cosmetic — generate a real random value with openssl rand -base64 32 and never reuse it elsewhere. NEXTAUTH_URL must match the address you actually reach the app on; if you put it behind a reverse proxy with TLS (which you should), use the https:// hostname, not localhost. And the ./data volume is where the archives live, so put it somewhere with room to grow.

Bring it up with docker compose up -d, wait for Postgres to settle, hit the port, and create your admin account on first load.

One decision the compose file above quietly makes for you: it exposes port 3000 directly. Don’t leave it that way for anything beyond a first test. Linkwarden holds an archive of everything you’ve ever found interesting, which is a startlingly complete profile of you — put it behind a reverse proxy with real TLS, and ideally behind some form of authentication if it’s reachable from outside your own network. The NEXTAUTH_URL I set earlier is the https:// hostname precisely because the app is meant to live behind that proxy. If you’re already running a self-hosted stack, this slots in alongside the others; the same reverse-proxy and backup discipline you’d apply to Nextcloud on Linux applies here without modification.

Troubleshooting the setup

Advertisement

The two failures that swallow most first-time setups are both mundane and both worth naming so you don’t lose an evening to them.

The container starts, then dies, complaining it can’t reach the database. Nine times out of ten this is a startup-ordering race: depends_on waits for the Postgres container to start, not for Postgres to be ready to accept connections, and Linkwarden’s migration step runs before the database is listening. The restart: always in the compose file papers over this — the app crashes, Docker restarts it, and by the second or third attempt Postgres is up — but if it’s still looping after a minute, check that your DATABASE_URL password exactly matches POSTGRES_PASSWORD (a copy-paste mismatch here is the classic culprit) and that both containers are on the same Docker network, which the default compose network gives you for free.

Archiving silently does nothing. Linkwarden renders pages using a headless Chromium under the hood, and that needs both enough memory and the right shared-memory allocation. On a small VPS you’ll see saves complete but the screenshot and PDF icons never light up. The fix is usually to give the container more headroom — headless Chromium is memory-hungry — and, if you’re on a constrained host, to add shm_size: '1gb' to the Linkwarden service so Chromium doesn’t run out of /dev/shm mid-render. Check the container logs; a crashed browser process announces itself loudly if you’re actually reading them.

The workflow, in practice

Once it’s up the loop is genuinely pleasant. You paste a URL (or hit the browser extension), optionally drop it into a collection and slap a tag or two on it, and save. In the background Linkwarden goes off and renders the page, grabbing the screenshot, PDF, and readable copy. After a few seconds the entry shows little icons telling you which formats are ready.

I organise by collection — “Homelab”, “Recipes”, “Things to read on a train”, “Articles I’ll cite in an argument later” — and lean hard on tags for the cross-cutting stuff. The search is the part I underrated at first. Because it indexes the archived text, I can find a half-remembered article by a phrase from its third paragraph, even if I gave it a useless title at save time. That alone has retired about forty browser tabs.

A few workflow details that turned it from “neat” into “load-bearing” for me. Collections can be nested and, more importantly, shared — you can make a collection public and hand someone a link, which is a far saner way to send a colleague “here’s everything I’ve read on X” than pasting twenty URLs into a chat. Each shared link serves the archived copy too, so it doesn’t rot on their end either. There’s also an API and, if you like living on the frontier, an import path from browser bookmarks and from other read-later services like Pocket and Omnivore — worth doing early, before you’ve built up two archives in parallel and have to reconcile them.

The one habit I’d urge on anyone: save at the moment of interest, not later. The whole failure mode Linkwarden solves is “I’ll get to it,” and the browser extension exists precisely so that saving is a single click while the tab is still open. Deferring the save is how you end up back at two hundred tabs, except now with a bookmark manager you feel guilty about ignoring. Make the click reflexive and the tool does its job; treat it as a chore and it becomes just another pile.

The migration story deserves a mention because it’s the hidden reason self-hosting this pays off. Every read-later service that has ever shut down — and several have — took its users’ archives with it. Linkwarden’s data is yours, in your Postgres and your volume, in open formats. When the next hosted bookmarking startup folds, you’ll be the one who still has everything.

The honest trade-offs

This is not magic, and I’d be doing you a disservice to pretend otherwise.

The biggest one is storage. Every archived page is a screenshot plus a PDF plus HTML, and that adds up faster than you’d think — a few hundred saves and you’re into gigabytes. If you’re the kind of person Linkwarden is for, you will fill a disk. Plan for it, and consider whether you really need full PDF archiving on every single link.

Second, some sites resist archiving. Pages hidden behind logins, aggressive paywalls, or heavy anti-bot measures will save as a screenshot of a cookie banner or a “please verify you are human” wall. The readable copy is your friend here, but it isn’t infallible. Sites that render everything client-side can also produce patchy captures, and increasingly common bot-detection services will serve the headless browser a challenge page instead of the article. There’s no clean fix for this — it’s the same arms race that breaks every archiving tool — but it’s worth knowing so you’re not surprised when a paywalled piece saves as a subscription prompt. For those, the screenshot at least records that the page existed, even if it can’t record what it said.

And as ever with self-hosting: this is now a thing you own. Backups of the Postgres database and the data volume are your responsibility, and — this is the part people forget — they have to be backed up together and consistently. The database rows point at files in the data volume; restore one without the other and you get an archive full of dangling references, which is somehow more dismal than plain link rot. Snapshot both at the same moment, pg_dump the database on a schedule, and test a restore at least once so you find the gotchas in a calm moment rather than a crisis.

The other quiet responsibility is knowing when it breaks. A bookmark archive is the kind of service you touch weekly but never think about — which means when the container silently stops archiving, or Postgres fills its disk, you won’t notice until you go looking for something and find nothing. This is exactly the sort of low-attention self-hosted service that benefits from being pointed at a simple uptime monitor; I run everything of this shape behind Uptime Kuma so a dead container pings me before I discover it the hard way. Archiving software you can’t tell has stopped is worse than useless, because it lulls you into trusting it.

The verdict

Linkwarden is for the chronic tab-hoarder, the researcher, the person who has been burned one too many times by a dead bookmark. If your bookmarking needs end at “save this URL”, a browser already does that for free and you should stop reading. But if you actually want to keep the things you save — to outlive the websites they came from — this is the tool. It’s been the rare piece of self-hosted software that made me change a bad habit rather than just enabling it. My tab count is down, my archive is up, and the next time a site 404s on me, I’ll be the one who still has the page.

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.