Published

Linkwarden

Collaborative bookmark manager that keeps a copy of every saved page: a screenshot, a PDF, a self-contained HTML file and a readable text view, with collections, tags, highlights and full-text search. The self-hosted stack is the app (web, background worker and a headless Chromium in one container), PostgreSQL and Meilisearch.

Resources by load level

What the app itself needs at each level, next to what we recommend for a server that runs only this app.

LevelWhat it meansApp requirementsRecommended
LightEstimateUp to 3 users, ~2,000 saved links with the default formats, ~20 new links a day; preservation at the default 5 links per batch, full-text search on.2.25 vCPU · 3 GiB RAM · 17.75 GiB disk2.82 vCPU · 5.1 GiB RAM · 29.958 GiB disk
MediumEstimateUp to 10 users, ~10,000 saved links, ~100 new links a day and occasional imports of a few thousand bookmarks; full-text search over all of them.3 vCPU · 4.5 GiB RAM · 52.5 GiB disk3.72 vCPU · 6.9 GiB RAM · 69.92 GiB disk
HeavyEstimateUp to 30 users, ~25,000 saved links, ~300 new links a day with regular imports and RSS feeds; the worker keeps 5 pages preserving at once.4 vCPU · 7.5 GiB RAM · 118.5 GiB disk4.92 vCPU · 10.5 GiB RAM · 145.82 GiB disk

Recommended = app requirements + system baseline + safety headroom, under sizing policy 2026.10.0. Options stay at their defaults. “Estimate” means the sources give no number for that case, so it is worked out from them by the method stated under Components. How the numbers are built

Size it in the Stack Sizer

Ready-to-run files

Support level Supported

Compose template from the official sources; not yet verified by a run.

Create docker-compose

Components

What each component needs at each load level, before baseline and headroom.

vCPU / GiB RAM / GiB disk
ComponentLightMediumHeavySources
Linkwarden (web, worker and headless Chromium) (app)1 / 1.5 / 101.5 / 2 / 412 / 2.5 / 100[11][12][13][19][29]
PostgreSQL (database)1 / 1 / 6.51 / 1 / 6.51 / 1 / 6.5[13][23][33][39][40][41][42][43][44][45]
Meilisearch (full-text search) (other)0.25 / 0.5 / 1.250.5 / 1.5 / 51 / 4 / 12[12][22][34][36]
How the estimates are worked out
  • Linkwarden (web, worker and headless Chromium) · Light: RAM: ~350 MB idle (docs) plus a headless Chromium rendering up to 5 pages per batch and two Node processes (web, worker); 1.5 GiB. vCPU: one core for page rendering, PDF and HTML capture. Disk: image 1,713,869,312 B unpacked (1.596 GiB) rounded up to 1.75 GiB + 2,000 links × an assumed 4 MiB of preserved files (7.8 GiB), rounded up to 10 GiB.
  • Linkwarden (web, worker and headless Chromium) · Medium: Imports keep the worker at its default 5 links per batch for hours, so more than light: 2 GiB RAM and 1.5 vCPU, within the docs' 4 GB / 2-core machine for the whole stack. Disk: 1.75 GiB image + 10,000 links × an assumed 4 MiB of preserved files (39.1 GiB), rounded up to 41 GiB.
  • Linkwarden (web, worker and headless Chromium) · Heavy: The docs' 4 GB / 2-core machine minus PostgreSQL (1 GiB) and a small Meilisearch (0.5 GiB) leaves ~2.5 GiB for Linkwarden at its defaults, which this level uses all the time: 2 vCPU / 2.5 GiB. Disk: 1.75 GiB image + 25,000 links × an assumed 4 MiB of preserved files (97.7 GiB), rounded up to 100 GiB.
  • PostgreSQL · Light: Embedded metadata DB with the image's default config: equals the PostgreSQL profile's light preset (≤ 5 GB data, ≤ 20 concurrent connections) at every load level. Light preset method: shared_buffers 25% of RAM, worst-case work_mem/maintenance_work_mem budget, 0.5 GiB image + 5 GiB data + 1 GiB WAL.
  • PostgreSQL · Medium: Readable text is stored per link in PostgreSQL (the main database growth, per the docs): 10,000 links × an assumed 50 KB = 0.5 GB, inside the sidecar's 5 GB. Prisma 6 opens physical CPUs × 2 + 1 connections per process (web, worker): 18 on a 4-core host, inside 20. Light preset method: shared_buffers 25% of RAM, 0.5 GiB image + 5 GiB data + 1 GiB WAL.
  • PostgreSQL · Heavy: 25,000 links × an assumed 50 KB of readable text and rows = 1.25 GB, still inside the sidecar's 5 GB; set TEXT_CONTENT_LIMIT to cap it. Two Prisma pools (web, worker) of physical CPUs × 2 + 1 stay within 20 connections up to 4 cores. Light preset method: shared_buffers 25% of RAM, 0.5 GiB image + 5 GiB data + 1 GiB WAL.
  • Meilisearch (full-text search) · Light: Each link is indexed as a document with its readable text (assumed ~20 KB): 2,000 × 20 KB = 40 MB raw. Meilisearch's measured example grew 9.1 MB of JSON to 224 MB on disk (~25×), so ~1 GB index. RAM: 0.5 GiB, above the ~305 MB of that example. Disk: 91.3 MiB compressed image × 3 (0.27 GiB) + 0.92 GiB index, rounded up to 1.25 GiB.
  • Meilisearch (full-text search) · Medium: 10,000 links × ~20 KB = 200 MB raw, ~25× on disk as in Meilisearch's measured example: 4.6 GiB index. RAM: 1.5 GiB, about a third of the index, the ratio the docs say does not materially hurt performance. Disk: 0.27 GiB image + 4.6 GiB index, rounded up to 5 GiB.
  • Meilisearch (full-text search) · Heavy: 25,000 links × ~20 KB = 500 MB raw, ~25× on disk as in Meilisearch's measured example: 11.4 GiB index. RAM: 4 GiB, about a third of the index (the docs' ratio). vCPU: one core for steady indexing in batches of 50 links. Disk: 0.27 GiB image + 11.4 GiB index, rounded up to 12 GiB.

