Linkwarden with Docker: Save Bookmarks and Keep Full-Page Copies (Linux, Windows, Mac)
Before you start
- 21 min read
Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#
Linkwarden is a free, self-hosted bookmark manager that saves a copy of every page you bookmark, so the content is still yours when the original site changes or disappears.

Logo: the official Linkwarden project (github.com/linkwarden/linkwarden)
For each link, a background worker opens the page in a real browser and keeps a readable version, a single HTML file, a full-page screenshot and a PDF. You organise links in collections and tags, search them, highlight text in the reader view, and share a collection with other people. Optional AI tagging can use a local model in Ollama.
| What | Value (official docs, read November 2026) |
|---|---|
| Current version | Linkwarden v2.16.3, released September 2026 (latest release) |
| Image to pin | ghcr.io/linkwarden/linkwarden:v2.16.3 (tag exists on the registry) |
| Time needed | About 20 minutes, plus a model download if you add Ollama (our estimate) |
| Size and RAM | Image about 1.5 GB; idle about 350 MB; docs call 4 GB RAM "a comfortable starting point" |
| Port | 3000 (web page and API) |
| Licence | AGPL-3.0 (LICENSE.md at the release tag) |
| Official docs | docs.linkwarden.app and the repository |
At a glance:
- What it is: three containers: Linkwarden (web app plus worker with its own browser), PostgreSQL and Meilisearch for search.
- What you keep: readable text, a single-file HTML copy, a screenshot and a PDF per link. The Wayback Machine snapshot is optional.
- Platforms: Docker on Linux, Windows or Mac. Images exist for x86-64 and ARM64.
- The storage catch: archives grow your disk quickly. The "Storage growth" section shows how to cap it.
- Not dead, not renamed: the repository is active. The project also sells a hosted Cloud plan; self-hosting stays free.
- Different from Karakeep? Yes. See our Karakeep guide and the comparison below.
Why this version? v2.16.3 is the latest release; its notes mention faster queries at scale. In a sample of the issue tracker we found no widespread install failures. One upgrade trap from 2.16.1 is in Common mistakes.
How it fits together#
You save a link in the app, a worker keeps copies of the page on disk, and PostgreSQL and Meilisearch let you find it again.
Drawn for this guide from the official compose file and docs. The dashed box is optional.
Before you start#
You need Docker with Compose and a machine with about 4 GB of RAM to be comfortable. Check Docker:
docker --version docker compose version
| System | What to install |
|---|---|
| Linux | Docker Engine and the Compose plugin (docs.docker.com/engine/install), openssl |
| Windows | Docker Desktop (WSL 2 backend), PowerShell |
| Mac | Docker Desktop, Terminal (openssl is included) |
Checklist:
- [ ] Port 3000 is free (the guide publishes it on
127.0.0.1only) - [ ] Enough disk: the image needs about 1.5 GB, and every saved page adds files
- [ ] A password manager ready for the secrets you will generate
- [ ] For the HTTPS section only: a domain name with a DNS record, and ports 80 and 443 free
Tested by DIY Lab Hub: not yet. Every command comes from the official compose file, the official docs and our other guides, read in November 2026. We did not run them.
Step 1: Create the folders#
Linkwarden keeps three folders: archives, the database and the search index. We use ~/docker_volumes/linkwarden, as all DIY Lab Hub guides do.
Linux#
mkdir -p ~/docker_volumes/linkwarden/{data,pgdata,meili_data,backups}
cd ~/docker_volumes/linkwarden
Windows (PowerShell)#
$d = "$HOME\docker_volumes\linkwarden" New-Item -ItemType Directory -Force "$d\data","$d\pgdata","$d\meili_data","$d\backups" | Out-Null Set-Location $d
Mac (Terminal)#
Same commands as Linux:
mkdir -p ~/docker_volumes/linkwarden/{data,pgdata,meili_data,backups}
cd ~/docker_volumes/linkwarden
Check it worked. The four folders exist: ls (Linux, Mac) or Get-ChildItem (Windows).
Tip: Create the folders yourself before the first start. If Docker creates them, they belong to root, which matters if you later want to run the containers as a normal user.
Step 2: Create the secrets file#
Linkwarden needs four settings in a .env file. The docs say NEXTAUTH_SECRET, POSTGRES_PASSWORD and MEILI_MASTER_KEY must all be different secret phrases. NEXTAUTH_URL must end in /api/v1/auth. We generate hex strings, which contain no special characters.
Linux#
cd ~/docker_volumes/linkwarden cat > .env <<EOF NEXTAUTH_URL=http://localhost:3000/api/v1/auth NEXTAUTH_SECRET=$(openssl rand -hex 32) POSTGRES_PASSWORD=$(openssl rand -hex 24) MEILI_MASTER_KEY=$(openssl rand -hex 32) EOF chmod 600 .env
Windows (PowerShell)#
Set-Location "$HOME\docker_volumes\linkwarden"
function New-Secret { $b = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); -join ($b | ForEach-Object { $_.ToString('x2') }) }
@"
NEXTAUTH_URL=http://localhost:3000/api/v1/auth
NEXTAUTH_SECRET=$(New-Secret)
POSTGRES_PASSWORD=$(New-Secret)
MEILI_MASTER_KEY=$(New-Secret)
"@ | Set-Content -Path .env -Encoding ascii
Mac (Terminal)#
Same commands as Linux:
cd ~/docker_volumes/linkwarden cat > .env <<EOF NEXTAUTH_URL=http://localhost:3000/api/v1/auth NEXTAUTH_SECRET=$(openssl rand -hex 32) POSTGRES_PASSWORD=$(openssl rand -hex 24) MEILI_MASTER_KEY=$(openssl rand -hex 32) EOF chmod 600 .env
Check it worked. The file has four lines and no empty value (cat .env on Linux and Mac, Get-Content .env on Windows). Save the three secrets in your password manager now.
Tip:
NEXTAUTH_SECRETsigns your sessions and the tokens for preserved content. Never paste your.envinto a chat, an issue or a screenshot.
Step 3: Write the Compose file and start Linkwarden#
This is the official compose file with three changes: the image is pinned to v2.16.3 (the official file uses latest), restart: unless-stopped replaces restart: always, and the web page is published on 127.0.0.1 only.
Linux#
cd ~/docker_volumes/linkwarden
cat > docker-compose.yml <<'EOF'
services:
postgres:
image: postgres:16-alpine
env_file: .env
restart: unless-stopped
volumes:
- ./pgdata:/var/lib/postgresql/data
linkwarden:
image: ghcr.io/linkwarden/linkwarden:v2.16.3
env_file: .env
environment:
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
volumes:
- ./data:/data/data
depends_on:
- postgres
- meilisearch
meilisearch:
image: getmeili/meilisearch:v1.13.3
env_file: .env
restart: unless-stopped
volumes:
- ./meili_data:/meili_data
EOF
docker compose up -d
docker compose ps
Windows (PowerShell)#
Save the same YAML as docker-compose.yml in $HOME\docker_volumes\linkwarden (for example with notepad docker-compose.yml, then paste the block above), then:
Set-Location "$HOME\docker_volumes\linkwarden" docker compose up -d docker compose ps
Mac (Terminal)#
Same commands as Linux. The first start downloads the images, which takes a few minutes:
cd ~/docker_volumes/linkwarden docker compose up -d docker compose ps
Check it worked. docker compose ps lists postgres, linkwarden and meilisearch as running. Then watch the first start:
docker compose logs -f linkwarden
The docs publish no expected log text. Wait until the log goes quiet (migrations run on the first start), press Ctrl+C, and open http://localhost:3000. You should see the sign-in page.
Tip: Use
docker compose downanddocker compose up -dafter every.envchange. A plainrestartdid not apply new variables in a reported case (issue #1598).
First run: create your account and save a first link#
- Open
http://localhost:3000and choose Sign Up on the login page. - Enter a name, e-mail address and a strong password.
- Use the button for a new link, paste a page address and save.
- Wait a little. The details show "Link preservation is in the queue", then the formats appear one by one.
The server administrator is user ID 1 by default (NEXT_PUBLIC_ADMIN), which is normally the first account you create.
Check it worked. Open the link's details. Under Preserved Formats you should see Readable, Webpage, Screenshot and PDF. If a format is missing, read the worker log (docker compose logs --tail=100 linkwarden) and the Troubleshooting table.
Save links from your browser and phone#
This part happens in the browser and on your devices, so it is the same on Linux, Windows and Mac. All routes need your instance address.
| Route | How |
|---|---|
| Browser extension | github.com/linkwarden/browser-extension. Enter your server address and sign in. |
| Phone apps | iOS and Android apps from the stores; the README says you need a Linkwarden account on your own instance first. |
| Import | Settings, Account, Import and Export: import a bookmarks HTML file from Chrome, Brave, Edge, Firefox or Safari. Folders become collections. |
The apps work over plain http:// at home. With your own certificate, read the HTTPS section: issue #1515 reports the app failing on a self-signed certificate while http:// worked.
Tip: Imports do not remove duplicates, and every imported link joins the preservation queue. Import a bookmarks file once, and before a big import read "Storage growth" below.
Optional: AI tags with Ollama#
Linkwarden can ask a model in Ollama to tag each new link. Everything stays on your machine.
The docs name a small model, phi3:mini-4k, and two variables. The feature has been in the app since 2.9 and you switch it on in Settings, Preferences. Linkwarden adds /api to the address itself, so do not add it.
Set up Ollama first (see our Ollama guide). The Linkwarden container needs an address it can reach; localhost inside the container is the container itself.
Linux#
ollama pull phi3:mini-4k sudo systemctl edit ollama
Ollama listens on 127.0.0.1 only by default (Ollama FAQ). Add these lines, save, and restart it:
[Service] Environment="OLLAMA_HOST=0.0.0.0:11434"
sudo systemctl restart ollama
Keep port 11434 closed to the internet. Then add to .env:
echo 'NEXT_PUBLIC_OLLAMA_ENDPOINT_URL=http://host.docker.internal:11434' >> ~/docker_volumes/linkwarden/.env echo 'OLLAMA_MODEL=phi3:mini-4k' >> ~/docker_volumes/linkwarden/.env
In docker-compose.yml, add this under the linkwarden: service so Linux knows the name:
extra_hosts:
- "host.docker.internal:host-gateway"
Windows (PowerShell)#
ollama pull phi3:mini-4k Add-Content "$HOME\docker_volumes\linkwarden\.env" "NEXT_PUBLIC_OLLAMA_ENDPOINT_URL=http://host.docker.internal:11434" Add-Content "$HOME\docker_volumes\linkwarden\.env" "OLLAMA_MODEL=phi3:mini-4k"
Docker Desktop already knows host.docker.internal. If the check below fails, set the user variable OLLAMA_HOST to 0.0.0.0:11434 in Windows settings and restart Ollama.
Mac (Terminal)#
ollama pull phi3:mini-4k echo 'NEXT_PUBLIC_OLLAMA_ENDPOINT_URL=http://host.docker.internal:11434' >> ~/docker_volumes/linkwarden/.env echo 'OLLAMA_MODEL=phi3:mini-4k' >> ~/docker_volumes/linkwarden/.env
If the check below fails, run launchctl setenv OLLAMA_HOST "0.0.0.0:11434" and restart the Ollama app.
Apply the change, then switch tagging on in Settings, Preferences:
docker compose down docker compose up -d
Check it worked. This throwaway container asks Ollama for its model list through the same route (our own check, not from the Linkwarden docs). It prints JSON that includes phi3:mini-4k:
docker run --rm --add-host=host.docker.internal:host-gateway curlimages/curl -s http://host.docker.internal:11434/api/tags
Then save a new link and read docker compose logs --tail=100 linkwarden. A tag error there names the line that failed.
Tip: Small models sometimes answer with extra words or a code block around the JSON, and Linkwarden then fails to read the tags. Issues #1576 and #1562 report exactly this, also with the docs' own
phi3:mini-4k. They are open and were filed on 2.13.x, so test with a few links before you rely on it.
Put it behind HTTPS (with or without Traefik)#
Use this section if the server has a domain, or if you want https:// at home. Local-only users can keep http://localhost:3000. The official docs show NGINX with Certbot; here are the two routes our other guides use. Both change two settings:
NEXTAUTH_URL=https://links.example.com/api/v1/auth BASE_URL=https://links.example.com
BASE_URL has no /api/v1/auth and matters for e-mail features such as password reset. Edit .env, then run docker compose down and docker compose up -d.
Both routes put Linkwarden on a shared Docker network called proxy. Create it once:
docker network create proxy
In docker-compose.yml, change the linkwarden: service as below and add the network block at the end of the file. The 127.0.0.1:3000 port line may stay.
linkwarden:
# ...everything from Step 3 stays...
networks:
- default
- proxy
networks:
proxy:
external: true
With Traefik#
On a server that already runs Traefik v3 with the Docker provider, add these labels under linkwarden:. This assumes an entrypoint called https; use the name of yours.
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.linkwarden.rule=Host(`links.<your-domain>`)"
- "traefik.http.routers.linkwarden.entrypoints=https"
- "traefik.http.routers.linkwarden.tls=true"
- "traefik.http.services.linkwarden.loadbalancer.server.port=3000"
Check it worked. curl -I https://links.<your-domain>/ returns an HTTP status line over HTTPS and the sign-in page opens with a valid certificate.
Without Traefik: Caddy#
Choose this if you have no Traefik. Caddy gets and renews its own certificates. Our Caddy guide explains it. We mount the conf folder, not a single file, as the Caddy image docs advise.
mkdir -p ~/docker_volumes/caddy/{conf,data,config}
Save as ~/docker_volumes/caddy/docker-compose.yml. The newest Caddy release is v2.11.9, but its Docker Hub tag did not exist yet in November 2026, so we pin 2.11.8:
services:
caddy:
image: caddy:2.11.8
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- ./data:/data
- ./config:/config
networks:
- proxy
networks:
proxy:
external: true
Save as ~/docker_volumes/caddy/conf/Caddyfile for a public domain (Let's Encrypt, automatic):
links.example.com {
reverse_proxy linkwarden:3000
}
For a home-only name, use Caddy's own certificate instead:
links.home.arpa {
tls internal
reverse_proxy linkwarden:3000
}
Start both stacks (docker compose up -d in ~/docker_volumes/caddy and in ~/docker_volumes/linkwarden). For a public domain, the DNS record must point at the server and ports 80 and 443 must reach it. For tls internal, each device must trust Caddy's root certificate; the commands for Linux, Windows and Mac are in the Caddy guide. Windows and Mac are clients here: run these commands on the server.
Check it worked. curl -I https://links.example.com/ returns an HTTP status line. For a home-only name, the browser shows a padlock only after the root is trusted on that device.
Tip: The Android app could not log in to an instance with a custom or self-signed certificate until a fix that commenters on issue #1515 confirmed in app version 1.4.0 (July 2026). Update the app first if it fails only on mobile.
Security and testing#
| Control | How |
|---|---|
| Secrets | Three different long values in .env, mode 600, never in a chat or a repository. |
| Sign-ups | NEXT_PUBLIC_DISABLE_REGISTRATION=true after you have your account. It has an open bug (below): test it. |
| Port | Keep 127.0.0.1:3000; reach it through HTTPS or a VPN. Never forward port 3000 raw. |
| Archives | The files can hold pages you saw while logged in (browser uploads capture what you see). Keep data private and encrypt backups you send away. |
| Private addresses | ALLOW_PRIVATE_NETWORK_ACCESS is false by default, so the server will not fetch links that resolve to internal addresses. Leave it. |
Quick tests after each change:
cd ~/docker_volumes/linkwarden docker compose ps docker compose logs --tail=100 linkwarden curl -I http://localhost:3000/
On Windows use curl.exe -I http://localhost:3000/. After you set NEXT_PUBLIC_DISABLE_REGISTRATION=true, open the sign-up page in a private window and confirm it is closed.
What leaves the machine: requests to the pages you save, the optional Wayback Machine submission, and any AI provider you configure. The docs describe no telemetry; we did not trace the network.
Storage growth: what archives cost#
Every link becomes several files, so your disk grows with your habits, not with the app. The docs give no average size per page, so measure your own.
The docs call the single HTML copy (Webpage) and the PDF the most expensive formats, the screenshot cheaper, and the readable text cheapest. All formats except the Wayback Machine snapshot are on for a new account, and a small preview image is always made. Measure after saving about 20 typical links:
cd ~/docker_volumes/linkwarden du -sh data pgdata meili_data ls data/archives | head
Set-Location "$HOME\docker_volumes\linkwarden"
foreach ($f in "data","pgdata","meili_data") { "{0}: {1:N1} MB" -f $f, ((Get-ChildItem $f -Recurse -File | Measure-Object Length -Sum).Sum / 1MB) }
Mac uses the Linux commands. Then choose your knobs (all from the official docs):
| Goal | Setting |
|---|---|
| Skip the costly formats | Settings, Preferences, Archive Settings: turn off Webpage and PDF. Tags can override this, so keep full copies only for tags you choose. |
| Bound one huge page | PDF_MAX_BUFFER, SCREENSHOT_MAX_BUFFER, MONOLITH_MAX_BUFFER, READABILITY_MAX_BUFFER (default 100 MB each). A bigger file is discarded, not stored. |
| Bound the database | TEXT_CONTENT_LIMIT caps stored readable text per link (unlimited by default). |
| Limit work per batch | ARCHIVE_TAKE_COUNT=1 (default 5) flattens memory peaks. |
| Move files off the server | S3-compatible storage with the SPACES_* variables. |
| Cap per user | MAX_LINKS_PER_USER (default 30000). |
| Let your browser do the work | Extension upload or SingleFile; then turn off the matching server formats. |
Tip: Do not combine
DISABLE_BROWSER=truewith browser uploads. The docs warn the worker then marks every format as unavailable and clears the file you uploaded. Turn off individual formats instead.
After a big import, memory settles higher and stays there. The docs say that is expected, not a leak; compare setups from a fresh start with docker stats.
Linkwarden or Karakeep?#
Both save links and keep copies. We wrote a Karakeep guide too. This table uses only each project's docs and that guide.
| Linkwarden 2.16.3 | Karakeep 0.34.1 (our guide) | |
|---|---|---|
| Containers | App with built-in browser, PostgreSQL, Meilisearch | Web, Chrome, Meilisearch |
| Database | PostgreSQL | SQLite |
| Copies kept | Readable, HTML file, screenshot, PDF | Archived pages, optional full-page archives |
| Teamwork | Collections you share, member permissions, public sharing | Not covered in our guide |
| AI tags | Optional: Ollama, OpenAI-compatible and others | Optional: Ollama |
| Licence | AGPL-3.0 | See the project's repository |
| Pick it when | You want several users, collections and PDF or screenshot copies | You want a lighter single-user stack with AI tags first |
Our reading, not the projects' claim: choose Linkwarden for shared collections and rich copies, Karakeep for a simple solo setup.
Common mistakes#
These come from the official docs' warnings, the issue tracker and common AI assistant answers.
| Mistake | Why it happens | Do this instead |
|---|---|---|
| Copying a new compose file over an old one | The 2.16.1 compose moved Meilisearch from 1.12.8 to 1.13.3. Old data then fails with Your database version (1.12.8) is incompatible with your current engine version (1.13.3). (issue #1806, closed) | Keep your Meilisearch tag, or move meili_data aside and run the maintainer's fix in Troubleshooting. Read release notes before you pull |
Leaving NEXTAUTH_URL on localhost after you add a domain | The docs require the full URL with the /api/v1/auth suffix, and sign-in redirects use it | Set NEXTAUTH_URL=https://<your-domain>/api/v1/auth and BASE_URL=https://<your-domain>, then down and up -d |
Trusting NEXT_PUBLIC_DISABLE_REGISTRATION=true | Issue #1485 (open) says sign-ups still work. One user fixed it by removing quotes around true in a list-style environment:; another says it still fails | Test in a private window. If it stays open, keep the instance off the internet or behind a VPN |
Restarting instead of recreating after editing .env | restart keeps the old environment. Issue #1598 (closed): the AI option appeared only after docker compose down and up | Always docker compose down, then docker compose up -d |
| Expecting every small Ollama model to tag cleanly | Issues #1576 and #1562 (open) show SyntaxError ... is not valid JSON when a model wraps or chats around its JSON | Start with the docs' phi3:mini-4k, test a few links, read the worker log, and stay without AI if tags fail |
| Running as another user without fixing folder owners | Issues #709 and #712 (closed) show no preserved formats and a missing browser with user: 10057:10057; the maintainer got it working as root | Follow the docs' non-root page: create data pgdata meili_data first, set user: on every service, chown existing folders |
Changing POSTGRES_PASSWORD after the first start | The Postgres image only reads it when the data folder is empty | Set it once. To change it later, change the password inside the database as well |
| Using a 1 GB machine | Issue #744 (open) collects reports of high CPU and memory on small boards; the docs call 4 GB comfortable | Use 4 GB, or follow the docs' lighter-setup page |
| AI assistants: inventing or renaming variables | Answers copied from older posts use DISABLE_PRESERVATION (now DISABLE_BROWSER), guess OLLAMA_URL, skip MEILI_MASTER_KEY, or offer SQLite | Use only names from the official environment variables page: NEXT_PUBLIC_OLLAMA_ENDPOINT_URL, OLLAMA_MODEL, and so on. The database is PostgreSQL |
Tip: Before you paste a compose file or
.envfrom anywhere, compare each image tag and variable with the official files.
Tip: After every change run
docker compose logs --tail=100 linkwarden. Preservation and AI errors print there with the exact text.
Tip: Pin the image tag. The docs say a pinned tag is what makes a rollback possible.
Troubleshooting#
| Problem | Solution |
|---|---|
| Meilisearch restarts after an update | Check docker compose logs meilisearch. For the version error, see "Meilisearch version error" below |
| Search finds nothing | MEILI_MASTER_KEY must be set in .env; Linkwarden only starts the search client when it is. Without it, search falls back to PostgreSQL titles, URLs, descriptions and tags |
| A link has no preserved formats | Check the worker log. Make sure formats are enabled in Archive Settings, the folders are writable, and a non-root setup follows the docs |
| Container restarts in a loop after an upgrade | docker compose logs linkwarden. Usual causes per the docs: a migration that could not run, or the database not reachable yet |
| Sign-in fails after adding HTTPS | NEXTAUTH_URL must be the HTTPS address ending in /api/v1/auth; recreate with down and up -d |
| No AI option in Settings | Recreate the containers (down, up -d), then refresh the page hard (issue #1598) |
Error auto tagging link ... is not valid JSON | The model's answer is not clean JSON (issues #1576, #1562). Try another model or provider |
Meilisearch version error#
The exact text is ERROR meilisearch: error=Your database version (1.12.8) is incompatible with your current engine version (1.13.3). The maintainer's answer in issue #1806: delete the meili_data contents, then tell Linkwarden to reindex:
cd ~/docker_volumes/linkwarden docker compose down sudo rm -rf meili_data/* docker compose up -d docker compose exec postgres psql -U postgres -d postgres -c 'UPDATE "Link" SET "indexVersion" = NULL;' docker compose restart linkwarden
On Windows run the same docker compose lines, and clear the folder with Remove-Item "$HOME\docker_volumes\linkwarden\meili_data\*" -Recurse -Force. The reporter confirmed that deleting the folder alone left search dead until the SQL line ran.
Known bugs (open, November 2026)#
- #1485: the registration switch may not work.
- #1576 and #1562: AI tagging fails when the model's reply is not plain JSON.
- #1883: a fresh podman install showed a 500
Invalid prisma.user.findUnique() invocationloop in Firefox but not Chromium. One report; we cannot confirm a cause.
Diagnostics to run first: docker compose ps, docker compose logs --tail=200 linkwarden, docker inspect <container> for the image tag.
Updates and backups#
Update. Read the release notes, take a backup, change the tag, then:
cd ~/docker_volumes/linkwarden docker compose pull docker compose up -d docker compose logs -f linkwarden
On Windows use Set-Location "$HOME\docker_volumes\linkwarden" first. The docs say migrations run on start, you can jump from any older release to the newest, and migrations cannot be undone. To roll back, restore the database backup and pin the older tag; both halves matter.
Back up. The docs' recipe is a database dump plus a tarball of the data folder and .env. Save it as backup.sh:
#!/bin/bash cd ~/docker_volumes/linkwarden || exit 1 mkdir -p backups docker compose exec -T postgres pg_dump -U postgres postgres > "backups/linkwarden-db-$(date +%F).sql" tar -czf "backups/linkwarden-data-$(date +%F).tar.gz" data .env chmod 600 backups/*
On Linux or Mac, run it nightly at 03:00 with crontab -e:
0 3 * * * /bin/bash $HOME/docker_volumes/linkwarden/backup.sh
On Windows, save this as backup.ps1 (a plain > redirect would write UTF-16 in Windows PowerShell, so we dump inside the container and copy the file out) and schedule it:
Set-Location "$HOME\docker_volumes\linkwarden" $d = Get-Date -Format yyyy-MM-dd docker compose exec -T postgres pg_dump -U postgres -f /tmp/linkwarden-db.sql postgres docker compose cp postgres:/tmp/linkwarden-db.sql "backups\linkwarden-db-$d.sql" tar -czf "backups\linkwarden-data-$d.tar.gz" data .env
schtasks /Create /SC DAILY /ST 03:00 /TN "Linkwarden backup" /TR "powershell.exe -NoProfile -File `"$HOME\docker_volumes\linkwarden\backup.ps1`""
Keep a copy on another machine; it holds your secrets and saved pages.
Restore test. Load the dump into a throwaway database, never over the live one:
docker run --rm -d --name lw-restore-test -e POSTGRES_PASSWORD=test postgres:16-alpine sleep 10 docker cp backups/linkwarden-db-<date>.sql lw-restore-test:/tmp/db.sql docker exec lw-restore-test psql -U postgres -v ON_ERROR_STOP=1 -f /tmp/db.sql docker exec lw-restore-test psql -U postgres -c '\dt' docker stop lw-restore-test
On Windows replace sleep 10 with Start-Sleep 10 and the path with backups\linkwarden-db-<date>.sql. The table list should show Linkwarden's tables (such as Link). Linkwarden's own Export Data restores links, tags and collections but not the preserved files, which are rebuilt; the folder backup keeps them.
Performance and resources#
The docs give these figures; measure your own with docker stats.
- Image: about 1.5 GB since release 2.15, down from about 3.0 GB.
- Idle memory: about 350 MB for the app, down from about 700 MB. PostgreSQL and Meilisearch add their own.
- Hardware: the setup page says 4 GB RAM is comfortable and any 2-core CPU works.
- No archives at all:
DISABLE_BROWSER=truekeeps bookmarks, tags and search but drops all copies. - Without Meilisearch: delete the service, its
depends_online andmeili_data, and leaveMEILI_MASTER_KEYempty. You lose advanced search and full-text search across saved pages.
FAQ#
Is Linkwarden free? Yes. Self-hosting is free under AGPL-3.0. The project also sells a hosted Cloud plan, and the README says subscriptions fund development.
Can I run it on a Raspberry Pi? Images exist for ARM64, so a 64-bit system can run it. Users on a Pi 4 report heavy CPU in issue #744 (older versions), so try the lighter setup.
Can several people use one instance? Yes. The README lists collections, members with their own permissions, and public sharing.
Does it send my links anywhere? Only to the pages themselves and to services you switch on, such as Wayback Machine or an AI provider. The docs describe no telemetry.
Sources#
- Linkwarden release v2.16.3, README, docker-compose.yml, .env.sample and LICENSE.md - read November 2026
- Linkwarden docs (source in the docs repository): setup, environment variables, AI tagging, lighter setup, non-root, reverse proxy, user content domain, upgrading, preservation, import and export - read November 2026
- Linkwarden image on GitHub Container Registry (tag
v2.16.3checked) - November 2026 - Ollama FAQ (
OLLAMA_HOST) and PostgreSQL image docs - November 2026 - Caddy release v2.11.9, Caddy reverse proxy quick start and Traefik Docker provider - November 2026
- GitHub issues #744, #709, #712, #1485, #1515, #1562, #1576, #1598, #1806, #1883 - read November 2026
- Our guides: Karakeep, Caddy, Ollama
Checked against the official docs on 2026-11-05.