...
πŸš€ install pinepods on almalinux vps
Learn how to install pinepods on almalinux vps!

This article provides a guide demonstrating how to install PinePods on AlmaLinux VPS using Docker Compose, PostgreSQL, Valkey, and a host-level Nginx reverse proxy with Let’s Encrypt SSL.

What is PinePods?

PinePods is a self-hosted, open-source podcast management system. In simple terms, it lets you run your own private podcast server where you can subscribe to podcasts, play episodes, download shows, track listening history, manage queues, and sync everything across devices.

Instead of relying entirely on a third-party podcast app’s cloud account, PinePods stores your podcast data on your own server. That makes it appealing for privacy-conscious users, families, and self-hosters who want control over subscriptions, playback history, downloads, and settings. The project is written largely around a Rust-powered backend and supports browser access plus companion mobile apps.

Key features include:

  • Self-hosted podcast library
  • Multi-device sync
  • Podcast playback in the browser
  • Mobile app support
  • Episode downloads and archiving
  • Listening history and statistics
  • Queue and playlist management
  • Multi-user support
  • Podcast search and discovery
  • gpodder-compatible syncing
  • OIDC / single sign-on support
  • Local podcast/media support

A good way to think of PinePods is: β€œPlex/Jellyfin-style self-hosting, but for podcasts.” You host the server, connect your devices, and keep ownership of your podcast experience.

PinePods is best deployed with containers. The current PinePods container includes the web UI, main API, gpodder sync service, and internal Nginx layer, while depending on two external services: a database and a Valkey/Redis cache.

Prerequisites

Replace these values with your real information:

DOMAIN="podcasts.example.com"
ADMIN_EMAIL="admin@example.com"
PINEPODS_DIR="/opt/pinepods"

Recommended VPS baseline:

  • OS: AlmaLinux 9 or Almalinux 10
  • CPU: 2 vCPU or more
  • RAM: 2 GB minimum, 4 GB+ recommended
  • Disk: 20 GB minimum, more if downloading/archiving podcasts
  • Access: root or sudo user

Before starting, point your domain’s DNS A record to the VPS public IP:

podcasts.example.com -> YOUR_SERVER_IP

Launch 100% ssd almalinux vps from $3. 19/mo!


Compare AlmaLinux VPS Plans

KVM-SSD-1
KVM-SSD-8
KVM-SSD-16
KVM-SSD-32
CPU
1 Core
2 Cores
4 Cores
8 Cores
Memory
1 GB
8 GB
16 GB
32 GB
Storage
16 GB NVMe
128 GB NVMe
256 GB NVMe
512 GB NVMe
Bandwidth
1 TB
4 TB
8 TB
16 TB
Network
1 Gbps
1 Gbps
1 Gbps
1 Gbps
Delivery Time
⏱️ Instant
⏱️ Instant
⏱️ Instant
⏱️ Instant
Location
US/EU/APAC
US/EU/APAC
US/EU/APAC
US/EU/APAC
Price
$7.58*
$39.50*
$79.40*
$151.22*
KVM-SSD-1
$7.58*
CPU 1 Core
Memory 1 GB
Storage 16 GB NVMe
Bandwidth 1 TB
Network 1 Gbps
Delivery Time ⏱️ Instant
Location US/EU/APAC
KVM-SSD-8
$39.50*
CPU 2 Cores
Memory 8 GB
Storage 128 GB NVMe
Bandwidth 4 TB
Network 1 Gbps
Delivery Time ⏱️ Instant
Location US/EU/APAC
KVM-SSD-16
$79.40*
CPU 4 Cores
Memory 16 GB
Storage 256 GB NVMe
Bandwidth 8 TB
Network 1 Gbps
Delivery Time ⏱️ Instant
Location US/EU/APAC
KVM-SSD-32
$151.22*
CPU 8 Cores
Memory 32 GB
Storage 512 GB NVMe
Bandwidth 16 TB
Network 1 Gbps
Delivery Time ⏱️ Instant
Location US/EU/APAC

How to Install PinePods on AlmaLinux VPS with Nginx Reverse Proxy

