Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/upstream-digests.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"linuxserver/plex:latest": "sha256:f6c58cb2f5e41cd1397bf2ed4e61ef63bd86e0736841b3ef426fabfe04606293",
"linuxserver/plex:latest": "sha256:7f9a1d574958fc2f177c14ca190d4b811a58c274477f5bae8fb44ee676fb96bf",
"plexinc/pms-docker:latest": "sha256:83a425ae9e133b1cb2cc3b809556e01c61cd8ff65c582e41b4374bc2210bac9e"
}
87 changes: 87 additions & 0 deletions .github/workflows/build-debug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Build Debug / Sanitizer Image

on:
workflow_dispatch:
inputs:
sanitizer:
description: 'Sanitizer type (address, thread, or empty for debug symbols only)'
required: false
default: 'address'
type: choice
options:
- address
- thread
- ''

jobs:
build-debug:
name: Build Debug / Sanitizer (${{ matrix.name }} / ${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- name: linuxserver
dockerfile: Dockerfile
image: ghcr.io/${{ github.repository_owner }}/plex-postgresql-debug
arch: amd64
platform: linux/amd64
runner: ubuntu-latest
# Optionally add plexinc variant if you use it

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push debug image
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }}
push: true
provenance: false
tags: |
${{ matrix.image }}:${{ github.sha }}-${{ matrix.arch }}
${{ matrix.image }}:latest-${{ matrix.arch }}
build-args: |
PLEX_PG_SANITIZE=${{ inputs.sanitizer }}
# Optionally add other debug flags via environment variables if needed

manifest-debug:
name: Create multi-arch manifest for debug
runs-on: ubuntu-latest
needs: build-debug
if: always()
steps:
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create and push manifest
run: |
set -eux
IMAGE="ghcr.io/${{ github.repository_owner }}/plex-postgresql-debug"
SHA="${{ github.sha }}"
docker manifest create "${IMAGE}:latest" \
"${IMAGE}:${SHA}-amd64"
docker manifest push "${IMAGE}:latest"
# Also tag with sanitizer type if needed
if [ -n "${{ inputs.sanitizer }}" ]; then
docker manifest create "${IMAGE}:${{ inputs.sanitizer }}" \
"${IMAGE}:${SHA}-amd64"
docker manifest push "${IMAGE}:${{ inputs.sanitizer }}"
fi
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.15] - 2026-08-31

### Changed
- Updated upstream base Docker images (linuxserver/plex:latest / plexinc/pms-docker:latest).

## [1.3.14] - 2026-08-18

### Changed
Expand Down
48 changes: 9 additions & 39 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ FROM alpine:3.15 AS builder
ARG PLEX_PG_SANITIZE
ENV PLEX_PG_SANITIZE=${PLEX_PG_SANITIZE}

# Install build dependencies
# Install build dependencies – includes sanitizer runtime libraries
RUN apk add --no-cache \
build-base \
sqlite-dev \
Expand All @@ -18,6 +18,8 @@ RUN apk add --no-cache \
# Verify musl version matches Plex (1.2.2)
RUN /lib/ld-musl-*.so.1 --version 2>&1 | head -2

# ... rest of builder stage unchanged ...

WORKDIR /build

# Install Rust toolchain
Expand Down Expand Up @@ -51,12 +53,15 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
FROM linuxserver/plex:latest

