Discover Docker containers¶
Turn labelled Docker containers into managed proxy hosts, over a read-only Engine API connection. Configure it under Integrations -> Docker discovery; the full field and label reference is Settings: Docker container discovery and the rationale is design/docker-discovery.md.
Prerequisites¶
- gpm able to reach the Docker Engine API read-only. A socket proxy is the recommended shape: see Engine access below.
- A certificate that already covers the hostnames the labels will publish; discovery never issues one.
- Every middleware, access list and upstream group the template or a profile names must already exist, or the first reconcile fails referential integrity and writes nothing.
- The domain suffixes those hostnames must fall inside
(
allowedDomainSuffixes).
Engine access¶
gpm issues exactly three GETs and has no code path that writes to the Engine:
| Request | Used for |
|---|---|
GET /version |
API version negotiation only. |
GET /containers/json?filters={"label":["gpm.rake.pro/enabled=true"]} |
The full-state list every reconcile is computed from. |
GET /events?filters={"type":["container"],...} |
A lifecycle stream that only says "reconcile sooner". Optional: without it the poll interval still keeps the config correct. |
The Docker socket is a root-equivalent credential. Anything that can write to it can start a privileged container and own the host. Pick one of:
| Option | Shape | Trade-off |
|---|---|---|
| Read-only socket proxy (recommended) | host: tcp://socket-proxy:2375, proxy allows CONTAINERS and EVENTS only |
gpm never sees a writable socket |
| Direct socket mount | -v /var/run/docker.sock:/var/run/docker.sock:ro |
The read-only flag bounds the file, not the API |
| Remote Engine over TLS | host: https://docker.example.com:2376 with tlsCA (plus tlsCert/tlsKey for mutual TLS) |
No skip-verify option; link-local destinations are refused at connect time |
Never publish the raw socket over plain tcp://: that hands the credential
to anyone who can reach the port.
services:
socket-proxy:
image: ghcr.io/example/docker-socket-proxy:1.0.0 # pin a digest or semver
environment:
CONTAINERS: "1" # GET /containers/json
EVENTS: "1" # GET /events
POST: "0" # refuse every write
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks: [edge]
gpm:
image: ghcr.io/rake-pro/go-proxy-manager:1.0.0 # pin a digest or semver
networks: [edge]
Steps¶
- Give gpm read-only Engine access using one of the options above.
- Add the settings block:
yaml
dockerDiscovery:
enabled: true
host: tcp://socket-proxy:2375
network: edge
pollInterval: 60s
allowedDomainSuffixes: [example.com]
template:
tls: { certificateRef: wildcard, forceSSL: true }
accessLists: [home-vpn]
robotsNoIndex: true
tags: [docker]
defaultDNS: { lanDirect: true }
profiles:
public-ratelimited:
tls: { certificateRef: wildcard, forceSSL: true }
middlewares: [rate-limit]
tags: [docker, public]
defaultDNS: { lanDirect: true, publicCname: true }
- Label the containers. A label can only ever name a profile the operator wrote, and an address inside the container's own network:
yaml
services:
grafana:
image: grafana/grafana:11.6.0
networks: [edge]
labels:
gpm.rake.pro/enabled: "true"
gpm.rake.pro/domains: "grafana.example.com"
gpm.rake.pro/port: "3000"
paste:
image: ghcr.io/example/paste:1.2.3
networks: [edge]
labels:
gpm.rake.pro/enabled: "true"
gpm.rake.pro/domains: "paste.example.com"
gpm.rake.pro/port: "8080"
gpm.rake.pro/profile: "public-ratelimited"
gpm.rake.pro/public-cname: "true"
networks:
edge:
external: true
- Preview before the first write:
GET /api/docker-discovery/plan(Preview changes in the settings UI). - Reconcile:
POST /api/docker-discovery/reconcile, or wait for the next event or poll.
Verify¶
| Check | Expected |
|---|---|
GET /api/docker-discovery/status |
reachable: true, endpoint as configured, lastSuccess close to lastRun, per-host actions and the resolved profile |
| The host list | One dkr-<name> host per labelled container, chipped as docker-managed |
POST /api/docker-discovery/reconcile |
Runs one on demand; 409 Conflict while one is in flight, never queued |
| A second reconcile | Every host unchanged, and no new commit in GET /api/history |
Two operational rules worth knowing before you alert on any of this:
- An unreadable Engine never empties the config. A managed host is deleted only after a complete, successful list that no longer derives it; everything else freezes the current state.
- Discovery is leader-only, like Ingress discovery. A follower runs neither reconciler and refuses config writes.
Scopes: docker-discovery:read for status and plan, docker-discovery:write
for reconcile.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
reachable: false, error names the socket path |
The socket is not mounted, or the path is wrong | Mount /var/run/docker.sock read-only, or point host at the socket proxy |
status 403 from the endpoint |
A socket proxy that does not allow CONTAINERS / EVENTS |
Allow exactly those two, nothing else |
did not return a JSON array |
host points at something that is not the Engine API |
Check the port, and that the proxy forwards /version and /containers/json. The run freezes rather than deleting anything |
lastSuccess far behind lastRun |
Every run since then has failed | Read error; managed hosts are frozen, not deleted, meanwhile |
Container skipped: gpm.rake.pro/port is required |
Zero or several exposed TCP ports | Set the port label explicitly |
Container skipped: outside allowedDomainSuffixes |
The hostname is not inside the configured bound | Add the suffix, or fix the label |
Container skipped: not managed by docker discovery |
A hand-written or Ingress-derived host already has that name or domain | Rename the container, or remove the other host |
Host disabled with profile ... is not defined |
The profile was renamed or retired; an unresolvable profile fails closed | Re-add the profile, or fix the container's profile label |
Host serves 502 after a container is recreated |
The container IP changed between reconciles | Wait for the next event-driven reconcile, or use usePublishedPorts |
| The first reconcile writes nothing and reports an error | The template names a certificate, middleware or access list that does not exist | Create those objects, then enable discovery |