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!