How to Deploy Linkwarden on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-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.
- 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)
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
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.PROXYsettings, 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 3000should show127.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.