Pages

2025-12-10

Migrating From Apache2 To Caddy

 I have wanted to investigate the feasibility of moving to a more modern webserver setup on my home server, specifically Caddy or Nginx as they both sounded like they are much easier to set up and more streamlined than apache; but never seemed to get around to it. Well, finally I manged to over this past weekend.


The main case, serving out static content from one or more domains with HTTPS cert setup, was surprisingly easy and I was impressed by its ease of use. Caddy has a nice config syntax which is easy to read, reasonably intuitive and supports single- or multi-file configurations, with a fragment syntax and include mechanism to keep large, complex configs more manageable.


In order to fully replace Apache on my home server, I needed to ensure any replacement would support a few things over and above serving just static content from a single domain:

  • Multiple domain hosting (virtual hosts)
  • UNIX ~username webspaces
  • Use traditional syslog format instead of JSON logs
  • PHP (for Nextcloud)
  • CGI (Common Gateway Interface)
  • Golang vanity URLs to serve out go modules

Hosting Multiple Domains (virtual hosts) with Caddy

Caddy's docs are pretty good (especially the Common Patterns section) but it's a bit tricky sometimes to know just which directive to use, where.
  • Setting up multiple domain serving was very straightforward -- basic Caddy syntax and documentation spells it out clearly, refer to the Common Patterns section of the Caddy docs.

UNIX ~username webspaces with Caddy

  • Hosting UNIX ~username style webspaces was pretty simple, just a basic use of the handle_path and root directives:

  • handle_path /~joebloggs* {
        root * /home/joebloggs/public_html
    }

Use Traditional syslog Format with Caddy instead of JSON Logs

  • This one was simple, just add the following entry in the log {..} section of Caddyfile config:
format transform "{common_log}"


Hosting PHP and Nextcloud from Caddy
  • Hosting a PHP app behind Caddy was trickier: I couldn't find examples that spelled out precisely the proper setup. I had to first discover what the fpm-php helper util/pkg was, and how to set it up, and that it has to run with the same user:group as Caddy, and the PHP files and data that are being served (by default fpm-php runs as nobody:nogroup); simply /etc/php/fpm-php[x.y]/fpm.d/www.conf to set user=caddy and group=caddy.
  • Then, finally, the Caddyfile entry for one's domain must include the proper root and php_fastcgi proxy config:
blitter.com {
        # dir as root of the website
        root * /var/www/www.blitter.com/htdocs

        # enable serving static files from root dir
        encode
        file_server
        php_fastcgi localhost:9000


Hosting CGI from ~user web spaces with Caddy

  • CGI was not too hard though it isn't spelled out in the Caddy Common Patterns documentation:

handle_path /~russtopia* {
    root * /home/russtopia/public_html

handle_path /cgi-bin/* {
    reverse_proxy localhost:10000 {
        transport fastcgi {
            env SCRIPT_FILENAME /home/russtopia/public_html/cgi-bin/{path}
        }
    }
}

An entry such as the above must be created for each unique user or app's local cgi-bin/ area. If CGI files must write to the user/app's backend filesystem, the intended output folder should have proper permissions to allow Caddy, if not running as root (and you don't need to run Caddy as root, so avoid it!) to access them. I created a new distinct 'cgi' group and made sure my user and caddy itself were members, and set user/group read/write perms on the cgi script's output folder.

Serving Golang Vanity URLs with Caddy

  • Golang 'vanity URLs' are a way to self-host one's own Go source package modules, or redirect domain assignments such that they are referred to just as modules on the big centralized source hosts like github.com, gitlab.io etc. For example, I have authored some Go programs and modules which I make available via the root name blitter.com/go/<module>. I want the Go tooling to be able to use go get, go install, and so on without issue.

    I also have a rather unique git setup: I run git-daemon with packages visible from the server's /var/git/ directory, of which many are symlinked to the repo's actual location within a Gogs installation which lives and runs under the git:git user/group. In this way I can do raw git checkouts via the git:// or ssh:// schemes, as well as via https:// to my Gogs instance.

    This setup in Caddy was not fully covered by any other online tutorial, at least not in my server's particular setup -- I use a /go/ URL endpoint within the overall blitter.com domain to host modules, rather than dedicating a go.blitter.com domain just for Go modules, so other tutorials didn't quite fit. So here's the entire setup that integrates the git-backend as well as golang vanity URL setup:
# Caddy tutorial on serving vanity go module URLs:
# https://abhijithota.com/posts/golang-vanity-urls-using-caddy/
####
(gomodhandler) {
        handle /go/{args[0]} {
                @from_go query go-get=1

                handle @from_go {
                        header Content-Type text/html
                        respond <<HTML
                        <!DOCTYPE html>
                        <html>
                        <head>
                        <meta name="go-import" content="blitter.com/go/{args[0]} git https://blitter.com/git/{args[0]}">
                        <!-- <meta http-equiv="refresh" content="0; url=https://blitter.com/git/{args[0]}" /> -->
                        </head>
                        </html>
                        HTML 200
                }
        }
}
####

www.blitter.com {
        redir https://blitter.com{uri}
}

blitter.com {
        ####> git-daemon ####
        # Caddy tutorial on git over HTTP(s) proxying git-daemon:
        # https://www.jamesatkins.com/posts/git-over-http-with-caddy/

        handle_path /git* {
                root * /var/git
        }

        handle_path /var/git* {
                root * /git
        }

        @git_cgi path_regexp "^.*/(HEAD|info/refs|objects/info/[^/]+|git-upload-pack)$"
        @git_static path_regexp "^.*/objects/([0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$"

        handle @git_cgi {
                reverse_proxy unix//run/git-cgi.socket {
                        transport fastcgi {
                                #env SCRIPT_FILENAME ${pkgs.git}/libexec/git-core/git-http-backend
                                env SCRIPT_FILENAME /usr/libexec/git-core/git-http-backend
                                env GIT_HTTP_EXPORT_ALL 1
                                env GIT_PROJECT_ROOT /var/git
                        }
                }
        }

        handle @git_static {
                file_server {
                        root /var/git
                }
        }
        ####< git-daemon ####

        import gomodhandler bacillus
        import gomodhandler brevity
        import gomodhandler chacha20
        import gomodhandler cryptmt
        import gomodhandler go-frodokem
        import gomodhandler goutmp
        import gomodhandler groestl
        import gomodhandler herradurakex
        import gomodhandler hkexsh
        import gomodhandler hopscotch
        import gomodhandler kyber
        import gomodhandler lpasswd
        import gomodhandler moonphase
        import gomodhandler mtwist
        import gomodhandler newhope
        import gomodhandler xs
        import gomodhandler xsd
}

Footnote: If switching on-the-fly to Caddy from Apache and vice-versa while testing the overall setup while an active Nextcloud instance is running involves also changing ownership of: nextcloud install and config dirs in /var/www/...; the nextcloud /data dir; *and* any existing session files, /tmp/sess_*).

Footnote 2: Sometime recently (as of go v1.2x) the go get system seemed to start requiring any go module have at least a latest tag applied otherwise the module will not be found. I had a few modules that lacked tags, so that was a real head-scratcher. Also do a go clean -modcache from time to time to ensure go is really fetching things as opposed to using cached copies, so if dependencies have broken it will be revealed when attempting go mod init && go mod tidy.