This is Complete Playbook — Nginx (PHP-FPM) + Nginx Proxy Manager + Cloudflared

Purpose A reproducible, detailed guide to host multiple websites on one ZimaOS machine using three Docker services:

  1. nginx (with php-fpm) — internal web server serving site files
  2. nginx proxy manager — GUI reverse-proxy + SSL (Let’s Encrypt) management
  3. cloudflared (custom) — single Cloudflare Tunnel exposing the proxy manager (and/or services) securely to the Internet This playbook reproduces the exact working configuration we used for yourdomain.com and includes prerequisites, commands, file examples, a Docker compose option, and troubleshooting tips (including DNS-challenge Let’s Encrypt via Cloudflare).

1) Prerequisites 

  1. ZimaOS server with Docker installed and operational.
  2. Domains managed in Cloudflare: yourdomain.com (ability to edit DNS and create API tokens).
  3. Familiarity with editing files on the host and restarting containers.
  4. Ports: internal nginx will listen on port 80 (HTTP). Nginx Proxy Manager UI is on 81. Cloudflared will run as a container and will talk to Nginx Proxy Manager or directly to nginx depending on which approach you take.
  5. Paths used in this playbook (you can adapt to your own):
    /DATA/AppData/nginx (nginx config + site files)
    /DATA/AppData/nginxproxymanager/data (NPM persistent DB)
    /DATA/AppData/nginxproxymanager/etc/letsencrypt (NPM Let’s Encrypt files)
    /DATA/AppData/cloudflared (cloudflared certs and config)

    Important decision: You want Nginx Proxy Manager (NPM) to manage Let’s Encrypt certificates using DNS challenge (Cloudflare). For DNS challenge, create a Cloudflare API Token with the necessary permissions for the zone(s).

2) Folder layout we used (create these first) 

sudo mkdir -p /DATA/AppData/nginx/{config,www,log,keys}
sudo mkdir -p /DATA/AppData/nginxproxymanager/data
sudo mkdir -p /DATA/AppData/nginxproxymanager/etc/letsencrypt
sudo mkdir -p /DATA/AppData/cloudflared
sudo chmod -R 755 /DATA/AppData

Place your static site files:

/DATA/AppData/nginx/www/yourdomain.com/...
/DATA/AppData/nginx/www/next-website.com/...

Your nginx container (we used linuxserver nginx image earlier) used /config inside the container for configs — the playbook below follows that structure.

3) High-level architecture (final design) 

Internet (HTTPS) -> Cloudflare Edge ↳ Cloudflare Tunnel (single named tunnel -> UUID) ↳ cloudflared container (runs on ZimaOS) ↳ forwards to --> nginx-proxy-manager (container, port 81 UI, proxies to internal services) ↳ nginx (container with php-fpm) serves actual application files (port 80 internally) Key points: - Cloudflare terminates TLS at edge; NPM can still request and store Let’s Encrypt certificates via DNS challenge. - NPM acts as the reverse-proxy that maps hostnames to the internal nginx service. - All domains are routed through a single cloudflared tunnel (UUID-based CNAME records).

4) Step-by-step instructions 

A. Prepare persistent folders (repeated but deliberate) 

sudo mkdir -p /DATA/AppData/nginx/config
sudo mkdir -p /DATA/AppData/nginx/www
sudo mkdir -p /DATA/AppData/nginx/log
sudo mkdir -p /DATA/AppData/nginx/keys
sudo mkdir -p /DATA/AppData/nginxproxymanager/data
sudo mkdir -p /DATA/AppData/nginxproxymanager/etc/letsencrypt
sudo mkdir -p /DATA/AppData/cloudflared
sudo chown -R ${USER}:${USER} /DATA/AppData
sudo chmod -R 755 /DATA/AppData

B. Nginx (with PHP-FPM) — internal server 

We assume you already have a working nginx+php-fpm container as you posted. Keep listen 80 only and server-blocks for each site. Example site config (we used /config/nginx/site-confs/yourdomain.conf in your container):

