Workload · Running your own read-it-later app

Self-hosted wallabag: keep your read-it-later list on a server of your own

You can host wallabag yourself with two containers: the official wallabag/wallabag image for the app and MariaDB for your saved articles, kept private on the server until you have replaced the built-in wallabag/wallabag password. After that, a small nginx setup gives it your own https address, and the browser extension and phone apps connect to it. Here is the whole route, step by step.

The short version
  1. Two containers: the app and its MariaDB database
  2. The built-in login is wallabag / wallabag, so you replace it while the site is still private
  3. Sign-ups stay closed unless you open them
  4. Wallabagger extension, Android and iOS apps over the API
  5. A nightly copy of the database and the article pictures

Is wallabag right for your own server?

Yes, if you want the articles you save to stay on hardware you rent and control. wallabag fetches each page, strips it down to the readable text and keeps it, so you can read later on the web, in the apps or in an export. It is free software under the MIT licence.

The trade is upkeep. With a server of your own, you manage the server; we keep the hardware and the network running. That means you install the updates and you look after your own backups. If that sounds like a chore rather than a hobby, a hosted reading service will suit you better.

What you are about to set up

  • The app container. wallabag/wallabag bundles nginx, PHP and wallabag itself. Everything is configured with environment variables whose names start with SYMFONY__ENV__; at each start the container writes them into its settings file.
  • The database container. MariaDB, as in the example compose file in the wallabag/docker README. Leave SYMFONY__ENV__DATABASE_DRIVER unset and the image falls back to SQLite, but the wallabag FAQ warns that SQLite loses data during migrations and keeps it for development only.
  • A web front door. nginx on the server itself, holding the certificate and passing visitors to the app.

On the first start, the app container waits for MariaDB, creates its own database and user there with the root password you give it, then installs wallabag. Those steps are in the image's entrypoint.sh; the README only hints at them in one line, under MYSQL_ROOT_PASSWORD.

The server that fits

wallabag's documentation does not name a minimum processor or amount of memory, so here is our own suggestion rather than an official one: a reading list for yourself or your household is a light job, and one of the small cartridges or an everyday one-processor server on the dedicated server page will carry it. Spend on disk rather than cores, because saved articles and their pictures are what grows. One thing to weigh on a cartridge: among them only the m510 and m710x come with a console screen, the rest with power control only, so a firewall typo there cannot be fixed from a console. Go over the ufw lines carefully before pasting.

Pick an Intel or AMD server. Our advice is to skip X-Gene, the older of our two ARM machines: its newest system is Ubuntu 18.04, an old footing for containers you plan to update for years. A ready server is yours within 30 minutes of the payment being approved; a custom build takes hours. If you also keep feeds or ebooks, FreshRSS and Calibre-Web can share the machine.

Step by step: from an empty server to your first login

  1. Start with your domain. Give read.example.org an A record holding the IPv4 address of the server, plus an AAAA record if you plan to use IPv6, so the certificate step later can find it.
  2. Install Docker Engine and its Compose plugin by following Docker's instructions for your system. Use Engine 28 or newer, where an app published on 127.0.0.1 really is reachable from the server alone.
  3. Create the folder /srv/wallabag. In it, write root.env holding only MYSQL_ROOT_PASSWORD= and a long random value from openssl rand -hex 32.
  4. Beside it, write app.env with these lines, each value your own: SYMFONY__ENV__DATABASE_DRIVER=pdo_mysql, SYMFONY__ENV__DATABASE_HOST=mariadb, SYMFONY__ENV__DATABASE_PORT=3306, SYMFONY__ENV__DATABASE_NAME=wallabag, SYMFONY__ENV__DATABASE_USER=wallabag, SYMFONY__ENV__DATABASE_PASSWORD= (another hex value), SYMFONY__ENV__DATABASE_CHARSET=utf8mb4, SYMFONY__ENV__SECRET= (a third hex value), SYMFONY__ENV__DOMAIN_NAME=https://read.example.org and SYMFONY__ENV__FOSUSER_REGISTRATION=false.
  5. Both files hold passwords, so make them readable by root alone with chmod 600.
  6. Paste the block below. The firewall goes up first, then the compose file is written and the two containers start.
  7. Watch sudo docker compose logs -f app until it says wallabag is ready!.
  8. Take the root.env line out of the app service (leave the one under mariadb) and, still in /srv/wallabag, start the stack again (sudo docker compose up -d). If you left it in, every later start would only print a warning asking you to remove it (that check is in entrypoint.sh), so stop handing it over.
