Skip to main content

Run Docker Registry at Home with HTTPS

· 3 min read
at15
Software Engineer

Run Docker Registry at home with HTTPS using letsencrypt.

Background

Just like everyone (on twitter), I spent a lot of time vibe coding personal tools such as bookmark manager, postgres UI etc. I want to deploy these applications on my home server as containers. I don't want to push these containers to public/private registry because it is faster and cheaper to use a local registry.

You can run docker registry as a docker container easily:

docker run -d --name registry -p 5000:5000 registry:3

However, you can only access it via http unless you give it a cert for https, or configure docker daemon and other places to trust the insecure registry.

One easy way to get a cert is using tools like mkcert to install a local CA and issue cert for e.g. docker.registry.lan. However, this does not work when you run registry on one machine (e.g. ubuntu) and want to access from other machines (e.g. mac). It also does not work when you access from a container unless you install the CA in the container.

ChatGPT suggested using step-ca, after looking at the instructions, I decided to just google homelab https and found a reddit post. I know Let's Encrypt, but I always thought you need to run a live server to pass the verification to get a cert. Turns out you can also update DNS record and get the cert without running a server for the (wildcard) domain. There are tons of tools out there, certbot, acme.sh. I decided to use lego because it is written in Go.

Get cert using lego with domain managed by Cloudflare

First get a Cloudflare token, go to https://dash.cloudflare.com/profile/api-tokens and use the Edit Zone DNS template:

  • Zone, DNS, Edit
  • Include, Specific zone, <your-domain>, e.g. example.com

Install lego:

go install github.com/go-acme/lego/v4/cmd/lego@latest

Set the token via env (doc):

CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \
lego --dns cloudflare -d '*.example.com' -d example.com run

Then you get files in ~/.lego/certificates/<your-domain>.crt|key. You can pass the crt and key file to docker registry container directly:

# https://distribution.github.io/distribution/about/deploying/#get-a-certificate
docker run -d \
--restart=always \
--name registry \
-v "$(pwd)"/certs:/certs \
-e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
-p 443:443 \
registry:3

I have a proxy running on my home server, I just let the proxy run with https and forward requests to registry container running plain http. The cert is for wildcard domain *.example.com so I can route based on host in the proxy, e.g. docker.example.com proxies to localhost:5000. This also allows all my other applications to get https, e.g. pgui.example.com.

Using a proxy server like caddy could simplify the setup (caddy has builtin https issuance and renewal). But I decided to write (vibe) my own proxy server in Go.

Configure router to resolve domain

I got the cert for domain *.example.com. However, if I do not do anything in my router, the DNS resolve will lead me to nothing because I don't even have a DNS record for that domain. The DNS verification method only checks TXT record for domain ownership. My router flint3 comes with OpenWRT and LuCI so I can update DNS to point the domain to local IP:

/example.com/192.168.1.123

Testing

Tag and push an image to the registry:

docker pull alpine:latest
docker tag alpine:latest docker.example.com/alpine:latest
docker push docker.example.com/alpine:latest

Pull the image from another machine on the same network:

docker pull docker.example.com/alpine:latest