For a few years now I've been running a variety of services on my humble homelab setup, built on Docker compose. Given my interest in Agentgateway I thought it would be nice to swap out the proxy I was using with Agentgateway.
To meet my needs, however, I want to simply label each docker service with something like gateway.enabled = true and gateway.port = 1234 (if there are multiple ports).
I thought of building out my own controller to manage this, but stumbled upon docker-gen which is a cool project to watch docker containers and run a Go template each time the container state changes, and write it to a file.
Agentgateway can run off of a file configuration, and hot-reload when the file changes, making this a great fit.
Heres the final configuration I ended up with:
services:
agentgateway:
container_name: agentgateway
# For now I am depending on some pre-release changes
image: ghcr.io/agentgateway/agentgateway:0.11.0-alpha.18e45de0f30a108df129aeb08d49453ea0527db3
ports:
- "80:80"
- "443:443"
- "127.0.0.1:15000:15000"
command:
- --file=/cfg/agentgateway.yaml
volumes:
- agw_data:/cfg:ro
- /volumes/letsencrypt:/etc/letsencrypt:ro
restart: unless-stopped
docker-gen:
image: nginxproxy/docker-gen:0.16.0
container_name: docker-gen
volumes:
- /var/run/docker.sock:/tmp/docker.sock:rw
- ./agentgateway-config.yaml:/cfg/agentgateway-config.yaml:ro
- agw_data:/out
command: -watch -only-exposed /cfg/agentgateway-config.yaml /out/agentgateway.yaml
restart: unless-stopped
Below has my configuration template.
Compared to the typical setup where we are matching address like *.example.com, I am doing a prefix match like homeassistant.*.
The reason for this is I have different suffixes for things like going through my VPN or going directly, etc.
{{define "containerAddress" -}}
{{ $addrLen := len $.Addresses }}
{{ $network := index $.Networks 0 }}
{{ with index $.Labels "gateway.port" }}
{{ $network.IP }}:{{.}}
{{ else }}
{{ $network.IP }}:{{ (index $.Addresses 0).Port }} # {{ $.Labels }}
{{ end }}
{{ end }}
{{define "routes"}}
{{ range $index, $value := $ }}
{{ if $value.State.Health.Status }}
{{ if ne $value.State.Health.Status "healthy" }}
# $value.Name is unhealthy, skipping
{{ continue }}
{{ end }}
{{ end }}
{{ if not (eq (index $value.Labels "gateway.enabled") "true") }}
{{ continue }}
{{ end }}
- name: {{$value.Name}}
matches:
-
headers:
- name: :authority
value:
regex: '{{ index $value.Labels "com.docker.compose.service" | default $value.Name}}([\.:].*)?'
path:
pathPrefix: /
backends:
- host: {{ eval "containerAddress" $value | trim }}
{{ end }}
{{ end }}
config:
adminAddr: 0.0.0.0:15000
logging:
level: info
binds:
- port: 80
listeners:
- protocol: HTTP
routes: {{ eval "routes" $ | nindent 4 }}
- port: 443
listeners:
- protocol: HTTPS
tls:
cert: /etc/letsencrypt/live/howardjohn.info/fullchain.pem
key: /etc/letsencrypt/live/howardjohn.info/privkey.pem
routes: {{ eval "routes" $ | nindent 4 }}
So basically we do some logic to select the desired IP:Port for each container. Then we define some routes for each container routing to this address. Finally, we expose these routes on port 80 (HTTP) and port 443 (HTTPS).
Overall things have been going smoothly!