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
+6 -3
View File
@@ -158,9 +158,12 @@ Everything in this section is **outside git**. Do it as root over SSH, in this o
The combined host is NOT on the tailnet (operator decision 2026-09-07): ssh is key-only on the The combined host is NOT on the tailnet (operator decision 2026-09-07): ssh is key-only on the
public IP, the CI front doors are public via traefik, and the opencode UI is public on the same public IP, the CI front doors are public via traefik, and the opencode UI is public on the same
443 — traefik routes `oc.ci.commoninternet.net` (a dynamic-config file that 443 — traefik routes `oc.ci.commoninternet.net` to an nginx vhost on the docker bridge that
`opencode-ui-route.service` drops into the traefik stack's file-providers volume) to an nginx enforces basic auth, via traefik labels on a tiny swarm relay stack (`opencode-ui`) that
vhost on the docker bridge that enforces basic auth. fail2ban guards sshd and that login `opencode-ui-route.service` deploys. (Not via traefik's file *directory* provider: enabling
that in the coop-cloud traefik recipe REPLACES its provider file, which holds the wildcard cert
and the `security` middleware, and every front door goes down — it did, for two minutes, on
2026-09-07.) fail2ban guards sshd and that login
(`nix/hosts/cc-ci/configuration.nix`; the nginx jail bans in the DOCKER-USER chain because the (`nix/hosts/cc-ci/configuration.nix`; the nginx jail bans in the DOCKER-USER chain because the
traffic is docker-forwarded, not host INPUT). traffic is docker-forwarded, not host INPUT).
+1 -2
View File
@@ -29,8 +29,7 @@
cc-ci-orchestrator.ciSshHost = "127.0.0.1"; cc-ci-orchestrator.ciSshHost = "127.0.0.1";
# The opencode UI: traefik (public 443, the *.ci.commoninternet.net cert) → nginx basic auth. # The opencode UI: traefik (public 443, the *.ci.commoninternet.net cert) → nginx basic auth.
cc-ci-orchestrator.opencodeUiHost = "oc.ci.commoninternet.net"; cc-ci-orchestrator.opencodeUiHost = "oc.ci.commoninternet.net";
cc-ci-orchestrator.opencodeUiTraefikProvidersDir = cc-ci-orchestrator.opencodeUiTraefikNetwork = "proxy";
"/var/lib/docker/volumes/traefik_ci_commoninternet_net_file-providers/_data";
# ---- no tailscale on this host (operator 2026-09-07) -------------------------------------- # ---- no tailscale on this host (operator 2026-09-07) --------------------------------------
# Everything is reached over the public IP: ssh (keys only), the CI front doors via traefik, # Everything is reached over the public IP: ssh (keys only), the CI front doors via traefik,
+36 -28
View File
@@ -40,17 +40,18 @@ in
''; '';
}; };
opencodeUiTraefikProvidersDir = lib.mkOption { opencodeUiTraefikNetwork = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "/var/lib/docker/volumes/traefik_ci_commoninternet_net_file-providers/_data"; example = "proxy";
description = '' description = ''
Host path of the traefik stack's file-providers directory. When set, a dynamic-config Name of the swarm overlay network the cc-ci traefik watches (cc-ci's swarm.nix creates
file routing opencodeUiHost on the `web-secure` entrypoint to nginx is written there after `proxy`). When set, a one-container swarm stack `opencode-ui` (a socat TCP relay to
deploy-proxy. Traefik only watches that directory when the coop-cloud traefik recipe is nginx on the docker bridge) is deployed with traefik labels routing opencodeUiHost on
deployed with FILE_PROVIDER_DIRECTORY_ENABLED=1 in its app env the `web-secure` entrypoint the same label mechanism every cc-ci service and recipe
(/root/.abra/servers/default/<traefik domain>.env) see README "Stage the workspace". uses, so it coexists with the traefik recipe's own file provider (the wildcard cert and
null = no traefik route. 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. # Only docker's bridge may reach the plain-HTTP backend; the public interface stays closed.
networking.firewall.interfaces.docker_gwbridge.allowedTCPPorts = [ cfg.opencodeUiBackendPort ]; 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. # The traefik side of the route: a swarm service carrying the router labels. It is a plain
systemd.services.opencode-ui-route = lib.mkIf (cfg.opencodeUiTraefikProvidersDir != null) { # TCP relay (socat) from the overlay network to nginx on the docker bridge; traefik's
description = "traefik route: ${cfg.opencodeUiHost} -> nginx (opencode UI basic auth)"; # 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" ]; wantedBy = [ "multi-user.target" ];
after = [ "deploy-proxy.service" "docker.service" ]; after = [ "deploy-proxy.service" "docker.service" "nginx.service" ];
wants = [ "deploy-proxy.service" ]; wants = [ "deploy-proxy.service" ];
path = [ pkgs.docker ];
serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; };
script = '' script = ''
install -d -m 0755 "${cfg.opencodeUiTraefikProvidersDir}" docker stack deploy --detach=true -c ${pkgs.writeText "opencode-ui-stack.yml" ''
install -m 0644 ${pkgs.writeText "opencode-ui.yml" '' # Deployed by opencode-ui-route.service (nix/modules/orchestrator-host.nix). Do not edit.
# Written by opencode-ui-route.service (nix/modules/orchestrator-host.nix). Do not edit. version: "3.8"
http: services:
routers: relay:
opencode-ui: image: alpine/socat:1.8.0.3
rule: Host(`${cfg.opencodeUiHost}`) command: ["TCP-LISTEN:${toString cfg.opencodeUiBackendPort},fork,reuseaddr", "TCP:172.18.0.1:${toString cfg.opencodeUiBackendPort}"]
entryPoints: [web-secure] networks: [ ${cfg.opencodeUiTraefikNetwork} ]
service: opencode-ui deploy:
tls: {} replicas: 1
services: labels:
opencode-ui: - "traefik.enable=true"
loadBalancer: - "traefik.http.routers.opencode-ui.rule=Host(`${cfg.opencodeUiHost}`)"
servers: - "traefik.http.routers.opencode-ui.entrypoints=web-secure"
- url: http://172.18.0.1:${toString cfg.opencodeUiBackendPort} - "traefik.http.routers.opencode-ui.tls=true"
''} "${cfg.opencodeUiTraefikProvidersDir}/opencode-ui.yml" - "traefik.http.services.opencode-ui.loadbalancer.server.port=${toString cfg.opencodeUiBackendPort}"
networks:
${cfg.opencodeUiTraefikNetwork}:
external: true
''} opencode-ui
''; '';
}; };
}; };