Skip to content
SHC Docs

Network architecture

SHC creates hierarchical Docker networks for each deployment, enabling DNS-based service discovery at multiple scopes:

NetworkSuffixScopeAlias FormatExample Alias
Deployment_dSame deployment{service}postgres
Environment_eSame environment, all stacks{service}.{stack}postgres.mydb
Tenant_tSame 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.

Deployment: {tenant}_{environment}_{stack}_d
Environment: {tenant}_{environment}_e
Tenant: {tenant}_t

Examples (stack “mydb”, environment “prod”, tenant “default”):

Deployment: default_prod_mydb_d
Environment: default_prod_e
Tenant: default_t
RuntimeNetwork DriverCross-NodeEncryption
SwarmoverlayYesOptional (—opt encrypted)
ComposebridgeNoN/A
PodmanbridgeNoN/A

In Swarm mode, networks are created as overlay networks, allowing containers on different nodes to communicate as if on the same network.

The deployment ID uniquely identifies each deployment:

Format: {tenant}_{environment}_{stack}
Example: default_prod_mydb

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.


Services in the same deployment can reach each other using just the service name:

Terminal window
# From app container in default_prod_mydb
ping postgres
# Resolves via alias on default_prod_mydb_d network

Services can reach other stacks in the same environment using {service}.{stack}:

Terminal window
# From webapp in default_prod_webapp
# Connecting to postgres in default_prod_mydb
psql -h postgres.mydb -U admin
# Resolves via alias on default_prod_e network

Services can reach other environments using {environment}.{service}.{stack}:

Terminal window
# From default_staging_webapp
# Connecting to postgres in default_prod_mydb
psql -h prod.postgres.mydb -U admin
# Resolves via alias on default_t tenant network

In Swarm mode, overlay networks provide cross-node communication:

Terminal window
# Service on Node A can reach service on Node B
# using the same DNS names as single-node
psql -h postgres.mydb -U admin
# Resolves via overlay network to container on any node

All services are automatically attached to networks during enrichment:

  1. Deployment network (_d) - For inter-service communication within the deployment
  2. Environment network (_e) - For cross-stack communication within the same environment
  3. 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.

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

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.

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 postureCert 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 existsLet’s Encrypt DNS-01 via that connection’s certResolver (le-dns-<impl>); no SHC leaf
exposure == "internet", LE enabled (leEnabled), no DNS-01 ownerLet’s Encrypt HTTP-01 via the default letsencrypt certResolver; no SHC leaf
exposure == "internet", LE disabled, no DNS-01 ownerSHC 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.

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.


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 status and 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:

postgres.socket.yaml
data:
hostname: "{{ service }}.{{ stack }}"
port: 5432

This allows consumers in any stack within the same environment to connect:

Terminal window
# Consumer connects using the provided hostname
psql -h postgres.mydb -U webapp_user