SSD Nodes Learn
Guides Matt ConnorBy Matt Connor

Traefik v2 to v3 migration, step by step

Traefik v3 renames ipWhiteList to ipAllowList and changes rule syntax. Upgrade from v2 with compatibility mode, then migrate labels one service at a time.

What changes between Traefik v2 and v3

A Traefik v2 to v3 migration is mostly a renaming job, and the famous rename is the ipWhiteList middleware becoming ipAllowList. Beyond that, v3 tightens the router rule syntax (PathPrefix loses its regex features, several matchers are renamed or removed), drops a few providers and options outright, and keeps everything else working: entrypoints, the ACME certificate setup, the Docker labels workflow, and your acme.json all carry over. v3 also ships a compatibility mode that keeps v2 rule syntax working, so you can upgrade the binary first and rewrite rules one service at a time instead of in one risky evening.

This guide assumes the label-based Docker Compose setup from the Traefik reverse proxy guide. That page is v3-native; this one is for the box still running a traefik:v2 tag.

The renames and removals

  • ipWhiteList is now ipAllowList, for both the HTTP and the TCP middleware. The options inside are unchanged, so sourcerange keeps its exact meaning. Current v3 releases, v3.5 included, still accept the old name as a deprecated alias and keep enforcing the list, so this one rename does not take anything down on the flip. Rename it anyway: the alias is scheduled for removal, and it disappears from the deprecation list silently, not loudly.
  • providers.docker.swarmMode=true is gone. Swarm gets its own provider, configured as providers.swarm.endpoint.
  • The pilot section is gone entirely.
  • experimental.http3 is gone. HTTP/3 is enabled on the entrypoint directly.
  • tls.caOptional is gone from the providers and from the forwardAuth middleware.
  • The InfluxDB v1 metrics provider, the Rancher provider, and the Marathon provider are gone.
  • Tracing moved to OpenTelemetry. The dedicated tracing backends, the Jaeger and Zipkin integrations among them, are gone, and v3 exports OTLP (the OpenTelemetry protocol) instead.
  • The deprecated ssl* options inside the headers middleware (sslRedirect, sslHost, and the rest) are gone. Entrypoint redirections and the redirectScheme middleware replaced them.

These removals matter more than they look, because Traefik refuses to start when its static configuration contains an option it does not know. A leftover pilot or swarmMode line stops the container at boot with an incompatible deprecated static option found message that names the leftover; an option Traefik has simply never heard of (a typo, or tls.caOptional) stops it with field not found instead. Clean the static configuration before you touch the image tag.

A middleware name Traefik genuinely does not know (a typo, or a name that was removed rather than aliased) fails differently: the router that references it loads with an error instead of a route, the dashboard marks it, and the API reports middleware "offce@docker" does not exist. Requests to that hostname get a 404 because the router never came up. Note that ipwhitelist is NOT in this category on current v3: it survives as a deprecated alias, so an unrenamed label keeps working quietly.

The rule syntax changes

Rules are where real rewriting can happen. The changes in v3:

  • Backticks are required around values inside matchers. v2 also accepted double quotes; v3 does not, so Host("app.example.com") must become Host(app.example.com).
  • PathPrefix no longer understands regular expressions or {id}-style placeholders. A v2 rule such as PathPrefix(/api/{version:v[0-9]+}) must become a PathRegexp matcher written in Go regular expression syntax.
  • Matchers now take a single value. v2 allowed Host(app.example.com,www.example.com); v3 wants Host(app.example.com) || Host(www.example.com). The exceptions are Header, HeaderRegexp, Query, and QueryRegexp, which still take a name plus a value.
  • Headers and HeadersRegexp are renamed to Header and HeaderRegexp.
  • HostHeader is removed. Use Host, which matches the same thing in v3.
  • Two matchers are new: QueryRegexp, and ClientIP for matching the client address inside a rule.

The good news: a plain Host(app.example.com) rule written with backticks is already valid v3 syntax. Most small Compose setups use exactly that, which means most labels migrate with zero rule edits.

Audit your labels before you start

You can measure the size of your migration with one search, because every breaking label change leaves a pattern grep can find:

