12 KiB
Docker Integration
Docker container discovery, connection management, and label-based route configuration.
Overview
The docker package implements Docker container integration, providing shared client connections, container parsing from Docker API responses, label processing for route configuration, and container filtering capabilities.
Primary consumers
internal/route/provider- Creates Docker-based route providersinternal/idlewatcher- Container idle detection- Operators - Configure routes via Docker labels
Non-goals
- Docker image building or management
- Container lifecycle operations (start/stop)
- Volume management
- Docker Swarm orchestration
Stability
Stable internal package. Public API consists of client management and container parsing functions.
Public API
Exported types
type SharedClient struct {
*client.Client
cfg types.DockerProviderConfig
refCount atomic.Int32
closedOn atomic.Int64
key string
addr string
dial func(ctx context.Context) (net.Conn, error)
unique bool
}
type Container struct {
DockerCfg types.DockerProviderConfig
Image Image
ContainerName string
ContainerID string
Labels map[string]string
ActualLabels map[string]string
Mounts []Mount
Network string
PublicPortMapping map[int]PortSummary
PrivatePortMapping map[int]PortSummary
Aliases []string
IsExcluded bool
IsExplicit bool
IsHostNetworkMode bool
Running bool
State string
PublicHostname string
PrivateHostname string
Agent *agentpool.Agent
IdlewatcherConfig *IdlewatcherConfig
}
Exported functions
func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, error)
Creates or returns a Docker client. Reuses existing clients for the same URL. Thread-safe.
func Clients() map[string]*SharedClient
Returns all currently connected clients. Callers must close returned clients.
func FromDocker(c *container.Summary, dockerCfg types.DockerProviderConfig) *types.Container
Converts Docker API container summary to internal container type. Parses labels for route configuration.
func UpdatePorts(ctx context.Context, c *Container) error
Refreshes port mappings from container inspect.
func DockerComposeProject(c *Container) string
Returns the Docker Compose project name.
func DockerComposeService(c *Container) string
Returns the Docker Compose service name.
func Dependencies(c *Container) []string
Returns container dependencies from labels.
func IsBlacklisted(c *Container) bool
Checks if container should be excluded from routing.
Architecture
Core components
graph TD
A[Docker API] --> B[SharedClient Pool]
B --> C{Client Request}
C -->|New Client| D[Create Connection]
C -->|Existing| E[Increment RefCount]
F[Container List] --> G[FromDocker Parser]
G --> H[Container Struct]
H --> I[Route Builder]
J[Container Labels] --> K[Label Parser]
K --> L[Route Config]
subgraph Client Pool
B --> M[clientMap]
N[Cleaner Goroutine]
end
Client lifecycle
stateDiagram-v2
[*] --> New: NewClient() called
New --> Shared: Refcount = 1, stored in pool
Shared --> Shared: Same URL, increment refcount
Shared --> Idle: Close() called, refcount = 0
Idle --> Closed: 10s timeout elapsed
Idle --> Shared: NewClient() for same URL
Closed --> [*]: Client closed
Unique --> [*]: Close() immediately
Container parsing flow
sequenceDiagram
participant Provider
participant SharedClient
participant DockerAPI
participant ContainerParser
participant RouteBuilder
Provider->>SharedClient: NewClient(cfg)
SharedClient->>SharedClient: Check Pool
alt Existing Client
SharedClient->>SharedClient: Increment RefCount
else New Client
SharedClient->>DockerAPI: Connect
DockerAPI-->>SharedClient: Client
end
Provider->>SharedClient: ListContainers()
SharedClient->>DockerAPI: GET /containers/json
DockerAPI-->>SharedClient: Container List
SharedClient-->>Provider: Container List
loop For Each Container
Provider->>ContainerParser: FromDocker()
ContainerParser->>ContainerParser: Parse Labels
ContainerParser->>ContainerParser: Resolve Hostnames
ContainerParser-->>Provider: *Container
end
Provider->>RouteBuilder: Create Routes
RouteBuilder-->>Provider: Routes
Client pool management
The docker package maintains a pool of shared clients:
var (
clientMap = make(map[string]*SharedClient, 10)
clientMapMu sync.RWMutex
)
func initClientCleaner() {
cleaner := task.RootTask("docker_clients_cleaner", true)
go func() {
ticker := time.NewTicker(cleanInterval)
for {
select {
case <-ticker.C:
closeTimedOutClients()
case <-cleaner.Context().Done():
// Cleanup all clients
}
}
}()
}
Configuration Surface
Docker provider configuration
providers:
docker:
local: ${DOCKER_HOST}
remote1:
scheme: tcp
host: docker1.local
port: 2375
remote2:
scheme: tls
host: docker2.local
port: 2375
tls:
ca_file: /path/to/ca.pem
cert_file: /path/to/cert.pem
key_file: /path/to/key.pem
Route configuration labels
Route labels use the format proxy.<alias>.<field> where <alias> is the route alias (or * for wildcard). The base labels apply to all routes.
| Label | Description | Example |
|---|---|---|
proxy.aliases |
Route aliases (comma-separated) | proxy.aliases: www,app |
proxy.exclude |
Exclude from routing | proxy.exclude: true |
proxy.network |
Docker network | proxy.network: frontend |
proxy.<alias>.host |
Override hostname | proxy.app.host: 192.168.1.100 |
proxy.<alias>.port |
Target port | proxy.app.port: 8080 |
proxy.<alias>.scheme |
HTTP scheme | proxy.app.scheme: https |
proxy.<alias>.* |
Any route-specific setting | proxy.app.no_tls_verify: true |
Wildcard alias
Use proxy.*.<field> to apply settings to all routes:
labels:
proxy.aliases: app1,app2
proxy.*.scheme: https
proxy.app1.port: 3000 # overrides wildcard
Idle watcher labels
| Label | Description | Example |
|---|---|---|
proxy.idle_timeout |
Idle timeout duration | proxy.idle_timeout: 30m |
proxy.wake_timeout |
Max time to wait for wake | proxy.wake_timeout: 10s |
proxy.stop_method |
Stop method (pause, stop, kill) | proxy.stop_method: stop |
proxy.stop_signal |
Signal to send (e.g., SIGTERM) | proxy.stop_signal: SIGTERM |
proxy.stop_timeout |
Stop timeout in seconds | proxy.stop_timeout: 30 |
proxy.depends_on |
Container dependencies | proxy.depends_on: database |
proxy.start_endpoint |
Optional path restriction | proxy.start_endpoint: /api/ready |
proxy.no_loading_page |
Skip loading page | proxy.no_loading_page: true |
Docker Compose labels
Those are created by Docker Compose.
| Label | Description |
|---|---|
com.docker.compose.project |
Compose project name |
com.docker.compose.service |
Service name |
com.docker.compose.depends_on |
Dependencies |
Dependency and Integration Map
Internal dependencies
internal/agentpool- Agent-based Docker host connectionsinternal/maxmind- Container geolocationinternal/types- Container and provider typesinternal/task/task.go- Lifetime management
External dependencies
github.com/docker/cli/cli/connhelper- Connection helpersgithub.com/moby/moby/client- Docker API clientgithub.com/docker/go-connections/nat- Port parsing
Integration points
// Route provider uses docker for container discovery
client, err := docker.NewClient(cfg)
containers, err := client.ContainerList(ctx, container.ListOptions{})
for _, c := range containers {
container := docker.FromDocker(c, cfg)
// Create routes from container
}
Observability
Logs
- Client initialization and cleanup
- Connection errors
- Container parsing errors
Metrics
No metrics are currently exposed.
Security Considerations
- Docker socket access requires proper permissions
- TLS certificates for remote connections
- Agent-based connections are authenticated via TLS
- Database containers are automatically blacklisted
Blacklist detection
Containers are automatically blacklisted if they:
- Mount database directories:
/var/lib/postgresql/data/var/lib/mysql/var/lib/mongodb/var/lib/mariadb/var/lib/memcached/var/lib/rabbitmq
- Expose database ports:
- 5432 (PostgreSQL)
- 3306 (MySQL/MariaDB)
- 6379 (Redis)
- 11211 (Memcached)
- 27017 (MongoDB)
Failure Modes and Recovery
| Failure | Behavior | Recovery |
|---|---|---|
| Docker socket inaccessible | NewClient returns error | Fix socket permissions |
| Remote connection failed | NewClient returns error | Check network/tls config |
| Container inspect failed | UpdatePorts returns error | Container may be stopped |
| Invalid labels | Container created with error | Fix label syntax |
| Agent not found | Panic during client creation | Add agent to pool |
Performance Characteristics
- Client pooling reduces connection overhead
- Reference counting prevents premature cleanup
- Background cleaner removes idle clients after 10s
- O(n) container parsing where n is container count
Usage Examples
Creating a Docker client
dockerCfg := types.DockerProviderConfig{
URL: "unix:///var/run/docker.sock",
}
client, err := docker.NewClient(dockerCfg)
if err != nil {
log.Fatal(err)
}
defer client.Close()
Using unique client
// Create a unique client that won't be shared
client, err := docker.NewClient(cfg, true)
if err != nil {
log.Fatal(err)
}
// Remember to close when done
client.Close()
Getting all clients
clients := docker.Clients()
for host, client := range clients {
log.Printf("Connected to: %s", host)
}
// Use clients...
// Close all clients when done
for _, client := range clients {
client.Close()
}
Parsing containers
containers, err := dockerClient.ContainerList(ctx, container.ListOptions{})
for _, c := range containers {
container := docker.FromDocker(c, dockerCfg)
if container.Errors != nil {
log.Printf("Container %s has errors: %v", container.ContainerName, container.Errors)
continue
}
log.Printf("Container: %s, Aliases: %v", container.ContainerName, container.Aliases)
}
Checking if container is blacklisted
container := docker.FromDocker(c, dockerCfg)
if docker.IsBlacklisted(container) {
log.Printf("Container %s is blacklisted, skipping", container.ContainerName)
continue
}