# Install PostgreSQL client for health checks, sqlite3 for schema fixes,
# python3 for data migration, gdb for debugging
# python3 for data migration, gdb for debugging, and conditional sanitizer runtime libraries
ARG PLEX_PG_SANITIZE
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
sqlite3 \
python3 \
gdb \
$(if [ "$PLEX_PG_SANITIZE" = "address" ]; then echo "libasan8"; fi) \
$(if [ "$PLEX_PG_SANITIZE" = "thread" ]; then echo "libtsan2"; fi) \
&& rm -rf /var/lib/apt/lists/*

# NOTE: Do NOT set LANG/LC_ALL/CHARSET here — Plex's bundled musl+boost::locale
Expand Down Expand Up @@ -118,54 +123,19 @@ RUN if [ -f /etc/s6-overlay/s6-rc.d/init-plex-claim/run ]; then \
echo "Patched init-plex-claim for PostgreSQL shim"; \
fi

# Keep upstream CrashUploader binary.
# With SIGCHLD forced to SIG_IGN, child exits should no longer destabilize Plex.

# s6 finish script — defense-in-depth for the BindAddrInUseException crash loop.
#
# PRIMARY FIX: PLEX_PG_SUPPRESS_DAEMON=1 injected below keeps PMS in the
# foreground so s6 never sees the run script exit during normal startup.
# This finish script is a safety net for the case where daemon suppression is
# disabled (PLEX_PG_SUPPRESS_DAEMON=0) or fails.
#
# HOW THE CRASH LOOP WORKS WITHOUT THE PRIMARY FIX:
# PMS calls daemon() → fork() → parent exits → s6 sees its watched PID exit
# → s6 runs finish + restarts → new PMS tries to bind 32400 → the re-exec'd
# child from the previous cycle still holds 32400 → BindAddrInUseException
# → SIGABRT → loop ~50 times.
#
# WHY THE OLD FINISH SCRIPT DID NOT WORK:
# s6-overlay v3 kills the finish script after S6_KILL_FINISH_MAXTIME ms
# (default 5000ms). The old script's `while pgrep ... do sleep 5; done`
# loop is killed on its first iteration. Also, `nc -z localhost 32400`
# races — the re-exec'd child may not have bound 32400 yet.
#
# THIS finish script sets a 30-second timeout file and polls at 1s intervals
# so s6 does not kill it before it can detect the child. It exits 125 to
# signal s6 that it should not restart immediately (s6-overlay v3: exit codes
# >= 125 in the finish script suppress the automatic restart).
RUN printf '#!/bin/bash\n# Exit code 125 tells s6-supervise not to restart the service.\n# See: https://skarnet.org/software/s6/s6-supervise.html\nexit_code=${1:-0}\nif [ "${exit_code}" = "0" ]; then\n deadline=30\n elapsed=0\n while [ $elapsed -lt $deadline ]; do\n if pgrep -x "Plex Media Server" >/dev/null 2>&1; then\n echo "[plex-pg] PMS re-exec child still running (${elapsed}s), suppressing restart"\n sleep 1\n elapsed=$((elapsed+1))\n else\n break\n fi\n done\n if pgrep -x "Plex Media Server" >/dev/null 2>&1; then\n echo "[plex-pg] PMS child still alive after ${deadline}s — suppressing restart, s6 will retry"\n exit 125\n fi\nfi\n' \
> /etc/s6-overlay/s6-rc.d/svc-plex/finish && \
chmod +x /etc/s6-overlay/s6-rc.d/svc-plex/finish && \
printf '30000\n' > /etc/s6-overlay/s6-rc.d/svc-plex/finish-timeout

# Inject shim env into the upstream svc-plex run script and wrap PMS
# with subreaper to prevent the BindAddrInUseException crash loop.
#
# PMS does vfork+execve to re-exec itself during startup: the parent exits
# while the child takes over on port 32400. s6 watches the parent PID, sees
# it exit, and immediately restarts PMS — but the child still holds port
# 32400, causing BindAddrInUseException → SIGABRT in a crash loop.
#
# FIX: subreaper sets PR_SET_CHILD_SUBREAPER, so the re-exec'd child is
# reparented to subreaper (not PID 1). subreaper waits for ALL descendants
# before exiting — s6 never sees a premature death.
# Inject shim env into the upstream svc-plex run script and wrap PMS with subreaper
RUN sed -i '/export PLEX_MEDIA_SERVER_INFO_PLATFORM_VERSION/a\
arch="$(uname -m)"\
\nif [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then\
\n export OPENSSL_armcap="${PLEX_PG_OPENSSL_ARMCAP:-0}"\
\nfi\
\nexport LD_LIBRARY_PATH="/usr/lib/plexmediaserver/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"\
\nexport LD_LIBRARY_PATH="/usr/local/lib/plex-postgresql:/usr/lib/plexmediaserver/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"\
\nexport LD_PRELOAD="/usr/local/lib/plex-postgresql/db_interpose_pg.so"\
' /etc/s6-overlay/s6-rc.d/svc-plex/run && \
sed -i 's|"/usr/lib/plexmediaserver/Plex Media Server"|/usr/local/bin/subreaper "/usr/lib/plexmediaserver/Plex Media Server"|g' \
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.14
1.3.15
8 changes: 4 additions & 4 deletions scripts/migrate_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ check_and_migrate() {
# Check if PostgreSQL already has data (check multiple tables, not just metadata_items)
local pg_count=$(psql -t -c "SELECT COUNT(*) FROM $PG_SCHEMA.metadata_items;" 2>/dev/null | tr -d ' ' || echo "0")
local pg_sections=$(psql -t -c "SELECT COUNT(*) FROM $PG_SCHEMA.library_sections;" 2>/dev/null | tr -d ' ' || echo "0")
local pg_accounts=$(psql -t -c "SELECT COUNT(*) FROM $PG_SCHEMA.accounts;" 2>/dev/null | tr -d ' ' || echo "0")
# Do NOT check accounts – it's always created as bootstrap data.
local pg_has_data=0
if [[ "$pg_count" -gt 0 ]] || [[ "$pg_sections" -gt 0 ]] || [[ "$pg_accounts" -gt 0 ]]; then
if [[ "$pg_count" -gt 0 ]] || [[ "$pg_sections" -gt 0 ]]; then
pg_has_data=1
fi

if [[ "$pg_has_data" -eq 1 ]]; then
echo -e "${YELLOW}PostgreSQL already has data (metadata_items=$pg_count, library_sections=$pg_sections, accounts=$pg_accounts).${NC}"

echo -e "${YELLOW}PostgreSQL already has data (metadata_items=$pg_count, library_sections=$pg_sections).${NC}"
if [[ "$MIGRATION_INTERACTIVE" == "1" ]]; then
echo ""
echo "Options:"
Expand Down