Network architecture
Network Scopes
Section titled “Network Scopes”SHC creates hierarchical Docker networks for each deployment, enabling DNS-based service discovery at multiple scopes:
| Network | Suffix | Scope | Alias Format | Example Alias |
|---|---|---|---|---|
| Deployment | _d | Same deployment | {service} | postgres |
| Environment | _e | Same environment, all stacks | {service}.{stack} | postgres.mydb |
| Tenant | _t | Same tenant, all environments + ingress | {environment}.{service}.{stack} | prod.postgres.mydb |
The tenant network doubles as the ingress network — Traefik is dynamically connected to each tenant’s _t network, providing per-tenant network isolation.
Network Naming
Section titled “Network Naming”Deployment: {tenant}_{environment}_{stack}_dEnvironment: {tenant}_{environment}_eTenant: {tenant}_tExamples (stack “mydb”, environment “prod”, tenant “default”):
Deployment: default_prod_mydb_dEnvironment: default_prod_eTenant: default_tNetwork Types by Runtime
Section titled “Network Types by Runtime”| Runtime | Network Driver | Cross-Node | Encryption |
|---|---|---|---|
| Swarm | overlay | Yes | Optional (—opt encrypted) |
| Compose | bridge | No | N/A |
| Podman | bridge | No | N/A |
In Swarm mode, networks are created as overlay networks, allowing containers on different nodes to communicate as if on the same network.
Deployment ID
Section titled “Deployment ID”The deployment ID uniquely identifies each deployment:
Format: {tenant}_{environment}_{stack}Example: default_prod_mydbNaming Constraints
Section titled “Naming Constraints”IMPORTANT: Underscores are NOT allowed in environment, stack, or tenant names. Use dashes instead:
- ✅
my-stack,prod-env,acme-corp - ❌
my_stack,prod_env,acme_corp
This ensures unambiguous parsing of deployment IDs and network names.
DNS Resolution
Section titled “DNS Resolution”Within Same Deployment
Section titled “Within Same Deployment”Services in the same deployment can reach each other using just the service name:
# From app container in default_prod_mydbping postgres# Resolves via alias on default_prod_mydb_d networkCross-Stack (Same Environment)
Section titled “Cross-Stack (Same Environment)”Services can reach other stacks in the same environment using {service}.{stack}:
# From webapp in default_prod_webapp# Connecting to postgres in default_prod_mydbpsql -h postgres.mydb -U admin# Resolves via alias on default_prod_e networkCross-Environment
Section titled “Cross-Environment”Services can reach other environments using {environment}.{service}.{stack}:
# From default_staging_webapp# Connecting to postgres in default_prod_mydbpsql -h prod.postgres.mydb -U admin# Resolves via alias on default_t tenant networkCross-Node (Swarm Mode Only)
Section titled “Cross-Node (Swarm Mode Only)”In Swarm mode, overlay networks provide cross-node communication:
# Service on Node A can reach service on Node B# using the same DNS names as single-nodepsql -h postgres.mydb -U admin# Resolves via overlay network to container on any nodeNetwork Attachment
Section titled “Network Attachment”All services are automatically attached to networks during enrichment:
- Deployment network (
_d) - For inter-service communication within the deployment - Environment network (
_e) - For cross-stack communication within the same environment - Tenant network (
_t) - For cross-environment communication and Traefik ingress
Traefik is dynamically connected to each tenant’s _t network, providing both ingress routing and per-tenant network isolation. A deployment installed with --host <hostname> gets an HTTP ingress route (a Traefik file-provider router on the _t network); whether that route sits behind the SHC SSO gate and whether it is reachable from the internet are the orthogonal access (--gated/--open) and exposure (--public) axes — see Access control.
Example Enriched Service
Section titled “Example Enriched Service”services: postgres: image: supabase/postgres:17.6.1.071 networks: default_prod_mydb_d: aliases: - postgres default_prod_e: aliases: - postgres.mydb default_t: aliases: - prod.postgres.mydbTraefik Integration
Section titled “Traefik Integration”Ingress is requested per deployment with --host <hostname> at install time
(repeatable). There is no public_hosts/public_ports field in app.yaml —
hostnames come from the install flags, and the access (--gated/--open)
and exposure (--public) axes decide gating and reachability (see
Access control).
Route generation (file provider, not docker labels)
Section titled “Route generation (file provider, not docker labels)”SHC does not emit traefik.http.routers.* docker labels for app routes.
Instead it writes a Traefik file-provider config into the dynamic dir
({state}/system/traefik/dynamic/<tenant>_<stack>_<env>.yml) that Traefik
watches. One --host yields one HTTP router on the websecure entrypoint
pointing at the service’s container over the _t network. Conceptually:
# {state}/system/traefik/dynamic/acme_nextcloud_prod.yml (rendered by SHC)http: routers: acme_prod_nextcloud: rule: "Host(`nextcloud.example.com`)" entrypoints: ["websecure"] service: acme_prod_nextcloud tls: {} # SHC-served leaf (see TLS below), or # tls: { certResolver: letsencrypt } # internet + LE enabled middlewares: ["shc-oidc@docker"] # only when access == "gated" services: acme_prod_nextcloud: loadBalancer: servers: - url: "http://<container>:<port>"When access == "gated" the router chains the shc-oidc@docker forward-auth
middleware; open routes through directly. The shc-oidc middleware is the
docker-label half (it lives on the forward-auth sidecar); app routers
themselves are file-provider entries. The single helper that renders these is
writeIngressRouteFile in modules/app/inject_traefik.go.
TLS Configuration
Section titled “TLS Configuration”The primary TLS path is a per-tenant internal CA leaf-signer, not
Let’s Encrypt. Each tenant has its own two-tier autogen CA — a self-signed
root + an issuing intermediate, minted by MintAutogenCA and stored in
modules/ca/ (modules/ca/ca.go, store.go). When SHC
renders an ingress route it mints a short-lived ECDSA P-256 leaf for the host
(SignLeaf / the leafSigner interface), writes the cert+key into the
dynamic certs/ dir, and adds a tls.certificates block so Traefik’s file
provider serves that leaf (serveSHCLeaf in inject_traefik.go). Traefik
SNI-matches each served leaf to its router. The tenant CA root is also
materialized as a bundle (certs/shc-ca-bundle.pem) so the forward-auth
sidecar trusts the SHC-served leaf.
TLS posture is decided per host (writeIngressRouteFile), with the
leafSigner as the master switch (nil signer ⇒ bare tls: {}):
| Host posture | Cert path |
|---|---|
exposure == "lan" (any access) | SHC internal-CA leaf (serveSHCLeaf) — reachable on the LAN with no public-CA challenge needed |
exposure == "internet", a zone-owning DNS-01 connection exists | Let’s Encrypt DNS-01 via that connection’s certResolver (le-dns-<impl>); no SHC leaf |
exposure == "internet", LE enabled (leEnabled), no DNS-01 owner | Let’s Encrypt HTTP-01 via the default letsencrypt certResolver; no SHC leaf |
exposure == "internet", LE disabled, no DNS-01 owner | SHC internal-CA leaf (fallback, stays reachable) |
So Let’s Encrypt (HTTP-01 or DNS-01) is reserved for internet-exposed hosts
that opt into it; everything else — and any internet host without a usable LE
challenge — serves the per-tenant internal-CA leaf. One route file can mix an
LE host (certResolver, no leaf) with an internal-CA host (bare tls: {} +
served leaf). The websecure entrypoint (port 443) and automatic HTTP→HTTPS
redirect apply across both paths.
DNS Providers (--host)
Section titled “DNS Providers (--host)”When deploying with --host, SHC creates DNS records using the resolved provider
for that deployment scope:
- A for IPv4 targets
- AAAA for IPv6 targets
- CNAME for hostname targets
Traefik is synced on-demand when DNS providers are added, removed, or modified. Only Traefik instances with deployments using affected providers are updated, and each sync writes all current providers to the Traefik instance.
Service Discovery Labels
Section titled “Service Discovery Labels”SHC adds container labels for service discovery:
labels: - "shc.tenant={tenant}" - "shc.stack={stack}" - "shc.environment={environment}" - "shc.node_id={node_id}" - "shc.app={app_name}" - "shc.service={service_name}" - "shc.deployment_id={deployment_id}"These labels enable:
- Scoping by tenant/stack/environment in
shc statusand the HUD - Health monitoring of specific deployments
- Metrics collection with labels
- Log aggregation by tenant or stack
Cross-Stack Communication via Integrations
Section titled “Cross-Stack Communication via Integrations”When apps integrate via plugs/sockets, they communicate over the shared environment network (_e). The socket advertises its environment-scoped alias:
data: hostname: "{{ service }}.{{ stack }}" port: 5432This allows consumers in any stack within the same environment to connect:
# Consumer connects using the provided hostnamepsql -h postgres.mydb -U webapp_user