server {
    listen 80;
    server_name yourdomain.com;

    root /config/www/yourdomain.com;   # adjust to your mounted volume
    index index.php index.html;

    access_log /config/log/nginx/yourdomain.com.access.log;
    error_log /config/log/nginx/yourdomain.com.error.log;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include /etc/nginx/fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

If the container uses /app/www or /config/www, put your site files in the matching folder on the host: /DATA/AppData/nginx/www/yourdomain.com -> mounted to /config/www/yourdomain.com

Reload nginx inside container after adding configs (use the container name you have):

sudo docker exec -it <nginx_container> nginx -t
sudo docker exec -it <nginx_container> nginx -s reload

Test locally:

curl -v http://192.168.0.1 -H "Host: yourdomain.com"

You must see the actual site HTML. If you see the default page, the server_name or config isn’t active.

C. Nginx Proxy Manager — install and configure 

Install using Docker Compose (recommended). Below is a production-ready minimal docker-compose.yml for nginx-proxy-manager and cloudflared (we will add cloudflared later). Place this in /DATA/AppData/nginxproxymanager/docker-compose.yml.

version: '3'
services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - '81:81'   # NPM UI
      - '8080:8080' # optional (internal)
    environment:
      DB_MYSQL_HOST: "db"
      DB_MYSQL_PORT: 3306
      DB_MYSQL_USER: "npm"
      DB_MYSQL_PASSWORD: "npm_password"
      DB_MYSQL_NAME: "npm"
    volumes:
      - /DATA/AppData/nginxproxymanager/data:/data
      - /DATA/AppData/nginxproxymanager/etc/letsencrypt:/etc/letsencrypt

  db:
    image: jc21/mariadb-aria:latest
    container_name: npm-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: 'root_password'
      MYSQL_DATABASE: 'npm'
      MYSQL_USER: 'npm'
      MYSQL_PASSWORD: 'npm_password'
    volumes:
      - /DATA/AppData/nginxproxymanager/mysql:/var/lib/mysql

networks:
  default:
    external: false
Start Nginx Proxy Manager and DB:
cd /DATA/AppData/nginxproxymanager
sudo docker compose up -d

Initial NPM UI login: - Open http://:81 and log in with default admin user (check jc21 docs for default credentials if needed — typically [email protected] / changeme on first run). Change them immediately.

Configure DNS API token for Cloudflare (required for DNS challenge) To request certificates using DNS challenge automatically from NPM you must provide Cloudflare credentials.

  • Create a Cloudflare API Token (minimum rights): - Go to Cloudflare Dashboard → My Profile → API Tokens → Create Token - Use template: Edit zone DNS (or custom with Zone DNS Edit for the zones you need).
  • Assign to both domains or * zone selection. - Copy the token string — treat it like a secret.
  • In Nginx Proxy Manager (UI):
    1. Log into NPM UI → SSL → Let's Encrypt (or in Proxy Host → SSL tab when issuing cert) → choose DNS Challenge → select provider Cloudflare → paste your API token.
    2. When creating a new Proxy Host, on the SSL tab, choose Request a new SSL Certificate and tick Force SSL and choose DNS provider Cloudflare (DNS challenge). NPM will use that token to create TXT records and issue certs.

Note: Older guides ask to place Cloudflare API key in environment variables. jc21/nginx-proxy-manager supports entering provider credentials in the UI when requesting certs. If your NPM version requires environment variables, set: CLOUDFLARE_API_TOKEN=your_token or provide global DNS credentials in NPM settings. Check the NPM UI / docs to confirm where to paste the token.

D. Cloudflared — create the tunnel and config (custom docker) 

1) Login and save cert.pem to persistent folder Mount to /home/nonroot/.cloudflared because the official image runs as nonroot.

sudo docker run -it --rm \
-v /DATA/AppData/cloudflared:/home/nonroot/.cloudflared \
cloudflare/cloudflared:latest tunnel login

Follow the URL printed, authenticate. After success, confirm:

ls -l /DATA/AppData/cloudflared/cert.pem

2) Create named tunnel

sudo docker run -it --rm \
  -v /DATA/AppData/cloudflared:/home/nonroot/.cloudflared \ cloudflare/cloudflared:latest tunnel create zimaos-tunnel

This writes /DATA/AppData/cloudflared/.json and prints the tunnel UUID. 3) Create config.yml (/DATA/AppData/cloudflared/config.yml) Example (this forwards both hostnames to nginx-proxy-manager, which will perform proxying to the internal nginx):

