Networking
Holden manages Docker networks for container communication. Each app gets its own isolated network, and you can create shared networks for cross-app communication.
Network Types
Section titled “Network Types”| Network | Joined when | Purpose |
|---|---|---|
holden-{app-id} | Always | Communication within an app |
| Traefik network | Has domain: | Public HTTP routing |
holden-{name} | Has networks: ["{name}"] | Cross-app communication |
Within an App
Section titled “Within an App”Every app gets its own isolated network (holden-myapp). Services in the same app can reach each other by service name:
services: web: image: myapp:latest env: WORKER_URL: http://worker:8080
worker: image: myapp:latest port: 8080The web service reaches worker at http://worker:8080. Simple hostnames, no configuration needed.
Public Routing with Traefik
Section titled “Public Routing with Traefik”Services with domain: join the Traefik network and get automatic routing labels:
services: web: image: myapp:latest domain: app.example.com port: 3000Holden adds Traefik labels for HTTPS routing, certificate generation, and load balancing. Traffic from the internet reaches your service through Traefik. During zero-downtime deploys, Holden manages router priority labels so the new container receives all traffic immediately.
Traefik Labels
Section titled “Traefik Labels”For the config above, Holden generates:
traefik.enable: "true"traefik.http.routers.holden-myapp-web.rule: "Host(`app.example.com`)"traefik.http.routers.holden-myapp-web.entrypoints: "websecure"traefik.http.routers.holden-myapp-web.tls.certresolver: "letsencrypt"traefik.http.services.holden-myapp-web.loadbalancer.server.port: "3000"Router and service names use the pattern holden-{app-id}-{service-name} with hyphens (Traefik doesn’t allow dots in names). The entrypoint and certresolver names default to websecure and letsencrypt but are configurable.
Container Naming
Section titled “Container Naming”Docker containers follow the pattern {app-id}-{service-name}:
myapp-webmyapp-workeranother-app-apiThis matches the hostname format used on shared networks, so what you see in docker ps matches what you’d use to reach that service.
Multiple Domains
Section titled “Multiple Domains”A service can respond to multiple domains:
services: web: image: myapp:latest domain: - app.example.com - www.example.com port: 3000By default, Holden redirects all domains to the first one (301 redirect). This keeps URLs consistent and is good for SEO.
To serve all domains without redirects:
services: web: domain: - app.example.com - www.example.com domain_mode: all| Mode | Behavior |
|---|---|
redirect (default) | Redirects all domains to the first one |
all | Serves content on all domains, no redirects |
In redirect mode, Holden generates separate routers:
# Primary routertraefik.http.routers.holden-myapp-web.rule: "Host(`app.example.com`)"
# Redirect routertraefik.http.routers.holden-myapp-web-redirect.rule: "Host(`www.example.com`)"traefik.http.routers.holden-myapp-web-redirect.middlewares: "holden-myapp-web-redirect"
# Redirect middlewaretraefik.http.middlewares.holden-myapp-web-redirect.redirectregex.regex: "^https?://[^/]+/(.*)"traefik.http.middlewares.holden-myapp-web-redirect.redirectregex.replacement: "https://app.example.com/$1"traefik.http.middlewares.holden-myapp-web-redirect.redirectregex.permanent: "true"In all mode, both domains share one router:
traefik.http.routers.holden-myapp-web.rule: "Host(`app.example.com`) || Host(`www.example.com`)"Cross-App Communication
Section titled “Cross-App Communication”Sometimes apps need to talk to each other internally, without going through the public internet. Use networks: to join a shared network:
# app2/holden.yml - a backend APIservices: api: image: backend:latest networks: ["backend"] port: 8080# app1/holden.yml - a frontend that calls the backendservices: web: image: frontend:latest domain: app.example.com networks: ["backend"] port: 3000 env: BACKEND_URL: http://app2-api:8080Both services join holden-backend (Holden creates it automatically). The frontend reaches the backend at app2-api:8080 — internally, not through Traefik.
Hostname Format
Section titled “Hostname Format”On shared networks, use the {app-id}-{service-name} pattern:
{app-id}-{service-name}Examples:
app2-api— the api service in app2myapp-worker— the worker service in myapp
Multiple Networks
Section titled “Multiple Networks”A service can join multiple networks:
services: api: image: myapi:latest networks: ["backend", "monitoring"] port: 8080Combining Public and Shared
Section titled “Combining Public and Shared”A service can have both domain: (public) and networks: (cross-app):
services: api: image: myapi:latest domain: api.example.com # Public access networks: ["backend"] # Also reachable by other apps port: 8080This is useful when an API needs to be accessible from the internet and by other internal services.
Summary
Section titled “Summary”flowchart TB
User[Internet]
traefik([traefik network])
backend([holden-backend])
subgraph app1["holden-app1"]
web1[web]
worker1[worker]
end
subgraph app2["holden-app2"]
api2[api]
db2[db]
end
User --> traefik --> web1
web1 <--> worker1
api2 <--> db2
web1 -.- backend -.- api2