
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
Compare AlmaLinux VPS Plans
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:
-
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
-
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, anddocker-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.
-
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.
-
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
PUIDandPGIDvariables for file permissions; the official compose example includesPUIDandPGID.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
PUIDandPGID. -
Create the Environment File
Create a
.envfile: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
-
Create the Docker Compose File
PinePodsβ current PostgreSQL compose example uses three services:
db,valkey, andpinepods. It exposes PinePods on port8040, usesmadeofpendletonwool/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:roWhy
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.
-
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-setuptool on startup.Test locally from the VPS:
curl -I http://127.0.0.1:8040
You should receive an HTTP response.
-
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.
-
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
-
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
-
Log In to PinePods
Pinepods login Open:
https://podcasts.example.com
Log in with the admin account from your
.envfile:Username: admin Password: the password you set in PINEPODS_ADMIN_PASSWORD
Configure initial setup:
Pinepods initial setup PinePods Home screen:
Pinepods home -
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 ;; esacMake it executable:
chmod +x /usr/local/bin/pinepods
Because the script needs variables from
.env, improve thebackup-dbsection: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 ;; esacUse it like this:
pinepods status pinepods logs pinepods update pinepods backup-db
-
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
-
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
-
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
80and443, while PinePods should only be bound to localhost on127.0.0.1:8040. -
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
8040publicly 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
8040to firewalld unless you intentionally want direct access without Nginx. -
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
:Zonly for private container-specific directories. -
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, andDB_NAMEvalues 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
.envmay 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.
-
-
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
Conclusion
You now know how to install PinePods on AlmaLinux VPS with Nginx reverse proxy.












