What you are about to run, and why it stays small
You get a reading page you open in any modern browser, a background scheduler that checks your feeds, and a database holding everything. The Miniflux FAQ explains that the app writes nothing to its own disk, not even site icons, so the database really is the whole story. That keeps the moving parts down to two.
- Miniflux: a single program written in Go, shipped as the
miniflux/minifluximage for 64-bit Intel and AMD (amd64), 64-bit ARM (arm64) and two 32-bit ARM variants (arm/v7, arm/v6). - PostgreSQL: version 11 or newer, and the only database Miniflux supports. There is no SQLite mode.
- A web server in front: nginx here, to give you HTTPS on your own domain.
If you would rather have extensions and a choice of database, read our FreshRSS walkthrough instead. Miniflux trades those for fewer things that can break.
Will a small server keep up?
Yes, for one reader or a handful. In its README the project says Miniflux needs only "a couple of MB" of memory and that its CPU use stays negligible across several hundred feeds. That covers Miniflux alone; the database needs its own memory and disk, and it grows with how much you keep.
| Setting | Default | What it means for you |
|---|---|---|
| POLLING_FREQUENCY | 60 minutes | How often a batch of feeds is picked for refresh |
| BATCH_SIZE | 100 feeds | How many feeds join each batch |
| CLEANUP_ARCHIVE_READ_DAYS | 60 days | When articles you have read are marked as removed |
| CLEANUP_ARCHIVE_UNREAD_DAYS | 180 days | When unread articles are marked as removed |
All four come from the configuration page, and -1 on either cleanup setting keeps those articles for good.
The server that fits
A feed reader is one job, and the small cartridge servers on our dedicated server page are built for exactly one job each: the whole machine with root, a port nobody meters, and no contract. Ready servers reach you within 30 minutes of the payment being approved; custom builds take hours, not minutes.
- If you ever shut yourself out with a bad firewall change, the out-of-band console (screen and power button) gets you back. Rack servers and blades have it, and so do two cartridge models, the m510 and m710x; the remaining cartridges only let you switch power.
- Skip the X-Gene ARM cartridge. Miniflux has an arm64 image, but that cartridge runs only Ubuntu 16.04 or 18.04, and those are too old for Docker Engine 28, which this setup needs.
- Put Ubuntu 24.04 on it (the reinstall option in the control panel does that), then add Docker Engine 28+ from Docker's official apt repository, Compose plugin included.
Up and running in one paste
Log in, become root with sudo -i, and paste the block. It turns the firewall on (with IPv6 rules switched on first, so both kinds of address are covered), then goes further only when Docker is version 28 or later and Miniflux has not been set up in /srv/miniflux before. That second check matters: running it again would generate a new database password that no longer matches the data.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
ver=$(docker version --format '{{.Server.Version}}')
if [ "${ver%%.*}" -ge 28 ] 2>/dev/null && [ ! -e /srv/miniflux/.env ]; then
install -d -m 700 /srv/miniflux && cd /srv/miniflux &&
install -m 600 /dev/null .env && install -m 600 /dev/null admin.env &&
pg=$(openssl rand -hex 24) && first=$(openssl rand -hex 12) &&
[ -n "$pg" ] && [ -n "$first" ] && echo "DB_PASS=$pg" > .env &&
printf 'CREATE_ADMIN=1\nADMIN_USERNAME=admin\nADMIN_PASSWORD=%s\n' "$first" > admin.env &&
touch public.env && cat > compose.yaml <<'EOF' &&
services:
miniflux:
image: miniflux/miniflux:2.3.4
restart: unless-stopped
ports: ["127.0.0.1:8080:8080"]
depends_on: {db: {condition: service_healthy}}
env_file: [admin.env, public.env]
environment:
- DATABASE_URL=postgres://miniflux:${DB_PASS}@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
db:
image: postgres:18
restart: unless-stopped
environment: [POSTGRES_USER=miniflux, "POSTGRES_PASSWORD=${DB_PASS}", POSTGRES_DB=miniflux]
volumes: ["pgdata:/var/lib/postgresql"]
healthcheck: {test: ["CMD", "pg_isready", "-U", "miniflux"], interval: 10s, start_period: 30s}
volumes:
pgdata:
EOF
docker compose -f /srv/miniflux/compose.yaml up -d
else echo "STOPPED: Docker Engine 28+ required, and /srv/miniflux/.env must not exist"; fi
What changed from the example on the Miniflux Docker page, all by us: the version is pinned to 2.3.4 (the docs advise pinning, and 2.3.4 was the latest GitHub release as we wrote this), the reader listens only on 127.0.0.1 instead of port 80, the passwords sit in two files readable by root alone instead of inside the YAML, the first-account settings get their own file, an empty public.env waits for later, and both containers restart on their own. The database image (postgres:18), where its data lives, its health check and the migrations setting stay as published.
Why 127.0.0.1? Leave the address out of a Docker port and it opens on every address the machine has, both 0.0.0.0 and [::]. Over IPv4, Docker routes that traffic around ufw. Over IPv6 it does the same only if you have turned on IPv6 networking inside Docker; if not, the [::] port is served by docker-proxy, and ufw does apply to that. Tying the port to 127.0.0.1 keeps it on the machine, and Docker Engine 28 and newer hold that binding to local traffic only.
Your account, before anyone else can see it
Miniflux creates the admin account on its first start from the three variables in admin.env. Whoever has access to Docker on the server can read those values with docker inspect, and the Miniflux Docker page says they can be dropped once the account exists, so you replace the password and clear them before going public.
- As root, set a shortcut for this step:
dc="docker compose -f /srv/miniflux/compose.yaml". Then check the account was made:$dc logs miniflux | grep -i createdshould show "Created new admin user", the message increate_admin.go. - On your own computer, run
ssh -L 9000:127.0.0.1:8080 you@server.example(your normal SSH login) and openhttp://localhost:9000. - Your username is
admin; as root,cat /srv/miniflux/admin.envshows the password. Import your OPML export from your old reader if you have one. - Set a password of your own with the documented command, which asks for the username and new password:
docker compose -f /srv/miniflux/compose.yaml exec miniflux miniflux -reset-password.
Opening it to the world with HTTPS
Create a DNS A record for your chosen name (plus AAAA if your server has IPv6) and give it time to resolve. This block clears admin.env, tells Miniflux its public address through BASE_URL, sets HTTPS=1 so cookies are marked secure and the HSTS header is sent, restarts the container with those values, and puts nginx in front with a free certificate from Let's Encrypt.
cd /srv/miniflux && : > admin.env &&
echo 'BASE_URL=https://news.example.com' > public.env && echo 'HTTPS=1' >> public.env &&
docker compose -f /srv/miniflux/compose.yaml up -d &&
apt install -y nginx certbot python3-certbot-nginx &&
cat > /etc/nginx/sites-available/news <<'EOF' &&
server {
server_name news.example.com;
listen 80;
listen [::]:80;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_redirect off;
proxy_set_header Host $host;
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;
}
}
EOF
ln -sf /etc/nginx/sites-available/news /etc/nginx/sites-enabled/news &&
nginx -t && systemctl reload nginx && ufw allow 80,443/tcp &&
certbot --nginx -d news.example.com
The nginx part follows the How-To page's reverse-proxy example, plus one line of ours, listen [::]:80, for IPv6 visitors. Because nginx runs directly on the server, ufw blocks it until the 80,443/tcp rule is added. certbot will want an email address and your agreement to its terms, so it comes last.
Reading on your phone, and sending articles on
Miniflux speaks two older sync languages that many apps understand. Turn one on under Settings > Integrations and choose a separate username and password for it, which you then type into the app.
- Google Reader API (a subset of it): NetNewsWire, Capy Reader, RSS Guard and Reeder Classic (version 5 on).
- Fever API (JSON only): Unread, Reeder Classic, FocusReader, FeedMe, Fluent Reader, NewsFlash. The docs note Reeder cannot trigger a refresh over Fever.
- Save buttons: send an article to wallabag, Instapaper, Pinboard, Linkwarden, Karakeep and other listed services.
- Alerts: push new entries to ntfy feed by feed (Miniflux 2.2.0 and later), or to Telegram, Matrix, Discord or Slack.
- Webhooks: two event types,
save_entryandnew_entries, posted as JSON to a URL you choose, with an HMAC-SHA256 signature sent as theX-Miniflux-Signatureheader.
One catch if those services share this server: Miniflux refuses requests to private and loopback addresses unless INTEGRATION_ALLOW_PRIVATE_NETWORKS=1 is set. Using each service's public name avoids the question.
Keeping your feeds safe
The Miniflux FAQ answers the backup question with a plain pg_dump. Our nightly job uses the custom dump format, reads the file back to make sure it is usable, archives the whole /srv/miniflux folder (Compose file, env files, this script), and deletes anything past two weeks old, but only after tonight's dump is safely on disk. You manage the server; we keep the hardware and the network running, so copies on another machine are yours to keep.
install -d -m 700 /var/backups/miniflux /var/log/miniflux-backup
cat > /srv/miniflux/backup.sh <<'EOF'
set -euo pipefail
umask 077
dc="docker compose -f /srv/miniflux/compose.yaml"
out=/var/backups/miniflux; day=$(date +%F)
$dc exec -T db pg_dump -U miniflux -Fc miniflux > "$out/db-$day.dump"
$dc exec -T db pg_restore -l < "$out/db-$day.dump" > /dev/null
tar -czf "$out/conf-$day.tgz" -C /srv/miniflux .
tar -tzf "$out/conf-$day.tgz" > /dev/null
find "$out" -name "db-$day.dump" -mtime -1 | grep -q . &&
find "$out" \( -name 'db-*.dump' -o -name 'conf-*.tgz' \) -mtime +14 -delete
EOF
printf '%s\n' '15 4 * * * root bash /srv/miniflux/backup.sh > /var/log/miniflux-backup/last.log 2>&1' > /etc/cron.d/miniflux-backup
bash /srv/miniflux/backup.sh && ls -l /var/backups/miniflux
Pull /var/backups/miniflux to another computer with rsync -a or scp from that side, and let that machine tidy its own old copies; rsync --delete could wipe them if this folder were ever emptied. The conf archive carries the database password, so guard it like the dump. To put a dump back, log in and become root with sudo -i, then run all the steps below in this same shell:
- Set the shortcut and the file, then prove it is readable:
dc="docker compose -f /srv/miniflux/compose.yaml"; B=/var/backups/miniflux/db-2026-10-23.dump; $dc exec -T db pg_restore --list < "$B" > /dev/null && echo readable. Go on only if you see readable. - Pause the reader:
$dc stop miniflux. - Keep what you have now, and test it:
K=/var/backups/miniflux/pre-restore.dump; [ ! -e "$K" ] && $dc exec -T db pg_dump --format=custom -U miniflux miniflux > "$K" && $dc exec -T db pg_restore --list < "$K" > /dev/null && echo kept. Go on only if you see kept. - Load the backup with
$dc exec -T db pg_restore --clean --if-exists -U miniflux -d miniflux < "$B", then$dc start minifluxand check your feeds. - Changed your mind? Pause the reader again (step 2), then run step 4 again with
"$K"instead of"$B". Removepre-restore.dumponce you are happy; step 3 will not run while it is still there.
Moving to a fresh server? Unpack the conf archive into /srv/miniflux first, before the database container starts for the first time: the postgres image applies POSTGRES_PASSWORD only while setting up a brand-new, empty volume. As root, start just the database (docker compose -f /srv/miniflux/compose.yaml up -d db), then set dc and B as in step 1 (with B pointing at the copied dump), load it as in step 4, then run docker compose -f /srv/miniflux/compose.yaml up -d.
Updates and the fine print
Before each update, check the release notes for anything breaking, as the upgrade page asks, and run bash /srv/miniflux/backup.sh. Then, as root, edit the version in compose.yaml and fetch the new image with docker compose -f /srv/miniflux/compose.yaml pull; once that has finished, docker compose -f /srv/miniflux/compose.yaml up -d swaps the container over; with RUN_MIGRATIONS=1 in place, the database schema is brought up to date as Miniflux starts. One tip of ours, not a Miniflux instruction: keep the database on postgres:18, since jumping to a new PostgreSQL major version means dumping and restoring into a new volume.
The licence is Apache 2.0, as the LICENSE file in the repository states. This is how we read it, not legal advice: a permissive licence, so using it for yourself or your business asks nothing of you, and if you hand copies to others you pass along the licence and the copyright notices and flag files you modified. You are not required to share source code.
Questions people ask
- Is Miniflux a good pick if I only follow a few dozen sites?
- It suits that well. It is one small program plus a database, it refreshes feeds on its own every hour by default, and there are no plugins to update. If you later want extensions, FreshRSS is the fuller alternative.
- Can I use my iPhone or Android feed app with it?
- Most likely. Turn on the Google Reader or Fever API in Settings > Integrations, set a username and password for it, and enter those in an app such as NetNewsWire, Unread or Capy Reader. Your web login is not used by the app.
- Why do the first-account settings get removed?
- They only matter on the very first start, and Miniflux skips them once the account exists. Removing them means the password no longer sits in the container's environment, where anyone able to run docker inspect could read it.
- What exactly do I need to back up?
- The PostgreSQL database and the folder holding your Compose file and env files. Miniflux keeps nothing else on disk, so the nightly dump plus that archive is the complete picture.