# Put your real tunnel UUID here&nbsp;<a href='/admin/powertools/edit-section/my-article/cloudflared-nginx-multi-site-setup?section=%23+Put+your+real+tunnel+UUID+here'><i class='fa fa-edit' style='font-size:initial' title='Edit Section'></i></a>
tunnel: 5024b7e0-f8af-49a5-93c9-6cd3cf764333
credentials-file: /home/nonroot/.cloudflared/5024b7e0-f8af-49a5-93c9-6cd3cf764333.json

ingress:
  - hostname: yourdomain.com
    service: http://nginx-proxy-manager:81
  - service: http_status:404

Important note: We’re pointing service to http://nginx-proxy-manager:81 because we want the Cloudflare Tunnel to forward external hostnames to Nginx Proxy Manager — then using NPM you configure per-host proxy rules to forward to the real backend (nginx PHP-FPM). This keeps SSL cert management inside NPM and makes routing flexible. 4) Run cloudflared container persistently Stop and remove any previous container named cloudflared then run:

sudo docker rm -f cloudflared || true
sudo docker run -d \
  --name cloudflared \
  --restart unless-stopped \
  -v /DATA/AppData/cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel run 5024b7e0-f8af-49a5-93c9-6cd3cf764333

Verify logs:

sudo docker logs -f cloudflared

Look for Registered tunnel connection lines.

E. Add DNS CNAME records for each hostname (point to tunnel UUID) 

In Cloudflare Dashboard → DNS for each domain, add records as follows (use tunnel UUID): Type Name Content (Target) Proxy status CNAME yourdomain.com 5024b7e0-f8af-49a5-93c9-6cd3cf764333.cfargotunnel.com Proxied (orange) CNAME www 5024b7e0-f8af-49a5-93c9-6cd3cf764333.cfargotunnel.com Proxied If a previous tunnel UUID was used, change the CNAME to the new UUID. If Cloudflare still references an old UUID you will receive Error 1033.

F. Configure Proxy Hosts in Nginx Proxy Manager (UI) 

  1. Open NPM UI: http://:81 and login.
  2. Click Proxy Hosts → Add Proxy Host. For yourdomain.com: - Domain Names: yourdomain.com (and www if desired) - Scheme: http - Forward Hostname / IP: 192.168.0.1 (or the internal IP/name of your nginx container) or if using Docker network, use the container name nginx or the host host.docker.internal if applicable. - Forward Port: 80 - Block Common Exploits: ✅ - Websocket Support: enable if needed SSL tab: - Check Request a new SSL certificate - Choose DNS Challenge → Provider: Cloudflare - Provide the Cloudflare API token (created earlier) when prompted - Check Force SSL and HTTP/2 Support Click Save — NPM will request a DNS-validated certificate using Cloudflare API and issue the cert automatically.

5) Docker Compose — Combined example (nginx, npm, cloudflared) 

Below is a combined docker-compose.yml that demonstrates the three services on a single custom network. Adjust image names / volumes to match your existing containers.

version: '3.8'
services:
  nginx:
    image: linuxserver/nginx:latest
    container_name: nginx
    volumes:
      - /DATA/AppData/nginx/config:/config
      - /DATA/AppData/nginx/www:/config/www
      - /DATA/AppData/nginx/log:/config/log
    networks:
      - webnet
    restart: unless-stopped

  npm-db:
    image: jc21/mariadb-aria:latest
    container_name: npm-db
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: npm
      MYSQL_USER: npm
      MYSQL_PASSWORD: npm_password
    volumes:
      - /DATA/AppData/nginxproxymanager/mysql:/var/lib/mysql
    networks:
      - webnet
    restart: unless-stopped

  nginx-proxy-manager:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    depends_on:
      - npm-db
    environment:
      DB_MYSQL_HOST: npm-db
      DB_MYSQL_PORT: 3306
      DB_MYSQL_USER: npm
      DB_MYSQL_PASSWORD: npm_password
      DB_MYSQL_NAME: npm
    ports:
      - '81:81'
    volumes:
      - /DATA/AppData/nginxproxymanager/data:/data
      - /DATA/AppData/nginxproxymanager/etc/letsencrypt:/etc/letsencrypt
    networks:
      - webnet
    restart: unless-stopped

  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    command: tunnel run 5024b7e0-f8af-49a5-93c9-6cd3cf7642c6
    volumes:
      - /DATA/AppData/cloudflared:/home/nonroot/.cloudflared
    networks:
      - webnet
    restart: unless-stopped

