Skip to content
85 changes: 85 additions & 0 deletions ct/pinepods.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/DevScripts/main"
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: untect (untect)
# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE
# Source: https://github.com/madeofpendletonwool/PinePods

APP="PinePods"
var_tags="${var_tags:-podcast;media}"
var_cpu="${var_cpu:-4}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
#var_arm64="${var_arm64:-no}"
var_unprivileged="${var_unprivileged:-1}"

header_info "$APP"
variables
color
catch_errors

function update_script() {
header_info
check_container_storage
check_container_resources

if [[ ! -d /opt/pinepods ]]; then
msg_error "No PinePods Installation Found!"
exit
fi

if check_for_gh_release "pinepods" "madeofpendletonwool/PinePods"; then
msg_info "Stopping PinePods"
systemctl stop pinepods
msg_ok "Stopped PinePods"

RUST_PROFILE="minimal" setup_rust
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="downloads backups local-media" fetch_and_deploy_gh_release "pinepods" "madeofpendletonwool/PinePods" "tarball"
PYTHON_VERSION=$(sed -n 's/^FROM python:\([0-9.]*\)-.*/\1/p' /opt/pinepods/dockerfile)
TRUNK_VERSION=$(sed -n 's#.*trunk/releases/download/\(v[0-9.]*\)/.*#\1#p' /opt/pinepods/dockerfile)
if [[ -z "$PYTHON_VERSION" || -z "$TRUNK_VERSION" ]]; then
msg_error "Could not read the Python or trunk version from the PinePods dockerfile"
exit 1
fi
setup_uv
fetch_and_deploy_gh_release "trunk" "trunk-rs/trunk" "prebuild" "$TRUNK_VERSION" "/usr/local/bin" "trunk-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.tar.gz"

msg_info "Rebuilding PinePods"
$STD rustup target add wasm32-unknown-unknown
cd /opt/pinepods/web
RUSTFLAGS="--cfg=web_sys_unstable_apis --cfg getrandom_backend=\"wasm_js\"" $STD trunk build --features server_build --release
rm -rf /var/www/html
cp -r /opt/pinepods/web/dist /var/www/html
cp -r /opt/pinepods/web/src/translations /var/www/html/static/translations
cd /opt/pinepods/rust-api
$STD cargo build --release
cp /opt/pinepods/rust-api/target/release/pinepods-api /usr/local/bin/pinepods-api
rm -rf /opt/pinepods/web/target /opt/pinepods/web/dist /opt/pinepods/rust-api/target
cp ~/.pinepods /pinepods/current_version
msg_ok "Rebuilt PinePods"

msg_info "Updating Python Runtime"
$STD uv venv --python "$PYTHON_VERSION" /opt/pinepods/.venv
$STD uv pip install --python /opt/pinepods/.venv/bin/python -r /opt/pinepods/requirements.txt
msg_ok "Updated Python Runtime"

msg_info "Starting PinePods"
systemctl start pinepods
msg_ok "Started PinePods"
msg_ok "Updated successfully!"
fi
exit
}

start
build_container
description

msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8040${CL}"
257 changes: 257 additions & 0 deletions install/pinepods-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
#!/usr/bin/env bash

# Copyright (c) 2021-2026 community-scripts ORG
# Author: untect
# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE
# Source: https://github.com/madeofpendletonwool/PinePods

source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os

msg_info "Installing Dependencies"
$STD apt install -y \
build-essential \
pkg-config \
libssl-dev \
libmariadb-dev \
nginx \
valkey-server
msg_ok "Installed Dependencies"

PG_VERSION="18" setup_postgresql
PG_DB_NAME="pinepods_database" PG_DB_USER="pinepods" setup_postgresql_db
RUST_PROFILE="minimal" setup_rust
setup_ffmpeg

fetch_and_deploy_gh_release "yt-dlp" "yt-dlp/yt-dlp" "singlefile" "latest" "/usr/local/bin" "$(arch_resolve "yt-dlp_linux" "yt-dlp_linux_aarch64")"
fetch_and_deploy_gh_release "pinepods" "madeofpendletonwool/PinePods" "tarball"

PYTHON_VERSION=$(sed -n 's/^FROM python:\([0-9.]*\)-.*/\1/p' /opt/pinepods/dockerfile)
TRUNK_VERSION=$(sed -n 's#.*trunk/releases/download/\(v[0-9.]*\)/.*#\1#p' /opt/pinepods/dockerfile)
if [[ -z "$PYTHON_VERSION" || -z "$TRUNK_VERSION" ]]; then
msg_error "Could not read the Python or trunk version from the PinePods dockerfile"
exit 1
fi
setup_uv
fetch_and_deploy_gh_release "trunk" "trunk-rs/trunk" "prebuild" "$TRUNK_VERSION" "/usr/local/bin" "trunk-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.tar.gz"

msg_info "Building PinePods"
$STD rustup target add wasm32-unknown-unknown
cd /opt/pinepods/web
RUSTFLAGS="--cfg=web_sys_unstable_apis --cfg getrandom_backend=\"wasm_js\"" $STD trunk build --features server_build --release
rm -rf /var/www/html
cp -r /opt/pinepods/web/dist /var/www/html
cp -r /opt/pinepods/web/src/translations /var/www/html/static/translations
cd /opt/pinepods/rust-api
$STD cargo build --release
cp /opt/pinepods/rust-api/target/release/pinepods-api /usr/local/bin/pinepods-api
rm -rf /opt/pinepods/web/target /opt/pinepods/web/dist /opt/pinepods/rust-api/target
msg_ok "Built PinePods"