To install PinePods on AlmaLinux VPS with Nginx reverse proxy, follow the steps below:

  1. Update AlmaLinux

    Log in via SSH as root or a sudo-capable user:

    ssh root@YOUR_SERVER_IP
    

    Update the system:

    dnf update -y
    reboot
    

    Reconnect after reboot:

    ssh root@YOUR_SERVER_IP
    

    Install useful tools:

    dnf install -y curl wget git nano vim tar unzip firewalld dnf-plugins-core
    

    Enable and start firewalld:

    systemctl enable --now firewalld
    
  2. Install Docker Engine and Docker Compose Plugin

    Docker’s official RHEL instructions recommend using Docker’s RPM repository and installing docker-ce, docker-ce-cli, containerd.io, docker-buildx-plugin, and docker-compose-plugin.

    Remove conflicting packages if present:

    dnf remove -y docker \
      docker-client \
      docker-client-latest \
      docker-common \
      docker-latest \
      docker-latest-logrotate \
      docker-logrotate \
      docker-engine \
      podman \
      runc
    

    Add the Docker repository:

    dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    

    Install Docker:

    dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    

    Enable and start Docker:

    systemctl enable --now docker
    

    Verify:

    docker --version
    docker compose version
    

    Optional: add your non-root user to the Docker group:

    usermod -aG docker YOUR_USERNAME
    

    Log out and back in before using Docker as that user.

  3. Install Nginx

    Install Nginx from AlmaLinux repositories:

    dnf install -y nginx
    

    Enable and start it:

    systemctl enable --now nginx
    

    Open HTTP and HTTPS:

    firewall-cmd --permanent --add-service=http
    firewall-cmd --permanent --add-service=https
    firewall-cmd --reload
    

    Check Nginx:

    systemctl status nginx
    

    Visit:

    http://YOUR_SERVER_IP
    

    You should see the default Nginx page or a basic response.

  4. Create the PinePods Directory Structure

    Create the application directory:

    mkdir -p /opt/pinepods
    cd /opt/pinepods
    

    Create persistent storage directories:

    mkdir -p pgdata downloads backups local-media
    

    Set sane ownership. PinePods supports PUID and PGID variables for file permissions; the official compose example includes PUID and PGID.

    For a simple root-managed deployment:

    chown -R root:root /opt/pinepods
    chmod -R 755 /opt/pinepods
    

    For a dedicated user:

    useradd --system --home /opt/pinepods --shell /sbin/nologin pinepods || true
    chown -R pinepods:pinepods /opt/pinepods
    

    Get the UID and GID if using a dedicated user:

    id pinepods
    

    Example output:

    uid=987(pinepods) gid=987(pinepods)
    

    You can use those values later for PUID and PGID.

  5. Create the Environment File

    Create a .env file:

    cd /opt/pinepods
    nano .env
    

    Add:

    # Public hostname
    PINEPODS_DOMAIN=podcasts.example.com
    PINEPODS_URL=https://podcasts.example.com
    
    # Timezone
    TZ=America/Chicago
    
    # PostgreSQL
    POSTGRES_DB=pinepods_database
    POSTGRES_USER=pinepods
    POSTGRES_PASSWORD=CHANGE_THIS_TO_A_LONG_RANDOM_DATABASE_PASSWORD
    
    # PinePods admin user
    PINEPODS_ADMIN_USERNAME=admin
    PINEPODS_ADMIN_PASSWORD=CHANGE_THIS_TO_A_LONG_RANDOM_ADMIN_PASSWORD
    PINEPODS_ADMIN_FULLNAME=PinePods Admin
    PINEPODS_ADMIN_EMAIL=admin@example.com
    
    # File ownership inside mounted volumes
    PUID=911
    PGID=911
    

    Generate strong passwords:

    openssl rand -base64 32
    

    Protect the file:

    chmod 600 /opt/pinepods/.env
    
  6. Create the Docker Compose File

    PinePods’ current PostgreSQL compose example uses three services: db, valkey, and pinepods. It exposes PinePods on port 8040, uses madeofpendletonwool/pinepods:latest, sets database variables, sets Valkey variables, and mounts persistent downloads/backups/local media directories.

    Create the compose file:

    nano /opt/pinepods/docker-compose.yml
    

    Add:

    services:
      db:
        image: postgres:17-alpine
        container_name: pinepods-db
        restart: unless-stopped
        environment:
          POSTGRES_DB: ${POSTGRES_DB}
          POSTGRES_USER: ${POSTGRES_USER}
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
          PGDATA: /var/lib/postgresql/data/pgdata
        volumes:
          - ./pgdata:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
          interval: 30s
          timeout: 10s
          retries: 5
    
      valkey:
        image: valkey/valkey:8-alpine
        container_name: pinepods-valkey
        restart: unless-stopped
        command: ["valkey-server", "--appendonly", "yes"]
        volumes:
          - ./valkey-data:/data
    
      pinepods:
        image: madeofpendletonwool/pinepods:latest
        container_name: pinepods
        restart: unless-stopped
        depends_on:
          db:
            condition: service_healthy
          valkey:
            condition: service_started
        ports:
          - "127.0.0.1:8040:8040"
        environment:
          # Public server info
          HOSTNAME: ${PINEPODS_URL}
          SEARCH_API_URL: "https://search.pinepods.online/api/search"
          PEOPLE_API_URL: "https://people.pinepods.online"
    
          # Default admin user
          USERNAME: ${PINEPODS_ADMIN_USERNAME}
          PASSWORD: ${PINEPODS_ADMIN_PASSWORD}
          FULLNAME: ${PINEPODS_ADMIN_FULLNAME}
          EMAIL: ${PINEPODS_ADMIN_EMAIL}
    
          # Database
          DB_TYPE: postgresql
          DB_HOST: db
          DB_PORT: 5432
          DB_USER: ${POSTGRES_USER}
          DB_PASSWORD: ${POSTGRES_PASSWORD}
          DB_NAME: ${POSTGRES_DB}
    
          # Valkey
          VALKEY_HOST: valkey
          VALKEY_PORT: 6379
    
          # Runtime
          DEBUG_MODE: "false"
          PUID: ${PUID}
          PGID: ${PGID}
          TZ: ${TZ}
          DEFAULT_LANGUAGE: "en"
        volumes:
          - ./downloads:/opt/pinepods/downloads
          - ./backups:/opt/pinepods/backups
          - ./local-media:/opt/pinepods/local-media
          - /etc/localtime:/etc/localtime:ro
    

    Why 127.0.0.1:8040:8040?

    This makes PinePods available only locally on the VPS. Public traffic will go through Nginx on ports 80/443 instead of directly exposing PinePods’ internal port to the internet.

  7. Start PinePods

    From the PinePods directory:

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

    Check containers:

    docker compose ps
    

    View logs:

    docker compose logs -f pinepods
    

    PinePods’ container startup runs database setup/migrations before launching the long-running services. The official container architecture notes that schema creation, migrations, and validation are handled by the pinepods-db-setup tool on startup.

    Test locally from the VPS:

    curl -I http://127.0.0.1:8040
    

    You should receive an HTTP response.

  8. Configure Nginx Reverse Proxy

    Create a new Nginx server block:

    nano /etc/nginx/conf.d/pinepods.conf
    

    Add this HTTP-only version first:

    server {
        listen 80;
        listen [::]:80;
    
        server_name podcasts.example.com;
    
        client_max_body_size 512M;
    
        location / {
            proxy_pass http://127.0.0.1:8040;
    
            proxy_http_version 1.1;
    
            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_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
    
            proxy_read_timeout 3600;
            proxy_send_timeout 3600;
        }
    }
    

    Replace:

    server_name podcasts.example.com;
    

    with your actual domain.

    Test Nginx:

    nginx -t
    

    Reload:

    systemctl reload nginx
    

    Visit:

    http://podcasts.example.com
    

    You should see PinePods.

  9. Install Certbot and Enable HTTPS

    Install EPEL:

    dnf install -y epel-release
    dnf update -y
    

    Install Certbot and the Nginx plugin:

    dnf install -y certbot python3-certbot-nginx
    

    Issue the SSL certificate:

    certbot --nginx -d podcasts.example.com
    

    Use a valid admin email when prompted.

    Choose the redirect option when Certbot asks whether to redirect HTTP to HTTPS.

    Test renewal:

    certbot renew --dry-run
    

    Check the timer:

    systemctl list-timers | grep certbot
    

    If no timer appears, enable one manually:

    systemctl enable --now certbot-renew.timer 2>/dev/null || true
    

    You can also use cron if needed:

    echo '15 3 * * * root certbot renew --quiet --post-hook "systemctl reload nginx"' > /etc/cron.d/certbot-renew
    
  10. Final Nginx Config Review

    After Certbot modifies the Nginx file, review it:

    cat /etc/nginx/conf.d/pinepods.conf
    

    A good HTTPS config should look similar to this:

    server {
        listen 80;
        listen [::]:80;
    
        server_name podcasts.example.com;
    
        return 301 https://$host$request_uri;
    }
    
    server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
    
        server_name podcasts.example.com;
    
        ssl_certificate /etc/letsencrypt/live/podcasts.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/podcasts.example.com/privkey.pem;
    
        client_max_body_size 512M;
    
        location / {
            proxy_pass http://127.0.0.1:8040;
    
            proxy_http_version 1.1;
    
            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 https;
    
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
    
            proxy_read_timeout 3600;
            proxy_send_timeout 3600;
        }
    }
    

    Test and reload:

    nginx -t
    systemctl reload nginx
    
  11. Log In to PinePods

    Pinepods login
    Pinepods login

    Open:

    https://podcasts.example.com
    

    Log in with the admin account from your .env file:

    Username: admin
    Password: the password you set in PINEPODS_ADMIN_PASSWORD
    

    Configure initial setup:

    Pinepods initial setup
    Pinepods initial setup

    PinePods Home screen:

    Pinepods home
    Pinepods home
  12. Create a Basic Management Script

    Create a helper script:

    nano /usr/local/bin/pinepods
    

    Add:

    #!/bin/bash
    cd /opt/pinepods || exit 1
    
    case "$1" in
      start)
        docker compose up -d
        ;;
      stop)
        docker compose down
        ;;
      restart)
        docker compose down
        docker compose up -d
        ;;
      status)
        docker compose ps
        ;;
      logs)
        docker compose logs -f "${2:-pinepods}"
        ;;
      update)
        docker compose pull
        docker compose up -d
        docker image prune -f
        ;;
      backup-db)
        mkdir -p /opt/pinepods/backups
        docker exec pinepods-db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "/opt/pinepods/backups/pinepods-db-$(date +%F-%H%M%S).sql"
        ;;
      *)
        echo "Usage: pinepods {start|stop|restart|status|logs|update|backup-db}"
        exit 1
        ;;
    esac
    

    Make it executable:

    chmod +x /usr/local/bin/pinepods
    

    Because the script needs variables from .env, improve the backup-db section:

    nano /usr/local/bin/pinepods
    

    Replace the script with:

    #!/bin/bash
    cd /opt/pinepods || exit 1
    
    set -a
    source /opt/pinepods/.env
    set +a
    
    case "$1" in
      start)
        docker compose up -d
        ;;
      stop)
        docker compose down
        ;;
      restart)
        docker compose down
        docker compose up -d
        ;;
      status)
        docker compose ps
        ;;
      logs)
        docker compose logs -f "${2:-pinepods}"
        ;;
      update)
        docker compose pull
        docker compose up -d
        docker image prune -f
        ;;
      backup-db)
        mkdir -p /opt/pinepods/backups
        docker exec pinepods-db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "/opt/pinepods/backups/pinepods-db-$(date +%F-%H%M%S).sql"
        ;;
      *)
        echo "Usage: pinepods {start|stop|restart|status|logs|update|backup-db}"
        exit 1
        ;;
    esac
    

    Use it like this:

    pinepods status
    pinepods logs
    pinepods update
    pinepods backup-db
    
  13. Create Automated Backups

    Create a backup script:

    nano /usr/local/sbin/backup-pinepods.sh
    

    Add:

    #!/bin/bash
    set -euo pipefail
    
    cd /opt/pinepods
    
    set -a
    source /opt/pinepods/.env
    set +a
    
    BACKUP_ROOT="/opt/pinepods/backups"
    DATE="$(date +%F-%H%M%S)"
    BACKUP_DIR="${BACKUP_ROOT}/${DATE}"
    
    mkdir -p "$BACKUP_DIR"
    
    echo "Backing up PostgreSQL database..."
    docker exec pinepods-db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "${BACKUP_DIR}/pinepods-db.sql"
    
    echo "Backing up compose and env files..."
    cp /opt/pinepods/docker-compose.yml "${BACKUP_DIR}/docker-compose.yml"
    cp /opt/pinepods/.env "${BACKUP_DIR}/env.backup"
    
    echo "Backing up media directories..."
    tar -czf "${BACKUP_DIR}/pinepods-files.tar.gz" \
      -C /opt/pinepods \
      downloads local-media
    
    echo "Pruning backups older than 14 days..."
    find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} \;
    
    echo "Backup completed: ${BACKUP_DIR}"
    

    Make it executable:

    chmod +x /usr/local/sbin/backup-pinepods.sh
    

    Test it:

    /usr/local/sbin/backup-pinepods.sh
    

    Add a daily cron job:

    echo '30 2 * * * root /usr/local/sbin/backup-pinepods.sh >/var/log/pinepods-backup.log 2>&1' > /etc/cron.d/pinepods-backup
    
  14. Update PinePods

    To update PinePods:

    cd /opt/pinepods
    docker compose pull
    docker compose up -d
    docker image prune -f
    

    Or use the helper:

    pinepods update
    

    Check logs after updating:

    pinepods logs
    
  15. Useful Commands

    Check containers:

    cd /opt/pinepods
    docker compose ps
    

    View PinePods logs:

    docker compose logs -f pinepods
    

    View database logs:

    docker compose logs -f db
    

    Restart only PinePods:

    docker compose restart pinepods
    

    Restart the full stack:

    docker compose restart
    

    Stop PinePods:

    docker compose down
    

    Start PinePods:

    docker compose up -d
    

    Check Nginx:

    nginx -t
    systemctl status nginx
    

    Reload Nginx:

    systemctl reload nginx
    

    Check listening ports:

    ss -tulpn
    

    You should see Nginx listening publicly on 80 and 443, while PinePods should only be bound to localhost on 127.0.0.1:8040.

  16. Firewall Hardening

    Confirm only SSH, HTTP, and HTTPS are open:

    firewall-cmd --list-all
    

    Typical public services:

    services: ssh http https
    

    If Docker exposed port 8040 publicly by mistake, fix the compose file so the port line is:

    ports:
      - "127.0.0.1:8040:8040"
    

    Then redeploy:

    cd /opt/pinepods
    docker compose up -d
    

    Do not add port 8040 to firewalld unless you intentionally want direct access without Nginx.

  17. Optional: SELinux Notes

    On most Docker-based AlmaLinux deployments, this compose setup should work without changing SELinux. If you encounter permission errors with mounted volumes, check logs:

    docker compose logs pinepods
    journalctl -xe
    

    You can inspect SELinux status:

    getenforce
    

    If the container cannot write to mounted directories, one practical Docker Compose adjustment is to add SELinux relabel flags to local bind mounts:

    volumes:
      - ./downloads:/opt/pinepods/downloads:Z
      - ./backups:/opt/pinepods/backups:Z
      - ./local-media:/opt/pinepods/local-media:Z
    

    Then redeploy:

    docker compose down
    docker compose up -d
    

    Use :Z only for private container-specific directories.

  18. Troubleshooting

    • PinePods shows 502 Bad Gateway

      Check whether PinePods is running:

      cd /opt/pinepods
      docker compose ps
      docker compose logs -f pinepods
      

      Test local access:

      curl -I http://127.0.0.1:8040
      

      If local access fails, the issue is inside Docker. If local access works, the issue is likely Nginx.

    • Nginx config fails

      Run:

      nginx -t
      

      Common issues:

      duplicate server name
      missing semicolon
      wrong certificate path
      wrong proxy_pass target
      

      Reload only after a successful config test:

      systemctl reload nginx
      
    • Certbot fails

      Confirm DNS points to the VPS:

      dig +short podcasts.example.com
      

      Confirm ports are open:

      firewall-cmd --list-services
      

      Confirm Nginx is listening on port 80:

      ss -tulpn | grep ':80'
      

      Then retry:

      certbot --nginx -d podcasts.example.com
      
    • Database connection errors

      Check the DB container:

      docker compose logs -f db
      

      Confirm environment values match:

      grep DB_ /opt/pinepods/docker-compose.yml
      grep POSTGRES /opt/pinepods/.env
      

      The PinePods DB_USER, DB_PASSWORD, and DB_NAME values must match the PostgreSQL service values.

    • Admin login does not work

      Check the initial logs:

      docker compose logs pinepods | grep -i password
      docker compose logs pinepods | grep -i admin
      

      If you started PinePods once with the wrong admin values, changing .env may not overwrite an already-created admin user in the database. In a brand-new install, you can reset everything by removing the database volume:

      cd /opt/pinepods
      docker compose down
      rm -rf pgdata
      mkdir pgdata
      docker compose up -d
      

      Only do this on a new install because it deletes the PinePods database.

  19. Recommended Production Checklist

    Before considering the deployment finished:

    • DNS points to the VPS
    • Docker is installed and enabled
    • PinePods containers are running
    • PinePods port 8040 is bound only to 127.0.0.1
    • Nginx reverse proxy works over HTTP
    • Let’s Encrypt SSL is active
    • HTTP redirects to HTTPS
    • Admin login works
    • Backups are configured
    • Updates have been tested
    • Firewall exposes only SSH, HTTP, and HTTPS

    Your PinePods instance should now be available at:

    https://podcasts.example.com
    

Launch 100% ssd almalinux vps from $3. 19/mo!

Conclusion

You now know how to install PinePods on AlmaLinux VPS with Nginx reverse proxy.

Avatar of editorial staff

Editorial Staff

Rad Web Hosting is a leading provider of web hosting, Cloud VPS, and Dedicated Servers in Dallas, TX.
lg