Convert Nginx to Caddy (Zoneminder)

1. The problem I’m having:

I am trying to configure caddy for use with Zoneminder. The problem is that there are a number of location and alias blocks that I am having a hard time getting to work.
The main webpage seems to work, but the api has some issues.

2. Error messages and/or full log output:

No error logs yet. Just working on the Caddyfile for now. If needed I can provide.

3. Caddy version:

2.6.4

4. How I installed and ran Caddy:

‘pkg install caddy’
‘service caddy start’

a. System environment:

FreeBSD 13.2 Jail

b. Command:

See above.

service caddy start

d. My complete Caddy config:


192.168.1.156:80 {
	
	root * /usr/local/www/zoneminder
	try_files {path} {path}/index.php

	php_fastcgi 127.0.0.1:9000

	handle /zm/cache* {
		root * /var/cache/zoneminder
	}

	handle /zm/api/* {
		root * /usr/local/www/zoneminder
		rewrite /zm/api/* /usr/local/www/zoneminder/api/app/webroot
		php_fastcgi 127.0.0.1:9000
	}

	handle /zm* {
		root * /usr/local/www/zoneminder
		php_fastcgi 127.0.0.1:9000
	}

}

Here is the nginx config file.

server {
		listen 80;

		root /usr/local/www/zoneminder;
		index index.php
		gzip off;

		location /cgi-bin/nph-zms {

			include fastcgi_params;
			fastcgi_param SCRIPT_FILENAME $request_filename;
			fastcgi_pass  unix:/var/run/fcgiwrap/fcgiwrap.sock;
		}

		location /zm/cache {

			alias /var/cache/zoneminder;
		}

		location /zm {

			alias	/usr/local/www/zoneminder;

			location ~ \.php$ {

				if (!-f $request_filename) { return 404; }
				include fastcgi_params;
				fastcgi_param SCRIPT_FILENAME $request_filename;
				fastcgi_index index.php;
				fastcgi_pass unix:/var/run/php-fpm.sock;
			}

			location ~ \.(jpg|jpeg|gif|png|ico)$ {
				access_log	off;
				expires	33d;
			}

			location /zm/api/ {
				alias	/usr/local/www/zoneminder;
				rewrite ^/zm/api(.+)$ /zm/api/app/webroot/index.php?p=$1 last;
			}
		}
	}

I have also tried without the rewrite line under the handle /zm/api/ block with no change.

5. Links to relevant resources:

I am trying to follow the Nginx file linked to here, but can’t seem to get it quite right.

This should probably do:

:80 {	
	root * /usr/local/www/zoneminder

	handle_path /zm/cache* {
		root * /var/cache/zoneminder
	}

	handle_path /zm/api/* {
		rewrite * /zm/api/app/webroot/index.php?p={path}&{query}
	}

	php_fastcgi unix//var/run/php-fpm.sock
	file_server
}
:80 {
        root * /usr/local/www/zoneminder

#       handle /cgi-bin/nph-zms* {
#               php_fastcgi unix//var/run/fcgiwrap/fcgiwrap.sock
#       }

        handle_path /zm/cache* {
                root * /var/cache/zoneminder
        }

        handle_path /zm/api/* {
                root * /usr/local/www/zoneminder
                rewrite * /zm/api/app/webroot/index.php?p={path}&{query}
        }

        handle_path /zm* {
                root * /usr/local/www/zoneminder
        }
        php_fastcgi unix//var/run/php-fpm.sock
        file_server
}

This worked for the most part. I had to add the block to handle_path /zm* and also the /api root needed to be set.
The issue i have now is the live streams. They don’t work. The /cgi-bin path actually is using a different php_fastcgi socket. I commented out the block about the /cgi-bin but either way the live stream doesn’t work as of yet.
Not sure how to handle that.

Makes sense :+1: but you probably don’t need root in there, it’s redundant because root is already set outside at the top. You’re only stripping the path prefix in this case.

You might need handle_path for that as well to strip the path prefix.

Ok. I’ve tried with route, handle, handle_path and just about every other config including rewrites and redirects without success.

Running the Nginx block works as expected, but for some reason I can’t get the stream to show with caddy.

Is the location block the same as handle?

No, location is closer to handle_path because location does path stripping.

Turn on the debug global option, show your Caddy logs. I can’t do much to help without seeing what exactly isn’t working.

{"level":"debug","ts":1691018434.9034503,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"unix//var/run/fcgiwrap/fcgiwrap.sock","duration":0.00199458,"request":{"remote_ip":"10.53.0.9","remote_port":"51854","proto":"HTTP/1.1","method":"GET","host":"192.168.1.156","uri":"/cgi-bin/nph-zms?scale=50&mode=jpeg&maxfps=30&monitor=1&rand=1691018433&connkey=642963","headers":{"X-Forwarded-For":["10.53.0.9"],"Accept-Encoding":["gzip, deflate"],"X-Forwarded-Host":["192.168.1.156"],"Accept":["image/webp,image/avif,image/jxl,image/heic,image/heic-sequence,video/*;q=0.8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"],"Cookie":[],"User-Agent":["Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"],"Accept-Language":["en-CA,en-US;q=0.9,en;q=0.8"],"Referer":["http://192.168.1.156/zm/?view=watch&mid=1"],"X-Forwarded-Proto":["http"]}},"headers":{"Status":["403 Forbidden"],"Content-Type":["text/plain"]},"status":403}

This appears to be the only error in the logs. The path seems to be resolving properly. But the 403 error has me confused.

Does your CGI upstream have any logs?

Try enabling capture_stderr to get possible errors from the CGI upstream written to your Caddy logs:

php_fastcgi unix//var/run/fcgiwrap/fcgiwrap.sock {
	capture_stderr
}

No new errors. Even with the stderr
Im using the FreeBSD fcgiwrap pkg. It runs as the www user, as does php-fpm and nginx

Caddy runs as root though. Should that make a difference? If I try to run caddy as www I get a bind error

Also I’m now doing a reverse_proxy to the unix socket with the same result.

fgciwrap does not have any logs that I can find.

{"level":"debug","ts":1691078784.9266353,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"unix//var/run/fcgiwrap/fcgiwrap.sock","duration":0.001545041,"request":{"remote_ip":"10.53.0.9","remote_port":"53524","proto":"HTTP/1.1","method":"GET","host":"192.168.1.156","uri":"/cgi-bin/nph-zms?scale=50&mode=single&maxfps=30&monitor=1&rand=1691078783","headers":{"Referer":["http://192.168.1.156/zm/?view=watch&mid=1"],"Cookie":[],"Content-Type":["text/plain"],"X-Forwarded-Proto":["http"],"X-Forwarded-Host":["192.168.1.156"],"User-Agent":["Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"],"Accept-Language":["en-CA,en-US;q=0.9,en;q=0.8"],"Accept":["image/webp,image/avif,image/jxl,image/heic,image/heic-sequence,video/*;q=0.8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"],"X-Forwarded-For":["10.53.0.9"],"Accept-Encoding":["gzip, deflate"]}},"headers":{"Content-Type":["text/plain"],"Status":["403 Forbidden"]},"status":403}

Same as before but ordered different.
Content type error 403
The file that needs to be executed is nph-zms

Then should /cgi-bin get stripped away? Try handle_path /cgi-bin* maybe.

{"level":"debug","ts":1691090860.42149,"logger":"h
ttp.handlers.reverse_proxy","msg":"upstream roundt
rip","upstream":"unix//var/run/fcgiwrap.sock","dur
ation":0.000175963,"request":{"remote_ip":"192.168
.1.188","remote_port":"53381","proto":"HTTP/1.1","
method":"GET","host":"192.168.1.156:8000","uri":"/
index.php?scale=21&mode=jpeg&maxfps=30&monitor=1&r
and=1691090859&connkey=582147","headers":{"Cookie"
:[],"User-Agent":["Mozilla/5.0 (iPhone; CPU iPhone
 OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTM
L, like Gecko) Version/17.0 Mobile/15E148 Safari/6
04.1"],"Referer":["http://192.168.1.156:8000/zm/?v
iew=montage"],"X-Forwarded-For":["192.168.1.188"],
"X-Forwarded-Proto":["http"],"Accept-Language":["e
n-CA,en-US;q=0.9,en;q=0.8"],"Accept-Encoding":["gz
ip, deflate"],"Accept":["image/webp,image/avif,ima
ge/jxl,image/heic,image/heic-sequence,video/*;q=0.
8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"
],"X-Forwarded-Host":["192.168.1.156:8000"]}},"err
or":"dialing backend: dial unix /var/run/fcgiwrap.
sock: connect: permission denied"}
{"level":"error","ts":1691090860.4215515,"logger":
"http.log.error","msg":"dialing backend: dial unix
 /var/run/fcgiwrap.sock: connect: permission denie
d","request":{"remote_ip":"192.168.1.188","remote_
port":"53381","proto":"HTTP/1.1","method":"GET","h
ost":"192.168.1.156:8000","uri":"/cgi-bin/nph-zms?
scale=21&mode=jpeg&maxfps=30&monitor=1&rand=169109
0859&connkey=582147","headers":{"Cookie":[],"User-Agent":["Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"],"Accept-Language":["en-CA,en-US;q=0.9,en;q=0.8"],"Referer":["http://192.168.1.156:8000/zm/?view=montage"],"Accept-Encoding":["gzip, deflate"],"Accept":["image/webp,image/avif,image/jxl,image/heic,image/heic-sequence,video/*;q=0.8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"],"Connection":["keep-alive"]}},"duration":0.000462393,"status":502,"err_id":"c26138edk","err_trace":"reverseproxy.statusErro
r (reverseproxy.go:1299

Permissions error seems like when accessing the socket.

EDIT: apologize for the format. I posted from mobile device.

I have tried with all the directives.
handle
handle_path
route
And just about every combination possible.

If I can’t get it I’ll just have to use the nginx server for Zoneminder.

Debug logs show that paths are translating properly as well.

Is there perhaps another cgi plug-in that I could use instead of fcgiwrap to test?

I just built with the caddy cgi plug-in for v2, but it comes up with unrecognized directive.

Caddy V2 CGI Plugin

So I got the cgi plug-in working, along with the stream. But it seems like I’m having too many issues with lag and streams stopping and so on.

I’ll probably stick with nginx for this one.

So I finally got everything working, except the API. Streams work, webpage works, but cant get the API to function.

Here is my working Caddyfile for the stream and webpage.
I first built caddy with the cgi plugin.


{      
        order cgi before respond
}

:80 {
        root * /usr/local/www/zoneminder

        redir / /zm/ 308


        handle_path /zm/cache* {
                root * /var/cache/zoneminder
        }
        
        handle_path /zm*

        handle_path /zm/api* {
                root * /usr/local/www/zoneminder/api
                rewrite * /zm/api/app/webroot/index.php?p={path}&{query}
        }

        cgi /cgi-bin/nph-zms /usr/local/www/zoneminder/cgi-bin/nph-zms {
                unbuffered_output
        }
        php_fastcgi unix//var/run/php-fpm.sock
        file_server
}

Streams now work, and the webpage, But not the API.

{"level":"error","ts":1691158022.3993447,"logger":"http.handlers.reverse_proxy","msg":"aborting with incomplete response","upstream":"unix//var/run/php-fpm.sock","duration":0.450596003,"request":{"remote_ip":"192.168.1.121","remote_port":"58406","client_ip":"192.168.1.121","proto":"HTTP/1.1","method":"GET","host":"192.168.1.156","uri":"/index.php","headers":{"Sec-Gpc":["1"],"Accept-Language":["en-US,en;q=0.9"],"Referer":["http://192.168.1.156/zm/api/host/getVersion.json"],"X-Forwarded-For":["192.168.1.121"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"],"Accept":["image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"],"Accept-Encoding":["gzip, deflate"],"X-Forwarded-Proto":["http"],"X-Forwarded-Host":["192.168.1.156"],"Cookie":[]}},"error":"writing: write tcp 192.168.1.156:80->192.168.1.121:58406: write: broken pipe"}

This is the error.

Caddy appends the request path to the configured root. Are you sure /usr/local/www/zoneminder/api/zm/api/app/webroot/index.php is a file that exists?

Please also enable the debug global option for more detailed logs.

{"level":"debug","ts":1691160523.795982,"logger":"http.handlers.reverse_proxy","msg":
"upstream roundtrip","upstream":"unix//var/run/php-fpm.sock","duration":0.007178256,"
request":{"remote_ip":"10.53.0.9","remote_port":"50230","client_ip":"10.53.0.9","prot
o":"HTTP/1.1","method":"GET","host":"192.168.1.156","uri":"/api/app/webroot/index.php
?p=%2Fzm%2Fapi%2Fhost%2FgetVersion.json","headers":{"Upgrade-Insecure-Requests":["1"]
,"User-Agent":["Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/60
5.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"],"Accept-Language
":["en-CA,en-US;q=0.9,en;q=0.8"],"X-Forwarded-For":["10.53.0.9"],"Accept":["text/html
,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Accept-Encoding":["gzip, de
flate"],"X-Forwarded-Proto":["http"],"X-Forwarded-Host":["192.168.1.156"],"Cookie":[]
}},"headers":{"Status":["404 Not Found"],"X-Powered-By":["PHP/8.1.20"],"Content-Lengt
h":["127"],"Content-Type":["application/json; charset=UTF-8"]},"status":404}
handle_path /zm/api* {                                                            
                root * /usr/local/www/zoneminder                                     
                rewrite * /api/app/webroot/index.php?p={path}&{query}                
        }

The file at /usr/local/www/zoneminder/api/app/webroot/index.php does exist.

Alright well the 404 is coming from your app. You’ll need to figure out why it’s doing that.

It’s being sent %2Fzm%2Fapi%2Fhost%2FgetVersion.json as the path in the ?p= query param (i.e. /zm/api/host/getVersion.json). Make sure your app can handle that path.