Fully verified19,653

Bookmarks and archiving

Linkwarden

Linkwarden saves, organizes, and searches bookmarks, preserves local copies of pages, and shares collections. This recipe includes PostgreSQL 16, Meilisearch 1.13.3, a separate origin for preserved HTML, and a web port safely bound to localhost.

Overview

Linkwarden is a bookmark manager and web archive for one user, a family, or a small team. It organizes links into collections, adds tags, provides full-text search, and preserves pages and documents for reading after the original disappears. Shared collections, a browser extension, and official Android and iOS clients are available.

This recipe runs pinned Linkwarden v2.16.2 with PostgreSQL 16 and Meilisearch 1.13.3. Preserved content, the database, and the search index each use persistent storage. The web port binds to localhost, internal services are not published, and preserved HTML is served from a separate origin.

Security and recipe boundaries

The full HTTP smoke test has passed on amd64; backup and restore remain untested in practice. A headless browser fetches untrusted URLs and can consume substantial CPU, RAM, and disk. Private-network access and insecure TLS are forced off, but that does not replace an egress firewall for untrusted users. Preserved HTML is isolated on a separate origin without shared cookies. Registration ships open so that the first account can be created, and nothing closes it afterwards: set NEXT_PUBLIC_DISABLE_REGISTRATION=true and recreate the container as soon as you have signed up, or anyone who reaches the URL can register.

Update tracking

These are stored results of separate checks, not real-time data. An unknown status means the check has not been performed yet.

New releases

Recipe version compared with the latest known upstream release. A version number, not a reading of what changed.

Current version v2.16.2Sep 1, 2026, 12:22 AM UTC

Critical vulnerabilities

Result of a dedicated Docker image scan, not a Compose configuration check.

Not checkedNot checked

Breaking updates

A new major version is found by comparing version numbers. A compatibility verdict is entered by hand after reading the release notes and the migration steps.

Not checkedNot checked

compose.yaml

Upstream Compose changes relative to the verified recipe.

Upstream Compose is not trackedNot checked

Outdated Docker images

Pinned image tags checked for newer versions.

Not checkedNot checked

From observation to action

Update plan

The recipe already pins the latest known release. The steps below show the evidence retained for that version.

v2.16.2v2.16.2
  1. Open the release evidence

    Open this release on GitHub ↗ The link is evidence to review, not proof of compatibility.

  2. Review the version boundary

    The version numbers do not cross a major compatibility boundary. No compatibility verdict has been recorded.

  3. Prepare image-tag changes

    No image-tag change is currently planned.

  4. Follow the documented migration shape

    Manual steps after the pull. Release-specific migration steps still come from the release notes and installation guide.

  5. Protect the rollback point

    No version change is currently planned. The recipe includes a backup procedure. Restore has not been tested.

  6. Verify the updated recipe

    An exact Fully verified check for this target was recorded on 2026-09-03.

Read the update procedure in the installation guide ↓

Specifications

Security and exposure

Installation levels

The status shows how fully the published recipe meets each level's requirements.

Partial

For home

A minimal setup for a local network or access through a VPN.

  • Single server
  • Local access
  • Minimum components

You still need to add: single server, local access, minimum components.

Partial

For a VPS

A public deployment with protected access and backups.

  • Domain
  • HTTPS via reverse proxy
  • Off-server backup

You still need to add: domain, https via reverse proxy, off-server backup.

Partial

Reliable

A setup designed for predictable operations and observability.

  • Separate database
  • Healthcheck
  • External monitoring
  • CPU and memory limits

You still need to add: external monitoring, cpu and memory limits.

Tailored to your server

Configuration generator

Answer five questions and download a ready-to-run Linkwarden bundle.

Runs locally
01 Do you have a domain?
02 Which reverse proxy?
03 Server architecture
04 Where should data be stored?

Use an absolute path for a separate disk, such as /srv/linkwarden.

An HTTPS URL on another registrable domain without shared cookies, for example https://saved.example.net. Point its DNS and TLS to the same reverse-proxy upstream.

05 Is VPN access required?

Your bundle is ready

The archive is created in your browser. Your answers are never sent anywhere.

Screenshots

Screenshots have not been added yet.

Quick start

Read the full guide first: it explains external access, backups, and data locations.

Prepare settingscp .env.example .env
Launchdocker compose up -d
Check statusdocker compose ps

Step-by-step guide

1. Check the Ubuntu or Debian server

