What you will set up
Linkwarden saves your bookmarks, and keeps its own copy of each page: a screenshot, a PDF, a readable text version and a single-file copy of the page itself. When a page changes or disappears, your copy stays. You sort links into collections, tag them, share collections with others, and search the text of every page you saved.
Here it runs as a pod under a user of its own called linkwarden, behind Caddy from the Podman guide: Linkwarden itself, PostgreSQL for its data, and Meilisearch for its search.
Every step below was run on a Melonslab server with Debian 13:
- Linkwarden 2.16.3 saved a page, and within a minute made all four copies of it.
- A search for a word that appeared only in the page's text found it.
- Once sign-ups were turned off, a new account could not be created.
- Everything came back by itself after a reboot.
The pod used about 500 MB of memory, and more for a short while each time it saves a page, since it opens the page in a browser.
Before you start
You need:
- a server set up as in the Podman guide, with Caddy running;
- an A record and an AAAA record for
links.example.compointing at your server.
Replace links.example.com with your own name throughout.
1. Create the user
As root:
useradd -m -s /bin/bash linkwarden
loginctl enable-linger linkwarden
machinectl shell linkwarden@
Everything up to step 4 runs as linkwarden.
2. Create the secrets
pw=$(openssl rand -hex 24)
printf %s "$pw" | podman secret create linkwarden-db-password -
printf 'postgresql://postgres:%s@127.0.0.1:5432/postgres' "$pw" | podman secret create linkwarden-database-url -
unset pw
openssl rand -hex 32 | tr -d '\n' | podman secret create linkwarden-nextauth-secret -
openssl rand -hex 32 | tr -d '\n' | podman secret create linkwarden-meili-key -
The first two hold the database password, once for PostgreSQL and once in the address Linkwarden connects to. The third signs Linkwarden's logins, and the fourth protects Meilisearch.
3. Describe the pod
mkdir -p ~/.config/containers/systemd
cd ~/.config/containers/systemd
Create linkwarden.pod:
[Pod]
PodName=linkwarden
# Only Caddy, on this server, can reach Linkwarden: the port is not open to the internet.
PublishPort=127.0.0.1:8096:3000
[Install]
WantedBy=default.target
Create linkwarden-db.container:
[Container]
ContainerName=linkwarden-db
Image=docker.io/library/postgres:16-alpine
Pod=linkwarden.pod
Volume=linkwarden-db:/var/lib/postgresql/data
Secret=linkwarden-db-password,type=env,target=POSTGRES_PASSWORD
AutoUpdate=registry
[Service]
Restart=always
Create linkwarden-search.container:
[Container]
ContainerName=linkwarden-search
Image=docker.io/getmeili/meilisearch:v1.13.3
Pod=linkwarden.pod
Volume=linkwarden-search:/meili_data
Environment=MEILI_ENV=production MEILI_NO_ANALYTICS=true
Secret=linkwarden-meili-key,type=env,target=MEILI_MASTER_KEY
[Service]
Restart=always
And linkwarden-app.container:
[Unit]
After=linkwarden-db.service linkwarden-search.service
[Container]
ContainerName=linkwarden-app
Image=ghcr.io/linkwarden/linkwarden:latest
Pod=linkwarden.pod
Volume=linkwarden-data:/data/data
Environment=NEXTAUTH_URL=https://links.example.com/api/v1/auth MEILI_HOST=http://127.0.0.1:7700
Secret=linkwarden-database-url,type=env,target=DATABASE_URL
Secret=linkwarden-nextauth-secret,type=env,target=NEXTAUTH_SECRET
Secret=linkwarden-meili-key,type=env,target=MEILI_MASTER_KEY
AutoUpdate=registry
[Service]
Restart=always
The saved copies of your pages go in the linkwarden-data volume. Meilisearch stays on the version that Linkwarden's own setup uses, because moving Meilisearch to a new version means migrating its data, while Linkwarden and PostgreSQL follow their releases. Linkwarden updates its database by itself when it starts.
Start it:
systemctl --user daemon-reload
systemctl --user start linkwarden-pod
systemctl --user enable --now podman-auto-update.timer
The first start downloads the images, which take about 2.2 GB of disk, and takes a couple of minutes.
4. Put Caddy in front
Go back to root with exit, switch to machinectl shell caddy@, and add this block at the end of ~/Caddyfile:
links.example.com {
reverse_proxy 127.0.0.1:8096
}
Restart Caddy with systemctl --user restart caddy.
5. Create your account, then close sign-ups
Open https://links.example.com/register, and create your account with a Display Name, a Username and a Password. Then log in. Create accounts for anyone else who will use it the same way.
Anyone who finds the address can sign up until you close it. As linkwarden, add this line to linkwarden-app.container, under the Environment=NEXTAUTH_URL line:
Environment=NEXT_PUBLIC_DISABLE_REGISTRATION=true
Then restart Linkwarden:
systemctl --user daemon-reload
systemctl --user restart linkwarden-app
A sign-up after that gets Registration is disabled.
6. Save links
Choose New Link, paste an address, and pick a collection and tags if you like. Within a minute, the link shows its preview, and its menu has the screenshot, the PDF, the readable version and the copy of the page. The search at the top looks through names, addresses, tags and the text of every saved page.
Linkwarden also has browser extensions and apps for phones, which log in with your server's address, https://links.example.com, and your account.
7. Back up
As linkwarden:
mkdir -p ~/backup
podman exec linkwarden-db pg_dump -U postgres postgres > ~/backup/linkwarden-db.sql
systemctl --user stop linkwarden-app linkwarden-search
podman volume export linkwarden-data --output ~/backup/linkwarden-data.tar
podman volume export linkwarden-search --output ~/backup/linkwarden-search.tar
systemctl --user start linkwarden-search linkwarden-app
That saves your accounts, links and collections, the saved copies of the pages, and the search index. Copy ~/backup to another machine, and keep it private.
Troubleshooting
Links never get a preview or copies. Linkwarden's worker makes them, and logs each link it works on: podman logs linkwarden-app | grep worker shows what happened.
A sign-up still works after step 5. The NEXT_PUBLIC_DISABLE_REGISTRATION line is missing, or Linkwarden was not restarted after daemon-reload.