YouTube Archive
    Open Source

    Deploy Pinchflat on a VPS

    A self-hosted YouTube media manager built on yt-dlp — single container, no Elasticsearch, drops cleanly into Plex, Jellyfin, or Kodi. Always-on archiving without leaving a desktop running.

    At a Glance

    Projectkieraneglin/pinchflat
    StackPhoenix/Elixir + yt-dlp + SQLite (WAL)
    Recommended PlanRamNode KVM 2 GB (small libraries); 4 GB if running alongside Jellyfin
    OSUbuntu 24.04 LTS / Debian 12
    Reverse ProxyNginx with WebSocket headers (LiveView is ALL websocket)

    Storage planning

    Decide upfront whether the VPS holds media long-term or acts as a download staging area that syncs to a NAS or object storage. Small VPS + nightly rclone to Backblaze B2 (or a home NAS over WireGuard) is usually cheaper at scale than perpetually upgrading the VPS disk.

    1

    Initial Server Hardening

    Non-root user + SSH lockdown
    adduser pinchflat
    usermod -aG sudo pinchflat
    rsync -a --chown=pinchflat:pinchflat ~/.ssh /home/pinchflat/
    /etc/ssh/sshd_config
    PermitRootLogin no
    PasswordAuthentication no
    PubkeyAuthentication yes
    Reload SSH + firewall
    systemctl reload ssh
    
    ufw default deny incoming
    ufw default allow outgoing
    ufw allow OpenSSH
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw enable

    On 2 GB plans, add a 2 GB swap file as headroom for occasional yt-dlp memory spikes:

    2 GB swapfile
    fallocate -l 2G /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile
    echo '/swapfile none swap sw 0 0' >> /etc/fstab
    2

    Install Docker

    Docker's official APT repo
    apt update
    apt install -y ca-certificates curl gnupg
    install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
      gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    chmod a+r /etc/apt/keyrings/docker.gpg
    
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
      https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
      tee /etc/apt/sources.list.d/docker.list > /dev/null
    
    apt update
    apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    usermod -aG docker pinchflat

    Log out + back in, then verify with docker run --rm hello-world.

    3

    Plan Your Directory Layout

    Two persistent directories: config (small — SQLite DB, logs, state) and downloads (large — all media). The container runs as UID 1000 by default.

    Create + own
    mkdir -p /srv/pinchflat/{config,downloads}
    chown -R 1000:1000 /srv/pinchflat

    Do NOT put config on a network share. Pinchflat uses SQLite WAL mode; SQLite on NFS or SMB is a recipe for database corruption. The downloads directory on a network share is fine. If you absolutely must, set JOURNAL_MODE=delete and accept the perf hit.

    4

    Deploy with Docker Compose

    /srv/pinchflat/docker-compose.yml
    services:
      pinchflat:
        image: ghcr.io/kieraneglin/pinchflat:latest
        container_name: pinchflat
        restart: unless-stopped
        ports:
          - "127.0.0.1:8945:8945"
        environment:
          - TZ=America/New_York
          - UMASK=022
          # Uncomment if config dir is on a network share
          # - JOURNAL_MODE=delete
        volumes:
          - ./config:/config
          - ./downloads:/downloads
        healthcheck:
          test: ["CMD", "wget", "--spider", "-q", "http://localhost:8945/healthcheck"]
          interval: 30s
          timeout: 5s
          retries: 3
          start_period: 30s
    Bring it up
    cd /srv/pinchflat
    docker compose up -d
    docker compose logs -f

    Critical: port binding is 127.0.0.1:8945:8945, not 8945:8945 — Pinchflat ships without authentication. Never expose it directly. TZ matters because the scheduler uses local time for "check every X hours" rules.

    5

    Nginx with WebSocket Support + Let's Encrypt

    Install
    apt install -y nginx certbot python3-certbot-nginx

    Pinchflat's UI runs entirely over Phoenix LiveView WebSockets. A proxy that doesn't forward Upgrade + Connection headers gives you a UI that loads but never updates.

    /etc/nginx/sites-available/pinchflat
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }
    
    server {
        listen 80;
        listen [::]:80;
        server_name pinchflat.example.com;
        location / { return 301 https://$host$request_uri; }
    }
    
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        http2 on;
        server_name pinchflat.example.com;
    
        ssl_certificate     /etc/letsencrypt/live/pinchflat.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/pinchflat.example.com/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    
        client_max_body_size 64M;
        proxy_buffering off;
        proxy_request_buffering off;
    
        proxy_connect_timeout 60s;
        proxy_send_timeout    3600s;
        proxy_read_timeout    3600s;
    
        location / {
            proxy_pass http://127.0.0.1:8945;
            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 X-Forwarded-Host  $host;
    
            # Critical for Phoenix LiveView
            proxy_set_header Upgrade    $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }
    Enable + cert
    ln -s /etc/nginx/sites-available/pinchflat /etc/nginx/sites-enabled/
    rm -f /etc/nginx/sites-enabled/default
    nginx -t
    systemctl reload nginx
    
    certbot --nginx -d pinchflat.example.com
    certbot renew --dry-run
    6

    Lock Down the UI with Authentication

    Option A — Pinchflat's built-in basic auth (newer versions). Add to compose env:

    compose env additions
        environment:
          - TZ=America/New_York
          - UMASK=022
          - BASIC_AUTH_USERNAME=admin
          - BASIC_AUTH_PASSWORD=use_a_long_random_string_here

    Option B — Nginx-level basic auth (works for older versions, or if you want the auth boundary at the edge):

    htpasswd
    apt install -y apache2-utils
    htpasswd -c /etc/nginx/.htpasswd admin
    chown root:www-data /etc/nginx/.htpasswd
    chmod 640 /etc/nginx/.htpasswd
    Add inside location / block
    auth_basic           "Pinchflat";
    auth_basic_user_file /etc/nginx/.htpasswd;
    7

    First-Run Configuration

    1. Set the media directory: /downloads inside the container maps to /srv/pinchflat/downloads on the host.
    2. Pick a media profile: Plex/Jellyfin presets generate directory layouts those servers pick up automatically.
    3. Add your first source: channel or playlist URL. "Fast indexing" uses YouTube's RSS feed (capped at the 15 most recent videos); full indexing backfills.
    4. Verify the schedule: stagger sources or lengthen the interval if you have many to avoid YouTube rate limits.
    8

    YouTube Cookies + Plex/Jellyfin Integration

    Cookies for age-gated, members-only, or private content — Pinchflat reads /config/cookies.txt in Netscape format. Use the Get cookies.txt LOCALLY Chrome extension on a fresh browser profile (don't reuse it for normal browsing — Pinchflat traffic patterns can lock the account).

    Upload cookies
    scp cookies.txt pinchflat@your-vps-ip:/srv/pinchflat/config/cookies.txt
    ssh pinchflat@your-vps-ip "sudo chown 1000:1000 /srv/pinchflat/config/cookies.txt && sudo chmod 600 /srv/pinchflat/config/cookies.txt"

    Bot detection: if you hit "Sign in to confirm you're not a bot" — use the cookies file even for non-restricted content, lower concurrency in your profile, and update yt-dlp regularly. Plex/Jellyfin: point the media server at /srv/pinchflat/downloads over NFS/SMB/Syncthing. Plex/Jellyfin presets generate .nfo files + season/episode filenames automatically.

    9

    Backups

    Config (small, critical): SQLite DB, profiles, source list, schedule. Lose this and you lose your library configuration. Back up daily with multiple versions.

    /usr/local/bin/pinchflat-backup.sh
    #!/bin/bash
    set -euo pipefail
    TS=$(date +%Y%m%d-%H%M%S)
    BACKUP_DIR=/srv/pinchflat/backups
    mkdir -p "$BACKUP_DIR"
    
    # SQLite online backup via the container
    docker exec pinchflat sqlite3 /config/db/pinchflat.db ".backup /config/db/backup.db"
    tar -czf "$BACKUP_DIR/config-$TS.tar.gz" -C /srv/pinchflat config
    rm -f /srv/pinchflat/config/db/backup.db
    
    find "$BACKUP_DIR" -name "config-*.tar.gz" -mtime +14 -delete
    Schedule
    chmod +x /usr/local/bin/pinchflat-backup.sh
    echo "30 4 * * * root /usr/local/bin/pinchflat-backup.sh" > /etc/cron.d/pinchflat-backup

    Downloads (large, regenerable): usually "don't bother — Pinchflat will redownload from YouTube". If source content is at risk of disappearing, mirror to S3 with rclone sync.

    10

    Updates

    Pull + restart
    cd /srv/pinchflat
    docker compose pull
    docker compose up -d
    docker image prune -f

    yt-dlp ships inside each Pinchflat release, so updating regularly matters for keeping up with YouTube API changes. For production, pin to a tagged release rather than :latest.

    11

    Monitoring

    Health check at /healthcheck; Prometheus metrics at /metrics (set PROMETHEUS_METRICS_ENABLED=true). For single-node, an UptimeRobot or Healthchecks.io ping is enough.

    /etc/cron.daily/pinchflat-disk-check
    #!/bin/bash
    THRESHOLD=90
    USAGE=$(df /srv/pinchflat/downloads | awk 'NR==2 {print $5}' | tr -d '%')
    if [ "$USAGE" -gt "$THRESHOLD" ]; then
        echo "Pinchflat downloads volume at ${USAGE}% on $(hostname)" | \
          mail -s "Pinchflat disk warning" you@example.com
    fi

    A full disk silently breaks downloads with confusing errors — this catches it.

    Troubleshooting

    • "Permission denied" in container logs: mounted host dirs not writable by UID 1000 — chown -R 1000:1000 /srv/pinchflat/config /srv/pinchflat/downloads.
    • UI loads but never updates / WebSocket close errors: reverse proxy not forwarding upgrade headers. Re-check the map + Upgrade/Connection blocks.
    • "Sign in to confirm you're not a bot": YouTube rate-limiting the VPS IP. Add a cookies file + reduce concurrent downloads.
    • DB errors after unclean shutdown: SQLite WAL on a network filesystem. Move config to local storage or set JOURNAL_MODE=delete; restore from backup if corrupted.
    • Plex/Jellyfin doesn't see new files: check ownership inside /srv/pinchflat/downloads is readable by the media server user, and that the server has an inotify watcher or scheduled scan.