Skip to content

Traefik Routing (Auto-HTTPS)

By default Oduflow uses port mode: each environment gets a dedicated host port (e.g. http://server:50001). This is simple and works well for local or single-developer setups.

For production-like access with HTTPS, Oduflow can deploy a Traefik reverse proxy that gives every environment its own subdomain with an automatically issued Let's Encrypt certificate.

Setup

  1. Configure a wildcard DNS record. Point *.dev.example.com to your server's IP address:
*.dev.example.com  →  A  →  203.0.113.10

Every environment will get a subdomain: feature-login.dev.example.com, fix-invoice.dev.example.com, etc.

  1. Set the configuration in oduflow.toml:
[routing]
mode = "traefik"
acme_email = "admin@example.com"

[team.1]
hostname = "dev.example.com"
environment_slots = 20
environment_hostname_mode = "branch"
  1. Start (or restart) Oduflow. On startup, Oduflow will create a Traefik v3 container that:
  2. Listens on ports 80 and 443
  3. Automatically redirects HTTP to HTTPS
  4. Obtains a TLS certificate from Let's Encrypt for each routed hostname via HTTP-01 challenge
  5. Routes requests to the correct Odoo container based on the subdomain
  6. Also routes the Oduflow server itself via the team hostname

Hostname and certificate strategies

The default environment_hostname_mode = "branch" keeps descriptive and backward-compatible routes: feature-login.dev.example.com, fix-invoice.dev.example.com, and so on. environment_slots = 20 limits how many environments may exist but does not change those names. This mode matches the *.dev.example.com DNS record shown above and works with a Cloudflare or other wildcard certificate for *.dev.example.com.

For Traefik HTTP-01 installations that need to bound Let's Encrypt issuance, opt into a reusable pool:

[team.1]
hostname = "dev.example.com"
environment_slots = 20
environment_hostname_mode = "slots"

Oduflow then allocates dev1.example.com through dev20.example.com and returns names to the pool on deletion. These names are one DNS level higher than branch-derived routes: configure individual dev1dev20 records or a wildcard record for *.example.com. A *.dev.example.com record or certificate does not cover dev1.example.com.

create_environment(hostname="qa") requests qa.example.com in either mode. The configured team hostname must include a distinct prefix (dev.example.com, not bare example.com) for pooled or explicit short names.

OAuth on each team's hostname

The self-hosted OAuth Authorization Server is enabled automatically whenever a team has an auth_token and runs on each team's own hostname in every routing mode. With Traefik, the incoming host already has a Let's Encrypt certificate; with tls = false, the upstream tunnel provides it. There is no separate OAuth section: point Claude.ai at https://<team-hostname>/mcp and complete the OAuth flow there.

Service routing with Traefik

Auxiliary services also get Traefik routing. A service named meilisearch with base domain dev.example.com becomes accessible at https://meilisearch.dev.example.com. Custom hostnames are also supported.

Routing extra domains to external services

Traefik in Oduflow can also forward a domain to a service that Oduflow does not manage — another Docker container, a process on the host, or a machine elsewhere. There are two ways, from simplest to most flexible.

1. Declarative routes in oduflow.toml

For the common "this hostname → that URL" case, add a [route.<name>] section:

[routing]
mode = "traefik"
acme_email = "admin@example.com"

[team.1]
hostname = "dev.example.com"

[route.legacy-api]
host = "api.example.com"
url  = "http://127.0.0.1:3000"

On the next start Oduflow generates a Traefik router for api.example.com and forwards it to http://127.0.0.1:3000. In TLS mode the route gets its own Let's Encrypt certificate (point the domain's DNS at this server first), exactly like a team hostname; behind a tls = false upstream it is served over plain HTTP on port 80.

Notes:

  • 127.0.0.1 / localhost mean "on the Docker host". Traefik runs in a container, so Oduflow rewrites an http:// loopback upstream to host.docker.internal (mapped to the host gateway). So http://127.0.0.1:3000 reaches a service listening on port 3000 of the host. Use the real IP/hostname for anything off the host. An https://localhost upstream is not rewritten (that would break backend TLS certificate verification) — for a TLS backend on the host, use its real hostname or a drop-in dynamic file with a serversTransport.
  • url must be http://… or https://…; host must be a plain hostname (no path) and unique across all routes and team hostnames.
  • These routes are declared once in config; the generated router set is rewritten on every restart, so hand-editing the generated file is pointless (use option 2 for custom Traefik config).

2. Drop-in Traefik dynamic files

For anything the simple host → url form can't express — middleware, header rewrites, custom TLS options, sticky sessions, multiple services — Oduflow mounts a dynamic-config directory that Traefik watches:

  • On the host it is <config-dir>/traefik-dynamic//etc/oduflow/traefik-dynamic/ when writable, otherwise ~/.oduflow/conf/traefik-dynamic/.
  • Oduflow writes and overwrites only oduflow.yml there (its own routers). Any other *.yml/*.yaml/*.toml file you place in that directory is loaded by Traefik and never touched by Oduflow — it survives restarts and upgrades.

For example, <config-dir>/traefik-dynamic/custom.yml:

http:
  routers:
    my-app:
      rule: "Host(`app.example.com`)"
      entryPoints: ["websecure"]
      tls:
        certResolver: letsencrypt
      service: my-app
      middlewares: ["my-headers"]
  middlewares:
    my-headers:
      headers:
        customRequestHeaders:
          X-Forwarded-Proto: "https"
  services:
    my-app:
      loadBalancer:
        servers:
          - url: "http://host.docker.internal:9000"

Traefik picks it up within a second (no restart needed). This is the full Traefik file-provider dynamic configuration, so use it when you outgrow the declarative routes above.

Behind a Cloudflare tunnel (or other TLS-terminating upstream)

If HTTPS is terminated upstream — for example by a Cloudflare tunnel (cloudflared) that already serves a valid certificate — Traefik should not obtain its own certificates or redirect to HTTPS. Set tls = false:

[routing]
mode = "traefik"
tls = false          # Traefik listens on plain HTTP :80 only

[team.1]
hostname = "dev.example.com"

With tls = false Traefik:

  • Listens on port 80 only (443 is not published), serving plain HTTP.
  • Does not redirect HTTP→HTTPS and does not request Let's Encrypt certificates (acme_email is not required).
  • Routes by the same Host rules as before, so each environment keeps its own subdomain.

Point the tunnel at the server's port 80 and route the wildcard hostname to it (e.g. *.dev.example.com → http://localhost:80). Cloudflare provides the certificate and forwards requests over HTTP; the tunnel sets X-Forwarded-Proto: https. On the web entrypoint Oduflow enables forwardedHeaders.insecure so Traefik passes those headers through (by default Traefik would overwrite X-Forwarded-Proto with the plain-HTTP connection scheme), letting Oduflow see the request as secure — the dashboard's session cookie stays Secure and every environment/service URL Oduflow reports is still https://…. Because this entrypoint trusts all forwarded headers, expose port 80 only to the tunnel, not to the public internet.

Changing tls on a running deployment recreates Traefik but not your environments. Each environment and service bakes its Traefik routing labels in at creation time — entrypoints=websecure (with Let's Encrypt) when tls = true, entrypoints=web when tls = false. Restarting Oduflow recreates the Traefik container in the new mode, but pre-existing environments and services keep their old labels: after the switch their routers point at an entrypoint that no longer matches, so they become unreachable until recreated (or, going false → true, get caught by the HTTP→HTTPS redirect). Treat tls as a deploy-time choice; if you must flip it on a live server, recreate the existing environments and services afterwards. The default is tls = true (Traefik terminates TLS with Let's Encrypt, as described above).

Plain HTTP, with nothing terminating TLS

tls = false on its own means "someone else terminates TLS", so the links Oduflow hands out — environment and service URLs, dashboard share links, MCP endpoints — stay https://. If there is no such upstream (a LAN box, an internal staging server, a local demo), those links point at an endpoint nobody serves. Say so explicitly with public_scheme:

[routing]
mode = "traefik"
tls = false
public_scheme = "http"   # no TLS anywhere: hand out http:// links

[team.1]
hostname = "dev.example.com"

public_scheme sets the scheme of every URL Oduflow reports; it defaults to https in traefik mode and http in port mode, and is independent of tls (which only decides whether Traefik terminates TLS). It also switches off forwardedHeaders.insecure on the web entrypoint: with no trusted terminator in front, that entrypoint is directly reachable and must not believe a client-supplied X-Forwarded-Proto.

Changing public_scheme recreates the Traefik container (the forwarded-headers argument changes) but not your environments — only the reported URLs change, so no environment or service needs recreating.

Plain HTTP is unencrypted

Odoo logins, session cookies, the dashboard password and MCP bearer tokens all travel in cleartext. Use this only on a trusted network, never on a public-facing server.