Dokploy Traefik Traffic Split logo

Dokploy Traefik Traffic Split

Community
Innei
dokploy-traefik-traffic-split

Use when you need to canary-deploy two HTTP backends on the same Dokploy-managed domain with a percentage split (e.g. migrating an SPA from one framework to another). Covers the critical SPA-asset trap that breaks naive sticky-cookie splits, a path-aware Traefik config that solves it, and a four-phase workflow (backup → dry-run on a canary host → roll to prod → quick rollback).

Overview

PublisherInnei
RepositorySKILL
Skill namedokploy-traefik-traffic-split
Stars
81
Forks
2
Bundled files
Instructions only
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by Innei on GitHub. Read the source before you install it.

Installation

Install the Dokploy Traefik Traffic Split AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/Innei/SKILL.git /tmp/SKILL
mkdir -p .claude/skills
cp -r /tmp/SKILL/skills/infrastructure/dokploy-traefik-traffic-split .claude/skills/dokploy-traefik-traffic-split
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Dokploy Traefik Traffic Split in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Dokploy Traefik Traffic Split on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Dokploy Traefik Traffic Split is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Dokploy Traefik Traffic Split for SPA Migrations

Canary two HTTP backends (e.g. an old Next.js app and a new Remix app) under one domain with a weighted split that survives SPAs with different build-output paths.

