Skip to Content

Production Guide: Deploy Miniflux with Docker Compose, Nginx, and PostgreSQL on Ubuntu

A production-first deployment blueprint with TLS, backups, security controls, troubleshooting, and day-2 operational practices.

Teams that ship internal tools quickly often discover the same bottleneck: application deployment is easy on day one, but production operations become painful by week three. Credential rotation is ad hoc, rollbacks are unclear, logs are fragmented, and nobody can confidently answer whether the service is healthy. This guide solves that problem for Miniflux by walking through a production-ready deployment using docker + compose+nginx+postgresql on Ubuntu, with practical guardrails for security, observability, and repeatable operations.

The objective is not just to make the app run, but to run it in a way your team can maintain during incidents, upgrades, and growth. You will build a clean directory structure, set strict file permissions, isolate credentials, configure reverse proxy and TLS, establish backup and restore workflows, and verify the environment with explicit health checks. Every major command is shown in copy-friendly blocks, and each stage includes what “good” output should look like.

By the end, you will have a documented deployment that a teammate can audit and operate without tribal knowledge. That is the difference between a successful demo and a reliable production service.

Architecture and flow overview

This deployment uses a straightforward but durable architecture:

  • Application container: Miniflux serves the web UI and API.
  • PostgreSQL container: stores durable application data with controlled persistence.
  • Nginx reverse proxy: terminates TLS and forwards traffic to the application network.
  • Docker network segmentation: internal service communication remains private.
  • Systemd service + restart policy: resilient startup behavior across host reboots.

Request flow is simple: users hit the public hostname over HTTPS, Nginx validates certificates and forwards upstream traffic to the application service, which reads/writes data in PostgreSQL. Operational flow is equally important: configuration changes are made in versioned files, applied via Compose, validated with health checks, and captured in logs for troubleshooting.

Prerequisites

  • Ubuntu 22.04/24.04 server with at least 2 vCPU, 4 GB RAM, and 30+ GB SSD.
  • Domain name pointed to your server (A/AAAA record).
  • Sudo-capable Linux user.
  • Open ports 22, 80, and 443 in firewall/security group.
  • Basic familiarity with shell commands, Docker, and text editors.

Step-by-step deployment

1) Prepare host packages and runtime

sudo apt update
sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg lsb-release ufw jq

# Docker engine + compose plugin
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
echo   "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu   $(. /etc/os-release && echo $VERSION_CODENAME) stable" |   sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin nginx certbot python3-certbot-nginx
sudo systemctl enable --now docker nginx

If the copy button does not work in your browser/editor, manually select the code block and copy.

2) Create deployment directories with strict permissions

sudo mkdir -p /opt/miniflux/{config,data,postgres,backups}
sudo chown -R $USER:$USER /opt/miniflux
chmod 750 /opt/miniflux

If the copy button does not work in your browser/editor, manually select the code block and copy.

3) Generate secrets and environment file

cd /opt/miniflux
APP_SECRET=$(openssl rand -hex 32)
DB_PASS=$(openssl rand -base64 36 | tr -d '
')

cat > .env <<EOF
MINIFLUX_DOMAIN=reader.example.com
MINIFLUX_ADMIN_USERNAME=admin
MINIFLUX_ADMIN_PASSWORD=CHANGE_ME_STRONG_PASSWORD
MINIFLUX_BASE_URL=https://reader.example.com
MINIFLUX_APP_SECRET=${APP_SECRET}
POSTGRES_DB=miniflux
POSTGRES_USER=miniflux
POSTGRES_PASSWORD=${DB_PASS}
TZ=UTC
EOF

chmod 600 .env

If the copy button does not work in your browser/editor, manually select the code block and copy.

4) Create Docker Compose stack

cat > /opt/miniflux/docker-compose.yml <<'YAML'
services:
  db:
    image: postgres:16-alpine
    container_name: miniflux-db
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - app_net

  miniflux:
    image: miniflux/miniflux:latest
    container_name: miniflux-app
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${POSTGRES_DB}?sslmode=disable
      RUN_MIGRATIONS: 1
      CREATE_ADMIN: 1
      ADMIN_USERNAME: ${MINIFLUX_ADMIN_USERNAME}
      ADMIN_PASSWORD: ${MINIFLUX_ADMIN_PASSWORD}
      BASE_URL: ${MINIFLUX_BASE_URL}
      LISTEN_ADDR: 0.0.0.0:8080
      POLLING_FREQUENCY: 30
      OAUTH2_PROVIDER: 
      LOG_DATE_TIME: 1
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app_net

networks:
  app_net:
    driver: bridge
YAML

If the copy button does not work in your browser/editor, manually select the code block and copy.

5) Configure Nginx reverse proxy

sudo tee /etc/nginx/sites-available/miniflux.conf >/dev/null <<'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name reader.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
        proxy_http_version 1.1;
        proxy_read_timeout 120s;
    }
}
NGINX

sudo ln -sf /etc/nginx/sites-available/miniflux.conf /etc/nginx/sites-enabled/miniflux.conf
sudo nginx -t && sudo systemctl reload nginx

