Linkwarden archives every bookmark as a screenshot, PDF and HTML file, and asks you to run Postgres and Meilisearch for it
⚡️⚡️⚡️ Self-hosted collaborative bookmark manager to collect, read, annotate, and fully preserve what matters, all in one place.
At a glance
- What is it?
- Linkwarden is an AGPL-3.0 bookmark manager that stores its own copy of each page it saves. This piece looks at how that capture works, what the Docker stack and .env file demand, and where the project leaves gaps you have to fill.
- Who is it for?
- Linkwarden fits people and small teams who want saved links to survive link rot and are willing to run three containers and fill in an .env file to get there. It is a poor fit if you want a bookmark list that works with no server at all, since even the mobile apps need an account first.
- Can I use it commercially?
- Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- linkwarden/linkwarden20,414 ★ · +1,249
| Repository | 2026-07-28 | 2026-11-13 |
|---|---|---|
| linkwarden/linkwarden | 19.2k | 20.4k |
1,249 Stars in the last 108 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Each saved link turns into three files on your disk
Linkwarden exists because pages disappear. When you save a link it captures a screenshot, a PDF and a single HTML file of the page, so the content is still readable after the original URL breaks. You can also send the page to the Wayback Machine at archive.org as an optional extra snapshot.
The single HTML file comes from monolith. The Dockerfile spends its whole first stage on it, compiling version 2.10.1 from source on a Rust 1.96 image:
FROM docker.io/rust:1.96-bookworm AS monolith-builder
ARG MONOLITH_VERSION=2.10.1
RUN set -eux && cargo install --locked monolith@${MONOLITH_VERSION}Screenshots and PDFs need a real browser, which the final stage installs through Playwright. Each output has its own size setting in .env.sample: PDF_MAX_BUFFER, SCREENSHOT_MAX_BUFFER, READABILITY_MAX_BUFFER, PREVIEW_MAX_BUFFER and MONOLITH_MAX_BUFFER, alongside BROWSER_TIMEOUT and a DISABLE_BROWSER switch. Archiving is the expensive part of the product, and the build is shaped around it.
The compose file starts three services and binds port 3000
The shipped docker-compose.yml is short enough to read in full:
services:
postgres:
image: postgres:16-alpine
env_file: .env
restart: always
volumes:
- ./pgdata:/var/lib/postgresql/data
linkwarden:
env_file: .env
environment:
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
restart: always
image: ghcr.io/linkwarden/linkwarden:latest
ports:
- 3000:3000
volumes:
- ./data:/data/data
depends_on:
- postgres
- meilisearch
meilisearch:
image: getmeili/meilisearch:v1.13.3
restart: always
env_file:
- .env
volumes:
- ./meili_data:/meili_dataThree directories next to the file hold all state: ./pgdata for Postgres, ./data for archived pages and uploads, and ./meili_data for the Meilisearch index behind full text search. A backup that copies only the database misses every screenshot and PDF, because those live in ./data. The app is published on port 3000 with no reverse proxy or TLS in the file, so putting it on a public hostname is your job.
The image is pinned to latest while Postgres and Meilisearch are pinned to versions
The two dependencies carry fixed tags, postgres:16-alpine and getmeili/meilisearch:v1.13.3, but the application itself is ghcr.io/linkwarden/linkwarden:latest. Releases have been frequent: v2.16.1 on 2026-08-17, v2.16.2 on 2026-08-31 and v2.16.3 on 2026-09-09, and the last push to the repository was on 2026-10-26.
The consequence is that two people who run the same compose file a month apart can end up on different application versions, and nothing in the file records which one you are on. If you need to know exactly what is running, or roll back after a bad upgrade, you have to change that image line yourself. The root package.json also carries a prisma:deploy script next to prisma:generate, so the database schema is managed by Prisma and is part of what moves when the image moves.
Two blank secrets in .env.sample decide whether the first boot works
Every service reads the same .env file, and the sample ships these lines empty:
NEXTAUTH_URL=http://localhost:3000/api/v1/auth
NEXTAUTH_SECRET=# Docker installation database settings
POSTGRES_PASSWORD=The compose file builds DATABASE_URL by interpolating POSTGRES_PASSWORD, so an empty value yields a connection string with no password in it. NEXTAUTH_URL points at localhost:3000; once the instance sits behind a real domain, that value has to change with it. For manual installs the sample expects a DATABASE_URL of its own, such as postgresql://user:password@localhost:5432/linkwarden.
The optional block is long. NEXT_PUBLIC_DISABLE_REGISTRATION, DISABLE_NEW_SSO_USERS and MAX_LINKS_PER_USER control who can sign up and how much they can store. ALLOW_PRIVATE_NETWORK_ACCESS and ALLOW_INSECURE_TLS govern which addresses the archiver is allowed to fetch. NEXT_PUBLIC_USER_CONTENT_DOMAIN and the RSS settings round it out. The full list with descriptions is on docs.linkwarden.app under self-hosting/environment-variables, not in the repository.
AI tagging stays local only if you point it at Ollama
Automatic tagging reads each link's content and assigns tags. It is optional, and the README calls it Local AI Tagging, but the .env.sample offers several backends:
# AI Settings
NEXT_PUBLIC_OLLAMA_ENDPOINT_URL=
OLLAMA_MODEL=The same file has OPENAI_API_KEY and OPENAI_MODEL, CUSTOM_OPENAI_BASE_URL and CUSTOM_OPENAI_NAME for third-party OpenAI-compatible services, AZURE_API_KEY, AZURE_RESOURCE_NAME and AZURE_MODEL, and ANTHROPIC_API_KEY and ANTHROPIC_MODEL. Because tagging is driven by page content, choosing any hosted provider means the text of what you bookmark is sent to that provider. For a self-hosted archive kept private for a reason, the Ollama variables are the only option in this list that keeps the content on hardware you control.
Web app and background worker are separate processes
The repository is a Yarn 4.12.0 workspace with apps/ and packages/ directories. The web interface and the worker that does the archiving are separate packages, and the root package.json starts them separately:
"web:start": "dotenv -- yarn workspace @linkwarden/web start",
"worker:start": "dotenv -- yarn workspace @linkwarden/worker start",The concurrently:start script runs both at once. A manual install outside Docker therefore means keeping two Node processes alive, plus Postgres and Meilisearch, plus the Playwright browser that the web package's postinstall step downloads. MAX_WORKERS and ARCHIVE_TAKE_COUNT in .env.sample are the knobs on that worker side. The Dockerfile builds on node:22.23.4-bookworm-slim, which is the clearest statement of the Node version the project builds against.
The mobile apps are clients for an instance, not a bookmark store
Linkwarden has official iOS and Android apps, but they do nothing on their own: you need a Linkwarden account first, either on the paid Cloud service or on an instance you deploy and maintain. An APK outside Google Play is available from the separate linkwarden/builds repository. Community clients cover gaps: My Links for iOS and macOS, LinkDroid and LinkGuardian for Android, link-my-harmony for HarmonyOS Next, and StarWarden for saving starred GitHub repositories.
The browser extension lives in its own repository, browser sync goes through Floccus, and pages can be uploaded from SingleFile. SSO is limited to Enterprise and self-hosted users. Translations come through Crowdin, and a new language is added only after it reaches 50% completion, so a partly translated interface in your language is not shipped until it crosses that line.
Editorial conclusion
Linkwarden fits people and small teams who want saved links to survive link rot and are willing to run three containers and fill in an .env file to get there. It is a poor fit if you want a bookmark list that works with no server at all, since even the mobile apps need an account first. Before deploying, decide whether you will stay on the latest image tag, set NEXTAUTH_SECRET and POSTGRES_PASSWORD, and choose whether AI tagging stays on a local Ollama endpoint or goes to a hosted provider.
Frequently asked questions
What is Linkwarden used for?
Linkwarden is a self-hosted, collaborative bookmark manager. It organizes saved links into collections and tags and keeps a screenshot, PDF and single HTML copy of each page so the content survives if the original goes offline.
Is Link Warden free?
Self-hosting Linkwarden is free, but you deploy and maintain the server yourself. Linkwarden Cloud is a paid subscription that also funds development.
Is Linkwarden open source?
Yes. The linkwarden/linkwarden repository is licensed under AGPL-3.0 and written mainly in TypeScript.
How do I install Linkwarden with Docker?
The repository ships a docker-compose.yml that runs postgres:16-alpine, getmeili/meilisearch:v1.13.3 and ghcr.io/linkwarden/linkwarden:latest on port 3000, all reading a shared .env file. Fill in NEXTAUTH_SECRET and POSTGRES_PASSWORD from .env.sample before starting it.
Official sources
Where this project is recommended
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/linkwarden-linkwarden)Star history chart for your README
[](https://hysenlabs.com/projects/linkwarden-linkwarden)Was this useful?