Configuring Git over HTTP(S) with Caddy

774 words, estimated reading time: 4 minute(s)
Originally published on May 17, 2025
Last modified on May 17, 2025

Since I switched to Caddy from Nginx, the only thing that worked on Nginx but I just couldn’t get working on Caddy was cloning Git repositories over HTTP.

I had initially tried sort of backporting the Nginx configuration, which didn’t work. After some time I ran into James Atkins' post in which he detailed how he tackled this issue. Again, this didn’t work for me (possibly because I use GitWeb rather than cgit), so I ended up just configuring a Git daemon as a workaround until I eventually figure it out.

That has finally happened, so here I’ll share my working configuration and try to explain it.

The configuration

First, let’s see the configuration itself:

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

root / /usr/share/gitweb
try_files {uri} gitweb.cgi

handle @git_cgi {
        reverse_proxy unix//run/fcgiwrap.socket {
                transport fastcgi {
                        env SCRIPT_FILENAME /usr/lib/git-core/git-http-backend
                        env GIT_PROJECT_ROOT /var/lib/git
                        env REQUEST_METHOD {method}
                        env QUERY_STRING {query}
                        env PATH_INFO {path}
                }
        }
}

handle @git_static {
        file_server {
                root /var/lib/git
        }
}

basicauth @push bcrypt restricted {
}

basicauth @push-info bcrypt restricted {
}

reverse_proxy unix//run/fcgiwrap.socket {
        transport fastcgi {
                env GITWEB_CONFIG /etc/gitweb.conf
                split .cgi
        }
}

handle /static/* {
        file_server {
                root /usr/share/gitweb
        }
}

Explanation

Matchers

The first two matchers (@git_{cgi,static}) are for repository data, of which the first matcher is handled by git-http-backend while the other is just plain old files which are served by Caddy for performance reasons.

The other two are for pushing over HTTP, which will be covered later.

root and try_files

My root directive may seem unusual at first since it matches / instead of *, however this was necessary because otherwise all requests, including the ones covered by the @git_* matchers ended up being handled by GitWeb with predictable results.

The try_files directive simply uses the GitWeb CGI script as the index document.

reverse_proxy

The “root” reverse_proxy is almost not worth explaining; it simply makes Caddy run GitWeb through FastCGI.

The various handles

This is the interesting part.

The /static/* handle takes care of GitWeb’s static files, which include a JavaScript file, a stylesheet and some (fav)icons.

The @git_cgi handle is probably the most important one and the next troublemaker after root: after resolving root any git clone would result in a HTTP 500 (actually, not cloning, but curling <repo>/info/refs). The gist of it is that git-http-backend relies on a number of environment values which do not seem to be filled in by Caddy, so it had to be done manually (for some reason, apparently only I seem to have to do this).

Figuring out what had to be added was puzzling, as FastCGI did not seem to have been logging in either the journal or somewhere in /var/log, so I ended up manually running git-http-backend with the aforementioned environment variables and that way I could know why the backend was failing. From there it was just a matter of adding the required environment variables to make the backend happy.

Particularly worth elaborating are {path} and {query}. git clone will issue a GET request on /<repo>.git/info/refs?service=git-upload-pack, and the backend does not like having the service query in its PATH_INFO, which would happen if {uri} was used instead. The query is still needed for detecting v2 protocol support, so it is passed to the backend separately in QUERY_STRING. git-http-backend(1) has more information on all of this.

Besides that, I also had to juggle around with permissions on the Git root, first to make the backend work at all and then to make pushing work; I eventually settled on git:www-data, 775 (on directories) and sudo -u www-data git config --global safe.directory '*' (user names might differ between distros, mine is Debian 12).

basicauth

They add a layer of authentication on authenticated pushes. As git-http-backend(1) suggests, I have added authentication to both ref advertisement and the call to git-receive-pack.

If you don’t need/want/whatever HTTP pushing, you could drop these directives and the two push matchers. I’d also recommend dropping |git-receive-pack from the @git_cgi regexp to make sure it may not be called.

Note: the directive is basic_auth on newer Caddy versions (2.6.2 is currently packaged in Debian 12). Keep this in mind if Caddy errors out here for you.

Final words

While this configuration works fine, I suspect it may be optimized (read: shortened) further. In particular, I suspect the push matchers and their respective basicauth directives could be squashed together, but I can’t really think of a way to do that (or any other potential optimization).

If you have any ideas on this or questions in general, feel free to use the reply button below or contact me via another medium.

Reply via email