Use Ubuntu 22.04+ or Debian 12+ with Docker Engine and Compose v2.24+. Allocate at least 2 CPUs, 2 GB RAM, and 5 GB disk; 4 GB RAM plus dedicated room for web archives is recommended. The official image declares amd64 and arm64.

docker --version
docker compose version

2. Prepare the recipe and independent secrets

mkdir -p ~/services/linkwarden
cd ~/services/linkwarden
cp .env.example .env
chmod 600 .env
nextauth_secret="$(openssl rand -hex 32)"
postgres_password="$(openssl rand -hex 32)"
meili_key="$(openssl rand -hex 32)"
sed -i "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$nextauth_secret|" .env
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$postgres_password|" .env
sed -i "s|^MEILI_MASTER_KEY=.*|MEILI_MASTER_KEY=$meili_key|" .env
unset nextauth_secret postgres_password meili_key

All three secrets are required, must differ, and must remain stable after the first start. A hexadecimal value is safe inside the PostgreSQL URL. Store the secrets in a password manager.

Every .env variable:

  • LINKWARDEN_PORT is the local web port, default 3000;
  • LINKWARDEN_URL is the exact public application URL without a trailing /;
  • LINKWARDEN_USER_CONTENT_URL is a separate HTTPS origin for preserved HTML, preferably on another registrable domain and without shared cookies;
  • NEXT_PUBLIC_DISABLE_REGISTRATION stays false only while creating the first account and must then become true;
  • NEXTAUTH_SECRET is the persistent NextAuth session and token secret;
  • POSTGRES_PASSWORD is an independent URL-safe PostgreSQL password;
  • MEILI_MASTER_KEY is an independent Meilisearch master key;
  • POSTGRES_DB and POSTGRES_USER are the database and role names, changed only before the first start;
  • LINKWARDEN_TIME_ZONE is an IANA time zone;
  • LINKWARDEN_DATA_VOLUME, LINKWARDEN_DB_VOLUME, and LINKWARDEN_MEILI_VOLUME name archive, PostgreSQL, and search-index volumes;
  • LINKWARDEN_BACKUP_DIR selects the host backup directory.

The recipe derives NEXTAUTH_URL as ${LINKWARDEN_URL}/api/v1/auth, while BASE_URL equals LINKWARDEN_URL. Do not omit /api/v1/auth from the auth URL.

3. Start and close registration

docker compose config
docker compose pull
docker compose up -d --wait --wait-timeout 900
docker compose ps
curl -I http://127.0.0.1:3000/

Open LINKWARDEN_URL, create the first account, immediately set NEXT_PUBLIC_DISABLE_REGISTRATION=true, and apply it:

docker compose up -d --force-recreate linkwarden

Use a private window to confirm sign-up is unavailable. Do not expose the service publicly before this step.

VPS deployment

Keep 127.0.0.1:${LINKWARDEN_PORT}:3000; only an HTTPS proxy on the host can reach the app. PostgreSQL and Meilisearch have no published ports. Allow only SSH, HTTP, and HTTPS through the firewall. Create DNS for both the application and user-content origins before the first start.

Trusted LAN access

Prefer ssh -L 3000:127.0.0.1:3000 user@server and use LINKWARDEN_URL=http://localhost:3000 for that route. For permanent LAN access, replace the localhost bind with one specific private IP, use a matching URL, and restrict the port with a firewall. Do not use 0.0.0.0 without network controls. Without HTTPS, preserved-content origin isolation is weaker, so this mode is for a trusted network only.

Domain, HTTPS, and preserved HTML

Set LINKWARDEN_URL=https://links.example.com and a separate LINKWARDEN_USER_CONTENT_URL=https://saved.example.net, then replace both hosts in the Caddy, Nginx, or Traefik example. For stronger isolation, use another registrable domain rather than an application subdomain, and never set shared parent-domain cookies. The certificate must cover both hosts; replace the Nginx sample paths with a suitable SAN or separate certificates. The proxy must support WebSockets and preserve Host and X-Forwarded-Proto.

Backup

chmod +x backup.sh restore.sh
./backup.sh

The script stops Linkwarden and Meilisearch, keeps PostgreSQL available for a native pg_dump, archives /data/data, /meili_data, .env, and Compose, then restarts the services. A UI JSON export is not a complete backup: it omits preserved pages, documents, and extracted text. The archive holds secrets and user content; encrypt it, copy it off the server, and test restores regularly.

Restore

Restore irreversibly replaces application data, PostgreSQL, and the Meilisearch index. Use the same Linkwarden, PostgreSQL, and Meilisearch versions with the active .env:

