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:
/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).
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.
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).
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
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.
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://
Configure DNS API token for Cloudflare (required for DNS challenge) To request certificates using DNS challenge automatically from NPM you must provide Cloudflare credentials.
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.
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/
# Put your real tunnel UUID here <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.
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.
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).
sudo docker logs -f cloudflared
look for Registered tunnel connection lines
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
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.
sudo docker run -it --rm -v /DATA/AppData/cloudflared:/home/nonroot/.cloudflared cloudflare/cloudflared:latest tunnel loginsudo docker run -it --rm -v /DATA/AppData/cloudflared:/home/nonroot/.cloudflared cloudflare/cloudflared:latest tunnel create zimaos-tunnelsudo 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>sudo docker compose up -dcurl -v http://192.168.0.1 -H "Host: yourdomain.com"sudo docker logs cloudflared --tail 200
• 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