A tiny read-only git web frontend — browse bare repos with just PHP and git, no database, no framework.
#!/usr/bin/env bash
# Anonymous, unauthenticated read-only `git clone` over HTTPS -- the same
# pattern GitHub/GitLab/Bitbucket use for public repos: git-http-backend
# behind the web server, no credentials needed for fetch/clone, push stays
# SSH-key-only (git-http-backend disables receive-pack over HTTP unless a
# repo explicitly sets http.receivepack=true, which nothing here does).
#
# Since foxygit already serves every repo's full history/contents to
# anyone who browses git.kristoffersson.info with zero auth, this doesn't
# change what's exposed -- it just adds a second, more useful way (`git
# clone` instead of clicking through a web UI) to get at the same data.
# All repos under /var/git/repos become clonable (GIT_HTTP_EXPORT_ALL=1);
# there's no per-repo opt-in.
#
# What it does:
# 1. apt-get install fcgiwrap
# 2. Installs a dedicated fcgiwrap instance (server/fcgiwrap-git.socket
# + .service) running as the `git` user -- same reasoning as the
# php-fpm pool in ../deploy.sh: /var/git/repos is 0700 git:git.
# 3. Adds a `handle_path /repos/*` route to the git.kristoffersson.info
# Caddy block that proxies to git-http-backend via that socket.
# Backs up the Caddyfile first. Idempotent -- safe to re-run.
#
# Run as root:
# sudo bash /home/mrfox/foxygit/server/setup-anon-clone.sh
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "run as root: sudo bash $0" >&2
exit 1
fi
ROOT="$(cd "$(dirname "$0")" && pwd)"
CADDYFILE="/etc/caddy/Caddyfile"
SOCK="/run/fcgiwrap-git.sock"
echo "==> 1/4 installing fcgiwrap"
if ! command -v fcgiwrap >/dev/null 2>&1; then
apt-get update -qq
apt-get install -y fcgiwrap
else
echo " already installed"
fi
# The Debian package enables its own default fcgiwrap.socket (as www-data,
# can't read /var/git). We don't use it -- disable so it's not just an idle
# unused (but still exposed-to-www-data) FastCGI worker sitting around.
if systemctl is-enabled --quiet fcgiwrap.socket 2>/dev/null; then
systemctl disable --now fcgiwrap.socket >/dev/null 2>&1 || true
echo " disabled the default fcgiwrap.socket (unused, replaced by fcgiwrap-git)"
fi
echo "==> 2/4 installing the fcgiwrap-git service (runs as the git user)"
install -m 0644 "$ROOT/fcgiwrap-git.socket" /etc/systemd/system/fcgiwrap-git.socket
install -m 0644 "$ROOT/fcgiwrap-git.service" /etc/systemd/system/fcgiwrap-git.service
systemctl daemon-reload
systemctl enable --now fcgiwrap-git.socket
if [ ! -S "$SOCK" ]; then
echo "WARNING: $SOCK did not appear, check: systemctl status fcgiwrap-git.socket" >&2
fi
echo "==> 3/4 updating Caddyfile ($CADDYFILE)"
python3 - "$CADDYFILE" <<'PYEOF'
import sys, subprocess, datetime
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as f:
content = f.read()
old_block = '''git.kristoffersson.info {
\troot * /var/www/foxygit/
\tphp_fastcgi unix//run/php/foxygit.sock
\tfile_server
\tencode gzip zstd
\theader {
\t\tStrict-Transport-Security "max-age=31536000; includeSubDomains"
\t\tX-Content-Type-Options "nosniff"
\t\tX-Frame-Options "DENY"
\t\t-Server
\t}
\tlog {
\t\toutput file /var/log/caddy/git.kristoffersson.info.log
\t}
}'''
new_block = '''git.kristoffersson.info {
\troot * /var/www/foxygit/
\thandle_path /repos/* {
\t\treverse_proxy unix//run/fcgiwrap-git.sock {
\t\t\ttransport fastcgi {
\t\t\t\tenv SCRIPT_FILENAME /usr/lib/git-core/git-http-backend
\t\t\t\tenv GIT_PROJECT_ROOT /var/git/repos
\t\t\t\tenv GIT_HTTP_EXPORT_ALL 1
\t\t\t\tenv PATH_INFO {http.request.uri.path}
\t\t\t}
\t\t}
\t}
\tphp_fastcgi unix//run/php/foxygit.sock
\tfile_server
\tencode gzip zstd
\theader {
\t\tStrict-Transport-Security "max-age=31536000; includeSubDomains"
\t\tX-Content-Type-Options "nosniff"
\t\tX-Frame-Options "DENY"
\t\t-Server
\t}
\tlog {
\t\toutput file /var/log/caddy/git.kristoffersson.info.log
\t}
}'''
if new_block in content:
print(" already up to date, nothing to change")
sys.exit(0)
if old_block not in content:
print("ERROR: expected git.kristoffersson.info block not found verbatim in "
+ path + " -- Caddyfile has changed since this script was written, "
+ "edit it by hand instead (add the handle_path block from this "
+ "script's source). No changes made.", file=sys.stderr)
sys.exit(1)
backup = path + ".bak." + datetime.datetime.now().strftime("%Y%m%d%H%M%S")
subprocess.run(["cp", path, backup], check=True)
print(" backup saved to " + backup)
content = content.replace(old_block, new_block, 1)
with open(path, "w", encoding="utf-8") as f:
f.write(content)
print(" block replaced")
PYEOF
echo "==> validating Caddy config"
caddy validate --config "$CADDYFILE"
echo "==> 4/4 reloading caddy"
systemctl reload caddy
cat <<'EOF'
Done. Try it:
git clone https://git.kristoffersson.info/repos/<reponame>.git
No credentials needed. Push still requires an SSH key (unchanged) --
git-http-backend only serves receive-pack (push) over HTTP if a repo's own
config explicitly sets http.receivepack=true, which none of them do.
EOF