Skip to main content Skip to navigation Skip to footer
Limited time: Design Partner Program — BUSINESS plan free for life

Networking

Expose services with the reverse proxy, Docker labels, TLS certificates, DNS, and TCP/UDP port forwarding.

Networking

This page explains how services running inside isolated Docker daemons become accessible from the internet. It covers the reverse proxy system, Docker labels for routing, TLS certificates, DNS, and TCP/UDP port forwarding.

For how services get their loopback IPs and the .rediacc.json slot system, see Services.

Network Isolation

Each repository is automatically isolated at the kernel level using network hooks. This requires Linux kernel 6.1 or later. No configuration is needed.

  • Automatic bind rewriting: Services can bind to 0.0.0.0 or 127.0.0.1 as usual. The kernel transparently rewrites the address to the service’s assigned loopback IP. No need to explicitly bind to ${SERVICE_IP}.
  • Cross-repo connection blocking: If a service tries to connect to a loopback IP outside its repository’s /26 subnet, the kernel blocks it. A process in repo A cannot reach services in repo B.
  • No application changes required: Services use 0.0.0.0 or localhost for binding, and the kernel ensures they only listen on their correct loopback IP. Isolation is fully transparent.

How It Works

Rediacc uses a two-component proxy system to route external traffic to containers:

  1. Route server, a systemd service that discovers running containers across all repository Docker daemons. It inspects container labels and generates routing configuration, served as a YAML endpoint.
  2. Traefik, a reverse proxy that polls the route server every 5 seconds and applies the discovered routes. It handles HTTP/HTTPS routing, TLS termination, and TCP/UDP forwarding.

The flow looks like this:

Internet → Traefik (ports 80/443/TCP/UDP)
               ↓ polls every 5s
           Route Server (discovers containers)
               ↓ inspects labels
           Docker Daemons (/var/run/rediacc/docker-*.sock)

           Containers (bound to 127.x.x.x loopback IPs)

When you add the right labels to a container and start it with renet compose, it automatically becomes routable, no manual proxy configuration needed.

The route server binary is kept in sync with your CLI version. When the CLI updates the renet binary on a machine, the route server is automatically restarted (~1–2 seconds). This causes no downtime, Traefik continues serving traffic with its last known configuration during the restart and picks up the new config on the next poll. Existing client connections are not affected. Your application containers are not touched.

Docker Labels

Routing is controlled by Docker container labels. There are two tiers:

Tier 1: rediacc.* Labels (Automatic)

These labels are automatically injected by renet compose when starting services. You do not need to add them manually.

LabelDescriptionExample
rediacc.service_nameService identitymyapp
rediacc.service_ipAssigned loopback IP127.0.11.2
rediacc.network_idRepository’s daemon ID2816
rediacc.repo_nameRepository namemarketing
rediacc.tcp_portsTCP ports the service listens on8080,8443
rediacc.udp_portsUDP ports the service listens on53

When a container has only rediacc.* labels (no traefik.enable=true), the route server generates an auto-route using the repository name and machine subdomain:

{service}.{repoName}.{machineName}.{baseDomain}

For example, a service named myapp in a repository called marketing on machine server-1 with base domain example.com gets:

myapp.marketing.server-1.example.com

For forks, the service name is combined with the reserved word fork and the tag:

{service}-fork-{tag}.{repoName}.{machineName}.{baseDomain}

For example, a fork of marketing tagged staging gets:

myapp-fork-staging.marketing.server-1.example.com

Each fork URL sits under the parent repo’s subdomain and is covered by its existing wildcard certificate, so no new certificate is needed. The -fork- separator prevents collisions with any real service names in the production repo. For services with custom domains, use Tier 2 labels or the rediacc.domain label.

Custom Domain via rediacc.domain

You can set a custom domain for a service using the rediacc.domain label in your docker-compose.yml. Both short names and full domains are supported:

labels:
  # Short name, resolved to cloud.example.com using the machine's baseDomain
  - "rediacc.domain=cloud"

  # Full domain, used as-is
  - "rediacc.domain=cloud.example.com"

A value without dots is treated as a short name and gets the machine’s baseDomain appended automatically. A value with dots is used as a full domain. This applies to both auto-route generation and CLI display.

When machineName is configured, custom domain services get two routes: one on the base domain (cloud.example.com) and one on the machine subdomain (cloud.server-1.example.com).

Tier 2: traefik.* Labels (User-Defined)

Add these labels to your docker-compose.yml when you want custom domain routing, TLS, or specific entrypoints. Setting traefik.enable=true tells the route server to use your custom rules instead of generating an auto-route.

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
  - "traefik.http.routers.myapp.entrypoints=websecure,websecure-v6"
  - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
  - "traefik.http.services.myapp.loadbalancer.server.port=8080"

These use standard Traefik v3 label syntax.

