DockerSelf-Hosting

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.

Linkwarden official logo

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.

WhatValue (official docs, read November 2026)
Current versionLinkwarden v2.16.3, released September 2026 (latest release)
Image to pinghcr.io/linkwarden/linkwarden:v2.16.3 (tag exists on the registry)
Time neededAbout 20 minutes, plus a model download if you add Ollama (our estimate)
Size and RAMImage about 1.5 GB; idle about 350 MB; docs call 4 GB RAM "a comfortable starting point"
Port3000 (web page and API)
LicenceAGPL-3.0 (LICENSE.md at the release tag)
Official docsdocs.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.

Linkwarden flow: you save a link from the web page, extension or phone app; the Linkwarden app queues it; the worker opens the page in a browser and writes readable, HTML, screenshot and PDF files to the data folder; PostgreSQL stores links and tags; Meilisearch indexes them for search; an optional Ollama model suggests tags

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:

Run in terminalbash
docker --version
docker compose version
SystemWhat to install
LinuxDocker Engine and the Compose plugin (docs.docker.com/engine/install), openssl
WindowsDocker Desktop (WSL 2 backend), PowerShell
MacDocker Desktop, Terminal (openssl is included)

Checklist:

  • [ ] Port 3000 is free (the guide publishes it on 127.0.0.1 only)
  • [ ] 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#

Run in terminalbash
mkdir -p ~/docker_volumes/linkwarden/{data,pgdata,meili_data,backups}
cd ~/docker_volumes/linkwarden

Windows (PowerShell)#

Codepowershell
$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:

Run in terminalbash
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#

Run in terminalbash
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)#

Codepowershell
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:

Run in terminalbash
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_SECRET signs your sessions and the tokens for preserved content. Never paste your .env into 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#

Run in terminalbash
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:

Codepowershell
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:

Run in terminalbash
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:

Run in terminalbash
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 down and docker compose up -d after every .env change. A plain restart did not apply new variables in a reported case (issue #1598).

  1. Open http://localhost:3000 and choose Sign Up on the login page.
  2. Enter a name, e-mail address and a strong password.
  3. Use the button for a new link, paste a page address and save.
  4. 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.

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.

RouteHow
Browser extensiongithub.com/linkwarden/browser-extension. Enter your server address and sign in.
Phone appsiOS and Android apps from the stores; the README says you need a Linkwarden account on your own instance first.
ImportSettings, 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#

Run in terminalbash
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:

File contenttext
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
Run in terminalbash
sudo systemctl restart ollama

Keep port 11434 closed to the internet. Then add to .env:

Run in terminalbash
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:

File contentyaml
    extra_hosts:
      - "host.docker.internal:host-gateway"

Windows (PowerShell)#

Run in terminalpowershell
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)#

Run in terminalbash
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:

Run in terminalbash
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:

Run in terminalbash
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:

File contentbash
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:

Run in terminalbash
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.

File contentyaml
  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.

File contentyaml
    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.

Run in terminalbash
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:

File contentyaml
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):

File contenttext
links.example.com {
    reverse_proxy linkwarden:3000
}

For a home-only name, use Caddy's own certificate instead:

File contenttext
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#

ControlHow
SecretsThree different long values in .env, mode 600, never in a chat or a repository.
Sign-upsNEXT_PUBLIC_DISABLE_REGISTRATION=true after you have your account. It has an open bug (below): test it.
PortKeep 127.0.0.1:3000; reach it through HTTPS or a VPN. Never forward port 3000 raw.
ArchivesThe 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 addressesALLOW_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:

Run in terminalbash
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:

Run in terminalbash
cd ~/docker_volumes/linkwarden
du -sh data pgdata meili_data
ls data/archives | head
Codepowershell
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):

GoalSetting
Skip the costly formatsSettings, Preferences, Archive Settings: turn off Webpage and PDF. Tags can override this, so keep full copies only for tags you choose.
Bound one huge pagePDF_MAX_BUFFER, SCREENSHOT_MAX_BUFFER, MONOLITH_MAX_BUFFER, READABILITY_MAX_BUFFER (default 100 MB each). A bigger file is discarded, not stored.
Bound the databaseTEXT_CONTENT_LIMIT caps stored readable text per link (unlimited by default).
Limit work per batchARCHIVE_TAKE_COUNT=1 (default 5) flattens memory peaks.
Move files off the serverS3-compatible storage with the SPACES_* variables.
Cap per userMAX_LINKS_PER_USER (default 30000).
Let your browser do the workExtension upload or SingleFile; then turn off the matching server formats.

Tip: Do not combine DISABLE_BROWSER=true with 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.3Karakeep 0.34.1 (our guide)
ContainersApp with built-in browser, PostgreSQL, MeilisearchWeb, Chrome, Meilisearch
DatabasePostgreSQLSQLite
Copies keptReadable, HTML file, screenshot, PDFArchived pages, optional full-page archives
TeamworkCollections you share, member permissions, public sharingNot covered in our guide
AI tagsOptional: Ollama, OpenAI-compatible and othersOptional: Ollama
LicenceAGPL-3.0See the project's repository
Pick it whenYou want several users, collections and PDF or screenshot copiesYou 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.

MistakeWhy it happensDo this instead
Copying a new compose file over an old oneThe 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 domainThe docs require the full URL with the /api/v1/auth suffix, and sign-in redirects use itSet NEXTAUTH_URL=https://<your-domain>/api/v1/auth and BASE_URL=https://<your-domain>, then down and up -d
Trusting NEXT_PUBLIC_DISABLE_REGISTRATION=trueIssue #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 failsTest in a private window. If it stays open, keep the instance off the internet or behind a VPN
Restarting instead of recreating after editing .envrestart keeps the old environment. Issue #1598 (closed): the AI option appeared only after docker compose down and upAlways docker compose down, then docker compose up -d
Expecting every small Ollama model to tag cleanlyIssues #1576 and #1562 (open) show SyntaxError ... is not valid JSON when a model wraps or chats around its JSONStart 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 ownersIssues #709 and #712 (closed) show no preserved formats and a missing browser with user: 10057:10057; the maintainer got it working as rootFollow 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 startThe Postgres image only reads it when the data folder is emptySet it once. To change it later, change the password inside the database as well
Using a 1 GB machineIssue #744 (open) collects reports of high CPU and memory on small boards; the docs call 4 GB comfortableUse 4 GB, or follow the docs' lighter-setup page
AI assistants: inventing or renaming variablesAnswers copied from older posts use DISABLE_PRESERVATION (now DISABLE_BROWSER), guess OLLAMA_URL, skip MEILI_MASTER_KEY, or offer SQLiteUse 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 .env from 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#

ProblemSolution
Meilisearch restarts after an updateCheck docker compose logs meilisearch. For the version error, see "Meilisearch version error" below
Search finds nothingMEILI_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 formatsCheck 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 upgradedocker 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 HTTPSNEXTAUTH_URL must be the HTTPS address ending in /api/v1/auth; recreate with down and up -d
No AI option in SettingsRecreate the containers (down, up -d), then refresh the page hard (issue #1598)
Error auto tagging link ... is not valid JSONThe 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:

Run in terminalbash
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() invocation loop 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:

Run in terminalbash
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:

File contentbash
#!/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:

File contenttext
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:

Codepowershell
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
Run in terminalpowershell
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:

Run in terminalbash
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=true keeps bookmarks, tags and search but drops all copies.
  • Without Meilisearch: delete the service, its depends_on line and meili_data, and leave MEILI_MASTER_KEY empty. 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#

Checked against the official docs on 2026-11-05.