When to use

  • Migrating a frontend between frameworks where build outputs live on different URL prefixes (/_next/* vs /assets/*, /build/*, etc.)
  • Running A/B between two long-running services already deployed in Dokploy
  • Any percentage-based traffic split on Dokploy's bundled Traefik where you also need session stickiness

If both backends are byte-identical builds behind the same image, a plain Dokploy "Replicas" bump is enough — this skill is overkill.

Core insight: the SPA asset trap

A naive sticky-cookie weighted split breaks SPAs with divergent asset paths.

  1. Browser hits /, no cookie, Traefik weighted picks lane A → returns HTML referencing /assets/<hash>.js plus a Set-Cookie.
  2. Browser's preload scanner fires 30+ asset requests in parallel. Some land at Traefik before the cookie is reliably attached (HTTP/2 stream ordering, request coalescing, or the browser hasn't committed the cookie yet).
  3. Cookieless asset requests get re-rolled by Traefik's weight → some land at lane B.
  4. Lane B has no /assets/<hash>.js → 404 storm, blank page.

sticky.cookie alone cannot fix this — the race is structural.

Fix: add path-prefix routers at higher priority. Any request whose path uniquely belongs to one backend bypasses the weighted service entirely and goes straight to that backend. Only document-class requests (HTML, feeds, API, shared paths) run through the weighted+sticky service.

Architecture

Host(`yourdomain.com`)
├─ priority 200: PathPrefix(`/_next/`)    → next-backend     (always)
├─ priority 200: PathPrefix(`/assets/`)   → remix-backend    (always)
└─ priority 100: (no path filter)         → weighted+sticky  (50/50, 90/10, ...)
                                            ├─ next-backend
                                            └─ remix-backend

The weighted service writes the sticky cookie on its responses (typically HTML). Path-forced routers never touch the weighted service, so they neither read nor write the cookie — and their responses don't depend on it, by design.

Why SSH-scp the file instead of Dokploy's API

POST /api/settings.updateTraefikFile is the documented path but has bitten us twice:

  • It does not accept an empty string when you want to disable a file (Zod rejects traefikConfig of length 0).
  • A YAML that parses but fails Traefik's schema validation (e.g. http: {} as a "neutral" placeholder) makes Traefik's file provider refuse to build any dynamic config — every router on the host disappears, including the Dokploy panel itself. The panel is then unreachable to fix it. You need SSH access to recover.

Push files with scp and delete with rm. Traefik watches the directory and reloads in seconds.

Procedure

Phase 0 — prerequisites and backup

bash
# Discover the two backends' internal hostnames and ports
curl -sS -H "x-api-key: $TOKEN" "https://<dokploy>/api/environment.one?environmentId=$ENV_ID" \
  | jq '.applications[] | {name, appName, port:(.domains[0].port // null)}'

# Backup everything you might touch
BAK=/tmp/traefik-backup-$(date +%Y%m%d-%H%M%S)
mkdir -p $BAK
ssh root@$HOST 'cat /etc/dokploy/traefik/dynamic/*.yml' > $BAK/all-dynamic.txt
ssh root@$HOST 'tar c /etc/dokploy/traefik/dynamic' > $BAK/dynamic.tar

Backend internal hostnames are the Dokploy appName (auto-generated, e.g. mxspace-shiroi-jpdr7k). They resolve inside Dokploy's docker network.

Phase 1 — dry-run on a canary host

Never test on the production hostname. Use a sibling subdomain that nothing depends on.

  1. Add canary.yourdomain.com as a DNS record pointing at the Dokploy server (proxied via CDN is fine).
  2. Write a dynamic file with Host(\canary.yourdomain.com`)`:
yaml
# /etc/dokploy/traefik/dynamic/canary-host.yml
http:
  routers:
    canary-next-static:
      rule: "Host(`canary.yourdomain.com`) && PathPrefix(`/_next/`)"
      service: canary-next-backend
      entryPoints: [websecure]
      tls: { certResolver: letsencrypt }
      priority: 200
    canary-remix-assets:
      rule: "Host(`canary.yourdomain.com`) && PathPrefix(`/assets/`)"
      service: canary-remix-backend
      entryPoints: [websecure]
      tls: { certResolver: letsencrypt }
      priority: 200
    canary-secure:
      rule: "Host(`canary.yourdomain.com`)"
      service: canary-weighted
      entryPoints: [websecure]
      tls: { certResolver: letsencrypt }
      priority: 100
    canary-web:
      rule: "Host(`canary.yourdomain.com`)"
      service: canary-weighted
      middlewares: [redirect-to-https]
      entryPoints: [web]
      priority: 100
  services:
    canary-weighted:
      weighted:
        sticky:
          cookie:
            name: canary_lane
            secure: true
            httpOnly: true
            sameSite: lax
        services:
          - { name: canary-next-backend,  weight: 50 }
          - { name: canary-remix-backend, weight: 50 }
    canary-next-backend:
      loadBalancer:
        passHostHeader: true
        servers: [{ url: "http://<next-appName>:<port>" }]
    canary-remix-backend:
      loadBalancer:
        passHostHeader: true
        servers: [{ url: "http://<remix-appName>:<port>" }]
  1. Push and watch logs:
bash
scp canary-host.yml root@$HOST:/etc/dokploy/traefik/dynamic/
ssh root@$HOST 'docker logs --since 30s dokploy-traefik 2>&1 | grep -iE "error|canary"'
# Expect: no errors. If you see "http cannot be a standalone element"
# or schema errors, fix the YAML and re-scp — Traefik will retry on its own.
  1. Verify three things:
bash
# (a) Cookieless distribution matches weights
for i in $(seq 1 20); do
  curl -sS "https://canary.yourdomain.com/?cb=$RANDOM" \
    | grep -oE "<distinguishing marker per backend>" | head -1
done | sort | uniq -c

# (b) Path-forced override: cross-cookie should still hit the right backend
curl -sS -o /dev/null -w "%{http_code}\n" \
  -H "cookie: canary_lane=<lane-A-hash>" \
  "https://canary.yourdomain.com/assets/<real-remix-file>.js"
# → 200 even though cookie says lane A

# (c) Sticky on document paths
COOKIE=$(curl -sS -i "https://canary.yourdomain.com/" \
  | grep -i "^set-cookie: canary_lane" | grep -oE "canary_lane=[a-f0-9]+")
for i in 1 2 3 4 5; do
  curl -sS -H "cookie: $COOKIE" "https://canary.yourdomain.com/" \
    | grep -oE "<distinguishing marker>"
done
# → 5/5 same backend
  1. Open the canary URL in a real browser, DevTools Network panel open, hard-refresh, navigate a few internal routes. Watch for any /some-path/*.js → 404. If anything 404s, your asset prefix list is incomplete — add another path-forced router.

Phase 2 — promote to production

Once canary passes for at least a few hours of organic traffic:

  1. Copy the canary file, rename, change the host:
bash
sed 's|canary.yourdomain.com|yourdomain.com|g; s|canary-|prod-|g; s|canary_lane|prod_lane|g' \
  canary-host.yml > prod-host.yml
  1. Start with a conservative weight — 90/10 stable side first.

  2. Push:

bash
scp prod-host.yml root@$HOST:/etc/dokploy/traefik/dynamic/
  1. Leave the old Dokploy-managed Application domain in place as a fallback. The new file's priority: 100 outranks the default (Traefik priority defaults to rule length, ~17 for a Host-only rule), so the weighted router wins. If your weighted file ever breaks, Traefik silently falls back to the lower-priority Dokploy router → 100% old backend. This is the kind of safety net you want during a migration.

  2. Verify externally (via CDN):

bash
bash -c '
a=0; b=0
for i in $(seq 1 20); do
  m=$(curl -sS "https://yourdomain.com/?cb=$RANDOM-$i" \
        | grep -oE "<marker A>|<marker B>" | head -1)
  case "$m" in
    "<marker A>") a=$((a+1));;
    "<marker B>") b=$((b+1));;
  esac
done
echo "A=$a B=$b"
'
  1. Purge the CDN cache for the host (Cloudflare: dashboard → Caching → Purge → https://yourdomain.com/*). Otherwise the CDN may serve stale HTML from before the split, hiding the change for hours.

Phase 3 — adjust weight

Either edit on the server (fastest) or scp a new file:

bash
ssh root@$HOST 'sed -i \
  "s/weight: 90$/weight: 70/; s/weight: 10$/weight: 30/" \
  /etc/dokploy/traefik/dynamic/prod-host.yml'

Traefik picks the change up within ~5 seconds. No restart required.

Cadence rule of thumb: 90/10 → observe a day → 70/30 → observe a day → 50/50 → observe → 10/90 → 100/0 then delete the file.

Phase 4 — rollback (instant)

bash
ssh root@$HOST 'rm /etc/dokploy/traefik/dynamic/prod-host.yml'

Traefik immediately falls back to the Dokploy-managed Application router (100% the original backend). No data lost, no certs to re-issue. If you also need to clear in-flight stickied users, purge the CDN cache and tell users to clear their cookie for the host.

Critical gotchas (paid for in incidents)

GotchaWhat happensAvoid by
Disabling via http: {} placeholderTraefik schema rejects the file → entire dynamic config build fails → all routers (including Dokploy panel) return 404 → panel becomes unreachable to fix it via UI/API. Requires SSH recovery.Disable by deleting the file (rm), never by writing a "neutral" placeholder.
Empty string sent to settings.updateTraefikFile APIZod rejects traefikConfig of length 0.Same as above — manage files via scp + rm.
Reading from Cloudflare cache during canary verificationA cf-cache-status: HIT, age: 7m response can convince you the split works perfectly when actually all traffic is the cached HTML from one backend.Always send ?cb=$RANDOM cache buster and confirm cf-cache-status: DYNAMIC (or test from inside the server bypassing CDN).
Backing service deleted from a @file referenceIf weighted.services[].name: foo@file and foo's file gets rewritten or deleted (e.g. when you remove a Dokploy domain), the weighted service references a non-existent backend → 502.Inline backend loadBalancer definitions in your own canary file. Don't depend on Dokploy-managed files.
Removing the Dokploy Application domain "to keep things clean"Triggers Dokploy to rewrite/delete that app's dynamic file. If your weighted file references it via @file, broken. Even if you inlined, you lose the priority-default fallback router.Leave the Dokploy-managed domain. Your weighted router at priority 100 will outrank it; it's a free safety net.
Same cookie name across canary host and prod hostBrowser sends one cookie to both, possibly steering users into wrong lane on whichever host they hit second.Use distinct cookie names (canary_lane, prod_lane).
Asset path collision between backends (/favicon.ico, /manifest.json, /robots.txt)Both backends serve these paths with different bytes. Sticky cookie keeps each user consistent within a session, but anonymous CDN hits may flap.Either tolerate it (a tiny percentage of cached responses), or set Cache-Control: private on those routes in both apps, or add a PathPrefix for the shared file mapped to a deterministic backend.
Reload via Dokploy settings.reloadTraefik APISometimes restarts the Traefik container, causing a 5–10 s window where every host (including the Dokploy panel) 521s through Cloudflare.The file watcher already reloads automatically on file change. Don't call the reload endpoint unless the watcher is genuinely stuck.

File reference (canonical template)

yaml
# /etc/dokploy/traefik/dynamic/<host>-canary.yml
http:
  routers:
    <host>-prefix-A:
      rule: "Host(`<host>`) && PathPrefix(`/<unique-prefix-A>/`)"
      service: <host>-backend-A
      entryPoints: [websecure]
      tls: { certResolver: letsencrypt }
      priority: 200
    <host>-prefix-B:
      rule: "Host(`<host>`) && PathPrefix(`/<unique-prefix-B>/`)"
      service: <host>-backend-B
      entryPoints: [websecure]
      tls: { certResolver: letsencrypt }
      priority: 200
    <host>-secure:
      rule: "Host(`<host>`)"
      service: <host>-weighted
      entryPoints: [websecure]
      tls: { certResolver: letsencrypt }
      priority: 100
    <host>-web:
      rule: "Host(`<host>`)"
      service: <host>-weighted
      middlewares: [redirect-to-https]
      entryPoints: [web]
      priority: 100
  services:
    <host>-weighted:
      weighted:
        sticky:
          cookie:
            name: <host>_lane
            secure: true
            httpOnly: true
            sameSite: lax
        services:
          - { name: <host>-backend-A, weight: 90 }
          - { name: <host>-backend-B, weight: 10 }
    <host>-backend-A:
      loadBalancer:
        passHostHeader: true
        servers: [{ url: "http://<appName-A>:<port>" }]
    <host>-backend-B:
      loadBalancer:
        passHostHeader: true
        servers: [{ url: "http://<appName-B>:<port>" }]

Quick verification commands

bash
# Live weight distribution (20 hits, no cookie)
bash -c '
a=0; b=0
for i in $(seq 1 20); do
  m=$(curl -sS "https://<host>/?cb=$RANDOM-$i" \
        | grep -oE "<marker A>|<marker B>" | head -1)
  case "$m" in "<marker A>") a=$((a+1));; "<marker B>") b=$((b+1));; esac
done
echo "A=$a B=$b"
'

# Sticky verification — same cookie 5x → same backend 5x
COOKIE=$(curl -sS -i "https://<host>/" | grep -i "^set-cookie: <host>_lane" \
         | grep -oE "<host>_lane=[a-f0-9]+")
for i in 1 2 3 4 5; do
  curl -sS -H "cookie: $COOKIE" "https://<host>/" \
    | grep -oE "<marker A>|<marker B>"
done

# Traefik error scan
ssh root@$HOST 'docker logs --since 5m dokploy-traefik 2>&1 \
  | grep -iE "error|invalid|cannot"'

Frequently asked questions

What does the Dokploy Traefik Traffic Split AI skill do?

Use when you need to canary-deploy two HTTP backends on the same Dokploy-managed domain with a percentage split (e.g. migrating an SPA from one framework to another). Covers the critical SPA-asset trap that breaks naive sticky-cookie splits, a path-aware Traefik config that solves it, and a four-phase workflow (backup → dry-run on a canary host → roll to prod → quick rollback).

Why use Dokploy Traefik Traffic Split on TypingMind?

Because you install it once and use it with any model. Dokploy Traefik Traffic Split is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Dokploy Traefik Traffic Split in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/Innei/SKILL/tree/main/skills/infrastructure/dokploy-traefik-traffic-split. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Dokploy Traefik Traffic Split?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Dokploy Traefik Traffic Split?

As many as you like. As long as a model supports skills, you can use Dokploy Traefik Traffic Split with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Dokploy Traefik Traffic Split AI skill free?

It is published on GitHub by Innei. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