./restore.sh ./backups/linkwarden-YYYYMMDDTHHMMSSZ.tar
curl -I http://127.0.0.1:3000/

The script first backs up the state being replaced, recreates all three volumes, restores the dump, and starts the stack. The archived configuration.env is retained for comparison only. This procedure has not passed a practical restore test; test it on a separate server first.

Update Linkwarden

Create a backup, read release notes, and replace the exact ghcr.io/linkwarden/linkwarden:v2.16.2 tag with a reviewed version. Never use latest, and do not update PostgreSQL or Meilisearch at the same time:

docker compose pull
docker compose up -d --wait --wait-timeout 900
curl -I http://127.0.0.1:3000/
docker compose logs --tail=200 linkwarden postgres meilisearch

Application database migrations run at startup. A PostgreSQL major upgrade requires a new empty volume and native-dump restore. A Meilisearch major change requires its upgrade guide; retain the old volume until search is verified.

Rollback

Never run an older Linkwarden over a database after newer migrations. Restore all previous exact tags and the complete pre-update archive with restore.sh. After a failed PostgreSQL or Meilisearch change, attach the old image only to its preserved old volume, or restore a compatible dump/backup into an empty volume.

Stop and remove

docker compose down preserves data. After verifying an off-server backup, remove everything irreversibly:

docker compose down
docker volume rm linkwarden-data linkwarden-postgres linkwarden-meilisearch
rm -rf ~/services/linkwarden

Substitute actual names when volume variables differ.

Sources: self-hosting setup, environment variables, user-content domain, release v2.16.2, and PostgreSQL upgrades.

Troubleshooting

Linkwarden does not become healthy

docker compose ps
docker compose logs --tail=200 linkwarden postgres meilisearch
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q linkwarden)"
curl -I http://127.0.0.1:3000/

Check the three required distinct secrets, dependency health, free RAM, and disk space. POSTGRES_PASSWORD must be URL-safe because it appears in DATABASE_URL. Never delete production volumes while diagnosing a failure.

Redirects or sign-in use the wrong URL

LINKWARDEN_URL must exactly match the public scheme and host without a trailing /. The recipe passes NEXTAUTH_URL=${LINKWARDEN_URL}/api/v1/auth and BASE_URL=${LINKWARDEN_URL}. Recreate Linkwarden after changing it and remove old cookies. Ensure the proxy forwards the original Host and X-Forwarded-Proto=https.

Registration is still available

Set NEXT_PUBLIC_DISABLE_REGISTRATION=true, run docker compose up -d --force-recreate linkwarden, and check in a private window. A simple restart is insufficient when the container was not recreated.

Preserved pages fail or use the application origin

Check LINKWARDEN_USER_CONTENT_URL, DNS, TLS, and the second proxy host. The origin must differ from the application and must not share its cookies. Never work around this by enabling private-network access or insecure TLS.

An internal or self-signed URL cannot be preserved

This is expected protection: Compose pins ALLOW_PRIVATE_NETWORK_ACCESS=false and ALLOW_INSECURE_TLS=false. Do not weaken them on a shared instance. For a trusted internal resource, prefer a public endpoint with valid HTTPS or a separate isolated instance with an egress firewall.

Search does not return new bookmarks

docker compose logs --tail=200 meilisearch linkwarden
docker compose exec meilisearch curl --fail http://127.0.0.1:7700/health

Ensure both services have the same MEILI_MASTER_KEY and the volume is not full. Do not delete the index before a complete backup. If upstream provides a reindex command for this version, run it only after preserving PostgreSQL and archives.

The reverse proxy returns 502

On the host, run curl -I http://127.0.0.1:3000/. Inspect the upstream, firewall, and proxy namespace. Inside a container, 127.0.0.1 refers to the proxy itself, not Linkwarden on the host; use a host gateway or shared private network.

Official sources

Latest release: 2026-08-31 · GitHub Stars: 19,653 · metadata checked Sep 1, 2026, 12:22 AM UTC

Disclaimer

Fossary is an independent informational catalog and is not affiliated with the developers of the listed applications. We collect information from public sources and verify published recipes, but we do not develop, distribute, or control these applications.

A listing or validation status is not a requirement, endorsement, or personal recommendation to use an application. You decide whether it is suitable and use it at your own risk. We do not warrant an application's security, availability, or fitness for purpose and, to the extent permitted by law, are not liable for resulting loss or damage. Review its license, security settings, data processing, and backup procedures before installation.