$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

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:

  1. Wallabag URL: https://read.example.com
  2. Client ID / Secret: generate these in Wallabag under Developer → Create a new client. Give the client a name (e.g., browser-ext).
  3. Username / Password: your Wallabag login credentials.
  4. 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

  1. Log into https://read.example.com and confirm articles appear in the library.
  2. Save a page from the browser extension — it should appear in Unread within a few seconds.
  3. Open the mobile app, pull to refresh, and verify the same article syncs down.
  4. Check TLS is valid: curl -I https://read.example.com should return HTTP/2 200 with 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.

tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

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