custard
A self-hostable, public-facing web code forge for your own git server. custard reads bare git repositories directly off disk (via go-git) and renders its own pages — repo browser, syntax-highlighted files, commit log + per-file diffs, branches/tags, and a GitHub-style issues view backed by in-repo Backlog.md tasks. It is read-only: it never writes git, so pushing and admin stay in your git server.
The look is the Bubble Tea / Charm terminal aesthetic on a CSS-token theme system — 7 themes (Flexoki / Uchu / Humdrum, each light + dark, plus e-ink).
It pairs naturally with Soft Serve (it can read
Soft Serve's database to serve only public repos), but it works with any directory of bare
*.git repos.
What you need
| Required? | For | |
|---|---|---|
| Go 1.26+ | to build | compiling the single static binary |
A dir of bare *.git repos |
yes | the content custard serves |
| Soft Serve | optional | private/hidden gating + HTTPS clone proxy |
| Backlog.md tasks in a repo | optional | the per-repo Issues tab |
| A server + domain + Caddy | to go public | TLS + reverse proxy |
Quick start (local)
go install github.com/a-h/templ/cmd/templ@latest # one-time: the template generator
templ generate # after editing any *.templ
go run ./cmd/custard --repos /path/to/bare/repos --addr :8080
# open http://localhost:8080
--repos is any directory containing name.git bare repositories. With no other flags,
custard lists every repo it finds (fine for a fully public/local setup).
Fonts
The Charm look uses three commercial faces that are not bundled (licensing). Without them
custard falls back to your system mono/sans — fully functional, just plainer. To get the
intended type, drop your own .woff2 files in web/static/fonts/ matching the @font-face
names in web/static/tokens.css (Awke, Untitled Sans, Name Mono). They're embedded into
the binary at build time.
Configuration
All via flags or env vars (flag wins):
| Flag | Env | Default | Purpose |
|---|---|---|---|
--repos |
REPOS_PATH |
./repos |
directory of bare *.git repos |
--addr |
LISTEN_ADDR |
:8080 |
listen address |
--base-url |
BASE_URL |
http://localhost:8080 |
public base URL |
--soft-serve-http |
SOFT_SERVE_HTTP |
http://localhost:23232 |
clone URL base shown in the footer |
--soft-serve-db |
SOFT_SERVE_DB |
(empty) | path to soft-serve.db; when set, only public (non-private, non-hidden) repos are served |
Private repos: if you point custard at a Soft Serve repos dir, set
--soft-serve-dbso private/hidden repos are hidden from the list and 404 on direct access. Without it, every repo on disk is public — only do that if they're all meant to be public.
Deploy (self-host, public)
One script provisions Caddy (auto-TLS) + a systemd service, generating all config from your settings — nothing to hand-edit on the server.
# 1. DNS: point your domain's A record at the server.
# 2. Configure:
cp deploy/deploy.env.example deploy/deploy.env
$EDITOR deploy/deploy.env # REMOTE, DOMAIN, RUN_USER, REPOS_PATH, ...
# 3. Deploy (re-run anytime to update):
deploy/deploy.sh
deploy.sh builds a static linux/amd64 binary, ships it to REMOTE, writes
/etc/systemd/system/custard.service (custard runs as RUN_USER, bound to 127.0.0.1:8080,
repos mounted read-only) and /etc/caddy/Caddyfile (TLS for DOMAIN; /git/* → your Soft
Serve clone backend; /dl/* for release tarballs), then starts both. See deploy/deploy.env.example
for every setting.
Not using Soft Serve? Leave SOFT_SERVE_DB empty (serves all repos) and SOFT_SERVE_BACKEND
empty (drops the /git clone proxy). custard still serves any bare repos in REPOS_PATH.
How it fits together
HTTPS read-only, on disk
browser ──▶ Caddy ──▶ custard ──▶ /path/to/repos/*.git (go-git)
│ └─▶ soft-serve.db (visibility, optional)
└─/git/*─▶ Soft Serve HTTP (clone, optional)
Releasing CLIs via a self-hosted Homebrew tap
The same server can host a Homebrew tap + release tarballs, so your
command-line repos install with no GitHub. custard serves the tarballs (/dl) and the tap
repo (/git); scripts/brew-release.sh does the rest.
# one-time: a `homebrew-tap` repo exists on the server (public)
# release a tagged repo (archives the tag from the server, publishes the tarball,
# writes a source-build formula, pushes the tap):
scripts/brew-release.sh <repo> <version> [go-package]
# e.g. scripts/brew-release.sh sportsball v0.1.0
# scripts/brew-release.sh sportsball v0.1.0 ./cmd/sportsball # if main isn't at root
Users then:
brew tap you/tap https://your-host/git/homebrew-tap.git
brew trust you/tap # recent Homebrew gates third-party taps
brew install <repo>
Formulas build from source (depends_on "go") — no per-arch bottles, no GitHub.
See PLAN.md for the phased build plan.