Tip: Internal-only services (databases, caches, message queues) should not have traefik.enable=true. They only need rediacc.* labels, which are injected automatically.

Exposing HTTP/HTTPS Services

Prerequisites

  1. Infrastructure configured on the machine (Machine Setup, Infrastructure Configuration):

    # Shared credentials (once per config, applies to all machines)
    rdc config infra set -m server-1 \
      --cert-email admin@example.com \
      --cf-dns-token your-cloudflare-api-token
    
    # Machine-specific settings
    rdc config infra set -m server-1 \
      --public-ipv4 203.0.113.50 \
      --base-domain example.com
    
    rdc config infra push -m server-1
  2. DNS records pointing your domain to the server’s public IP (see DNS Configuration below).

Adding Labels

Add traefik.* labels to the services you want to expose in your docker-compose.yml:

services:
  myapp:
    image: myapp:latest
    environment:
      - LISTEN_ADDR=0.0.0.0:8080
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
      - "traefik.http.routers.myapp.entrypoints=websecure,websecure-v6"
      - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
      - "traefik.http.services.myapp.loadbalancer.server.port=8080"

  database:
    image: postgres:17
    # No traefik labels, database is internal only
LabelPurpose
traefik.enable=trueEnables custom Traefik routing for this container
traefik.http.routers.{name}.ruleRouting rule, typically Host(\domain`)`
traefik.http.routers.{name}.entrypointsWhich ports to listen on: websecure (HTTPS IPv4), websecure-v6 (HTTPS IPv6)
traefik.http.routers.{name}.tls.certresolverCertificate resolver, use letsencrypt for automatic Let’s Encrypt
traefik.http.services.{name}.loadbalancer.server.portThe port your application listens on inside the container

The {name} in labels is an arbitrary identifier, it just needs to be consistent across related router/service/middleware labels.

Note: The rediacc.* labels (rediacc.service_name, rediacc.service_ip, rediacc.network_id) are injected automatically by renet compose. You do not need to add them to your compose file.

TLS Certificates

TLS certificates are obtained automatically via Let’s Encrypt using the Cloudflare DNS-01 challenge. Credentials are configured once per config (shared across all machines):

rdc config infra set -m server-1 \
  --cert-email admin@example.com \
  --cf-dns-token your-cloudflare-api-token

Auto-routes use wildcard certificates at the repo subdomain level (*.marketing.server-1.example.com) instead of per-service certs. The certificate is provisioned automatically by Traefik on the first repo up; no manual step required. Forks reuse the parent repo’s existing wildcard, so they never trigger a new certificate request. Custom domain routes use machine-level wildcards (*.server-1.example.com).

Requires Cloudflare credentials. Wildcard certs use DNS-01 challenge. Without --cf-dns-token (and optionally --cert-email), Traefik cannot complete the challenge and HTTPS will not work. HTTP remains functional. Configure credentials with rdc config infra set before first deploy.

For Tier 2 routes with traefik.http.routers.{name}.tls.certresolver=letsencrypt, wildcard domain SANs are automatically injected based on the route’s hostname.

The Cloudflare DNS API token needs Zone:DNS:Edit permission for the domains you want to secure.

TLS Certificate Lifecycle

The full path a Let’s Encrypt cert takes from issuance to each repo’s containers:

  1. Issuance on the host. A machine-level Traefik container (rediacc-proxy, deployed to /opt/rediacc/proxy/) owns ACME renewal. It stores all state in /opt/rediacc/proxy/letsencrypt/acme.json on the host. Renewal triggers automatically ~30 days before expiry; no operator action needed as long as --cf-dns-token is configured.

  2. Per-repo dumping (optional). Services that need cert files inside their own container (for example, a mail server that reads a .pem directly) deploy a small traefik-certs-dumper container alongside themselves. The dumper bind-mounts /opt/rediacc/proxy/letsencrypt read-only and writes the extracted cert + key into the repo’s data volume as cert.pem / key.pem. For this to work, the per-repo Docker daemon must have /opt/rediacc/proxy in its mount-namespace allowlist. This is already included by default.

  3. Client-side cache (rediacc.json). The CLI caches a compressed copy of acme.json under acmeCertCache in your config file, keyed by baseDomain. This lets multiple machines share certs (via rdc config cert-cache push -m <machine>) and acts as an offline inventory.

Sync triggers for the client cache:

  • Automatically after rdc repo up, but only if the local cache for the machine’s baseDomain is older than 6 hours. Fresh caches are left alone so back-to-back deploys don’t thrash SSH.
  • On demand: rdc config cert-cache pull -m <machine> (force pull) or rdc machine query --name <machine> --sync-certs (pull as a side effect of a status query).
  • On rdc config infra push, the cache is pushed up to the machine (local certs with longer expiry win over remote).

Cache maintenance:

  • Stale auto-route entries (old network-ID tagged domains like service-3200.rediacc.io) are pruned during every pull.
  • Certs whose notAfter is more than 7 days in the past are removed outright. They’re inert and only bloat the cache.
  • rdc config cert-cache clear wipes everything; rdc config cert-cache status shows the inventory.

Troubleshooting: if traefik-certs-dumper crashloops with /traefik/acme.json: no such file or directory, the per-repo daemon cannot see the host’s letsencrypt store. Verify (a) /opt/rediacc/proxy/letsencrypt/acme.json exists on the host (this is the responsibility of the host-level rediacc-proxy), and (b) the per-repo daemon was started with a recent enough renet that allowlists /opt/rediacc/proxy. Redeploy the repo with rdc repo up after upgrading renet to apply.

Experimental: The auto-sync cadence and expiry-based pruning shipped in renet 0.9+. Older CLI/renet versions use purely manual sync via rdc config cert-cache pull.

TCP/UDP Port Forwarding

For non-HTTP protocols (mail servers, DNS, databases exposed externally), use TCP/UDP port forwarding.

Step 1: Register Ports

Add the required ports during infrastructure configuration:

rdc config infra set -m server-1 \
  --tcp-ports 25,143,465,587,993 \
  --udp-ports 53

rdc config infra push -m server-1

This creates Traefik entrypoints named tcp-{port} and udp-{port}.

After adding or removing ports, always re-run rdc config infra push to update the proxy configuration.

Step 2: Add TCP/UDP Labels

Use traefik.tcp.* or traefik.udp.* labels in your compose file:

services:
  mail-server:
    image: ghcr.io/docker-mailserver/docker-mailserver:latest
    labels:
      - "traefik.enable=true"

      # SMTP (port 25)
      - "traefik.tcp.routers.mail-smtp.entrypoints=tcp-25"
      - "traefik.tcp.routers.mail-smtp.rule=HostSNI(`*`)"
      - "traefik.tcp.routers.mail-smtp.service=mail-smtp"
      - "traefik.tcp.services.mail-smtp.loadbalancer.server.port=25"

      # IMAPS (port 993), TLS passthrough
      - "traefik.tcp.routers.mail-imaps.entrypoints=tcp-993"
      - "traefik.tcp.routers.mail-imaps.rule=HostSNI(`mail.example.com`)"
      - "traefik.tcp.routers.mail-imaps.tls.passthrough=true"
      - "traefik.tcp.routers.mail-imaps.service=mail-imaps"
      - "traefik.tcp.services.mail-imaps.loadbalancer.server.port=993"

Key concepts:

  • HostSNI(\*`)` matches any hostname (for protocols that don’t send SNI, like plain SMTP)
  • tls.passthrough=true means Traefik forwards the raw TLS connection without decrypting, the application handles TLS itself
  • Entrypoint names follow the convention tcp-{port} or udp-{port}

Plain TCP Example (Database)

To expose a database externally without TLS passthrough (Traefik forwards raw TCP):

services:
  postgres:
    image: postgres:17
    labels:
      - "traefik.enable=true"
      - "traefik.tcp.routers.mydb.entrypoints=tcp-5432"
      - "traefik.tcp.routers.mydb.rule=HostSNI(`*`)"
      - "traefik.tcp.services.mydb.loadbalancer.server.port=5432"

Port 5432 is pre-configured (see below), so no --tcp-ports setup is needed.

Security note: Exposing a database to the internet is a risk. Use this only when remote clients need direct access. For most setups, keep the database internal and connect through your application.

Pre-Configured Ports

The following TCP/UDP ports have entrypoints by default (no need to add via --tcp-ports). Entrypoints are only generated for configured address families, IPv4 entrypoints require --public-ipv4, IPv6 entrypoints require --public-ipv6:

PortProtocolCommon Use
80HTTPWeb (auto-redirect to HTTPS)
443HTTPSWeb (TLS)
3306TCPMySQL/MariaDB
5432TCPPostgreSQL
6379TCPRedis
27017TCPMongoDB
11211TCPMemcached
5672TCPRabbitMQ
9092TCPKafka
53UDPDNS
10000–10010TCPDynamic range (auto-allocation)

DNS Configuration

Automatic DNS (Cloudflare)

When --cf-dns-token is configured, rdc config infra push automatically creates DNS records for the machine subdomain in Cloudflare:

RecordTypeContentCreated by
server-1.example.comA / AAAAMachine public IPpush-infra
*.server-1.example.comA / AAAAMachine public IPpush-infra
*.marketing.server-1.example.comA / AAAAMachine public IPrepo up

Machine-level records are created by push-infra and cover custom domain routes (rediacc.domain). Per-repo wildcard records are created automatically by repo up and cover auto-routes for that repository.

This is idempotent, existing records are updated if the IP changes, and left unchanged if already correct.

The base domain wildcard (*.example.com) must be created manually if you use custom domain labels like rediacc.domain=erp.

Manual DNS

If not using Cloudflare or managing DNS manually, create A (IPv4) and/or AAAA (IPv6) records:

# Machine subdomain (for custom domain routes like rediacc.domain=erp)
server-1.example.com           A     203.0.113.50
*.server-1.example.com         A     203.0.113.50
*.server-1.example.com         AAAA  2001:db8::1

# Per-repo wildcards (for auto-routes like myapp.marketing.server-1.example.com)
*.marketing.server-1.example.com    A     203.0.113.50
*.marketing.server-1.example.com    AAAA  2001:db8::1

# Base domain wildcard (for custom domain services like rediacc.domain=erp)
*.example.com                  A     203.0.113.50

With Cloudflare DNS configured, per-repo wildcard records are created automatically by repo up. With multiple machines, each machine gets its own DNS records pointing to its own IP.

Middlewares

Traefik middlewares modify requests and responses. Apply them via labels.

HSTS (HTTP Strict Transport Security)

labels:
  - "traefik.http.middlewares.myapp-hsts.headers.stsSeconds=15768000"
  - "traefik.http.middlewares.myapp-hsts.headers.stsIncludeSubdomains=true"
  - "traefik.http.middlewares.myapp-hsts.headers.stsPreload=true"
  - "traefik.http.routers.myapp.middlewares=myapp-hsts"

Large File Upload Buffering

labels:
  - "traefik.http.middlewares.myapp-buffering.buffering.maxRequestBodyBytes=536870912"
  - "traefik.http.routers.myapp.middlewares=myapp-buffering"

Multiple Middlewares

Chain middlewares by comma-separating them:

labels:
  - "traefik.http.routers.myapp.middlewares=myapp-hsts,myapp-buffering"

For the full list of available middlewares, see the Traefik middleware documentation.

Diagnostics

If a service is not accessible, SSH into the server and check the route server endpoints:

Health Check

curl -s http://127.0.0.1:7111/health | python3 -m json.tool

Shows overall status, number of discovered routers and services, and whether auto-routes are enabled.

Discovered Routes

curl -s http://127.0.0.1:7111/routes.json | python3 -m json.tool

Lists all HTTP, TCP, and UDP routers with their rules, entrypoints, and backend services.

Port Allocations

curl -s http://127.0.0.1:7111/ports | python3 -m json.tool

Shows TCP and UDP port mappings for dynamically allocated ports.

Common Issues

ProblemCauseSolution
Service not in routesContainer not running or missing labelsVerify with docker ps on the repository’s daemon; check labels
Certificate not issuedDNS not pointing to server, or invalid Cloudflare tokenVerify DNS resolution; check Cloudflare API token permissions
502 Bad GatewayApplication not listening on the declared portVerify the app is running and the port matches loadbalancer.server.port
TCP port not reachablePort not registered in infrastructureRun rdc config infra set --tcp-ports ... and push-infra
Route server running old versionBinary was updated but service not restartedHappens automatically on provisioning; manual: sudo systemctl restart rediacc-router
STUN/TURN relay not reachableRelay addresses cached at startupRecreate the service after DNS or IP changes so it picks up the new network config

Complete Example

This deploys a web application with a PostgreSQL database. The app is publicly accessible at app.example.com with TLS; the database is internal only.

docker-compose.yml

services:
  webapp:
    image: myregistry/webapp:latest
    environment:
      DATABASE_URL: postgresql://app:changeme@postgres:5432/webapp
      LISTEN_ADDR: 0.0.0.0:3000
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.webapp.rule=Host(`app.example.com`)"
      - "traefik.http.routers.webapp.entrypoints=websecure,websecure-v6"
      - "traefik.http.routers.webapp.tls.certresolver=letsencrypt"
      - "traefik.http.services.webapp.loadbalancer.server.port=3000"
      # HSTS
      - "traefik.http.middlewares.webapp-hsts.headers.stsSeconds=15768000"
      - "traefik.http.middlewares.webapp-hsts.headers.stsIncludeSubdomains=true"
      - "traefik.http.routers.webapp.middlewares=webapp-hsts"

  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: webapp
      POSTGRES_USER: app
      POSTGRES_PASSWORD: changeme
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    # No traefik labels, internal only

Rediaccfile

#!/bin/bash

up() {
    mkdir -p data/postgres
    renet compose -- up -d
}

down() {
    renet compose -- down
}

DNS

Create an A record pointing app.example.com to your server’s public IP:

app.example.com   A   203.0.113.50

Deploy

rdc repo up --name my-app -m server-1

Within a few seconds, the route server discovers the container, Traefik picks up the route, requests a TLS certificate, and https://app.example.com is live.