networks:
  webnet:
    driver: bridge

Bring containers up:

cd /path/to/docker-compose
sudo docker compose up -d

Note:Using a single Docker network webnet allows you to refer to services by container name (e.g. nginx-proxy-manager:81). Make sure the config.yml ingress service: entries use the correct service hostnames when referencing containers (e.g. http://nginx-proxy-manager:81).

6) Verification and tests (end-to-end) 

  1. Confirm cloudflared is running and registered:
    sudo docker logs -f cloudflared

    look for Registered tunnel connection lines

  2. Confirm DNS: nslookup yourdomain.com should show CNAME pointing to .cfargotunnel.com.
  3. In NPM UI, verify Proxy Hosts have yourdomain.com configured and SSL status says certificate issued.
  4. Visit https://yourdomain.com in browser — should serve your sites over HTTPS.

7) Troubleshooting & tips (expanded) 

Symptom: Error 1033 or HTTP 530 (Cloudflare Tunnel error) • Cause: DNS CNAME points to an old or incorrect tunnel UUID. • Fix: Update Cloudflare DNS CNAME to point to .cfargotunnel.com, ensure proxy status is ON (orange cloud). Restart cloudflared. Symptom: cloudflared cannot write cert.pem • Cause: Mounted wrong path inside container. New images run as nonroot and expect /home/nonroot/.cloudflared. • Fix: Mount host folder to /home/nonroot/.cloudflared. Symptom: NPM fails to issue Let’s Encrypt certificate using DNS challenge • Cause A: Cloudflare token missing permissions (requires DNS edit on zone). • Cause B: You didn’t provide the token to NPM (UI or env) or token is incorrect. • Fix: Create API token with Zone.Zone (read), Zone.DNS (edit) for the relevant zones. In NPM UI when selecting DNS challenge provider Cloudflare, paste the API token. Then request certificate again. Symptom: NPM requests HTTP validation instead of DNS validation • Cause: You selected HTTP challenge or the domain isn’t proxied correctly. • Fix: Choose DNS Challenge in the SSL tab and ensure token is present. Symptom: Cloudflared logs show failed to sufficiently increase receive buffer size • Optional fix (increase UDP receive buffer):

sudo sysctl -w net.core.rmem_max=2500000
sudo sysctl -w net.core.rmem_default=2500000

Symptom: Curl to hostname still returns default Nginx page • Cause: Nginx server block not enabled or server_name doesn’t match header. • Fix: Ensure server block exists with correct server_name and reload nginx. Test with curl -H "Host: yourdomain.com" http://192.168.0.1.

Extra tip: Use container names in cloudflared config when services are on same Docker network If you run cloudflared, nginx-proxy-manager and nginx on the same Docker network, you can use service: http://nginx-proxy-manager:81 instead of http://192.168.0.1:81 in /DATA/AppData/cloudflared/config.yml.

8) Quick commands cheat sheet 

  1. Login and save cert.pem (interactive):
    sudo docker run -it --rm -v /DATA/AppData/cloudflared:/home/nonroot/.cloudflared cloudflare/cloudflared:latest tunnel login
  2. Create tunnel:
    sudo docker run -it --rm -v /DATA/AppData/cloudflared:/home/nonroot/.cloudflared cloudflare/cloudflared:latest tunnel create zimaos-tunnel
  3. Start cloudflared persistently:
    sudo docker rm -f cloudflared || true
    sudo docker run -d --name cloudflared --restart unless-stopped -v /DATA/AppData/cloudflared:/home/nonroot/.cloudflared cloudflare/cloudflared:latest tunnel run <tunnel-uuid>
  4. Compose up NPM and nginx:
    sudo docker compose up -d
  5. Test host header:
    curl -v http://192.168.0.1 -H "Host: yourdomain.com"
  6. View cloudflared logs:
    sudo docker logs cloudflared --tail 200

    Final notes 

    • This playbook intentionally maps Cloudflare Tunnel to Nginx Proxy Manager (so NPM can provide per-host proxying and certificate management via DNS challenge). If you prefer to have the tunnel forward to nginx directly (bypassing NPM), you can change the service: entries in config.yml to http://192.168.0.1:80 and manage certificates differently. • Keep your /DATA/AppData/cloudflared directory backed up (the .json file is a credential for that tunnel).

Edit Page On Grav

Previous Post