grep -rnE 'ipwhitelist|HostHeader|Headers\(|PathPrefix\(`[^`]*\{|Host\(`[^`]*`,' docker-compose*.yml

Every hit is one line to edit. ipwhitelist becomes ipallowlist. HostHeader becomes Host. Headers becomes Header. A {...} placeholder inside PathPrefix becomes a PathRegexp matcher. A comma inside Host() becomes two Host() matchers joined by ||. Zero hits means your labels are already valid v3 syntax, and the migration shrinks to the static configuration plus the image tag.

What stays the same

Entrypoints and their HTTP-to-HTTPS redirect, the ACME resolvers with both challenge types, exposedByDefault, the router and service labels, loadbalancer.server.port, and the dashboard all work in v3 as they did in v2. Your certificates carry over too, because v3 keeps reading the acme.json that v2 wrote. Back the file up anyway before you start, since a rollback that loses it walks straight into the Let's Encrypt duplicate-certificate rate limit:

cp ./letsencrypt/acme.json ./letsencrypt/acme.json.v2-backup

The migration path

Step 1: pin what you run today. Change any traefik:latest or traefik:v2 tag to the exact release you are on, for example traefik:v2.11, and commit the whole compose directory to git. Every later step becomes reversible with a checkout. If recreating a single service with docker compose up -d <service> is not second nature yet, the Docker Compose basics guide covers the operations this migration leans on.

Step 2: clean the static configuration and turn on compatibility mode. Remove every option v3 dropped (pilot, swarmMode, tls.caOptional, experimental.http3), then tell v3 to treat rules as v2 syntax by default. In traefik.yml:

core:
  defaultRuleSyntax: v2

Or as a flag in the compose command: list: --core.defaultRuleSyntax=v2. Compatibility mode covers rule syntax only. It does not resurrect removed options, and it does not rename middlewares for you.

Step 3: prepare the middleware renames. Search your compose files for the old names: grep -rn ipwhitelist docker-compose*.yml. Edit every ipwhitelist label to ipallowlist, but do not apply the change yet, because the new name does not exist in v2. These edits ship together with the flip in the next step. (If one slips through, current v3 still honours the old name as a deprecated alias, so the list keeps enforcing; fix it in the next pass rather than at 2am.)

Step 4: flip the image tag. Set the Traefik image to the current v3 release, traefik:v3.5 at the time of writing, then:

docker compose up -d
docker compose logs -f traefik

Because compatibility mode is on, your v2 rules keep matching, and because the up -d also recreated the services whose middleware labels you renamed, those routers come up clean. A healthy log has no field not found line and no does not exist line.

Be honest with yourself about the window this step opens. A router that references a middleware name v3 genuinely does not know (a typo, or a removed option) is down from the moment the new Traefik starts until its app container is recreated, which on one box is the few seconds docker compose up -d needs to work through the list. If a route truly cannot blink, drop the renamed middleware from that router's middlewares label before the flip and re-add it after, and decide in advance whether that route can live without its IP allow list for the minute in between.

Step 5: migrate rules service by service. Work through one app at a time: rewrite its rule to v3 syntax, recreate just that service with docker compose up -d app, and test it before moving on. If one service has a rule you cannot rewrite yet, give that single router the escape hatch label traefik.http.routers.app.ruleSyntax=v2 and keep moving.

Step 6: turn compatibility mode off. When every rule is v3 syntax, delete defaultRuleSyntax and any ruleSyntax labels, restart Traefik, and confirm every router still shows green in the dashboard. Do not settle in with compatibility mode on: Traefik deprecated both options in v3.4 and removes them in the next major version, so they are a bridge, not a destination.

Before and after: one service's labels

Here is one app carrying every famous change at once: a multi-value Host, a PathPrefix placeholder, and an ipWhiteList middleware. The v2 block:

  app:
    image: app:1.4
    restart: unless-stopped
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.app.rule=Host(`app.example.com`,`www.example.com`) && PathPrefix(`/api/{version:v[0-9]+}`)
      - traefik.http.routers.app.entrypoints=websecure
      - traefik.http.routers.app.tls.certresolver=le
      - traefik.http.routers.app.middlewares=office
      - traefik.http.middlewares.office.ipwhitelist.sourcerange=10.0.0.0/24
      - traefik.http.services.app.loadbalancer.server.port=8080

