Skip to content

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.

NetworkJoined whenPurpose
holden-{app-id}AlwaysCommunication within an app
Traefik networkHas domain:Public HTTP routing
holden-{name}Has networks: ["{name}"]Cross-app communication

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: 8080

The web service reaches worker at http://worker:8080. Simple hostnames, no configuration needed.

Services with domain: join the Traefik network and get automatic routing labels:

services:
web:
image: myapp:latest
domain: app.example.com
port: 3000

Holden 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.

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.

Docker containers follow the pattern {app-id}-{service-name}:

myapp-web
myapp-worker
another-app-api

This matches the hostname format used on shared networks, so what you see in docker ps matches what you’d use to reach that service.

A service can respond to multiple domains:

services:
web:
image: myapp:latest
domain:
- app.example.com
- www.example.com
port: 3000

By 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
ModeBehavior
redirect (default)Redirects all domains to the first one
allServes content on all domains, no redirects

In redirect mode, Holden generates separate routers:

# Primary router
traefik.http.routers.holden-myapp-web.rule: "Host(`app.example.com`)"
# Redirect router
traefik.http.routers.holden-myapp-web-redirect.rule: "Host(`www.example.com`)"
traefik.http.routers.holden-myapp-web-redirect.middlewares: "holden-myapp-web-redirect"
# Redirect middleware
traefik.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`)"

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 API
services:
api:
image: backend:latest
networks: ["backend"]
port: 8080
# app1/holden.yml - a frontend that calls the backend
services:
web:
image: frontend:latest
domain: app.example.com
networks: ["backend"]
port: 3000
env:
BACKEND_URL: http://app2-api:8080

Both services join holden-backend (Holden creates it automatically). The frontend reaches the backend at app2-api:8080 — internally, not through Traefik.

On shared networks, use the {app-id}-{service-name} pattern:

{app-id}-{service-name}

Examples:

  • app2-api — the api service in app2
  • myapp-worker — the worker service in myapp

A service can join multiple networks:

services:
api:
image: myapi:latest
networks: ["backend", "monitoring"]
port: 8080

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: 8080

This is useful when an API needs to be accessible from the internet and by other internal services.

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