diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..441234e --- /dev/null +++ b/Caddyfile @@ -0,0 +1,31 @@ +{ + # General options + http_port 80 + https_port 443 + + # Logging configuration + log { + output stdout + format console + } +} + +# Main server block for caddy.localhost +caddy.localhost { + tls internal # Use Caddy's automatic HTTPS with internal CA + + # Reverse proxy configuration (pick one or the other) + ## With Varnish as a caching layer (recommended if using PixivFE's internal image proxy) + reverse_proxy varnish:80 + + ## Bypass Varnish and send requests directly to PixivFE on port 8282 + #reverse_proxy pixivfe:8282 + + # Header manipulation + header { + # Cross-Origin isolation headers + Cross-Origin-Embedder-Policy "require-corp" + Cross-Origin-Opener-Policy "same-origin" + Cross-Origin-Resource-Policy "same-origin" + } +} diff --git a/compose.stack.yaml b/compose.stack.yaml new file mode 100644 index 0000000..c7b6a78 --- /dev/null +++ b/compose.stack.yaml @@ -0,0 +1,79 @@ +### compose.stack.yaml +## Intended to provide replicate a production environment by placing a reverse proxy in front of PixivFE, +## with Varnish as caching layer (optional) + +### Instructions: +## To run using a fresh build (this is probably what you want during development): +# docker compose -f compose.stack.yaml up --build + +## To run using a cached build: +# docker compose -f compose.stack.yaml up + +## To reload Caddy configuration without downtime: +# docker exec -w /etc/caddy caddy caddy reload + +## To access pixivfe: +# Visit https://caddy.localhost/ in your browser +# and accept any security warnings shown (expected as Caddy uses self-signed certs by default) + +services: + caddy: + container_name: caddy + image: caddy:2.8.4-alpine + restart: unless-stopped + ports: + - 80:80 + - 443:443 + networks: + internal: {} + volumes: + - type: bind + source: ./Caddyfile + target: /etc/caddy/Caddyfile + read_only: true + - caddy_data:/data + - caddy_config:/config + + varnish: + container_name: varnish + image: varnish:7.5.0 + restart: unless-stopped + networks: + internal: {} + volumes: + - type: bind + source: ./default.vcl + target: /etc/varnish/default.vcl + read_only: true + + pixivfe: + container_name: pixivfe + hostname: pixivfe + image: vnpower/pixivfe:latest + build: + context: . + dockerfile: Dockerfile + init: true + networks: + internal: {} + env_file: + - path: .env + required: true + restart: unless-stopped + cap_drop: + - ALL + read_only: true + user: 10001:10001 + security_opt: + - no-new-privileges:true + +# Named volumes for persistent data +volumes: + caddy_data: {} + caddy_config: {} + +# Network configuration +# Required for internal communication between the web server and PixivFE. +networks: + internal: + driver: bridge diff --git a/default.vcl b/default.vcl new file mode 100644 index 0000000..7b7a514 --- /dev/null +++ b/default.vcl @@ -0,0 +1,89 @@ +vcl 4.1; + +backend pixivfe { + .host = "pixivfe"; + .port = "8282"; +} + +sub vcl_recv { + if (req.http.host == "caddy.localhost") { + set req.backend_hint = pixivfe; + + # Step 1: Check for login status + if (req.http.cookie ~ "pixivfe-Token") { + set req.http.X-User-Logged-In = "true"; + } else { + set req.http.X-User-Logged-In = "false"; + } + + # For logged-in users, include the token in the cache key + if (req.http.X-User-Logged-In == "true") { + set req.http.X-User-Token = regsub(req.http.cookie, ".*pixivfe-Token=([^;]+).*", "\1"); + } + } +} + +sub vcl_backend_response { + if (bereq.backend == pixivfe) { + # Step 2: Implement different caching strategies + + # set grace to 1 minute globally + set beresp.grace = 1m; + + ## landing page; should be cached conservatively when it contains personalised content for logged in users, but be more aggressive for the shared page shown to anonymous visitors + if (bereq.url == "/") { + if (beresp.status == 200) { + if (bereq.http.X-User-Logged-In == "false") { + # Non-logged in users: cache for 8h + set beresp.ttl = 8h; + set beresp.http.Cache-Control = "public, max-age=28800"; + } else { + # Logged in users: cache for 30 seconds, vary by user token + set beresp.ttl = 30s; + set beresp.uncacheable = false; + set beresp.http.Cache-Control = "private, max-age=30"; + set beresp.http.Vary = "X-User-Token"; + } + } + return (deliver); + } + + ## pages with user content that changes dynamically every request as per the Pixiv upstream; use a moderate caching strategy, we don't need to fetch new content *every* time + if (bereq.url ~ "^/discovery" || bereq.url ~ "^/discovery/novel" || bereq.url ~ "^/newest") { + if (beresp.status == 200) { + set beresp.ttl = 30s; + set beresp.uncacheable = false; + set beresp.http.Cache-Control = "public, max-age=30"; + } + return (deliver); + } + + ## pages with user content that changes dynamically, but either over longer timeframes (ranking pages) or not appreciably (loading in under 100ms is probably more important than knowing the *exact* number of bookmarks a work has); we can be more aggressive + if (bereq.url ~ "^/ranking" || bereq.url ~ "^/rankingCalendar" || bereq.url ~ "^/artworks/" || bereq.url ~ "^/novel/" || bereq.url ~ "^/users/") { + if (beresp.status == 200) { + set beresp.ttl = 8h; + set beresp.http.Cache-Control = "public, max-age=28800"; + } + return (deliver); + } + + # proxied static media (images, short video, etc); a aggressive caching strategy is safe + if (bereq.url ~ "^/proxy/") { + if (beresp.status == 200) { + set beresp.ttl = 24h; + set beresp.http.Cache-Control = "public, max-age=31536000"; # 1 year + } + return (deliver); + } + + # if nothing matches, be safe and dont cache anything + set beresp.http.Cache-Control = "private, no-store"; + } +} + +sub vcl_hash { + # Step 3: Include user token in hash for logged-in users + if (req.http.X-User-Logged-In == "true") { + hash_data(req.http.X-User-Token); + } +}