And the same service migrated to v3:

  app:
    image: app:1.4
    restart: unless-stopped
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.app.rule=(Host(`app.example.com`) || Host(`www.example.com`)) && PathRegexp(`^/api/v[0-9]+`)
      - traefik.http.routers.app.entrypoints=websecure
      - traefik.http.routers.app.tls.certresolver=le
      - traefik.http.routers.app.middlewares=office
      - traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.0/24
      - traefik.http.services.app.loadbalancer.server.port=8080

Two labels changed. The rule split its multi-value Host into two matchers joined by || and swapped the placeholder for PathRegexp, and the middleware label swapped ipwhitelist for ipallowlist. The entrypoint, the certificate resolver, the router-to-middleware wiring, and the service port did not move.

Test each service with the dashboard

After every flip, open the dashboard's HTTP routers page. Every router should be green. A router with an error badge names its exact problem, which is usually a middleware that does not exist under its new name or a rule v3 cannot parse. Then confirm from outside, one hostname at a time:

curl -sI https://app.example.com/api/v1/status

A 200 or your app's normal redirect means routing and TLS both survived. A 404 from Traefik means the router did not come up; go back to the dashboard and read its error. Keep docker compose logs -f traefik open in a second terminal while you work, because every parsing failure lands there the moment a container restarts.

Rollback honesty

Keep the v2 compose file, its static configuration, and the acme.json backup until every service routes on v3 and has been exercised for real. Rolling back means checking out the pre-migration commit and running docker compose up -d, and it must be the whole file, not just the image tag, because v3-only labels are wrong under v2 in exactly the way v2 labels were wrong under v3: ipallowlist does not exist in v2, and a PathRegexp matcher will not parse there either. If acme.json was lost or damaged along the way, restore the backup copy before starting v2, so the rollback does not spend your Let's Encrypt rate limit re-issuing five certificates at once.

FAQ

Do I have to rewrite every router rule for Traefik v3?

No. A plain Host(app.example.com) rule written with backticks is valid in both versions, and that covers most Compose setups. Rewriting is only needed where a rule used v2-only features: regex or placeholders inside Path and PathPrefix, several hostnames inside one Host(), quotes instead of backticks, or the removed Headers, HeadersRegexp, and HostHeader matchers.

What happened to ipWhiteList in Traefik v3?

It was renamed to ipAllowList, with the configuration inside unchanged, so a v2 label like traefik.http.middlewares.office.ipwhitelist.sourcerange=10.0.0.0/24 becomes the same line with ipallowlist in it. Current v3 releases, v3.5 included, still accept the old name as a deprecated alias, so an unrenamed label keeps enforcing the allowlist quietly. Treat that as borrowed time rather than a reason to skip the rename: the alias is scheduled for removal, and a middleware name Traefik genuinely does not know fails loudly instead, with a router error and a 404. The dashboard shows the error, and requests to that hostname return 404.

Can Traefik v3 still read v2 rule syntax?

Yes. Set core.defaultRuleSyntax: v2 in the static configuration to keep v2 syntax as the default while you migrate, and use the per-router ruleSyntax=v2 label for individual stragglers after you switch the default back. Treat both as temporary: Traefik deprecated them in v3.4 and removes them in the next major version.

Will my Let's Encrypt certificates survive the upgrade?

Yes. Traefik v3 keeps reading the acme.json file that v2 wrote, so certificates are not re-issued just because the binary changed. Copy the file somewhere safe before you begin anyway, because a rollback or a deleted volume that loses acme.json forces re-issuing every certificate at once, and Let's Encrypt allows only five duplicate certificates per week for the same set of hostnames.

Why does Traefik v3 fail to start after the upgrade?

Almost always because the static configuration still contains an option v3 removed, and Traefik refuses to start on options it does not recognise. For the well-known leftovers (pilot, providers.docker.swarmMode, experimental.http3) the log says incompatible deprecated static option found and names the culprit; for anything v3 has never heard of, such as tls.caOptional, it says field not found with the node. Delete or replace each one, then start the container again.