# Deploying Wuxia for WordPress Follow this guide from top to bottom and you end with a site whose pages load **and whose episodes play**. Every command is meant to be copied as-is; replace only the values in `ALL_CAPS` (such as `YOUR_DOMAIN`). Prefer a guide at your side? An AI assistant can walk you through it: see [Deploy with an AI assistant](#6-deploy-with-an-ai-assistant). - [Before you start: pick your hosting](#1-before-you-start-pick-your-hosting) - [Path A: a new Ubuntu server](#2-path-a-a-new-ubuntu-server-recommended) - [Path B: a hosting panel that runs nginx](#3-path-b-a-hosting-panel-that-runs-nginx) - [Path C: a server that already runs nginx](#4-path-c-a-server-that-already-runs-nginx) - [Path D: Docker with Nginx Proxy Manager](#5-path-d-docker-with-nginx-proxy-manager) - [Deploy with an AI assistant](#6-deploy-with-an-ai-assistant) - [Cloudflare](#7-cloudflare) - [Install the theme and plugin](#8-install-the-theme-and-plugin) - [Check that video plays](#9-check-that-video-plays) - [Troubleshooting](#10-troubleshooting) - [Updates and backups](#11-updates-and-backups) ## How video reaches a viewer ``` viewer ──▶ Cloudflare ──▶ nginx ──▶ WordPress checks the signed /media/ link, names the file │ └──▶ nginx fetches the file from your storage (R2, S3, Telegram, …) and streams it ``` WordPress never streams video itself, and viewers never see your storage address. This needs **two nginx rules** ([`server/nginx-wuxia-media.conf`](server/nginx-wuxia-media.conf)). Without them the site works but every episode answers `501 Media is served through nginx`. ## 1. Before you start: pick your hosting | Your hosting | Pages | Video | Follow | |---|---|---|---| | A VPS or dedicated server you can `ssh` into (Ubuntu 24.04) | Yes | Yes | [Path A](#2-path-a-a-new-ubuntu-server-recommended) | | A panel that runs nginx and accepts custom nginx config (CloudPanel, RunCloud, SpinupWP, GridPane, Plesk with nginx) | Yes | Yes | [Path B](#3-path-b-a-hosting-panel-that-runs-nginx) | | A server where you already run WordPress on nginx | Yes | Yes | [Path C](#4-path-c-a-server-that-already-runs-nginx) | | A server with Docker, where you want [Nginx Proxy Manager](https://nginxproxymanager.com) to handle domains and HTTPS certificates | Yes | Yes | [Path D](#5-path-d-docker-with-nginx-proxy-manager) | | Shared hosting, cPanel, Apache or LiteSpeed | Yes | **No** | Move to one of the rows above | How to tell what an existing site runs: in a terminal, `curl -sI https://YOUR_DOMAIN/xmlrpc.php`. Behind Cloudflare the `server:` header always says `cloudflare`, so ask your host, or look in its control panel for "nginx" or "Apache/LiteSpeed". A server with 2 vCPU, 4 GB RAM and 40 GB of disk is plenty to start. The disk also holds the video cache (20 GB by default). ## 2. Path A: a new Ubuntu server (recommended) Log in as a user with `sudo` on a fresh **Ubuntu 24.04**. ### 2.1 Firewall and packages ```bash sudo apt update && sudo apt -y upgrade sudo ufw allow OpenSSH sudo ufw allow 80,443/tcp sudo ufw --force enable sudo apt -y install nginx mariadb-server \ php8.3-fpm php8.3-mysql php8.3-curl php8.3-xml php8.3-mbstring \ php8.3-zip php8.3-gd php8.3-intl unzip curl ``` ### 2.2 Database ```bash sudo mysql_secure_installation # answer Y to every question sudo mysql -e "CREATE DATABASE wuxia CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'wuxia'@'localhost' IDENTIFIED BY 'A_STRONG_DB_PASSWORD'; GRANT ALL ON wuxia.* TO 'wuxia'@'localhost'; FLUSH PRIVILEGES;" ``` ### 2.3 WordPress files ```bash sudo mkdir -p /var/www/wuxia curl -sL https://wordpress.org/latest.tar.gz | sudo tar -xz -C /var/www/wuxia --strip-components=1 sudo chown -R www-data:www-data /var/www/wuxia ``` ### 2.4 The Cloudflare origin certificate Cloudflare talks to your server over HTTPS with a free certificate that lasts 15 years. 1. In Cloudflare: **SSL/TLS → Origin Server → Create Certificate** (keep the defaults) → **Create**. 2. On the server, paste the two blocks it shows: ```bash sudo mkdir -p /etc/nginx/certs sudo nano /etc/nginx/certs/origin.pem # paste "Origin Certificate", save sudo nano /etc/nginx/certs/origin.key # paste "Private Key", save sudo chmod 600 /etc/nginx/certs/origin.key ``` ### 2.5 nginx: visitor IPs behind Cloudflare Without this every visitor looks like a Cloudflare address, and the media rate limit would slow everyone together. ```bash { for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do echo "set_real_ip_from $ip;"; done echo "real_ip_header CF-Connecting-IP;"; } | sudo tee /etc/nginx/conf.d/cloudflare-realip.conf ``` ### 2.6 nginx: the media rules ```bash sudo curl -sL -o /etc/nginx/wuxia-media.conf \ https://raw.githubusercontent.com/Wuxia-KH/wuxia-press-published-/main/server/nginx-wuxia-media.conf sudo mkdir -p /var/cache/nginx/wuxia && sudo chown -R www-data /var/cache/nginx/wuxia ``` Part 1 of that file belongs inside `http { }` and part 2 inside `server { }`. Split it into two files: ```bash sudo sed -n '/PART 1: http/,/PART 2: server/p' /etc/nginx/wuxia-media.conf | sudo tee /etc/nginx/conf.d/wuxia-media-http.conf >/dev/null sudo sed -n '/PART 2: server/,$p' /etc/nginx/wuxia-media.conf | sudo tee /etc/nginx/snippets/wuxia-media-server.conf >/dev/null ``` The file already points at Ubuntu 24.04's PHP (`unix:/run/php/php8.3-fpm.sock`) and CA bundle, so nothing needs changing on this path. ### 2.7 nginx: the site ```bash sudo tee /etc/nginx/sites-available/wuxia >/dev/null <<'EOF' server { listen 80; server_name YOUR_DOMAIN www.YOUR_DOMAIN; return 301 https://$host$request_uri; } server { listen 443 ssl; http2 on; server_name YOUR_DOMAIN www.YOUR_DOMAIN; ssl_certificate /etc/nginx/certs/origin.pem; ssl_certificate_key /etc/nginx/certs/origin.key; root /var/www/wuxia; index index.php; client_max_body_size 32m; server_tokens off; # Video (Wuxia). Its ^~ locations take priority over the rules below. include snippets/wuxia-media-server.conf; location ~ /\. { deny all; } location = /xmlrpc.php { deny all; } location ~* ^/wp-content/uploads/.*\.php$ { deny all; } location / { try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { try_files $uri =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php/php8.3-fpm.sock; } location ~* \.(css|js|webp|avif|jpg|jpeg|png|svg|ttf|woff2)$ { expires 30d; access_log off; } } EOF sudo sed -i 's/YOUR_DOMAIN/example.com/g' /etc/nginx/sites-available/wuxia # ← your domain here sudo ln -sf /etc/nginx/sites-available/wuxia /etc/nginx/sites-enabled/wuxia sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t && sudo systemctl reload nginx ``` `nginx -t` must print `syntax is ok` and `test is successful`. If not, see [Troubleshooting](#10-troubleshooting). Now set up [Cloudflare](#7-cloudflare), then open `https://YOUR_DOMAIN` and run the WordPress installer (database `wuxia`, user `wuxia`, the password from 2.2). ## 3. Path B: a hosting panel that runs nginx Create the WordPress site in the panel as usual, then add the rules as **custom nginx configuration**: | Panel | Where | |---|---| | CloudPanel | Site → **Vhost** (edit the file directly) | | RunCloud | Web Application → **NGINX Config** → add a config of type `location.main-before` and one of type `http` | | SpinupWP | Site → **Custom Nginx Config** (or `/etc/nginx/sites-available//server/` over SSH) | | GridPane | `/var/www//nginx/` includes over SSH | | Plesk | Domain → **Apache & nginx Settings** → *Additional nginx directives*, with "Proxy mode" **off** | Then: 1. From [`nginx-wuxia-media.conf`](server/nginx-wuxia-media.conf), put **part 1** where the panel accepts `http`-level config. If it has none, ask your host to add those two lines (`limit_req_zone …` and `proxy_cache_path …`); both are harmless to other sites. 2. Put **part 2** in the site's server block. 3. Change `fastcgi_pass` to the one the panel already uses for PHP. Look for `fastcgi_pass` in the site's vhost; copy that value exactly. 4. Create the cache folder, or ask your host to: `mkdir -p /var/cache/nginx/wuxia`, owned by nginx's user. 5. Save; the panel reloads nginx. If it reports an error, the message names the line. ## 4. Path C: a server that already runs nginx 1. Download [`nginx-wuxia-media.conf`](server/nginx-wuxia-media.conf). 2. **Part 1** inside `http { }` (usually a new file in `/etc/nginx/conf.d/`). 3. **Part 2** inside the site's `server { }`, next to its other `location` blocks. 4. Lines marked `CHANGE`: set `fastcgi_pass` to the same value as your `location ~ \.php$` block; on RHEL, Alma or Rocky set the CA bundle to `/etc/pki/tls/certs/ca-bundle.crt`. 5. `sudo mkdir -p /var/cache/nginx/wuxia && sudo chown -R www-data /var/cache/nginx/wuxia` (the user is `nginx` on RHEL-family systems). 6. If the site is behind Cloudflare, add the visitor-IP file from [2.5](#25-nginx-visitor-ips-behind-cloudflare). 7. `sudo nginx -t && sudo systemctl reload nginx` ## 5. Path D: Docker with Nginx Proxy Manager [Nginx Proxy Manager](https://nginxproxymanager.com) gives you a web panel for domains and free HTTPS certificates. It only forwards traffic, so the Wuxia media rules live in a small nginx container behind it: ``` viewer ──▶ Cloudflare (optional) ──▶ Nginx Proxy Manager (HTTPS) ──▶ nginx + Wuxia media rules ──▶ WordPress (PHP-FPM) ──▶ MariaDB ``` Everything runs in Docker from one folder. This setup was tested end to end: an episode's playlist and video segments played through Nginx Proxy Manager, from nginx's cache on the second request. ### 5.1 Docker and the firewall On a fresh Ubuntu 24.04 (Debian works too): ```bash curl -fsSL https://get.docker.com | sudo sh sudo ufw allow OpenSSH sudo ufw allow 80,443/tcp sudo ufw --force enable ``` ### 5.2 The files ```bash mkdir -p ~/wuxia/nginx && cd ~/wuxia RAW=https://raw.githubusercontent.com/Wuxia-KH/wuxia-press-published-/main/server curl -sLo docker-compose.yml "$RAW/nginx-proxy-manager/docker-compose.yml" curl -sLo nginx/site.conf "$RAW/nginx-proxy-manager/nginx/site.conf" curl -sLo nginx/nginx-wuxia-media.conf "$RAW/nginx-wuxia-media.conf" sed -n '/PART 1: http/,/PART 2: server/p' nginx/nginx-wuxia-media.conf > nginx/wuxia-media-http.conf sed -n '/PART 2: server/,$p' nginx/nginx-wuxia-media.conf | sed 's#unix:/run/php/php8.3-fpm.sock#wordpress:9000#' > nginx/wuxia-media-server.conf for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do echo "set_real_ip_from $ip;"; done > nginx/cloudflare.conf ls nginx ``` `ls` lists `cloudflare.conf`, `nginx-wuxia-media.conf`, `site.conf`, `wuxia-media-http.conf` and `wuxia-media-server.conf`. The Cloudflare file lets nginx see visitors' real addresses behind Cloudflare; without Cloudflare it does nothing. ### 5.3 Passwords, then start This puts random database passwords into `docker-compose.yml`; you never need to type them. ```bash sed -i "s/A_STRONG_DB_PASSWORD/$(openssl rand -hex 16)/g; s/A_STRONG_ROOT_PASSWORD/$(openssl rand -hex 16)/" docker-compose.yml sudo docker compose up -d sudo docker compose ps ``` All four services (`npm`, `db`, `wordpress`, `web`) show `Up` (or `running`). ### 5.4 Open the Nginx Proxy Manager panel The panel (port 81) listens only on the server itself, so it is never exposed to the internet. From **your own computer**, open a tunnel: ```bash ssh -L 8181:127.0.0.1:81 YOUR_USER@YOUR_SERVER_IP ``` Keep that window open and browse to `http://localhost:8181`. On the first visit, create the admin account (name, email, password). Older versions instead show a login: `admin@example.com` / `changeme`, then ask you to change both. ### 5.5 The proxy host 1. In Cloudflare (or your DNS), point `YOUR_DOMAIN` and `www` at the server's IP (see [Cloudflare](#7-cloudflare)). 2. In the panel: **Hosts → Proxy Hosts → Add Proxy Host**, **Details** tab: | Field | Value | |---|---| | Domain Names | `YOUR_DOMAIN` and `www.YOUR_DOMAIN` | | Scheme | `http` | | Forward Hostname / IP | `web` | | Forward Port | `80` | | Cache Assets | off | | Block Common Exploits | off (not needed: WordPress and nginx check every media link themselves) | | Websockets Support | off | 3. **SSL** tab: **Request a new SSL Certificate**, turn on **Force SSL** and **HTTP/2 Support**, agree to the terms, **Save**. - With Cloudflare's orange cloud on, tick **Use a DNS Challenge**, choose **Cloudflare**, and paste an API token that can edit DNS for the zone (Cloudflare → My Profile → API Tokens → *Edit zone DNS* template). - Or use a Cloudflare Origin Certificate: **SSL Certificates → Add SSL Certificate → Custom**, upload the certificate and key from Cloudflare's **SSL/TLS → Origin Server**, then pick it on the proxy host's SSL tab. ### 5.6 WordPress Open `https://YOUR_DOMAIN`: the WordPress installer asks only for a site title and your admin account (the database is already connected). Then continue with [Install the theme and plugin](#8-install-the-theme-and-plugin). On this path `wp-config.php` settings go in `docker-compose.yml`, under the `wordpress` service's `WORDPRESS_CONFIG_EXTRA`, one `define` per line after the one already there; then run `sudo docker compose up -d`. ## 6. Deploy with an AI assistant AI assistants such as ChatGPT, Claude, Gemini and Copilot can read this guide and walk you through it, one step at a time. It is published for them as plain text at [`llms-full.txt`](https://wuxia-kh.github.io/wuxia-press-published-/llms-full.txt), with an index at [`llms.txt`](https://wuxia-kh.github.io/wuxia-press-published-/llms.txt). Copy this into the assistant and fill in the brackets: ```text Help me deploy "Wuxia for WordPress" so that its pages load and its episodes play. First read the deployment guide and follow it exactly: https://wuxia-kh.github.io/wuxia-press-published-/llms-full.txt My setup: - Hosting: [a new Ubuntu 24.04 VPS | a hosting panel called ... | a server already running nginx | Docker with Nginx Proxy Manager] - Domain: [example.com] - Cloudflare in front: [yes | no] Give me one step at a time with the exact command, and tell me what I should see before I go on. If something fails, use the guide's troubleshooting table. Never ask me to paste passwords, API keys or tokens into this chat. ``` - **Assistants that can run commands** on your server (Claude Code, Codex CLI, Gemini CLI and similar) can follow the guide directly. Read each command before you approve it. - **Keep secrets out of the chat.** The guide generates passwords on the server; an assistant never needs to see them. - **The guide is the reference.** If an assistant's advice disagrees with it, trust the guide, especially the nginx rules. ## 7. Cloudflare 1. Add the domain; create `A` records for `@` and `www` pointing at the server, **Proxied** (orange cloud). 2. **SSL/TLS → Overview → Full (strict).** Never *Flexible*: it loops wp-admin redirects. 3. **Caching → Cache Rules → Create rule** "Wuxia media": *URI Path starts with* `/media/` → **Eligible for cache**, Edge TTL **Use cache-control header if present**. Segments then come from Cloudflare instead of your server. 4. **Caching → Cache Rules**: bypass cache for `/wp-admin`, `/wp-login.php` and `/wp-json/`. 5. Never cache episode pages (`/episode/…`) or put `/media/` behind a Worker: the first sets each viewer's session, the second skips WordPress's checks. 6. Optional: **Security → WAF → Rate limiting rules**, `/media/` at about 600 requests per minute per IP. ## 8. Install the theme and plugin 1. Download [`wuxia-core.zip`](https://github.com/Wuxia-KH/wuxia-press-published-/releases/latest/download/wuxia-core.zip) and [`wuxia-theme.zip`](https://github.com/Wuxia-KH/wuxia-press-published-/releases/latest/download/wuxia-theme.zip). 2. **Plugins → Add New Plugin → Upload Plugin** → `wuxia-core.zip` → **Activate**. 3. **Appearance → Themes → Add New Theme → Upload Theme** → `wuxia-theme.zip` → **Activate**. 4. **Settings → Permalinks** → **Post name** → **Save**. 5. Recommended, **before adding storage**: give stored credentials their own key. Add to `wp-config.php`, above `/* That's all, stop editing! */`: ```php define( 'WUXIA_MEDIA_KEY', 'PASTE_64_RANDOM_CHARACTERS' ); ``` Make one with `openssl rand -hex 32`. Back it up with the database: if it is lost or changed, storage credentials must be entered again. Without it, the site's WordPress salts are used. 6. Optional, for password-reset and notification mail through an SMTP relay (Brevo, Mailgun, …), also in `wp-config.php`: ```php define( 'WUXIA_SMTP_HOST', 'smtp-relay.brevo.com' ); define( 'WUXIA_SMTP_PORT', '587' ); define( 'WUXIA_SMTP_USER', 'YOUR_SMTP_LOGIN' ); define( 'WUXIA_SMTP_PASS', 'YOUR_SMTP_KEY' ); define( 'WUXIA_MAIL_FROM', 'no-reply@YOUR_DOMAIN' ); ``` 7. **Drama Manager → Storage Providers**: connect your storage. **Drama Manager → Import**: upload iVault JSON, or **Add drama**. **Drama Manager → Settings**: Khmer or English. ## 9. Check that video plays 1. Open any episode and press play. 2. Open the browser's developer tools → **Network**, filter `media`. 3. `master.m3u8` and the segment files (`.ts`, `.m4s`, `.single`, `.married`) must answer **200**. 4. Click a segment: its response headers include `x-wuxia-origin-cache: MISS` the first time and `HIT` after, so nginx's cache works. From a terminal, a quick check that the rules are in place (any episode number works; a bad link is expected): ```bash curl -s -o /dev/null -w "%{http_code}\n" https://YOUR_DOMAIN/media/1/0.AAAAAAAAAAAAAAAAAAAAAA/master.m3u8 ``` `403` or `404` means the rules are working. **`501` means they are not** (see below). ## 10. Troubleshooting | You see | Meaning | Fix | |---|---|---| | `501` "Media is served through nginx" | The request reached WordPress without the media rules. | Add part 2 to the site's `server { }` and reload nginx. On Apache or LiteSpeed hosting this cannot be fixed; move to nginx. | | `nginx -t`: *"proxy_cache" zone "wuxia_media" is unknown* | Part 1 is missing. | Add part 1 inside `http { }`. | | `nginx -t`: *"limit_req_zone" directive is not allowed here* | Part 1 was put inside `server { }`. | Move it to `http { }`. | | Episodes fail and the error log says *Permission denied* under `/var/cache/nginx/wuxia` | nginx's workers can't write the cache folder. | `sudo chown -R www-data /var/cache/nginx/wuxia` (`nginx` on RHEL-family systems), then reload nginx. | | `502` on every media request, and nginx's error log says *connect() to unix:… failed* | `fastcgi_pass` in part 2 doesn't match your PHP. | Copy the value from your `location ~ \.php$` block. `ls /run/php/` lists the sockets. | | `502` "Storage did not answer" | nginx couldn't reach your storage. | Check the provider in **Storage Providers**. The error log may say *could not be resolved* (the `resolver` line) or *certificate verify failed* (the CA-bundle line). | | `503` "Storage is busy" | Telegram asked to slow down. | It retries by itself. | | `403` "This link has expired" | Media links last six hours. | Reload the page. | | `404` | That episode or file doesn't exist. | Check the episode's link and provider. | | `429` | Too many requests from one IP. | Behind Cloudflare, do step [2.5](#25-nginx-visitor-ips-behind-cloudflare) so viewers aren't counted as one. | | wp-admin loops "too many redirects" | Cloudflare SSL is *Flexible*. | Set **Full (strict)** with the origin certificate. | | Path D: Nginx Proxy Manager shows *502 Bad Gateway* | It can't reach the `web` container. | `sudo docker compose ps`: all four services must be up. The proxy host forwards to `web`, port `80`, scheme `http`. | | Path D: the certificate request fails | Let's Encrypt couldn't check the domain. | The domain must already point at the server. Behind Cloudflare's orange cloud, use **Use a DNS Challenge** with a Cloudflare API token. | | Path D: `501` although everything runs | `site.conf` or the split media files are missing. | In `~/wuxia`, `ls nginx` must list the five files from step 5.2; then `sudo docker compose restart web`. | | Episode pages or `/shorts/` answer 404 | Permalinks are "Plain". | **Settings → Permalinks → Post name → Save**. | nginx's error log is `/var/log/nginx/error.log`; media requests are also in the access log. On Path D: `sudo docker compose logs web` (and `logs npm` for the proxy). ## 11. Updates and backups - **Update**: download the new zips and upload them the same way; WordPress asks to replace the installed version. Every release lists `SHA256SUMS`. Check each release's notes for a new `nginx-wuxia-media.conf`. - **Replaced a video at the same address?** Empty nginx's cache or it keeps the old copy for up to 7 days: `sudo rm -rf /var/cache/nginx/wuxia/*` (Path D: `sudo docker compose exec web sh -c 'rm -rf /var/cache/nginx/wuxia/*'`) - **Path D, updating the containers**: `cd ~/wuxia && sudo docker compose pull && sudo docker compose up -d` - **Back up** the database and `wp-config.php` (it holds `WUXIA_MEDIA_KEY`; on Path D, `docker-compose.yml`) every day, and keep a copy off the server. Path D: `sudo docker compose exec -T db sh -c 'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" wuxia' | gzip > ~/wuxia-$(date +%F).sql.gz`. Paths A to C: ```bash sudo mysqldump --single-transaction wuxia | gzip > ~/wuxia-$(date +%F).sql.gz ``` --- ## File: server/nginx-wuxia-media.conf ``` # Wuxia video playback for nginx (any nginx server, no Docker needed). # # Why: the plugin never lets PHP stream video. WordPress checks each signed # /media/ link and names the real file; nginx fetches it from your storage and # streams it, keeping the storage address and credentials hidden. Without these # rules every episode answers "501 Media is served through nginx". # # Apache, LiteSpeed and most shared hosting cannot run this. There the pages # work but episodes do not play; use a server (or VPS) with nginx. # # How: put PART 1 inside `http { }` (for example /etc/nginx/conf.d/wuxia.conf) # and PART 2 inside your site's `server { }`, next to its other locations. # Change the two lines marked CHANGE, then: sudo nginx -t && sudo systemctl reload nginx # # Check: open an episode; in the browser's Network tab its .m3u8 and .ts files # answer 200 (403 "expired" after six hours is normal: reload the page). # ---------------------------------------------------------------- PART 1: http { } # A viewer fetches a segment every few seconds; seeking fetches a few at once. limit_req_zone $binary_remote_addr zone=wuxia_media_rate:10m rate=30r/s; # Each storage file once on this server's disk, shared by every viewer. Size it # to your disk (20g is about 50 hours of 720p); unused files go after 7 days. # The folder must exist and belong to nginx's user: # sudo mkdir -p /var/cache/nginx/wuxia && sudo chown -R www-data /var/cache/nginx/wuxia proxy_cache_path /var/cache/nginx/wuxia levels=1:2 keys_zone=wuxia_media:50m max_size=20g inactive=7d use_temp_path=off; # -------------------------------------------------------------- PART 2: server { } # WordPress answers every /media/ request (the flag below tells the plugin # nginx is in front); for video files it hands the fetch back to nginx. location ^~ /media/ { limit_req zone=wuxia_media_rate burst=80 nodelay; limit_req_status 429; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/index.php; fastcgi_param SCRIPT_NAME /index.php; fastcgi_param WUXIA_MEDIA_ACCEL 1; # CHANGE: the same fastcgi_pass as your site's `location ~ \.php$` block, # e.g. unix:/run/php/php8.3-fpm.sock or 127.0.0.1:9000 fastcgi_pass unix:/run/php/php8.3-fpm.sock; } # Reachable only from the location above (X-Accel-Redirect). The storage # address and credentials come from WordPress in response headers and never # reach the viewer; the viewer's own headers are not sent to storage. location = /_wuxia_upstream { internal; set $wuxia_from_php $upstream_http_x_wuxia_key; if ($wuxia_from_php) { set $wuxia_url $upstream_http_x_wuxia_url; set $wuxia_auth $upstream_http_x_wuxia_auth; set $wuxia_cache $upstream_http_x_wuxia_cache; set $wuxia_key $upstream_http_x_wuxia_key; } proxy_cache wuxia_media; proxy_cache_key $wuxia_key; proxy_cache_valid 200 7d; proxy_cache_valid 404 1m; proxy_cache_lock on; proxy_cache_lock_timeout 15s; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_background_update on; proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires; # nginx looks up storage hosts itself; any public DNS resolver works. resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off; resolver_timeout 5s; proxy_pass $wuxia_url; proxy_http_version 1.1; proxy_ssl_server_name on; proxy_ssl_verify on; # CHANGE on RHEL/Alma/Rocky: /etc/pki/tls/certs/ca-bundle.crt proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; proxy_ssl_verify_depth 3; proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_pass_request_headers off; proxy_set_header Authorization $wuxia_auth; proxy_set_header User-Agent "wuxia-media"; # Nothing about the storage behind reaches the viewer. proxy_hide_header Set-Cookie; proxy_hide_header Cache-Control; proxy_hide_header Expires; proxy_hide_header Content-Disposition; proxy_hide_header Access-Control-Allow-Origin; proxy_hide_header Via; proxy_hide_header X-Cache; proxy_hide_header X-Served-By; proxy_hide_header X-Timer; proxy_hide_header X-Jsd-Version; proxy_hide_header X-Jsd-Version-Type; proxy_hide_header Fastly-Restarts; proxy_hide_header X-GitHub-Request-Id; proxy_hide_header X-Amz-Request-Id; proxy_hide_header X-Amz-Id-2; proxy_hide_header Cf-Ray; proxy_hide_header Alt-Svc; proxy_hide_header Strict-Transport-Security; proxy_hide_header Report-To; proxy_hide_header Nel; proxy_hide_header Timing-Allow-Origin; proxy_hide_header X-Content-Type-Options; proxy_hide_header Cross-Origin-Resource-Policy; proxy_hide_header Access-Control-Expose-Headers; add_header Cache-Control $wuxia_cache always; add_header X-Content-Type-Options "nosniff" always; add_header Cross-Origin-Resource-Policy "same-origin" always; add_header Content-Disposition "inline" always; add_header X-Robots-Tag "noindex, nofollow" always; add_header X-Wuxia-Origin-Cache $upstream_cache_status always; } ``` --- ## File: server/nginx-proxy-manager/docker-compose.yml ``` # Wuxia for WordPress behind Nginx Proxy Manager: Path D of the deployment guide, # https://wuxia-kh.github.io/wuxia-press-published-/deploy.html#path-d # # Replace A_STRONG_DB_PASSWORD and A_STRONG_ROOT_PASSWORD (the guide does it # with one command), then: docker compose up -d services: # Nginx Proxy Manager: the only thing the internet reaches. HTTPS # certificates, and the admin panel on port 81. npm: image: jc21/nginx-proxy-manager:latest restart: unless-stopped ports: - "80:80" - "443:443" # The admin panel: only on this server. Reach it through an SSH tunnel # (see the guide); never open it to the internet. - "127.0.0.1:81:81" volumes: - ./npm/data:/data - ./npm/letsencrypt:/etc/letsencrypt db: image: mariadb:11 restart: unless-stopped environment: MARIADB_DATABASE: wuxia MARIADB_USER: wuxia MARIADB_PASSWORD: A_STRONG_DB_PASSWORD MARIADB_ROOT_PASSWORD: A_STRONG_ROOT_PASSWORD volumes: - db:/var/lib/mysql # WordPress as PHP-FPM: no web server of its own. wordpress: image: wordpress:php8.3-fpm restart: unless-stopped depends_on: [db] environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_NAME: wuxia WORDPRESS_DB_USER: wuxia WORDPRESS_DB_PASSWORD: A_STRONG_DB_PASSWORD # Nginx Proxy Manager ends HTTPS; tell WordPress the visitor used it. WORDPRESS_CONFIG_EXTRA: | if ( ( $$_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '' ) === 'https' ) { $$_SERVER['HTTPS'] = 'on'; } volumes: - wp:/var/www/html # nginx with the Wuxia media rules, between the proxy and WordPress. web: image: nginx:stable restart: unless-stopped depends_on: [wordpress] volumes: - wp:/var/www/html:ro - ./nginx/site.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx/wuxia-media-http.conf:/etc/nginx/conf.d/wuxia-media-http.conf:ro - ./nginx/cloudflare.conf:/etc/nginx/conf.d/cloudflare.conf:ro - ./nginx/wuxia-media-server.conf:/etc/nginx/snippets/wuxia-media-server.conf:ro - media_cache:/var/cache/nginx/wuxia volumes: db: wp: media_cache: ``` --- ## File: server/nginx-proxy-manager/nginx/site.conf ``` # The nginx between Nginx Proxy Manager and WordPress: it serves the site and # carries the Wuxia media rules (snippets/wuxia-media-server.conf). # Path D of the Wuxia for WordPress deployment guide. # Behind Nginx Proxy Manager: the visitor's address arrives in X-Forwarded-For. # Trust it only from Docker's private network (the proxy). set_real_ip_from 172.16.0.0/12; set_real_ip_from 10.0.0.0/8; set_real_ip_from 192.168.0.0/16; real_ip_header X-Forwarded-For; real_ip_recursive on; server { listen 80; server_name _; root /var/www/html; index index.php; client_max_body_size 64m; server_tokens off; # Video (Wuxia). Its ^~ locations take priority over the rules below. include snippets/wuxia-media-server.conf; location ~ /\. { deny all; } location = /xmlrpc.php { deny all; } location ~* ^/wp-content/uploads/.*\.php$ { deny all; } location / { try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { try_files $uri =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass wordpress:9000; } location ~* \.(css|js|webp|avif|jpg|jpeg|png|svg|ttf|woff2)$ { expires 30d; access_log off; } } ```