opencode UI route via traefik labels on a swarm relay, not the file-directory provider

Enabling FILE_PROVIDER_DIRECTORY_ENABLED in the coop-cloud traefik recipe
REPLACES its single provider file (wildcard cert + `security` middleware),
which took every cc-ci front door down for two minutes on 2026-09-07.
Reverted. The route is now what every recipe does: a one-container swarm
stack (`opencode-ui`, alpine/socat relay to nginx on the docker bridge)
carrying the traefik router labels, deployed by opencode-ui-route.service.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqkQq3CDmFWcQ7u1LzoyRz
This commit is contained in:
2026-09-07 20:57:36 +00:00
co-authored by Claude Fable 5.1
parent 6cc78cf758
commit 72d4a31910
3 changed files with 43 additions and 33 deletions
+36 -28
View File
@@ -40,17 +40,18 @@ in
'';
};
opencodeUiTraefikProvidersDir = lib.mkOption {
opencodeUiTraefikNetwork = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/docker/volumes/traefik_ci_commoninternet_net_file-providers/_data";
example = "proxy";
description = ''
Host path of the traefik stack's file-providers directory. When set, a dynamic-config
file routing opencodeUiHost on the `web-secure` entrypoint to nginx is written there after
deploy-proxy. Traefik only watches that directory when the coop-cloud traefik recipe is
deployed with FILE_PROVIDER_DIRECTORY_ENABLED=1 in its app env
(/root/.abra/servers/default/<traefik domain>.env) see README "Stage the workspace".
null = no traefik route.
Name of the swarm overlay network the cc-ci traefik watches (cc-ci's swarm.nix creates
`proxy`). When set, a one-container swarm stack `opencode-ui` (a socat TCP relay to
nginx on the docker bridge) is deployed with traefik labels routing opencodeUiHost on
the `web-secure` entrypoint the same label mechanism every cc-ci service and recipe
uses, so it coexists with the traefik recipe's own file provider (the wildcard cert and
the `security` middleware live there; switching traefik to a file *directory* replaces
that file and takes every front door down learned 2026-09-07). null = no route.
'';
};
@@ -223,30 +224,37 @@ SSHCFG
# Only docker's bridge may reach the plain-HTTP backend; the public interface stays closed.
networking.firewall.interfaces.docker_gwbridge.allowedTCPPorts = [ cfg.opencodeUiBackendPort ];
# The traefik side of the route: a dynamic-config file in the stack's watched providers dir.
systemd.services.opencode-ui-route = lib.mkIf (cfg.opencodeUiTraefikProvidersDir != null) {
description = "traefik route: ${cfg.opencodeUiHost} -> nginx (opencode UI basic auth)";
# The traefik side of the route: a swarm service carrying the router labels. It is a plain
# TCP relay (socat) from the overlay network to nginx on the docker bridge; traefik's
# X-Forwarded-For passes through untouched, which is what nginx's real_ip reads.
systemd.services.opencode-ui-route = lib.mkIf (cfg.opencodeUiTraefikNetwork != null) {
description = "swarm stack opencode-ui: traefik labels ${cfg.opencodeUiHost} -> nginx basic auth";
wantedBy = [ "multi-user.target" ];
after = [ "deploy-proxy.service" "docker.service" ];
after = [ "deploy-proxy.service" "docker.service" "nginx.service" ];
wants = [ "deploy-proxy.service" ];
path = [ pkgs.docker ];
serviceConfig = { Type = "oneshot"; RemainAfterExit = true; };
script = ''
install -d -m 0755 "${cfg.opencodeUiTraefikProvidersDir}"
install -m 0644 ${pkgs.writeText "opencode-ui.yml" ''
# Written by opencode-ui-route.service (nix/modules/orchestrator-host.nix). Do not edit.
http:
routers:
opencode-ui:
rule: Host(`${cfg.opencodeUiHost}`)
entryPoints: [web-secure]
service: opencode-ui
tls: {}
services:
opencode-ui:
loadBalancer:
servers:
- url: http://172.18.0.1:${toString cfg.opencodeUiBackendPort}
''} "${cfg.opencodeUiTraefikProvidersDir}/opencode-ui.yml"
docker stack deploy --detach=true -c ${pkgs.writeText "opencode-ui-stack.yml" ''
# Deployed by opencode-ui-route.service (nix/modules/orchestrator-host.nix). Do not edit.
version: "3.8"
services:
relay:
image: alpine/socat:1.8.0.3
command: ["TCP-LISTEN:${toString cfg.opencodeUiBackendPort},fork,reuseaddr", "TCP:172.18.0.1:${toString cfg.opencodeUiBackendPort}"]
networks: [ ${cfg.opencodeUiTraefikNetwork} ]
deploy:
replicas: 1
labels:
- "traefik.enable=true"
- "traefik.http.routers.opencode-ui.rule=Host(`${cfg.opencodeUiHost}`)"
- "traefik.http.routers.opencode-ui.entrypoints=web-secure"
- "traefik.http.routers.opencode-ui.tls=true"
- "traefik.http.services.opencode-ui.loadbalancer.server.port=${toString cfg.opencodeUiBackendPort}"
networks:
${cfg.opencodeUiTraefikNetwork}:
external: true
''} opencode-ui
'';
};
};