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
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ PLAUSIBLE_HOSTNAME=plausible.${DOMAIN}
PLEX_HOSTNAME=plex.${DOMAIN}
PLEXIO_HOSTNAME=plexio.${DOMAIN}
PORTAINER_HOSTNAME=portainer.${DOMAIN}
POSTER_CACHE_HOSTNAME=poster-cache.${DOMAIN}
PROWLARR_HOSTNAME=prowlarr.${DOMAIN}
QUETRE_HOSTNAME=quetre.${DOMAIN}
RADARR_HOSTNAME=radarr.${DOMAIN}
Expand Down
19 changes: 19 additions & 0 deletions apps/aiometadata/.env
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,25 @@ CATALOG_LIST_ITEMS_SIZE=20
# Optional: Override the logo URL in the manifest
# ADDON_LOGO_URL=https://yourdomain.com/yourlogo.png


# -- Poster Reverse Proxy Cache (Optional) --
# Route poster images through the bundled `poster-cache` nginx service (defined in
# this app's compose.yaml). It caches posters on disk so repeated requests skip
# upstream latency and, with cache warming, serves them instantly.
# To enable: add the `poster-cache` profile to COMPOSE_PROFILES in the root .env,
# then uncomment the four variables below.
#
# Public HTTPS URL of the proxy, embedded in responses so Stremio fetches posters
# through it. Uses POSTER_CACHE_HOSTNAME from the root .env.
# POSTER_PROXY_PREFIX_URL=https://${POSTER_CACHE_HOSTNAME}
# Internal Docker URL for server-side poster warming (container-to-container).
# Optional — falls back to POSTER_PROXY_PREFIX_URL when unset.
# POSTER_WARMUP_URL=http://poster-cache:8888
# Delay between poster warm batches, in milliseconds. (Default: 50)
# POSTER_WARMUP_DELAY_MS=50
# Concurrent poster warm requests per batch. (Default: 1; 5 suits self-hosters)
# POSTER_WARMUP_CONCURRENCY=5

# -- Database & Caching --
# Connection URI for the database. SQLite is used for local storage, but PostgreSQL is also supported.
# For SQLite (default):
Expand Down
32 changes: 32 additions & 0 deletions apps/aiometadata/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,36 @@ services:
retries: 5
profiles:
- aiometadata
- all

# Optional poster reverse-proxy cache (AIOMetadata guide §3). Opt-in: add the
# `poster-cache` profile to COMPOSE_PROFILES and uncomment the POSTER_* vars in
# apps/aiometadata/.env. Behind Authelia — /purge and /stats require 2FA, image
# fetches bypass (rules in apps/authelia/config/configuration.yml).
poster-cache:
image: nginx:alpine
container_name: poster-cache
restart: unless-stopped
volumes:
- ./poster-cache-nginx.conf:/etc/nginx/nginx.conf:ro
- ./poster-cache-stats.sh:/stats.sh:ro
- ./poster-cache-purge-handler.sh:/purge-handler.sh:ro
- ${DOCKER_DATA_DIR}/poster-cache:/var/cache/nginx
Comment thread
coderabbitai[bot] marked this conversation as resolved.
entrypoint: ["/bin/sh", "-c", "chown -R nginx:nginx /var/cache/nginx && nc -lk -p 9888 -e /purge-handler.sh & /stats.sh & exec nginx -g 'daemon off;'"]
expose:
- "8888"
labels:
- "traefik.enable=true"
- "traefik.http.routers.poster-cache.rule=Host(`${POSTER_CACHE_HOSTNAME?}`)"
- "traefik.http.routers.poster-cache.entrypoints=websecure"
- "traefik.http.routers.poster-cache.tls.certresolver=letsencrypt"
- "traefik.http.routers.poster-cache.middlewares=authelia@docker"
- "traefik.http.services.poster-cache.loadbalancer.server.port=8888"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8888/health"]
interval: 30s
timeout: 10s
retries: 3
Comment thread
IbbyLabs marked this conversation as resolved.
profiles:
- poster-cache
- all
87 changes: 87 additions & 0 deletions apps/aiometadata/poster-cache-nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
user nginx;
worker_processes auto;

events {
worker_connections 1024;
}

