Posted on 3/21/2025, 6:30 PM
I've always been a proponent of self-hosting. Recently I've been doing more work with Go and wanted to host my own modules. I'm not super fond of hosting my code on GitHub because I feel like I don't totally own anything that I don't have root on. So I decided to look into how Go fetches modules and what it takes to host my own.
Why Self-Host?
There are a few reasons why I like to self-host:
- Ownership: Like I mentioned, if I don't have root on the box that is hosting my code then I don't feel in control.
- Learning: Touching every part of the stack gives you insights into how everything fits together.
- Fun: I really enjoy building software, so learning new things is always interesting to me.
How Go Fetches Modules
While I've used Go, I haven't had to think too much about how it actually works under-the-hood. Luckily, Go's documentation is very clear about how it fetches modules, if a little dense. Here's the gist of it:
- Module Paths: The
module
path ingo.mod
can provide a VCS specifier (to point to a repo), or no specifier (to use HTTP). - No VCS Specifiers: A
<meta>
tag pulled from the module path (taken as an HTTP URL) points to a VCS ormod
can be used to point to a URL that implements the GOPROXY protocol for the module. - Version Tags: If using a VCS, then it is dependent on the VCS. Using
mod
and the GOPROXY protocol only requires a few static files to specify versions and module bundles.
Once I realized that all Go needs is a directory structure containing list
, info
, mod
, and zip
files to fetch modules, I realized it was easily doable. This is when the idea of CONR came to be.
Writing CONR
CONR (Code Only, No Repo) is a tool I wrote originally[1] in TypeScript/zx and translated into Go with ChatGPT. The goal: point CONR at your existing Git repositories, and it automatically does the heavy lifting to package them for self-hosting. Here's a quick rundown:
- Prepare Repos: Make sure your Go modules have valid semantic version tags (e.g.
v1.0.0
). - Install CONR:
go install code.nicktrevino.com/conr/cmd/conr@latest
- Run CONR:
conr --mod /path/to/myrepo --outDir ./dist --showSkips
--mod
multiple times if you have multiple modules you'd like to package up.When done, the dist
folder will contain a structure that Go's module downloader loves:
dist/<module/path>/@v/list
dist/<module/path>/@v/v1.0.0.mod
dist/<module/path>/@v/v1.0.0.zip
dist/<module/path>/@v/v1.0.0.info
dist/<module/path>/@latest
These files map directly to the requests Go makes when you go get
a module. From here all that is left is to throw them on the internet at <module/path>
with the proper <meta>
tag. For example, I host my modules at /go.mod/
on this subdomain and have my <meta name="go-import" content="code.nicktrevino.com mod https://code.nicktrevino.com/go.mod">
tag on both CONR's home page, and the root of the subdomain. I placed it on both pages because Go will follow and double-check the meta tags.
Hosting the Generated Files
So on to hosting the files. I've mentioned Caddy before, but it has to be one of my favorite pieces of software recently. Caddy makes static file hosting a breeze with its file_server
directive. While I use Cloudflare Tunnel to expose my services, Caddy makes a lot of sense for self-hosting due to automatic TLS handling (if configured). Here's an example Caddyfile
that will serve everything from /www
which you can dump the output into:
{
admin off
debug on
}
:443 {
log
handle /go.mod/* {
header Content-Type text/plain
}
file_server browse {
root /www
}
}
browse
parameter to file_server
, but you can certainly leave it off if it makes you more comfortable.Once Caddy was running, I tested with fetching another module and go get
fetched from my domain successfully!
Final Thoughts
Finishing this project felt liberating. I discovered that Go modules are fundamentally a simple set of files served at predictable URLs, and was able to easily follow the Go documentation to build CONR. In the end, combining that with a minimal Caddy setup gave me everything I needed for a robust, self-hosted module repository that I trust.
Hopefully I've inspired others to take the self-hosting journey for their own Go modules!