Ports

Public ports are meant to be reached from outside the stack, usually through a reverse proxy; internal ones stay on the stack's own network.

ComponentPortExposurePurposeSources
Linkwarden (web, worker and headless Chromium)3000/tcpPublic, through a proxyWeb UI, REST API, browser extension and mobile app (HTTP; put behind TLS)[2][4]
PostgreSQL5432/tcpInternal onlyPostgreSQL connections from the web and worker processes[2][44]
Meilisearch (full-text search)7700/tcpInternal onlyMeilisearch API (indexing by the worker, searches by the web app)[6][7]

Every port in the catalog, with conflicts between apps

User and permissions (UID/GID)

The user each container's process runs as, and the paths it writes to. A bind mount for one of these paths must be writable by that UID:GID.

ComponentUID:GIDUserStarts as rootWrites toSources
Linkwarden (web, worker and headless Chromium)Derived0:0rootNo/data/data/data/apps/web/.next/cache[4][5][14]
PostgreSQL999:999postgresYes, prepares its directories, then drops to this user/var/lib/postgresql/var/run/postgresql[37][38][44]
Meilisearch (full-text search)Derived0:0rootNo/meili_data[7][8]
  • Linkwarden (web, worker and headless Chromium): The Dockerfile sets no USER, so web, worker and Chromium run as root and never drop privileges; the docs confirm this and show user: 1000:1000 with pre-owned folders as the alternative. Preserved files go to /data/data; the Next.js image cache is pre-created world-writable.
  • Meilisearch (full-text search): The Meilisearch Dockerfile sets no USER and starts /bin/meilisearch under tini, so it runs as root; its database lives in /meili_data/data.ms.

Storage growth

How much each component's data grows per month at each load level, on top of the disk in the profile. A value marked Assumption is ANAROQ's stated assumption, not a figure from the sources; the notes say what it rests on.