http {
# Cache storage on disk — adjust max_size to suit available space
proxy_cache_path /var/cache/nginx/posters
levels=1:2
keys_zone=poster_cache:10m
max_size=10g
inactive=30d
use_temp_path=off;

# Restore double-slash after scheme when a reverse proxy (e.g. Traefik)
# collapses "https://" to "https:/".
# Input: /https:/api.example.com/path -> https://api.example.com/path
# Input: /https://api.example.com/path -> https://api.example.com/path
map $request_uri $upstream_url {
~^/(https?):/([^/].*)$ $1://$2;
~^/(https?://.*)$ $1;
default "";
}
Comment thread
IbbyLabs marked this conversation as resolved.

# Extract scheme + host from the upstream URL for resolving relative redirects
map $upstream_url $upstream_origin {
~^(https?://[^/]+) $1;
default "";
}

log_format cache '$remote_addr - [$time_local] "$request" $status '
'$body_bytes_sent $upstream_cache_status';
access_log /var/log/nginx/access.log cache;

server {
listen 8888;

location = /health {
access_log off;
return 200 'ok';
}

location = /stats {
access_log off;
default_type application/json;
alias /tmp/cache-stats.json;
}

location = /purge {
access_log off;
default_type application/json;
proxy_pass http://127.0.0.1:9888;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

location / {
resolver 127.0.0.11 valid=30s ipv6=off;

if ($upstream_url = "") {
return 400;
}

proxy_pass $upstream_url;
proxy_ssl_server_name on;

# Rewrite relative upstream redirects into absolute URLs.
# Some upstreams (e.g. openposterdb) return relative 302 Location headers
# like "/c/abc/path" which the client would resolve against the proxy host.
# This rewrites them to point to the actual upstream origin.
# e.g. Location: /c/abc/path → Location: https://openposterdb.com/c/abc/path
proxy_redirect / $upstream_origin/;

proxy_cache poster_cache;
proxy_cache_key $upstream_url;
proxy_cache_valid 200 30d;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
proxy_ignore_headers Cache-Control Expires Vary;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;

add_header X-Cache-Status $upstream_cache_status;

proxy_set_header Host $proxy_host;
proxy_set_header Accept-Encoding "";
}
}
}
12 changes: 12 additions & 0 deletions apps/aiometadata/poster-cache-purge-handler.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
# HTTP handler for /purge — called by nc -lk -e
read -r method path _
# Consume remaining headers
while read -r line; do
line=$(printf '%s' "$line" | tr -d '\r\n')
[ -z "$line" ] && break
done

touch /tmp/purge-cache
BODY='{"success":true,"message":"cache purge scheduled"}'
printf "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" ${#BODY} "$BODY"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
40 changes: 40 additions & 0 deletions apps/aiometadata/poster-cache-stats.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/sh
# Periodically writes cache stats to a JSON file served by nginx
CACHE_DIR="/var/cache/nginx/posters"
STATS_FILE="/tmp/cache-stats.json"
MAX_SIZE="${POSTER_CACHE_MAX_SIZE:-10g}"
INACTIVE="${POSTER_CACHE_INACTIVE:-30d}"

while true; do
if [ -d "$CACHE_DIR" ]; then
size_bytes=$(du -sb "$CACHE_DIR" 2>/dev/null | cut -f1)
file_count=$(find "$CACHE_DIR" -type f 2>/dev/null | wc -l)
size_human=$(awk "BEGIN {
b = ${size_bytes:-0};
if (b >= 1000000000) printf \"%.1fG\", b/1000000000;
else if (b >= 1000000) printf \"%.1fM\", b/1000000;
else if (b >= 1000) printf \"%.1fK\", b/1000;
else printf \"%dB\", b;
}")
else
size_bytes=0
size_human="0B"
file_count=0
fi

# Check for purge flag
if [ -f /tmp/purge-cache ]; then
rm -f /tmp/purge-cache
rm -rf "$CACHE_DIR"
mkdir -p "$CACHE_DIR"
chown nginx:nginx "$CACHE_DIR"
size_bytes=0
size_human="0B"
file_count=0
fi

cat > "$STATS_FILE" <<EOF
{"cached_images":${file_count},"disk_usage":"${size_human}","disk_usage_bytes":${size_bytes},"max_size":"${MAX_SIZE}","inactive":"${INACTIVE}"}
EOF
Comment thread
IbbyLabs marked this conversation as resolved.
sleep 30
done
1 change: 1 addition & 0 deletions apps/authelia/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ services:
TEMPLATE_SEANIME_HOSTNAME: ${SEANIME_HOSTNAME?}
TEMPLATE_XRDB_HOSTNAME: ${XRDB_HOSTNAME?}
TEMPLATE_ERDB_HOSTNAME: ${ERDB_HOSTNAME?}
TEMPLATE_POSTER_CACHE_HOSTNAME: ${POSTER_CACHE_HOSTNAME?}
labels:
- "traefik.enable=true"
- "traefik.http.routers.authelia.rule=Host(`${AUTHELIA_HOSTNAME?}`)"
Expand Down
11 changes: 11 additions & 0 deletions apps/authelia/config/configuration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,17 @@ access_control:
- '^/fonts(/.*)?$'
policy: 'bypass'

## Poster cache — protect the admin endpoints, bypass everything else so
## Stremio can fetch poster images without a login.
- domain: '{{ env "TEMPLATE_POSTER_CACHE_HOSTNAME" }}'
resources:
- '^/purge$'
- '^/stats$'
policy: 'two_factor'

- domain: '{{ env "TEMPLATE_POSTER_CACHE_HOSTNAME" }}'
policy: 'bypass'

- domain: '*.{{ env "TEMPLATE_DOMAIN" }}'
policy: 'two_factor'

Expand Down
1 change: 1 addition & 0 deletions apps/cloudflare-ddns/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ services:
${PLEX_HOSTNAME},
${PLEXIO_HOSTNAME},
${PORTAINER_HOSTNAME},
${POSTER_CACHE_HOSTNAME},
${PROWLARR_HOSTNAME},
${QUETRE_HOSTNAME},
${RADARR_HOSTNAME},
Expand Down