Self-Host Wallabag for Read-It-Later
Deploy Wallabag on your own server with Docker and PostgreSQL, configure browser extensions and mobile apps, and import your Pocket or Instapaper reading list.
Before you start
- ▸Docker Engine 24+ and Docker Compose v2 installed on the host
- ▸A domain or subdomain with DNS A record pointing to the server's public IP
- ▸Ports 80 and 443 open at the network/firewall level
- ▸Basic familiarity with editing YAML files and running Docker commands
Wallabag is an open-source read-it-later service — a self-hosted alternative to Pocket or Instapaper. You save articles once, read them anywhere, and your data stays on your own server. This guide walks through a production-ready Docker deployment behind a reverse proxy, browser extension setup, mobile client configuration, and importing your existing reading list via OPML.
Architecture Overview
The stack is intentionally minimal: a Wallabag container backed by PostgreSQL (more reliable than the default SQLite for multi-device use), served through a Caddy reverse proxy that handles TLS automatically. Caddy can be swapped for nginx or Traefik if you already run one.
Prerequisites
- A Linux host with Docker Engine 24+ and Docker Compose v2 installed
- A domain name or subdomain pointing to the host's public IP (required for TLS and mobile apps)
- Ports 80 and 443 open in your firewall
Step 1 — Create the Project Directory
mkdir -p ~/wallabag && cd ~/wallabag
Step 2 — Write the Compose File
Create compose.yaml with the following content. Replace every CHANGE_ME value before starting the stack.
cat > compose.yaml <<'EOF'
services:
wallabag:
image: wallabag/wallabag:latest
restart: unless-stopped
environment:
SYMFONY__ENV__DATABASE_DRIVER: pdo_pgsql
SYMFONY__ENV__DATABASE_HOST: db
SYMFONY__ENV__DATABASE_PORT: 5432
SYMFONY__ENV__DATABASE_NAME: wallabag
SYMFONY__ENV__DATABASE_USER: wallabag
SYMFONY__ENV__DATABASE_PASSWORD: CHANGE_ME_DB_PASS
SYMFONY__ENV__SECRET: CHANGE_ME_APP_SECRET_32CHARS
SYMFONY__ENV__DOMAIN_NAME: https://read.example.com
SYMFONY__ENV__FOSUSER_REGISTRATION: "false"
SYMFONY__ENV__FOSUSER_CONFIRMATION: "false"
volumes:
- wallabag_data:/var/www/wallabag/web/assets
- wallabag_images:/var/www/wallabag/web/images
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: wallabag
POSTGRES_USER: wallabag
POSTGRES_PASSWORD: CHANGE_ME_DB_PASS
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U wallabag"]
interval: 10s
retries: 5
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
wallabag_data:
wallabag_images:
pg_data:
caddy_data:
caddy_config:
EOF
Security note: Use a randomly generated 32-character string for SYMFONY__ENV__SECRET. You can generate one with openssl rand -hex 16. The database password should be equally strong.
Step 3 — Configure Caddy
cat > Caddyfile <<'EOF'
read.example.com {
reverse_proxy wallabag:80
}
EOF
Replace read.example.com with your actual domain. Caddy fetches a Let's Encrypt certificate automatically on first start as long as port 80 and 443 are reachable.
Step 4 — Start the Stack and Initialise the Database
docker compose up -d
Watch the wallabag container logs until the app is ready — first boot runs database migrations and can take 60–90 seconds:
docker compose logs -f wallabag
Look for a line containing NOTICE: ready to handle connections. Then create the initial admin user:
docker compose exec wallabag \
php bin/console wallabag:install --env=prod
The installer prompts for an admin username, email, and password. After it completes, visit https://read.example.com and log in.
Step 5 — Open the Firewall
Only ports 80 and 443 need to be reachable externally. The database port never leaves the Docker network.
Debian / Ubuntu (ufw)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
Fedora / RHEL / Rocky (firewalld)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
Arch (nftables example snippet)
sudo nft add rule inet filter input tcp dport { 80, 443 } accept
Step 6 — Install the Browser Extension
Wallabag ships official extensions for Firefox and Chrome-based browsers.
- Firefox: Install wallabagger from AMO.
- Chrome / Edge / Brave: Install wallabagger from the Chrome Web Store.
After installing, click the extension icon and enter your settings:
- Wallabag URL:
https://read.example.com - Client ID / Secret: generate these in Wallabag under Developer → Create a new client. Give the client a name (e.g., browser-ext).
- Username / Password: your Wallabag login credentials.
- Click Check config. A green tick confirms the connection.
You can now save any page to Wallabag by clicking the extension icon or using the keyboard shortcut it registers.
Step 7 — Set Up Mobile Clients
Wallabag has first-party and third-party apps for Android and iOS.
Android
- Official app: Wallabag for Android (also on F-Droid). Open the app, enter your server URL, and authenticate using OAuth — the app guides you through generating a client ID and secret.
- Third-party: Listolet supports Wallabag and offers a cleaner reading view.
iOS
- Official app: Wallabag 2 Official on the App Store. Authentication flow is identical to Android — OAuth client credentials.
Both apps support offline reading and will sync starred, archived, and tagged states back to the server when connectivity returns.
Step 8 — Import an OPML or Pocket Export
Wallabag accepts several import formats through its built-in importer. In the web UI go to All articles → Import. Options include Pocket, Instapaper, Pinboard, Readability, Firefox bookmarks, and a generic wallabag JSON export.
For large Pocket exports (thousands of articles), the web importer can time out. Use the console importer instead:
# Copy your export file into the container first
docker compose cp ~/pocket_export.html wallabag:/tmp/pocket_export.html
# Run the import
docker compose exec wallabag \
php bin/console wallabag:import:redis-worker pocket \
--env=prod -vv
For a standard Pocket HTML export you can also use the direct command:
docker compose exec wallabag \
php bin/console wallabag:import pocket \
--username=youradmin \
--filepath=/tmp/pocket_export.html \
--env=prod
Output will report how many entries were imported and how many were skipped as duplicates. Processing time scales with article count — fetching full content for 2,000 articles can take several minutes.
Verification
- Log into
https://read.example.comand confirm articles appear in the library. - Save a page from the browser extension — it should appear in Unread within a few seconds.
- Open the mobile app, pull to refresh, and verify the same article syncs down.
- Check TLS is valid:
curl -I https://read.example.comshould returnHTTP/2 200with no certificate warnings.
curl -I https://read.example.com
Troubleshooting
Wallabag shows a 500 error on first load
Usually a cache or permission issue. Clear the Symfony cache inside the container:
docker compose exec wallabag php bin/console cache:clear --env=prod
Articles fetch but images are broken
Wallabag downloads images to the web/images directory. If the SYMFONY__ENV__DOMAIN_NAME variable is wrong or missing the https:// prefix, image URLs will be malformed. Double-check the value and restart:
docker compose up -d --force-recreate wallabag
Mobile app cannot authenticate
Each device needs its own OAuth client. Go to Developer → Clients management and create a dedicated client per device. Reusing browser extension credentials across apps often causes token conflicts.
Database container is unhealthy
Check PostgreSQL logs and disk space. The health check uses pg_isready; if the volume is full the check will fail silently from the application's perspective:
docker compose logs db
df -h
Keeping Wallabag Updated
The wallabag/wallabag:latest tag tracks the most recent stable release. To update safely, pull new images and let the container run the built-in migration on startup:
docker compose pull
docker compose up -d
Migrations are idempotent — re-running them on an already-current database is safe. Back up the PostgreSQL volume before any major version jump (2.5 → 2.6, for example) with docker compose exec db pg_dump -U wallabag wallabag > backup.sql.
Frequently asked questions
- Can I use SQLite instead of PostgreSQL to keep things simpler?
- Yes, Wallabag supports SQLite out of the box and the official Docker image defaults to it. However, SQLite is not safe for concurrent writes from multiple devices and browser extensions simultaneously. PostgreSQL is strongly recommended for anything beyond a single-user, single-device setup.
- Why do I need to create a separate OAuth client for the browser extension and each mobile app?
- Each OAuth client issues its own access and refresh tokens. Sharing one client across multiple apps means a token refresh from one device can invalidate the session on another, causing random logouts. One client per app avoids this entirely.
- Does Wallabag fetch the full article text, or just save the URL?
- Wallabag fetches and stores the full parsed article content at save time using its built-in content extractor. This means articles are available offline in mobile apps even if the original page is later deleted or paywalled.
- How do I back up Wallabag data?
- Back up two things: the PostgreSQL database with pg_dump, and the Docker volumes for images and assets. Run pg_dump inside the db container and copy the output off the server. For a complete disaster-recovery backup, also snapshot the pg_data volume itself.
- Can I disable public registration so only I can use the instance?
- Yes — the Compose file already sets SYMFONY__ENV__FOSUSER_REGISTRATION to false, which removes the registration form entirely. New users can only be added by an admin through the internal user management panel or the console command wallabag:user:create.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.