GiB per month
ComponentLightMediumHeavySources
Linkwarden (web, worker and headless Chromium)Assumption2.34411.71935.156[12][19]
PostgreSQLAssumption0.030.140.42[13]
Meilisearch (full-text search)Assumption0.281.44.2[22][34]
What the growth figures rest on
  • Linkwarden (web, worker and headless Chromium): New links × 30 days × an assumed 4 MiB of preserved files per link (screenshot, PDF, HTML and readable text, all on by default): 20, 100 and 300 links a day. Nothing expires: files are removed only with their link. Each format is capped at 100 MB (*_MAX_BUFFER).
  • PostgreSQL: New links × 30 days × an assumed 50 KB of readable text and rows per link; the docs name stored readable text as the main database growth, which TEXT_CONTENT_LIMIT caps (unlimited by default).
  • Meilisearch (full-text search): New links × 30 days × ~20 KB per indexed link × ~25 (the on-disk growth in Meilisearch's measured example). LMDB never returns freed space to the system, so the index does not shrink when links are deleted.

Upgrade and rollback

How to move to a new version safely, and how to go back if it fails.

Back up first

Migrations cannot be undone: first take a pg_dump of the linkwarden database and an archive of the data volume (preserved files) and the .env. [17]

Upgrade steps

  1. Read the release notes of every version between yours and the new one: breaking changes are called out there. [17]
  2. Change the pinned linkwarden tag, then run docker compose pull and docker compose up -d. Migrations run on their own before the app listens; follow docker compose logs -f linkwarden until it is ready. [4][17]
  3. Change the Meilisearch tag only to the one the new release's docker-compose.yml pins, and migrate its database with a dump or a dumpless upgrade: a Meilisearch database only works with the version that created it. [24][25][35]DerivedLinkwarden's guide covers its own image only; the Meilisearch step follows from upstream's compose bumping Meilisearch inside 2.16.x and from the Meilisearch upgrade guide.

Roll back

Restore the database dump from before the upgrade and pin the image back to the previous tag. Both are needed: an older release cannot read a schema a newer migration has changed. [17]

Known breaking changes

  • In 2.16.1: The official compose moved Meilisearch from v1.12.8 to v1.13.3. A Meilisearch database only opens in the version that created it, so an existing index needs a dump or a dumpless upgrade. [24][25][35]
  • In 2.15.0: DISABLE_PRESERVATION was renamed DISABLE_BROWSER; an old .env silently keeps preservation on. The image was rewritten (about 1.5 GB instead of 3 GB, ~350 MB idle). [13][17][26][27]
  • In 2.0.0: NEXTAUTH_URL must end in /api/v1/auth, and instances coming from v1 run a one-time migration script. [18]

Limits

  • Preservation runs a headless Chromium inside the app container, 5 links per batch with up to 5 minutes per page. On a small server set ARCHIVE_TAKE_COUNT=1, turn off the PDF and Webpage formats, or set DISABLE_BROWSER=true for bookmarks only. [12][13]
  • The server visits pages as an anonymous visitor: logins, paywalls and bot blocks end up in the copy, and private-network addresses are refused by default. Capture such pages from your own browser with the extension or SingleFile. [12][19]
  • Each collection owner can store up to 30,000 links (MAX_LINKS_PER_USER), uploads are capped at 10 MB and each generated format at 100 MB. Files stay on the server's disk; S3-compatible storage needs SPACES_* settings the template doesn't write. [12][19]

Security

  • Serve Linkwarden over HTTPS from a reverse proxy and set NEXTAUTH_URL to https://<domain>/api/v1/auth; sign-in and OAuth callbacks are built from it. [12][15]
  • Registration is open by default and the first account becomes the administrator. Create it right after the first start, then set NEXT_PUBLIC_DISABLE_REGISTRATION=true; the administrator can still add users. [12][20]
  • The archiving Chromium opens untrusted pages without its own sandbox (Playwright's default) as root, so the container is the only boundary. The template grants no extra capabilities; the docs show how to run as user 1000:1000. [14][21][32]
  • Preserved HTML is served from the app's own origin unless NEXT_PUBLIC_USER_CONTENT_DOMAIN points at a second domain. For untrusted pages, add that domain at the proxy as the docs describe. [16]

Backup

  • Back up three things together: a pg_dump of the database, the data volume with the preserved files, and the .env (NEXTAUTH_SECRET and the passwords). [17]
  • Keep the Meilisearch volume too: PostgreSQL records which links are already indexed, so an empty index is not refilled on its own and full-text search would miss older links. [8][22]

Latest release

Newest upstream release
Linkwarden 2.16.3 [17][30]
This profile was checked against

Linkwarden 2.16.3

This profile was checked against the newest release.

Checked on
14 Oct 2026
State
Checked within the last 90 days
Where releases are published
GitHub Releases

Latest releases of every app

Version

Profile status
Published
Checked against release
Linkwarden 2.16.3
Profile version
1.0.0
Figures verified
14 Oct 2026
Catalog release
2026.10.10
Sizing policy
2026.10.0
Compose file
Unverified
Official site
linkwarden.app

Sources

  1. Docker Docs: Control startup order in Compose (pg_isready healthcheck, service_healthy) (Official docs · read 11 Oct 2026)
  2. linkwarden repository: docker-compose.yml at v2.16.3 (postgres:16-alpine, linkwarden:latest on 3000 with ./data:/data/data, getmeili/meilisearch:v1.13.3; env_file .env for all three) (Official Compose · read 14 Oct 2026)
  3. linkwarden repository: .env.sample at v2.16.3 (NEXTAUTH_URL, NEXTAUTH_SECRET, POSTGRES_PASSWORD, MEILI_HOST, MEILI_MASTER_KEY, NEXT_PUBLIC_DISABLE_REGISTRATION) (Official Compose · read 14 Oct 2026)
  4. linkwarden repository: Dockerfile at v2.16.3 (node:22.23-bookworm-slim, no USER, Chromium headless shell, prisma migrate deploy at start, HEALTHCHECK curl http://127.0.0.1:3000/, EXPOSE 3000) (Official repository · read 14 Oct 2026)
  5. linkwarden repository: packages/filesystem/createFolder.ts at v2.16.3 (files under STORAGE_FOLDER, default 'data', two levels above apps/web: /data/data) (Official repository · read 14 Oct 2026)
  6. linkwarden repository: packages/lib/meilisearchClient.ts at v2.16.3 (client only when MEILI_MASTER_KEY is set; MEILI_HOST default http://meilisearch:7700) (Official repository · read 14 Oct 2026)
  7. meilisearch repository: Dockerfile at v1.13.3 (alpine:3.20 with curl, no USER, WORKDIR /meili_data, EXPOSE 7700, no HEALTHCHECK) (Official repository · read 14 Oct 2026)
  8. Meilisearch Docs: Docker (data in /meili_data, MEILI_MASTER_KEY, dumps in /meili_data/dumps) (Official docs · read 14 Oct 2026)
  9. Meilisearch Docs: Health (GET /health returns {"status": "available"}) (Official docs · read 14 Oct 2026)
  10. Meilisearch Docs: Basic security (the master key must be at least 16 bytes) (Official docs · read 14 Oct 2026)
  11. Linkwarden Docs: Setup (4 GB memory comfortable, any 2-core machine, ~1.5 GB image plus data; required NEXTAUTH_SECRET, POSTGRES_PASSWORD, MEILI_MASTER_KEY; Postgres 16 and up) (Official docs · read 14 Oct 2026)
  12. Linkwarden Docs: Environment variables (ARCHIVE_TAKE_COUNT 5, BROWSER_TIMEOUT 5 min, *_MAX_BUFFER 100 MB, MAX_LINKS_PER_USER 30000, ALLOW_PRIVATE_NETWORK_ACCESS false) (Official docs · read 14 Oct 2026)
  13. Linkwarden Docs: Running a lighter setup (idle ~350 MB since 2.15, image ~1.5 GB, Meilisearch optional, DISABLE_BROWSER, ARCHIVE_TAKE_COUNT=1, textContent is the main DB growth) (Official docs · read 14 Oct 2026)
  14. Linkwarden Docs: Running as a non-root user (containers run as root by default; user: 1000:1000 with pre-owned data folders) (Official docs · read 14 Oct 2026)
  15. Linkwarden Docs: Reverse proxy (NGINX with TLS, WebSocket upgrade headers, NEXTAUTH_URL=https://<host>/api/v1/auth) (Official docs · read 14 Oct 2026)
  16. Linkwarden Docs: User content domain (serve preserved HTML from a separate origin via NEXT_PUBLIC_USER_CONTENT_DOMAIN) (Official docs · read 14 Oct 2026)
  17. Linkwarden Docs: How to upgrade (back up with pg_dump and tar of data and .env, pull and up -d, migrations at start, pin a tag to roll back; DISABLE_PRESERVATION is now DISABLE_BROWSER) (Official docs · read 14 Oct 2026)
  18. Linkwarden Docs: Upgrading to Linkwarden v2 (NEXTAUTH_URL must end in /api/v1/auth; migration script) (Official docs · read 14 Oct 2026)
  19. Linkwarden Docs: Preservation (readable, webpage, screenshot and PDF formats on by default; queued worker; files stored per collection on disk or S3) (Official docs · read 14 Oct 2026)
  20. linkwarden repository: postUser.ts at v2.16.3 (with NEXT_PUBLIC_DISABLE_REGISTRATION=true only the admin, user NEXT_PUBLIC_ADMIN or 1, can create users) (Official repository · read 14 Oct 2026)
  21. linkwarden repository: apps/worker/lib/browser.ts at v2.16.3 (chromium.launch with Playwright's default options, or connectOverCDP with PLAYWRIGHT_WS_URL) (Official repository · read 14 Oct 2026)
  22. linkwarden repository: apps/worker/workers/linkIndexing.ts at v2.16.3 (whole link rows sent to the Meilisearch index 'links' in batches; indexVersion kept in PostgreSQL) (Official repository · read 14 Oct 2026)
  23. linkwarden repository: packages/prisma/package.json at v2.16.3 (Prisma 6) (Official repository · read 14 Oct 2026)
  24. linkwarden repository: docker-compose.yml at v2.16.0 (getmeili/meilisearch:v1.12.8) (Official Compose · read 14 Oct 2026)
  25. linkwarden repository: docker-compose.yml at v2.16.1 (getmeili/meilisearch:v1.13.3) (Official Compose · read 14 Oct 2026)
  26. linkwarden repository: .env.sample at v2.14.0 (DISABLE_PRESERVATION) (Official repository · read 14 Oct 2026)
  27. linkwarden repository: .env.sample at v2.15.0 (DISABLE_BROWSER replaces it) (Official repository · read 14 Oct 2026)
  28. linkwarden repository: LICENSE.md at v2.16.3 (GNU AGPL v3) (Official repository · read 14 Oct 2026)
  29. GHCR manifest of ghcr.io/linkwarden/linkwarden:v2.16.3 (linux/amd64): 567,246,590 B compressed; layers decompressed and counted: 1,713,869,312 B (Measurement · read 14 Oct 2026)
  30. GHCR tags of ghcr.io/linkwarden/linkwarden (newest release v2.16.3, built 2026-09-09 from git tag v2.16.3 = 474dd11) (Official repository · read 14 Oct 2026)
  31. Linkwarden: Pricing (Linkwarden Cloud, hosted by the Linkwarden team; per-user plan) (Official docs · read 14 Oct 2026)
  32. Playwright Docs: BrowserType.launch (chromiumSandbox defaults to false) (Official docs · read 14 Oct 2026)
  33. Prisma Docs: Connection pool (Prisma 6 default pool size: physical CPUs × 2 + 1 per client) (Official docs · read 14 Oct 2026)
  34. Meilisearch Docs: Storage (LMDB; 9.1 MB of raw JSON measured at 224 MB on disk and ~305 MB RAM; RAM-to-disk ~1/3 fine; disk use never shrinks) (Official docs · read 14 Oct 2026)
  35. Meilisearch Docs: Update to the latest version (a database only works with the version that created it; dump or dumpless upgrade) (Official docs · read 14 Oct 2026)
  36. Docker Hub: getmeili/meilisearch tag v1.13.3 (linux/amd64 95,776,018 B compressed) (Official repository · read 14 Oct 2026)
  37. docker-library/postgres: 18/trixie Dockerfile (postgres uid/gid 999, VOLUME /var/lib/postgresql) (Official repository · read 14 Oct 2026)
  38. docker-library/postgres: 18/trixie docker-entrypoint.sh (chown PGDATA, then gosu postgres) (Official repository · read 14 Oct 2026)
  39. PostgreSQL 18 documentation: 19.4 Resource Consumption (Official docs · read 10 Oct 2026)
  40. PostgreSQL 18 documentation: 19.3 Connections and Authentication (Official docs · read 10 Oct 2026)
  41. PostgreSQL 18 documentation: 19.10 Vacuuming (autovacuum settings) (Official docs · read 10 Oct 2026)
  42. PostgreSQL 18 documentation: 19.5 Write Ahead Log (max_wal_size) (Official docs · read 10 Oct 2026)
  43. PostgreSQL 18 documentation: 51.2 How Connections Are Established (Official docs · read 10 Oct 2026)
  44. Docker Official Image postgres: documentation (docker-library/docs) (Official docs · read 10 Oct 2026)
  45. Docker Hub: library/postgres tag 18.6 (compressed image sizes) (Official repository · read 10 Oct 2026)

All self-hosted apps