Skip to content

How to Deploy Linkwarden on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Linkwarden on a VPS with Docker Compose — a collaborative bookmark manager that keeps a screenshot, PDF and HTML copy of every page, behind HTTPS with registration closed.

Before you start
  • A VPS with 4 GB RAM (the documented minimum)
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain you can point at the server
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Linkwarden is

Linkwarden is a collaborative bookmark manager built around preservation. When you save a link it keeps more than the URL: a screenshot, a PDF and a single-file HTML copy of the page, plus a reader view where you can highlight and annotate. Links go into collections and tags, collections can be shared with other users, and everything is full-text searchable.

It is a TypeScript / Next.js application under the AGPL-3.0 licence. The official Compose file runs three containers: Linkwarden itself, PostgreSQL and Meilisearch for search.

The archive copies are the reason to pick it. Links rot; a bookmark to a page that no longer exists is worth nothing, while a PDF of it is. If you care more about quickly capturing notes, images and links with AI tagging, Karakeep is the closer alternative.

Server sizing

The catalog lists 4 GB of RAM as the minimum. On our test box — a GCP e2-standard-2 with Ubuntu 26.04 and Docker 29.8.1 — the stack idled at about 503 MB of RAM, and images plus data used about 3.0 GB of disk. The gap between those numbers is archiving: rendering a page to a screenshot and a PDF is the expensive step, and it happens for every link you save. Size for that, not for idle.

Disk grows faster here than with a plain bookmark list, because every link carries its own screenshot, PDF and HTML copy. Start with 40 GB and watch ./data.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed, with a ufw firewall. If not, start with Docker & Compose on Ubuntu.

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Linkwarden (Docker Compose)

The upstream install downloads the official Compose file and writes a .env beside it. Start with the Compose file:

mkdir -p ~/linkwarden && cd ~/linkwarden
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/linkwarden/linkwarden/refs/heads/main/docker-compose.yml

It publishes port 3000 on every interface; bind it to loopback so Caddy is the only way in:

cd ~/linkwarden
sed -i 's|- 3000:3000|- 127.0.0.1:3000:3000|' docker-compose.yml
grep -n '3000' docker-compose.yml

Then the environment. Linkwarden needs four values: the auth URL, a session secret, the Postgres password, and a Meilisearch master key. Generate the three secrets inline. NEXTAUTH_URL is your public address followed by /api/v1/auth:

cd ~/linkwarden
[ -f .env ] || cat > .env <<EOF
NEXTAUTH_URL=https://links.example.com/api/v1/auth
NEXTAUTH_SECRET=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -hex 32)
MEILI_MASTER_KEY=$(openssl rand -hex 32)
EOF
docker compose up -d
docker compose ps

The Compose file uses ghcr.io/linkwarden/linkwarden:latest and pins Postgres and Meilisearch to specific versions. Postgres and Meilisearch publish no ports, so they are reachable only from the Linkwarden container.

Wait for the app to answer — the first start runs database migrations:

for i in $(seq 1 60); do
  code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/)
  case "$code" in 200|302|307) echo "Linkwarden is up ($code)"; break ;; esac
  sleep 5
done
case "$code" in 200|302|307) true ;; *) docker compose logs --tail 50 linkwarden; false ;; esac

HTTPS + domain

Point an A record for links.example.com at the server — the hostname must match NEXTAUTH_URL. Then terminate TLS in front of 127.0.0.1:3000, following Automatic HTTPS with Caddy:

links.example.com {
    reverse_proxy 127.0.0.1:3000
}

If Caddy runs as a container, 127.0.0.1 is its own loopback; put it in the same Compose project and use reverse_proxy linkwarden:3000.

First account, then close registration

Open https://links.example.com and register. Then turn registration off so nobody else who finds the URL can create an account. The .env sample in the repository lists NEXT_PUBLIC_DISABLE_REGISTRATION for this:

cd ~/linkwarden
grep -q '^NEXT_PUBLIC_DISABLE_REGISTRATION=' .env || echo "NEXT_PUBLIC_DISABLE_REGISTRATION=true" >> .env
docker compose up -d

Check it in a private browser window: the registration option should be gone. When someone else needs an account, remove the line, let them register, and put it back — or share a collection with an existing user instead.

Then install the browser extension and point it at your server, so saving a link is one click.

Tuning archiving

The same .env sample lists the knobs that control how much work each link creates. Among them:

  • ARCHIVE_TAKE_COUNT — how many links are archived per batch.
  • MAX_LINKS_PER_USER — a cap per account.
  • PDF_MAX_BUFFER, SCREENSHOT_MAX_BUFFER, READABILITY_MAX_BUFFER, MONOLITH_MAX_BUFFER — size limits for each archive format.
  • PROXY settings, to send the archiver's requests through a proxy.

Read the descriptions in the upstream .env.sample before changing them, and remember that every .env change needs docker compose up -d.

Collaboration

Linkwarden is built for more than one person. Collections can be shared with other users on the instance, so a team or a household can keep a common reading list or research folder while each person keeps private collections too. Because every shared link carries its own archived copies, a colleague opening a link months later still sees what was saved, even if the original page has changed or disappeared. If you only ever need a private list, closing registration after creating your own account is all the user management you'll do.

Securing it

  • Keep registration closed once your accounts exist.
  • Keep port 3000 on loopback. ss -ltnp | grep 3000 should show 127.0.0.1:3000.
  • Treat the archiver as a browser running on your server. Anyone with an account can make it fetch any URL. Keep accounts to people you trust.
  • Protect .env. It holds the session secret and database password: chmod 600 ~/linkwarden/.env.

Backups

State lives in three bind-mounted folders next to the Compose file: ./pgdata (the database), ./data (the screenshots, PDFs and HTML copies) and ./meili_data (the search index). Take a logical dump of the database while it runs, then archive the archives:

cd ~/linkwarden
docker compose exec -T postgres pg_dump -U postgres postgres > linkwarden-db-$(date +%F).sql
sudo tar czf linkwarden-data-$(date +%F).tar.gz data .env docker-compose.yml
ls -la linkwarden-*

sudo is there because the containers own the files in ./data. Add meili_data to the archive too, so the search index comes back with the rest. Copy the dump and the archive off the server, and restore into a scratch instance once so you know it works.

Upgrades

cd ~/linkwarden
docker compose pull
docker compose up -d

latest moves with each release, and migrations run on start. Back up first, and read the release notes before upgrading across a major version. When upstream changes the pinned Postgres or Meilisearch version in its Compose file, don't just swap the image tag on an existing install: a Postgres major-version change needs a dump and restore.

Troubleshooting

Login loops or redirects to the wrong host. NEXTAUTH_URL doesn't match the address in the browser, or is missing the /api/v1/auth suffix.

Links are saved but have no screenshot or PDF. Archiving runs in the background; check docker compose logs linkwarden for errors on that link. Some sites block automated browsers, and very large pages can hit the buffer limits above.

The box runs out of memory. Archiving many links at once is heavy; lower ARCHIVE_TAKE_COUNT or add RAM.

Search finds nothing. Check the meilisearch container is running and MEILI_MASTER_KEY hasn't changed since the index was built.

Verification + next steps

You're done when you can load https://links.example.com over a valid certificate, save a link and open its screenshot and PDF copies, find it by searching for a word from the page, confirm registration is closed, and have a database dump and data archive off the box.

For a lighter, link-only alternative, see linkding and Karakeep vs linkding. For hosts, see Best VPS for Self-Hosting.

Next steps

How to self-host Linkwarden →More self-hosted bookmarks tools →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.