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 handle
s
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 curl
ing <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