If the copy button does not work in your browser/editor, manually select the code block and copy.

6) Start application stack and expose local upstream

cd /opt/miniflux
docker compose pull
docker compose up -d

# Optional: local forward only
sudo ss -ltnp | grep 8080 || true

If the copy button does not work in your browser/editor, manually select the code block and copy.

7) Issue TLS certificate and enforce HTTPS

sudo certbot --nginx -d reader.example.com --non-interactive --agree-tos -m [email protected] --redirect
sudo systemctl status certbot.timer --no-pager

If the copy button does not work in your browser/editor, manually select the code block and copy.

8) Add daily PostgreSQL backup job with retention

cat >/opt/miniflux/backup.sh <<'BASH'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/miniflux
source .env
stamp=$(date +%F-%H%M%S)
out="/opt/miniflux/backups/miniflux-${stamp}.sql.gz"
docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" miniflux-db   pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" | gzip > "$out"
find /opt/miniflux/backups -type f -name '*.sql.gz' -mtime +14 -delete
BASH
chmod +x /opt/miniflux/backup.sh
( crontab -l 2>/dev/null; echo "17 2 * * * /opt/miniflux/backup.sh" ) | crontab -

If the copy button does not work in your browser/editor, manually select the code block and copy.

Configuration and secrets handling

Store all mutable credentials in .env with file mode 600. Do not commit secrets to Git. For teams, move secret distribution to a managed vault (1Password Secrets Automation, Vault, or cloud-native secret manager) and template environment files during deployment. Rotate POSTGRES_PASSWORD, MINIFLUX_APP_SECRET, and admin credentials on a schedule; every rotation should include a short runbook entry documenting who rotated, when, and how rollback is performed.

Use separate accounts for operations and application administration. If SSO is required, plan identity integration after baseline stability is achieved, not during first deployment. Keep your initial stack intentionally boring; complexity belongs in controlled change windows once monitoring and backups are proven.

Verification checklist

Run these checks before you consider deployment complete:

# Containers healthy
docker ps --format 'table {{.Names}}	{{.Status}}	{{.Ports}}'

# App endpoint and TLS
curl -I https://reader.example.com

# Database readiness from inside container
docker exec miniflux-db pg_isready -U miniflux -d miniflux

# Logs should not show crash loops
docker logs --tail=80 miniflux-app
docker logs --tail=80 miniflux-db

If the copy button does not work in your browser/editor, manually select the code block and copy.

You should see HTTP 200/302 over HTTPS, no repeated restarts, and a healthy PostgreSQL readiness response. Log noise is normal during first migration; persistent authentication failures are not.

Common issues and fixes

Proxy returns 502 Bad Gateway

Usually means Miniflux is not bound where Nginx expects. Confirm container is healthy and Nginx points to the correct upstream (127.0.0.1:8080 in this guide). If app runs on an internal Docker network only, either publish port 8080 to localhost or move Nginx into the same Docker network.

TLS certificate issuance fails

Check DNS propagation and ensure port 80 is publicly reachable for ACME challenge. Temporarily disable strict firewall rules that block challenge validation, then re-enable after certificate issuance.

Database authentication errors after password changes

Update both .env and existing database credentials, then restart services in order. Inconsistent secret rotation between app and DB is a common root cause.

Feeds poll slowly or time out

Increase polling interval conservatively, check outbound network egress, and verify DNS reliability on host. Add resource limits if noisy neighboring workloads starve this stack.

FAQ

Can I run this stack behind Cloudflare?

Yes. Keep origin TLS enabled, set the DNS record to proxied mode, and verify WebSocket/HTTP behavior if you add extensions. Start with direct DNS first, then layer Cloudflare after baseline validation.

How should I handle upgrades safely?

Take a fresh DB backup, pin image tags in Compose, deploy in maintenance window, run health checks, and keep a rollback command ready. Avoid same-window changes to proxy and database settings.

Do I need separate backup storage?

Absolutely. Local backups are useful for quick restore but not disaster recovery. Sync backup artifacts to an off-host target (S3-compatible bucket or object storage) with retention and restore tests.

What is the minimum monitoring setup?

At minimum capture host metrics, container restart counts, disk usage, certificate expiry, and HTTP availability checks. Alert on failure patterns, not single transient blips.

How do I rotate admin credentials without downtime?

Create a second admin account temporarily, rotate primary credentials, verify login paths, then remove temporary access. This avoids lockout and supports auditable change management.

Can I migrate from SQLite to PostgreSQL later?

It is possible but operationally cleaner to start on PostgreSQL from day one for production. If migration is unavoidable, schedule downtime and rehearse export/import in staging first.

Should I enable auto-updates for containers?

Use controlled updates rather than unattended upgrades in production. Automatic updates can introduce untested behavior at inconvenient times and complicate incident response.

Related guides

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy Grafana Loki with Kubernetes + Helm + ingress-nginx on Ubuntu
A production-oriented Loki deployment on Kubernetes with Helm, ingress-nginx, secure ingestion, retention controls, and operational runbooks.