Cannot properly convert Apache2 to Caddy

1. The problem I’m having:

I have a Laravel application. Static files for the admin panel are located in the /public folder. All static files are accessible via /, but I need them to be accessible via /admin and to add the /admin prefix to all admin panel pages, while requests still go to /. I’ve written a Caddyfile, which works in principle, but I don’t like its appearance because it uses an additional port, and the configuration file itself is very large. Also, there is an issue with the /exchangers path because there is a similar path on Nuxt. If I reload the page on the /exchangers page from the Nuxt side, it redirects to /admin/exchangers, which is incorrect behavior. /admin/exchangers and just /exchangers should be isolated from each other. How can I improve it and make more clear?

2. Caddy version: v2.7.6

3. How I installed and ran Caddy:

a. System environment: MacOS 14.4.1, arm64, Docker 24.0.7

b. My complete Caddy config:

:80 {
    encode gzip

    handle /admin/* {
        uri strip_prefix /admin
        reverse_proxy http://caddy:81

    handle /login* {
        reverse_proxy http://caddy:81

    handle /logout* {
        reverse_proxy http://caddy:81

    handle /es/* {
        reverse_proxy /es http://golang:8080

    handle /ws/* {
        reverse_proxy /ws/ http://rust:8081

    handle /phantom/* {
        reverse_proxy http://phantom:8083

    handle /exchangers* {
        reverse_proxy http://node:3000

    handle / {
        reverse_proxy http://node:3000

    handle {
        reverse_proxy http://node:3000

    @except {
        not path /admin*
        not path /sitemap
        not path /login*
        not path /logout*
        not path /contacts*
        not path /icons*
        not path /_*
        not path /imgs*
        not path /fonts*
        not path /favicon*
        not path /content/*
        not path /ws/*
        not path /es/*
        not path /phantom/*
        not path /

    redir /es /es/
    redir /phantom /phantom/
    redir /ws /ws/
    redir /admin /admin/
    redir @except /admin{uri}

:81 {
    root * /var/www/public
    php_fastcgi php-fpm:9000

You can replace handle + uri strip_prefix with simply handle_path which has built-in path stripping logic.

You should remove the matchers on reverse_proxy because you’ve already done matching on the handle. Also, the matchers are incorrect because path matching is exact in Caddy, so given a path /es/foo, it would match the handle but it would not match the proxy since it’s looking for only /es.

Also, you can omit http:// on the proxy upstream addresses, it’s not needed (HTTP is the default anyway).

The first handle here is redundant, the 2nd one will catch any otherwise unmatched request anyway.

You can merge these by using a named matcher instead:

@auth path /login* /logout*
handle @auth {
	reverse_proxy caddy:81

If you need to repeat routes in more than one handle, you can either use Snippets or Named Routes.

The advantage of named routes in this case would be that it’s only a single instance of the handlers in memory, so it’s very slightly more efficient. You’d just add a &(php) named route, then replace your reverse_proxy with an invoke php.

What you should be doing is adjust your routing in your Laravel app to add an /admin prefix to the routes. It shouldn’t be Caddy’s responsibility to redirect to /admin, you should make sure whatever links that are produced already include /admin in whatever HTML is produced.

Alternatively, use subdomains. You can have an which only has this admin site and nothing else.