Expose HTTP traffic

To expose a deployment on the public Internet (or on a private hostname behind your firewall), put a reverse proxy in front of it. Ring doesn't terminate TLS or do L7 routing itself; that's a proxy's job.

The recommended path is Sozune, the companion proxy of Ring. It reads Docker container labels to route traffic, terminates TLS via Let's Encrypt automatically, and gates traffic on the container's Docker HEALTHCHECK, which Ring writes for you when you declare a readiness: true health check. The two are designed to be used together: deploy Sozune as a Ring deployment, label your services, done.

If you already run Traefik / Caddy / nginx, the broad shape is the same; see how-to: isolate namespaces and route traffic for those alternatives.

This page shows the end-to-end recipe with Sozune. For the underlying mechanism (how Ring translates a command readiness check into a Docker HEALTHCHECK), see Health checks (design) → proxy integration.

What you get

  • HTTP and HTTPS termination on ports 80 / 443
  • Let's Encrypt certificates provisioned and renewed automatically
  • Per-deployment routing via labels, with no central config file to edit on every change
  • Traffic only flows once Ring's readiness check passes (zero-downtime rolling updates work without dropping a single request)

1. Deploy Sozune in your production namespace

Sozune must share the per-namespace Docker bridge with the backends it routes to. The simplest layout: put Sozune in the same namespace as the services it fronts.

# sozune.yaml
deployments:
  sozune:
    name: sozune
    namespace: production
    runtime: docker
    image: "ghcr.io/kemeter/sozune:latest"
    replicas: 1

    ports:
      - { published: 80,  target: 80 }
      - { published: 443, target: 443 }

    volumes:
      - type: bind
        source: /var/run/docker.sock
        destination: /var/run/docker.sock
        driver: local
        permission: ro
      - type: bind
        source: /opt/sozune/config.yaml
        destination: /etc/sozune/config.yaml
        driver: local
        permission: ro
      - type: bind
        source: /opt/sozune/acme
        destination: /var/lib/sozune/acme
        driver: local
        permission: rw

Create the config file on the host before applying:

sudo mkdir -p /opt/sozune/acme
sudo tee /opt/sozune/config.yaml > /dev/null <<'EOF'
providers:
  docker:
    enabled: true

entrypoints:
  http:
    address: ":80"
  https:
    address: ":443"

acme:
  email: "you@example.com"
  storage: /var/lib/sozune/acme/acme.json
EOF

Then apply:

ring apply -f sozune.yaml

Docker socket warning. Mounting /var/run/docker.sock gives Sozune full read access to the host's Docker daemon. Treat the Sozune container as privileged. Mount read-only (permission: ro), since Sozune only reads container metadata and doesn't need to start or stop containers.

2. Label your backend deployment

Add labels: to any Ring deployment that should be exposed. The label format is sozune.http.<service>.<key> where <service> is an arbitrary name unique to this deployment.

deployments:
  api:
    name: api
    namespace: production
    runtime: docker
    image: "myapp:v1.2.3"
    replicas: 3

    labels:
      "sozune.enable": "true"
      "sozune.http.api.host": "api.example.com"
      "sozune.http.api.port": "8080"
      "sozune.http.api.tls": "true"
      "sozune.http.api.httpsRedirect": "true"

    health_checks:
      - type: http
        url: "http://localhost:8080/health"
        interval: "5s"
        timeout: "2s"
        threshold: 3
        on_failure: restart

      # Readiness gate: drives both Ring's drain timing AND Sozune routing
      - type: command
        command: "curl -fsS http://localhost:8080/ready"
        interval: "5s"
        timeout: "2s"
        threshold: 3
        on_failure: alert
        readiness: true

Apply, then point DNS for api.example.com at your Ring host. Sozune picks up the new container within seconds, provisions the Let's Encrypt cert, and starts routing.

3. The readiness gate is automatic

Here's why Ring + Sozune is more than just labels:

Container stateDocker HEALTHCHECK statusSozune behaviour
Just startedstartingNot routed
Readiness probe failingunhealthyRemoved from rotation
Readiness probe greenhealthyRouted

Ring writes the Docker HEALTHCHECK from the command health check marked readiness: true. Sozune reads State.Health.Status and routes accordingly. During a rolling update, the new container only starts receiving traffic once its readiness probe is green, and Ring only drains the old version once Sozune has had time to switch over.

You don't configure any of this. It's the consequence of declaring readiness: true on a type: command check.

Minimum labels for HTTP (no TLS)

labels:
  "sozune.enable": "true"
  "sozune.http.api.host": "api.example.com"
  "sozune.http.api.port": "8080"

Full labels reference

LabelPurpose
sozune.enableRequired. "true" to enable discovery
sozune.networkDocker network name (when the container is on more than one)
sozune.http.<svc>.hostHostname the route matches
sozune.http.<svc>.portBackend container port
sozune.http.<svc>.pathPath prefix
sozune.http.<svc>.pathRegexRegex path match
sozune.http.<svc>.priorityRoute priority (default: by specificity)
sozune.http.<svc>.methodsComma-separated HTTP methods
sozune.http.<svc>.tls"true" to terminate TLS for this route
sozune.http.<svc>.httpsRedirect"true" to redirect HTTP → HTTPS
sozune.http.<svc>.headers.<name>Add a request header
sozune.http.<svc>.headers.response.<name>Add a response header
sozune.http.<svc>.auth.basicBasic-auth credentials
sozune.http.<svc>.forwardAuth.addressForward-auth endpoint
sozune.http.<svc>.ratelimit.averageAverage requests/sec
sozune.http.<svc>.ratelimit.burstBurst capacity
sozune.http.<svc>.compress"zstd", "br", "gzip"
sozune.http.<svc>.stripPrefix"true" to strip the matched path before forwarding
sozune.http.<svc>.addPrefixPrefix to prepend before forwarding
sozune.http.<svc>.stickySession"true" for cookie-based session affinity
sozune.http.<svc>.backendTimeoutPer-route timeout (e.g. "30s")
sozune.tcp.<svc>.entrypointTCP routing: required entrypoint name
sozune.tcp.<svc>.portBackend TCP port

For the canonical reference, see the Sozune Docker provider docs.

Cross-namespace routing

The simplest setup keeps Sozune in the same namespace as its backends, so they share the ring_<namespace> bridge automatically. If you need a single Sozune fronting multiple namespaces, attach Sozune's container to each backend network:

docker network connect ring_staging $(docker ps -q --filter "name=production_sozune")
docker network connect ring_data    $(docker ps -q --filter "name=production_sozune")

The docker network connect calls are not managed by Ring; they belong to the host's Docker config and need to be re-run if Sozune's container is recreated. For most setups, one Sozune per namespace is simpler.

Limits

  • Sozune is a separate process you operate. Ring runs the container, but you still pin its version, monitor it, and provision the certificate storage volume.
  • Docker socket access = host access. Run Sozune in a namespace you trust, keep the socket mount read-only.
  • One Sozune per host port. Two Sozune deployments can't both publish :80, so pick one as the edge.
  • No automatic routing across hosts. Ring is single-node; Sozune routes to containers on the same node. Multi-host needs an external load balancer in front of Sozune.

See also