Linkwarden Bookmark Manager: Archive and Organize Webpages via Docker
Never lose a webpage again. Deploy Linkwarden bookmark manager with Docker Compose to capture PDFs and screenshots of links automatically.
Linkwarden Bookmark Manager: Archive and Organize Webpages via Docker
Webpages are ephemeral. URLs rot, content changes, and websites go offline. Traditional bookmark managers only save links, leaving you vulnerable to "link rot." Linkwarden is an open-source, collaborative bookmark manager designed to solve this by archiving webpages in multiple formats (PDF, screenshots, and monolithic HTML) automatically.
In this guide, we will walk through self-hosting Linkwarden on a Linux VPS using Docker Compose. We'll detail the architectural components, draft a robust docker-compose.yml config, configure full-text search with Meilisearch, and secure the setup behind an Nginx reverse proxy with SSL.
1. Architectural Overview
To host Linkwarden efficiently, it helps to understand its three core components: 1. Linkwarden Core App: A Next.js application that handles user authentication, dashboard UI, link categorization (Collections/Tags), and triggers the archival background jobs. It comes with built-in Playwright/Chromium to crawl links. 2. PostgreSQL Database: Relational database storing user metadata, Collections, link references, tags, and transaction logs. 3. Meilisearch Engine: A lightweight, lightning-fast search engine providing typo-tolerant, full-text search capabilities across your saved pages' text content.
graph TD
Client[Browser / Extension] -->|HTTPS| ReverseProxy[Nginx Reverse Proxy]
ReverseProxy -->|Port 3000| Linkwarden[Linkwarden Core Service]
Linkwarden -->|Port 5432| Postgres[(PostgreSQL Database)]
Linkwarden -->|Port 7700| Meilisearch[Meilisearch Engine]
Linkwarden -->|Internal Playwright| Chromium[Chromium Archiving Engine]
Linkwarden -->|Saves PDFs/HTML| Storage[(Local Data Volume)]
2. Directory Structure and Environment Preparation
First, establish a dedicated directory structure for Linkwarden on your VPS to keep volumes clean and maintainable.
mkdir -p /opt/linkwarden && cd /opt/linkwarden
mkdir -p pgdata data meili_data backups
Assign the appropriate permissions to ensure Docker containers can read and write data:
sudo chown -R 1000:1000 data meili_data pgdata backups
3. Docker Compose Configuration
Create a docker-compose.yml file in /opt/linkwarden. This configuration coordinates PostgreSQL, Meilisearch, and Linkwarden with isolated networking and persistent storage.
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: linkwarden-db
restart: always
env_file: .env
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./pgdata:/var/lib/postgresql/data
networks:
- linkwarden-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
meilisearch:
image: getmeili/meilisearch:v1.12.8
container_name: linkwarden-search
restart: always
env_file: .env
environment:
MEILI_ENV: production
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
volumes:
- ./meili_data:/meili_data
networks:
- linkwarden-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7700/health"]
interval: 10s
timeout: 5s
retries: 5
linkwarden:
image: ghcr.io/linkwarden/linkwarden:latest
container_name: linkwarden-web
restart: always
ports:
- "127.0.0.1:3000:3000"
env_file: .env
environment:
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
- MEILI_HOST=http://meilisearch:7700
volumes:
- ./data:/data/data
depends_on:
postgres:
condition: service_healthy
meilisearch:
condition: service_healthy
networks:
- linkwarden-network
networks:
linkwarden-network:
driver: bridge
4. Environment File (.env) Setup
Linkwarden relies on dynamic configuration variables. Create .env inside /opt/linkwarden and configure the parameters below.
Generate secure, cryptographically random strings for security keys before pasting:
# Generate a secret key for authentication
openssl rand -base64 32
# Generate a master key for Meilisearch
openssl rand -base64 32
# Generate a strong password for PostgreSQL
openssl rand -base64 24
Here is the .env template:
# --- CORE AUTHENTICATION ---
# The canonical URL pointing to your Linkwarden instance's auth callback.
# Replace with your actual domain name!
NEXTAUTH_URL=https://links.yourdomain.com/api/v1/auth
NEXTAUTH_SECRET=SUPER_SECRET_RANDOM_BASE64_STRING
# --- DATABASE CONFIG ---
# Must match the DB password in postgres service
POSTGRES_PASSWORD=STRONG_DATABASE_PASSWORD
# --- MEILISEARCH CONFIG ---
# Used to secure communications with the Meilisearch API
MEILI_MASTER_KEY=STRONG_MEILI_MASTER_KEY
# --- REGISTRATION AND PERMISSIONS ---
# Allow or disable new user sign-ups. Set to 'true' after creating your primary admin account!
NEXT_PUBLIC_DISABLE_REGISTRATION=false
NEXT_PUBLIC_CREDENTIALS_ENABLED=true
# --- FILES & STORAGE ---
# Local path mapped inside the linkwarden container
STORAGE_FOLDER=/data/data
# --- ARCHIVAL ENGINE PARAMETERS ---
# Timeout duration in milliseconds for the Playwright engine when downloading pages
BROWSER_TIMEOUT=60000
# Max buffer sizes for generated archives (in bytes)
PDF_MAX_BUFFER=52428800 # 50MB
SCREENSHOT_MAX_BUFFER=52428800 # 50MB
5. Tag and Collection Taxonomy Organization
In Linkwarden, organizing bookmarks relies on a dual-layer system combining Collections and Tags. This structure keeps resources manageable even as your database scales into thousands of URLs.
5.1 Collections (Hierarchical Directories)
- Collections behave like standard filesystem directories but with collaborative features.
- They allow hierarchical folder nesting.
- Collaboration: You can share Collections with other users on your instance, giving them permissions to view, edit, or contribute links (Reader, Editor, or Admin roles).
5.2 Tags (Flat Taxonomy)
- While a link can only belong to a single Collection, it can have multiple Tags.
- Use Tags for cross-cutting concerns (e.g., a link within your "DevOps" collection might be tagged with
#docker,#monitoring, and#tutorial). - Typo-tolerant querying provided by Meilisearch runs directly against tag lists, letting you filter complex datasets instantly.
6. Reverse Proxy Setup with Nginx & Let's Encrypt
Exposing port 3000 directly to the internet is insecure. It is standard practice to run Linkwarden behind an Nginx reverse proxy with SSL certificates.
6.1 Nginx Site Configuration
Create an Nginx configuration file at /etc/nginx/sites-available/linkwarden.conf:
server {
listen 80;
server_name links.yourdomain.com;
# Redirect all HTTP requests to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name links.yourdomain.com;
# SSL configuration (will be populated by Certbot)
# ssl_certificate /etc/letsencrypt/live/links.yourdomain.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/links.yourdomain.com/privkey.pem;
client_max_body_size 100M; # Crucial for importing large bookmark files
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Real IP forwarding
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
6.2 Enabling and Securing with SSL
Enable the site configuration and request a Let's Encrypt certificate:
sudo ln -s /etc/nginx/sites-available/linkwarden.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d links.yourdomain.com
7. Running the Stack
Once configurations are set, orchestrate your Docker environment:
# Pull the latest image assets
docker compose pull
# Start services in background daemon mode
docker compose up -d
Verify service stability:
docker compose ps
docker compose logs -f linkwarden
Navigate to https://links.yourdomain.com, register your user profile, and then modify .env to set:
NEXT_PUBLIC_DISABLE_REGISTRATION=true
Then restart the stack to apply:
docker compose up -d
8. Deep Dive: Archiving Engines and Performance Tuning
Linkwarden's core differentiator is its automated archiving engine, powered internally by Playwright. When a link is added, a background worker uses Chromium to load the webpage, rendering its layout before archiving it in three states: 1. Screenshot: Renders the visible viewport as a PNG/JPEG image. 2. PDF: Prints the viewport dynamically into a clean vector document. 3. Monolith (HTML): Converts the webpage into a single self-contained HTML file where all external assets (CSS, JS, images) are base64-encoded directly in the document body.
Resource Allocation
Running Chromium within containerized limits can be resource-intensive. If your VPS frequently encounters CPU or memory exhaustion: * Limit maximum Playwright workers by defining MAX_WORKERS in .env: ini MAX_WORKERS=2 * Increase container memory capacity. Ensure the VPS has a minimum of 2GB RAM. If running on a 1GB RAM node, configure a swap file to prevent the OOM (Out Of Memory) killer from terminating your Chromium engine.
9. Backup & Restore Strategy
Since Linkwarden stores metadata relationally in PostgreSQL but archives binary PDFs/screenshots on disk under ./data, a complete backup requires capturing both components.
9.1 Database Backup Script
Generate a cron-ready PostgreSQL dump:
docker exec -t linkwarden-db pg_dumpall -c -U postgres > /opt/linkwarden/backups/db_backup_$(date +%F).sql
9.2 File Volume Backup
Compress the storage assets folder:
tar -czf /opt/linkwarden/backups/files_backup_$(date +%F).tar.gz -C /opt/linkwarden data/
Combine these into a nightly cron task to secure your data against disk failure.