msg_info "Setting up Python Runtime"
$STD uv venv --python "$PYTHON_VERSION" /opt/pinepods/.venv
$STD uv pip install --python /opt/pinepods/.venv/bin/python -r /opt/pinepods/requirements.txt
msg_ok "Set up Python Runtime"

msg_info "Configuring PinePods"
mkdir -p /opt/pinepods/{downloads,backups,local-media} /opt/pinepods_data /pinepods
cp ~/.pinepods /pinepods/current_version
cat <<EOF >/opt/pinepods_data/.env
DB_TYPE=postgresql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=${PG_DB_USER}
DB_PASSWORD=${PG_DB_PASS}
DB_NAME=${PG_DB_NAME}
VALKEY_HOST=127.0.0.1
VALKEY_PORT=6379
SEARCH_API_URL=https://search.pinepods.online/api/search
PEOPLE_API_URL=https://people.pinepods.online
HOSTNAME=http://${LOCAL_IP}:8040
SERVER_URL=http://${LOCAL_IP}:8040
DEFAULT_LANGUAGE=en
DEBUG_MODE=false
EOF
chmod 600 /opt/pinepods_data/.env
msg_ok "Configured PinePods"

msg_info "Configuring Nginx"
cat <<'EOF' >/etc/nginx/sites-available/pinepods
map $http_x_forwarded_proto $fwd_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
map $http_x_forwarded_host $fwd_host {
default $http_x_forwarded_host;
'' $http_host;
}

server {
listen 8040;
server_name _;
client_max_body_size 0;

root /var/www/html;
index index.html;

add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;

location /rss/ {
rewrite ^/rss/(\d+)(?:/(\d+))?$ /api/feed/$1$2 last;
proxy_pass http://127.0.0.1:8032;
proxy_set_header Host $http_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-Host $fwd_host;
proxy_set_header X-Forwarded-Proto $fwd_proto;
proxy_set_header Api-Key $arg_api_key;
add_header Content-Type "application/rss+xml; charset=utf-8";
expires 1h;
add_header Cache-Control "public, no-transform";
}

location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
try_files $uri $uri/ /index.html;
}

location /api {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;

proxy_pass http://127.0.0.1:8032;
proxy_set_header Host $http_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-Host $fwd_host;
proxy_set_header X-Forwarded-Proto $fwd_proto;

if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}

location ~ ^/(api/2|auth|subscriptions|devices|updates|episodes|settings|lists|favorites|sync-devices|search|suggestions|toplist|tag|tags|data)/ {
proxy_pass http://127.0.0.1:8032;
proxy_set_header Host $http_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-Host $fwd_host;
proxy_set_header X-Forwarded-Proto $fwd_proto;

add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always;

if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}

proxy_read_timeout 300;
proxy_send_timeout 300;
}

location /api/gpodder {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;

proxy_pass http://127.0.0.1:8032;
proxy_set_header Host $http_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-Host $fwd_host;
proxy_set_header X-Forwarded-Proto $fwd_proto;
proxy_read_timeout 1800;
proxy_send_timeout 1800;

if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}

location /ws/api/ {
proxy_pass http://127.0.0.1:8032;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $http_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-Host $fwd_host;
proxy_set_header X-Forwarded-Proto $fwd_proto;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}

location ~* \.wasm$ {
types {
application/wasm wasm;
}
}
}
EOF
nginx_enable_site "pinepods"
msg_ok "Configured Nginx"

msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/pinepods.service
[Unit]
Description=PinePods Service
After=network.target postgresql.service valkey-server.service
Requires=postgresql.service valkey-server.service

[Service]
Type=simple
User=root
TimeoutStartSec=infinity
WorkingDirectory=/opt/pinepods
EnvironmentFile=/opt/pinepods_data/.env
ExecStartPre=/opt/pinepods/.venv/bin/python /opt/pinepods/startup/setup_database_new.py
ExecStart=/usr/local/bin/pinepods-api
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now pinepods
msg_ok "Created Service"

motd_ssh
customize
cleanup_lxc
45 changes: 45 additions & 0 deletions json/pinepods.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "PinePods",
"slug": "pinepods",
"categories": [
13
],
"date_created": "2026-11-02",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 8040,
"documentation": "https://www.pinepods.online/docs/Introduction",
"website": "https://www.pinepods.online/",
"repository": "https://github.com/madeofpendletonwool/PinePods",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/pinepods.webp",
"description": "Pinepods is a complete podcast management system that allows you to play, download, and keep track of podcasts you enjoy. All self hosted and enjoyed on your own server!",
"install_methods": [
{
"type": "default",
"script": "ct/pinepods.sh",
"config_path": "/opt/pinepods_data/.env",
"resources": {
"cpu": 4,
"ram": 4096,
"hdd": 10,
"os": "Debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "There is no default account. The first visit to the web UI asks you to create the admin user.",
"type": "info"
},
{
"text": "HOSTNAME and SERVER_URL in /opt/pinepods_data/.env must match the address you open PinePods at. Change them for a new IP or a reverse proxy, then run systemctl restart pinepods.",
"type": "info"
}
]
}
Loading