# 1) firewall first, IPv6 covered: ufw guards SSH now and nginx later, NOT ports Docker publishes
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw --force enable
# 2) the two containers (app.env and root.env are already in /srv/wallabag)
cd /srv/wallabag
sudo tee compose.yaml > /dev/null <<'EOF'
services:
  app:
    image: wallabag/wallabag:2.6.14
    restart: unless-stopped
    env_file:
      - app.env
      - root.env
    ports:
      - "127.0.0.1:8090:80"
    volumes:
      - wallabag-images:/var/www/wallabag/web/assets/images
    depends_on:
      mariadb:
        condition: service_healthy
  mariadb:
    image: mariadb:11.4
    restart: unless-stopped
    env_file:
      - root.env
    volumes:
      - mariadb-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "/usr/local/bin/healthcheck.sh", "--innodb_initialized"]
      interval: 20s
      timeout: 3s
volumes:
  wallabag-images:
  mariadb-data:
EOF
sudo docker compose up -d

A word on the firewall: ufw is not the gatekeeper for ports Docker publishes. Map a port on 0.0.0.0 or [::] and IPv4 visitors bypass ufw; with IPv6 turned on in the Docker daemon settings, IPv6 visitors bypass it as well. That is why the app is published on 127.0.0.1 only, and why the Engine version matters.

Two parts of the compose file are our choices: pinning mariadb:11.4 (the README leaves it untagged) and making the app wait until the database healthcheck passes; the healthcheck line itself is copied from the README. The named volumes follow the docker run example on the installation page.

Why random hex? The image ships a default SECRET that is the same on every install, so replace it. And because the container puts the database password inside a SQL command, plain letters and digits avoid quoting trouble. That second reason is our own precaution.

Change the built-in login while the site is private

A wallabag installed from this image starts with the username wallabag and the password wallabag (the image README states it), and InstallCommand.php gives that account full admin rights. Because the app only answers on 127.0.0.1, nobody on the internet can try it yet. Use that quiet moment:

  1. On your own computer, open a tunnel with your usual login: ssh -N -L 9090:127.0.0.1:8090 you@your-server.
  2. Visit http://localhost:9090 and sign in with wallabag / wallabag.
  3. Under Config, choose a new password (at least 8 characters), then put your own email address under User information.
  4. Leave the tunnel. Apart from SSH, nothing on your server can be reached from outside yet.

Sign-ups stay off because FOSUSER_REGISTRATION is false. Want to add your partner or a colleague? Create their account from the console instead of opening registration to the whole web: in /srv/wallabag, run sudo docker compose exec app su -s /bin/sh -c "bin/console fos:user:create --env=prod" nobody and answer its questions.

Password-reset mail needs somewhere to go. The image default is smtp://127.0.0.1, which leads nowhere inside the container, so reset emails will not arrive until you set the two mail variables in app.env: SYMFONY__ENV__MAILER_DSN for the relay and SYMFONY__ENV__FROM_EMAIL for the sender.

Give it your own https address

nginx runs directly on the server, so the firewall blocks it until you let web traffic in; the block does that with the Nginx Full profile. It then asks for the certificate while the default nginx page still answers on port 80, and only when the certificate exists does it write and switch on the site that uses it.

sudo apt install -y nginx certbot
sudo ufw allow 'Nginx Full'
if sudo certbot certonly --webroot -w /var/www/html -d read.example.org --deploy-hook "systemctl reload nginx"; then
sudo tee /etc/nginx/sites-available/read.example.org > /dev/null <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name read.example.org;
    location /.well-known/acme-challenge/ { root /var/www/html; }
    location / { return 301 https://$host$request_uri; }
}
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name read.example.org;
    ssl_certificate     /etc/letsencrypt/live/read.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/read.example.org/privkey.pem;
    client_max_body_size 100M;
    location / {
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}
EOF
sudo ln -s /etc/nginx/sites-available/read.example.org /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
else
echo "Certificate not issued: fix the DNS record or port 80 and run this again"
fi

The forwarded headers follow the nginx example in the wallabag/docker README, and the upload limit matches the 100M set for the container's own nginx in that repository's root/etc/nginx/nginx.conf, which leaves room for large imports.

Save from your browser and your phone

The extension and the apps sign in through wallabag's API, so first make an API client: open API clients management in wallabag (the form lives at /developer/client/create), give it a name such as "My phone", and keep the Client ID and secret private.

