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
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)
🔑 Key Points
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
Then:
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
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.
Add CNAME Records
For each domain or subdomain that should route through your Cloudflare Tunnel, add entries like the following (replace the example UUID with your actual tunnel UUID):
| Type | Name | Content (Target) | Proxy Status |
|---|---|---|---|
| CNAME | yourdomain.com |
5024b7e0-f8af-49a5-93c9-6cd3cf764333.cfargotunnel.com |
Proxied (🟧 Orange Cloud) |
| CNAME | www |
5024b7e0-f8af-49a5-93c9-6cd3cf764333.cfargotunnel.com |
Proxied |
🧠 Tip:
Changes to DNS may take a few minutes to propagate. You can verify propagation using tools like:
nslookup yourdomain.comOpen your browser and go to:
http://your-npm-url:81
Log in with your Nginx Proxy Manager credentials.
Add a New Proxy Host
yourdomain.com):
yourdomain.com, www.yourdomain.com (optional)http192.168.0.1, or nginx, or host.docker.internal80Configure SSL (DNS Challenge with Cloudflare)
Switch to the SSL tab and set the following:
Save Configuration
Click Save — NPM will automatically request a DNS-validated Let’s Encrypt certificate via Cloudflare API and issue it for your domain.
🧠 Tip:
If certificate issuance fails, double-check:
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
cloudflared Cannot Write cert.pemcloudflared images run as non-root and expect the directory:/home/nonroot/.cloudflared/home/nonroot/.cloudflaredcloudflared Logs Show “Failed to Sufficiently Increase Receive Buffer Size”sudo sysctl -w net.core.rmem_max=2500000
sudo sysctl -w net.core.rmem_default=2500000sudo 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 the Cloudflare Tunnel to Nginx Proxy Manager (NPM).
This allows NPM to:
👉 If you prefer the tunnel to forward directly to Nginx (bypassing NPM):
config.yml file service: entries to:
http://192.168.0.1:80Keep a backup of your Cloudflared configuration directory:
/DATA/AppData/cloudflared
Inside this folder is your tunnel credential file:
This file acts as the authentication key for your tunnel — losing it means you’ll need to recreate and reauthorize the tunnel in Cloudflare.