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¶
- Configure a wildcard DNS record. Point
*.dev.example.comto your server's IP address:
Every environment will get a subdomain: feature-login.dev.example.com, fix-invoice.dev.example.com, etc.
- 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"
- Start (or restart) Oduflow. On startup, Oduflow will create a Traefik v3 container that:
- Listens on ports 80 and 443
- Automatically redirects HTTP to HTTPS
- Obtains a TLS certificate from Let's Encrypt for each routed hostname via HTTP-01 challenge
- Routes requests to the correct Odoo container based on the subdomain
- 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:
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 dev1…dev20 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/localhostmean "on the Docker host". Traefik runs in a container, so Oduflow rewrites anhttp://loopback upstream tohost.docker.internal(mapped to the host gateway). Sohttp://127.0.0.1:3000reaches a service listening on port 3000 of the host. Use the real IP/hostname for anything off the host. Anhttps://localhostupstream 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 aserversTransport.urlmust behttp://…orhttps://…;hostmust 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.ymlthere (its own routers). Any other*.yml/*.yaml/*.tomlfile 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_emailis not required). - Routes by the same
Hostrules 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
tlson 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) whentls = true,entrypoints=webwhentls = 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, goingfalse → true, get caught by the HTTP→HTTPS redirect). Treattlsas a deploy-time choice; if you must flip it on a live server, recreate the existing environments and services afterwards. The default istls = 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.