Where you readWhat to know
Wallabagger for Firefox or ChromeType your address without http://, tick https, then add the client details and your login. Its docs warn it stores your password in plain text in the browser.
Android appNo slash at the end of the address, use https, and turn two-factor login off: the app cannot handle it.
iOS appAddress first, then Client ID and secret, then the same login you use on the web.
Any browserThe bag it! bookmarklet on the How to page needs no client at all.

Keep your articles safe, and update without losing them

wallabag's backup page lists three things worth keeping: the settings, the database and the downloaded pictures. With this image your settings are app.env, so keep it with compose.yaml and root.env. The script below writes the database and pictures into a private folder, and the database password never appears in the dump command's arguments.

# file: /usr/local/sbin/backup-wallabag.sh   run it with: sudo bash /usr/local/sbin/backup-wallabag.sh
set -eu
umask 077
dest=/var/backups/readlater
mkdir -p -m 700 "$dest"
cd /srv/wallabag
stamp=$(date +%F)
docker compose exec -T mariadb sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mariadb-dump -uroot --single-transaction --databases wallabag' > "$dest/database-$stamp.sql"
docker compose exec -T app tar -C /var/www/wallabag/web/assets -cf - images > "$dest/pictures-$stamp.tar"
ls -l "$dest"

To run it every night, save this single line as the file /etc/cron.d/backup-wallabag: 15 4 * * * root bash /usr/local/sbin/backup-wallabag.sh > /var/log/backup-wallabag.log 2>&1. Then copy the folder, plus compose.yaml, app.env and root.env from /srv/wallabag, somewhere off the server. To be honest about checking: listing the folder with sudo ls and reading a pictures archive with sudo tar -tf only tell you the files exist and open. A restore is what tells you the articles come back.

Bringing a backup back, from /srv/wallabag. Setting up a new server? Start the stack once with root.env still listed under the app, so the database user exists, then restore. Replace the date with yours.

  1. Make sure both halves are there: sudo test -s /var/backups/readlater/database-2026-10-21.sql && sudo test -s /var/backups/readlater/pictures-2026-10-21.tar && echo found. Carry on only if you see found.
  2. Pause the app: sudo docker compose stop app.
  3. Feed the database back in: sudo cat /var/backups/readlater/database-2026-10-21.sql | sudo docker compose exec -T mariadb sh -c 'env MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mariadb -u root'.
  4. Unpack the pictures into the app: sudo cat /var/backups/readlater/pictures-2026-10-21.tar | sudo docker compose exec -T app tar -C /var/www/wallabag/web/assets -xf -.
  5. Start the app again with sudo docker compose start app, log in and check an article that has pictures.

Updating, after a fresh backup: set the new version in the image: line, pull it with sudo docker compose pull, restart (sudo docker compose up -d), and finish with sudo docker compose run --rm app migrate so the database matches the new version. The wallabag/docker README offers migrate for exactly this, and the upgrade notes warn that a skipped migration leaves wallabag broken.

Questions people ask

Is the wallabag Docker image the official one?
Yes. wallabag/wallabag is built from the wallabag/docker repository on GitHub, which belongs to the wallabag project, and the installation docs point to it. Pin a version tag such as 2.6.14 so updates happen when you choose.
Can my family share one wallabag?
Yes. Each account has its own saved articles. Keep public sign-up off and create each person's account yourself: from /srv/wallabag, run sudo docker compose exec app su -s /bin/sh -c "bin/console fos:user:create --env=prod" nobody. It runs as the nobody user, the way the image runs its own commands, and asks for the name, email and password.
What happens to the article pictures?
If you turn on the option to download images locally (in Internal settings), wallabag stores each article's pictures in web/assets/images and points the article at your copy. That folder is the named volume in the compose file, and it is the second half of every backup.
What does the MIT licence let me do?
In a general reading, not legal advice: run wallabag, change it and pass it on, for free. The notice-keeping duty applies to whoever distributes copies of the software; running it for yourself distributes nothing.
Can I move my saved links over from another app?
Yes. The import docs walk through Instapaper, Pocket, Readability, Pinboard, elCurator and older wallabag exports. Bookmarks exported from Firefox or Chrome go through the wallabag:import console command, choosing --importer=firefox or --importer=chrome.

Give your reading list a home

Choose a whole server for wallabag: no contract, and nobody metering the port.

Get a server for wallabag

Read next