Merge branch 'v3.0' of github.com:traefik/traefik
This commit is contained in:
commit
17c183f226
71 changed files with 2677 additions and 1190 deletions
|
@ -66,7 +66,6 @@ providers:
|
||||||
docker:
|
docker:
|
||||||
endpoint: "tcp://10.10.10.10:2375"
|
endpoint: "tcp://10.10.10.10:2375"
|
||||||
exposedByDefault: true
|
exposedByDefault: true
|
||||||
swarmMode: true
|
|
||||||
|
|
||||||
tls:
|
tls:
|
||||||
ca: dockerCA
|
ca: dockerCA
|
||||||
|
@ -86,7 +85,6 @@ providers:
|
||||||
docker:
|
docker:
|
||||||
endpoint: "xxxx"
|
endpoint: "xxxx"
|
||||||
exposedByDefault: true
|
exposedByDefault: true
|
||||||
swarmMode: true
|
|
||||||
|
|
||||||
tls:
|
tls:
|
||||||
ca: xxxx
|
ca: xxxx
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
labels:
|
labels:
|
||||||
- traefik.http.routers.blog.rule=Host(`example.com`) && Path(`/blog`)
|
- traefik.http.routers.blog.rule=Host(`example.com`) && Path(`/blog`)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
labels:
|
labels:
|
||||||
- traefik.http.routers.blog.rule=(Host(`example.com`) && Path(`/blog`)) || Host(`blog.example.org`)
|
- traefik.http.routers.blog.rule=(Host(`example.com`) && Path(`/blog`)) || Host(`blog.example.org`)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
labels:
|
labels:
|
||||||
- traefik.http.routers.blog.rule=Host(`example.com`) && Path(`/blog`)
|
- traefik.http.routers.blog.rule=Host(`example.com`) && Path(`/blog`)
|
||||||
|
|
|
@ -87,7 +87,7 @@ A certificate resolver requests certificates for a set of domain names inferred
|
||||||
|
|
||||||
!!! example "Domain from Router's Rule Example"
|
!!! example "Domain from Router's Rule Example"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
labels:
|
labels:
|
||||||
- traefik.http.routers.blog.rule=Host(`monitoring.yak-bebop.ts.net`) && Path(`/metrics`)
|
- traefik.http.routers.blog.rule=Host(`monitoring.yak-bebop.ts.net`) && Path(`/metrics`)
|
||||||
|
@ -141,7 +141,7 @@ A certificate resolver requests certificates for a set of domain names inferred
|
||||||
|
|
||||||
!!! example "Domain from Router's tls.domain Example"
|
!!! example "Domain from Router's tls.domain Example"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
labels:
|
labels:
|
||||||
- traefik.http.routers.blog.rule=Path(`/metrics`)
|
- traefik.http.routers.blog.rule=Path(`/metrics`)
|
||||||
|
|
|
@ -211,7 +211,7 @@ spec:
|
||||||
- bar.example.org
|
- bar.example.org
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
## Dynamic configuration
|
## Dynamic configuration
|
||||||
labels:
|
labels:
|
||||||
- "traefik.tls.stores.default.defaultgeneratedcert.resolver=myresolver"
|
- "traefik.tls.stores.default.defaultgeneratedcert.resolver=myresolver"
|
||||||
|
|
|
@ -14,7 +14,7 @@ The AddPrefix middleware updates the path of a request before forwarding it.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Prefixing with /foo
|
# Prefixing with /foo
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.add-foo.addprefix.prefix=/foo"
|
- "traefik.http.middlewares.add-foo.addprefix.prefix=/foo"
|
||||||
|
|
|
@ -14,7 +14,7 @@ The BasicAuth middleware restricts access to your services to known users.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Declaring the user list
|
# Declaring the user list
|
||||||
#
|
#
|
||||||
# Note: when used in docker-compose.yml all dollar signs in the hash need to be doubled for escaping.
|
# Note: when used in docker-compose.yml all dollar signs in the hash need to be doubled for escaping.
|
||||||
|
@ -88,7 +88,7 @@ The `users` option is an array of authorized users. Each user must be declared u
|
||||||
Please note that these keys are not hashed or encrypted in any way, and therefore is less secure than other methods.
|
Please note that these keys are not hashed or encrypted in any way, and therefore is less secure than other methods.
|
||||||
You can find more information on the [Kubernetes Basic Authentication Secret Documentation](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret)
|
You can find more information on the [Kubernetes Basic Authentication Secret Documentation](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret)
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Declaring the user list
|
# Declaring the user list
|
||||||
#
|
#
|
||||||
# Note: when used in docker-compose.yml all dollar signs in the hash need to be doubled for escaping.
|
# Note: when used in docker-compose.yml all dollar signs in the hash need to be doubled for escaping.
|
||||||
|
@ -177,7 +177,7 @@ The file content is a list of `name:hashed-password`.
|
||||||
- If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`.
|
- If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`.
|
||||||
- Because it does not make much sense to refer to a file path on Kubernetes, the `usersFile` field doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead.
|
- Because it does not make much sense to refer to a file path on Kubernetes, the `usersFile` field doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.basicauth.usersfile=/path/to/my/usersfile"
|
- "traefik.http.middlewares.test-auth.basicauth.usersfile=/path/to/my/usersfile"
|
||||||
```
|
```
|
||||||
|
@ -233,7 +233,7 @@ http:
|
||||||
|
|
||||||
You can customize the realm for the authentication with the `realm` option. The default value is `traefik`.
|
You can customize the realm for the authentication with the `realm` option. The default value is `traefik`.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.basicauth.realm=MyRealm"
|
- "traefik.http.middlewares.test-auth.basicauth.realm=MyRealm"
|
||||||
```
|
```
|
||||||
|
@ -270,7 +270,7 @@ http:
|
||||||
|
|
||||||
You can define a header field to store the authenticated user using the `headerField`option.
|
You can define a header field to store the authenticated user using the `headerField`option.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.my-auth.basicauth.headerField=X-WebAuth-User"
|
- "traefik.http.middlewares.my-auth.basicauth.headerField=X-WebAuth-User"
|
||||||
```
|
```
|
||||||
|
@ -309,7 +309,7 @@ http:
|
||||||
|
|
||||||
Set the `removeHeader` option to `true` to remove the authorization header before forwarding the request to your service. (Default value is `false`.)
|
Set the `removeHeader` option to `true` to remove the authorization header before forwarding the request to your service. (Default value is `false`.)
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.basicauth.removeheader=true"
|
- "traefik.http.middlewares.test-auth.basicauth.removeheader=true"
|
||||||
```
|
```
|
||||||
|
|
|
@ -18,7 +18,7 @@ This can help services avoid large amounts of data (`multipart/form-data` for ex
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Sets the maximum request body to 2MB
|
# Sets the maximum request body to 2MB
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=2000000"
|
- "traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=2000000"
|
||||||
|
@ -66,7 +66,7 @@ The `maxRequestBodyBytes` option configures the maximum allowed body size for th
|
||||||
|
|
||||||
If the request exceeds the allowed size, it is not forwarded to the service, and the client gets a `413` (Request Entity Too Large) response.
|
If the request exceeds the allowed size, it is not forwarded to the service, and the client gets a `413` (Request Entity Too Large) response.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=2000000"
|
- "traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=2000000"
|
||||||
```
|
```
|
||||||
|
@ -105,7 +105,7 @@ _Optional, Default=1048576_
|
||||||
|
|
||||||
You can configure a threshold (in bytes) from which the request will be buffered on disk instead of in memory with the `memRequestBodyBytes` option.
|
You can configure a threshold (in bytes) from which the request will be buffered on disk instead of in memory with the `memRequestBodyBytes` option.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.limit.buffering.memRequestBodyBytes=2000000"
|
- "traefik.http.middlewares.limit.buffering.memRequestBodyBytes=2000000"
|
||||||
```
|
```
|
||||||
|
@ -146,7 +146,7 @@ The `maxResponseBodyBytes` option configures the maximum allowed response size f
|
||||||
|
|
||||||
If the response exceeds the allowed size, it is not forwarded to the client. The client gets a `500` (Internal Server Error) response instead.
|
If the response exceeds the allowed size, it is not forwarded to the client. The client gets a `500` (Internal Server Error) response instead.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.limit.buffering.maxResponseBodyBytes=2000000"
|
- "traefik.http.middlewares.limit.buffering.maxResponseBodyBytes=2000000"
|
||||||
```
|
```
|
||||||
|
@ -185,7 +185,7 @@ _Optional, Default=1048576_
|
||||||
|
|
||||||
You can configure a threshold (in bytes) from which the response will be buffered on disk instead of in memory with the `memResponseBodyBytes` option.
|
You can configure a threshold (in bytes) from which the response will be buffered on disk instead of in memory with the `memResponseBodyBytes` option.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.limit.buffering.memResponseBodyBytes=2000000"
|
- "traefik.http.middlewares.limit.buffering.memResponseBodyBytes=2000000"
|
||||||
```
|
```
|
||||||
|
@ -226,7 +226,7 @@ You can have the Buffering middleware replay the request using `retryExpression`
|
||||||
|
|
||||||
??? example "Retries once in the case of a network error"
|
??? example "Retries once in the case of a network error"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.limit.buffering.retryExpression=IsNetworkError() && Attempts() < 2"
|
- "traefik.http.middlewares.limit.buffering.retryExpression=IsNetworkError() && Attempts() < 2"
|
||||||
```
|
```
|
||||||
|
|
|
@ -17,7 +17,7 @@ It makes reusing the same groups easier.
|
||||||
|
|
||||||
Below is an example of a Chain containing `AllowList`, `BasicAuth`, and `RedirectScheme`.
|
Below is an example of a Chain containing `AllowList`, `BasicAuth`, and `RedirectScheme`.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.router1.service=service1"
|
- "traefik.http.routers.router1.service=service1"
|
||||||
- "traefik.http.routers.router1.middlewares=secured"
|
- "traefik.http.routers.router1.middlewares=secured"
|
||||||
|
|
|
@ -30,7 +30,7 @@ To assess if your system is healthy, the circuit breaker constantly monitors the
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Latency Check
|
# Latency Check
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.latency-check.circuitbreaker.expression=LatencyAtQuantileMS(50.0) > 100"
|
- "traefik.http.middlewares.latency-check.circuitbreaker.expression=LatencyAtQuantileMS(50.0) > 100"
|
||||||
|
|
|
@ -15,7 +15,7 @@ The activation of compression, and the compression method choice rely (among oth
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Enable compression
|
# Enable compression
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-compress.compress=true"
|
- "traefik.http.middlewares.test-compress.compress=true"
|
||||||
|
@ -82,7 +82,7 @@ Content types are compared in a case-insensitive, whitespace-ignored manner.
|
||||||
|
|
||||||
Note that `application/grpc` is never compressed.
|
Note that `application/grpc` is never compressed.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-compress.compress.excludedcontenttypes=text/event-stream"
|
- "traefik.http.middlewares.test-compress.compress.excludedcontenttypes=text/event-stream"
|
||||||
```
|
```
|
||||||
|
@ -125,7 +125,7 @@ _Optional, Default=1024_
|
||||||
|
|
||||||
Responses smaller than the specified values will not be compressed.
|
Responses smaller than the specified values will not be compressed.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-compress.compress.minresponsebodybytes=1200"
|
- "traefik.http.middlewares.test-compress.compress.minresponsebodybytes=1200"
|
||||||
```
|
```
|
||||||
|
|
|
@ -18,7 +18,7 @@ when it is not set by the backend.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Enable auto-detection
|
# Enable auto-detection
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.autodetect.contenttype=true"
|
- "traefik.http.middlewares.autodetect.contenttype=true"
|
||||||
|
|
|
@ -14,7 +14,7 @@ The DigestAuth middleware restricts access to your services to known users.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Declaring the user list
|
# Declaring the user list
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.digestauth.users=test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e"
|
- "traefik.http.middlewares.test-auth.digestauth.users=test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e"
|
||||||
|
@ -72,7 +72,7 @@ The `users` option is an array of authorized users. Each user will be declared u
|
||||||
- If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`.
|
- If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`.
|
||||||
- For security reasons, the field `users` doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead.
|
- For security reasons, the field `users` doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.digestauth.users=test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e"
|
- "traefik.http.middlewares.test-auth.digestauth.users=test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e"
|
||||||
```
|
```
|
||||||
|
@ -132,7 +132,7 @@ The file content is a list of `name:realm:encoded-password`.
|
||||||
- If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`.
|
- If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`.
|
||||||
- Because it does not make much sense to refer to a file path on Kubernetes, the `usersFile` field doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead.
|
- Because it does not make much sense to refer to a file path on Kubernetes, the `usersFile` field doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.digestauth.usersfile=/path/to/my/usersfile"
|
- "traefik.http.middlewares.test-auth.digestauth.usersfile=/path/to/my/usersfile"
|
||||||
```
|
```
|
||||||
|
@ -188,7 +188,7 @@ http:
|
||||||
|
|
||||||
You can customize the realm for the authentication with the `realm` option. The default value is `traefik`.
|
You can customize the realm for the authentication with the `realm` option. The default value is `traefik`.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.digestauth.realm=MyRealm"
|
- "traefik.http.middlewares.test-auth.digestauth.realm=MyRealm"
|
||||||
```
|
```
|
||||||
|
@ -225,7 +225,7 @@ http:
|
||||||
|
|
||||||
You can customize the header field for the authenticated user using the `headerField`option.
|
You can customize the header field for the authenticated user using the `headerField`option.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.my-auth.digestauth.headerField=X-WebAuth-User"
|
- "traefik.http.middlewares.my-auth.digestauth.headerField=X-WebAuth-User"
|
||||||
```
|
```
|
||||||
|
@ -264,7 +264,7 @@ http:
|
||||||
|
|
||||||
Set the `removeHeader` option to `true` to remove the authorization header before forwarding the request to your service. (Default value is `false`.)
|
Set the `removeHeader` option to `true` to remove the authorization header before forwarding the request to your service. (Default value is `false`.)
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.digestauth.removeheader=true"
|
- "traefik.http.middlewares.test-auth.digestauth.removeheader=true"
|
||||||
```
|
```
|
||||||
|
|
|
@ -18,7 +18,7 @@ The Errors middleware returns a custom page in lieu of the default, according to
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Dynamic Custom Error Page for 5XX Status Code
|
# Dynamic Custom Error Page for 5XX Status Code
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-errors.errors.status=500-599"
|
- "traefik.http.middlewares.test-errors.errors.status=500-599"
|
||||||
|
|
|
@ -16,7 +16,7 @@ Otherwise, the response from the authentication server is returned.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Forward authentication to example.com
|
# Forward authentication to example.com
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.address=https://example.com/auth"
|
- "traefik.http.middlewares.test-auth.forwardauth.address=https://example.com/auth"
|
||||||
|
@ -72,7 +72,7 @@ The following request properties are provided to the forward-auth target endpoin
|
||||||
|
|
||||||
The `address` option defines the authentication server address.
|
The `address` option defines the authentication server address.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.address=https://example.com/auth"
|
- "traefik.http.middlewares.test-auth.forwardauth.address=https://example.com/auth"
|
||||||
```
|
```
|
||||||
|
@ -109,7 +109,7 @@ http:
|
||||||
|
|
||||||
Set the `trustForwardHeader` option to `true` to trust all `X-Forwarded-*` headers.
|
Set the `trustForwardHeader` option to `true` to trust all `X-Forwarded-*` headers.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.trustForwardHeader=true"
|
- "traefik.http.middlewares.test-auth.forwardauth.trustForwardHeader=true"
|
||||||
```
|
```
|
||||||
|
@ -150,7 +150,7 @@ http:
|
||||||
The `authResponseHeaders` option is the list of headers to copy from the authentication server response and set on
|
The `authResponseHeaders` option is the list of headers to copy from the authentication server response and set on
|
||||||
forwarded request, replacing any existing conflicting headers.
|
forwarded request, replacing any existing conflicting headers.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.authResponseHeaders=X-Auth-User, X-Secret"
|
- "traefik.http.middlewares.test-auth.forwardauth.authResponseHeaders=X-Auth-User, X-Secret"
|
||||||
```
|
```
|
||||||
|
@ -197,7 +197,7 @@ set on forwarded request, after stripping all headers that match the regex.
|
||||||
It allows partial matching of the regular expression against the header key.
|
It allows partial matching of the regular expression against the header key.
|
||||||
The start of string (`^`) and end of string (`$`) anchors should be used to ensure a full match against the header key.
|
The start of string (`^`) and end of string (`$`) anchors should be used to ensure a full match against the header key.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.authResponseHeadersRegex=^X-"
|
- "traefik.http.middlewares.test-auth.forwardauth.authResponseHeadersRegex=^X-"
|
||||||
```
|
```
|
||||||
|
@ -245,7 +245,7 @@ The `authRequestHeaders` option is the list of the headers to copy from the requ
|
||||||
It allows filtering headers that should not be passed to the authentication server.
|
It allows filtering headers that should not be passed to the authentication server.
|
||||||
If not set or empty then all request headers are passed.
|
If not set or empty then all request headers are passed.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.authRequestHeaders=Accept,X-CustomHeader"
|
- "traefik.http.middlewares.test-auth.forwardauth.authRequestHeaders=Accept,X-CustomHeader"
|
||||||
```
|
```
|
||||||
|
@ -298,7 +298,7 @@ _Optional_
|
||||||
`ca` is the path to the certificate authority used for the secured connection to the authentication server,
|
`ca` is the path to the certificate authority used for the secured connection to the authentication server,
|
||||||
it defaults to the system bundle.
|
it defaults to the system bundle.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.tls.ca=path/to/local.crt"
|
- "traefik.http.middlewares.test-auth.forwardauth.tls.ca=path/to/local.crt"
|
||||||
```
|
```
|
||||||
|
@ -355,7 +355,7 @@ _Optional_
|
||||||
`cert` is the path to the public certificate used for the secure connection to the authentication server.
|
`cert` is the path to the public certificate used for the secure connection to the authentication server.
|
||||||
When using this option, setting the `key` option is required.
|
When using this option, setting the `key` option is required.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.tls.cert=path/to/foo.cert"
|
- "traefik.http.middlewares.test-auth.forwardauth.tls.cert=path/to/foo.cert"
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.tls.key=path/to/foo.key"
|
- "traefik.http.middlewares.test-auth.forwardauth.tls.key=path/to/foo.key"
|
||||||
|
@ -420,7 +420,7 @@ _Optional_
|
||||||
`key` is the path to the private key used for the secure connection to the authentication server.
|
`key` is the path to the private key used for the secure connection to the authentication server.
|
||||||
When using this option, setting the `cert` option is required.
|
When using this option, setting the `cert` option is required.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.tls.cert=path/to/foo.cert"
|
- "traefik.http.middlewares.test-auth.forwardauth.tls.cert=path/to/foo.cert"
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.tls.key=path/to/foo.key"
|
- "traefik.http.middlewares.test-auth.forwardauth.tls.key=path/to/foo.key"
|
||||||
|
@ -484,7 +484,7 @@ _Optional, Default=false_
|
||||||
|
|
||||||
If `insecureSkipVerify` is `true`, the TLS connection to the authentication server accepts any certificate presented by the server regardless of the hostnames it covers.
|
If `insecureSkipVerify` is `true`, the TLS connection to the authentication server accepts any certificate presented by the server regardless of the hostnames it covers.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-auth.forwardauth.tls.insecureSkipVerify=true"
|
- "traefik.http.middlewares.test-auth.forwardauth.tls.insecureSkipVerify=true"
|
||||||
```
|
```
|
||||||
|
|
|
@ -17,7 +17,7 @@ The GrpcWeb middleware converts gRPC Web requests to HTTP/2 gRPC requests before
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-grpcweb.grpcweb.allowOrigins=*"
|
- "traefik.http.middlewares.test-grpcweb.grpcweb.allowOrigins=*"
|
||||||
```
|
```
|
||||||
|
|
|
@ -20,7 +20,7 @@ A set of forwarded headers are automatically added by default. See the [FAQ](../
|
||||||
|
|
||||||
The following example adds the `X-Script-Name` header to the proxied request and the `X-Custom-Response-Header` header to the response
|
The following example adds the `X-Script-Name` header to the proxied request and the `X-Custom-Response-Header` header to the response
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.testHeader.headers.customrequestheaders.X-Script-Name=test"
|
- "traefik.http.middlewares.testHeader.headers.customrequestheaders.X-Script-Name=test"
|
||||||
- "traefik.http.middlewares.testHeader.headers.customresponseheaders.X-Custom-Response-Header=value"
|
- "traefik.http.middlewares.testHeader.headers.customresponseheaders.X-Custom-Response-Header=value"
|
||||||
|
@ -69,7 +69,7 @@ http:
|
||||||
In the following example, requests are proxied with an extra `X-Script-Name` header while their `X-Custom-Request-Header` header gets stripped,
|
In the following example, requests are proxied with an extra `X-Script-Name` header while their `X-Custom-Request-Header` header gets stripped,
|
||||||
and responses are stripped of their `X-Custom-Response-Header` header.
|
and responses are stripped of their `X-Custom-Response-Header` header.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.testheader.headers.customrequestheaders.X-Script-Name=test"
|
- "traefik.http.middlewares.testheader.headers.customrequestheaders.X-Script-Name=test"
|
||||||
- "traefik.http.middlewares.testheader.headers.customrequestheaders.X-Custom-Request-Header="
|
- "traefik.http.middlewares.testheader.headers.customrequestheaders.X-Custom-Request-Header="
|
||||||
|
@ -123,7 +123,7 @@ http:
|
||||||
Security-related headers (HSTS headers, Browser XSS filter, etc) can be managed similarly to custom headers as shown above.
|
Security-related headers (HSTS headers, Browser XSS filter, etc) can be managed similarly to custom headers as shown above.
|
||||||
This functionality makes it possible to easily use security features by adding headers.
|
This functionality makes it possible to easily use security features by adding headers.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.testHeader.headers.framedeny=true"
|
- "traefik.http.middlewares.testHeader.headers.framedeny=true"
|
||||||
- "traefik.http.middlewares.testHeader.headers.browserxssfilter=true"
|
- "traefik.http.middlewares.testHeader.headers.browserxssfilter=true"
|
||||||
|
@ -170,7 +170,7 @@ instead the response will be generated and sent back to the client directly.
|
||||||
Please note that the example below is by no means authoritative or exhaustive,
|
Please note that the example below is by no means authoritative or exhaustive,
|
||||||
and should not be used as is for production.
|
and should not be used as is for production.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
|
- "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
|
||||||
- "traefik.http.middlewares.testheader.headers.accesscontrolallowheaders=*"
|
- "traefik.http.middlewares.testheader.headers.accesscontrolallowheaders=*"
|
||||||
|
|
|
@ -14,7 +14,7 @@ To proactively prevent services from being overwhelmed with high load, the numbe
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-inflightreq.inflightreq.amount=10"
|
- "traefik.http.middlewares.test-inflightreq.inflightreq.amount=10"
|
||||||
```
|
```
|
||||||
|
@ -57,7 +57,7 @@ http:
|
||||||
The `amount` option defines the maximum amount of allowed simultaneous in-flight request.
|
The `amount` option defines the maximum amount of allowed simultaneous in-flight request.
|
||||||
The middleware responds with `HTTP 429 Too Many Requests` if there are already `amount` requests in progress (based on the same `sourceCriterion` strategy).
|
The middleware responds with `HTTP 429 Too Many Requests` if there are already `amount` requests in progress (based on the same `sourceCriterion` strategy).
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-inflightreq.inflightreq.amount=10"
|
- "traefik.http.middlewares.test-inflightreq.inflightreq.amount=10"
|
||||||
```
|
```
|
||||||
|
@ -122,7 +122,7 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and select
|
||||||
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` |
|
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` |
|
||||||
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` |
|
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` |
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.depth=2"
|
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.depth=2"
|
||||||
```
|
```
|
||||||
|
@ -176,7 +176,7 @@ http:
|
||||||
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` |
|
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` |
|
||||||
| `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` |
|
| `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` |
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7"
|
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7"
|
||||||
```
|
```
|
||||||
|
@ -222,7 +222,7 @@ http:
|
||||||
|
|
||||||
Name of the header used to group incoming requests.
|
Name of the header used to group incoming requests.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requestheadername=username"
|
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requestheadername=username"
|
||||||
```
|
```
|
||||||
|
@ -262,7 +262,7 @@ http:
|
||||||
|
|
||||||
Whether to consider the request host as the source.
|
Whether to consider the request host as the source.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requesthost=true"
|
- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requesthost=true"
|
||||||
```
|
```
|
||||||
|
|
|
@ -12,7 +12,7 @@ IPAllowList accepts / refuses requests based on the client IP.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Accepts request from defined IP
|
# Accepts request from defined IP
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7"
|
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7"
|
||||||
|
@ -83,7 +83,7 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and take th
|
||||||
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` |
|
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` |
|
||||||
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` |
|
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` |
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Allowlisting Based on `X-Forwarded-For` with `depth=2`
|
# Allowlisting Based on `X-Forwarded-For` with `depth=2`
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7"
|
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7"
|
||||||
|
@ -149,7 +149,7 @@ http:
|
||||||
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` |
|
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` |
|
||||||
| `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` |
|
| `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` |
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Exclude from `X-Forwarded-For`
|
# Exclude from `X-Forwarded-For`
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7"
|
- "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7"
|
||||||
|
|
|
@ -12,7 +12,7 @@ Controlling connections
|
||||||
|
|
||||||
## Configuration Example
|
## Configuration Example
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# As a Docker Label
|
# As a Docker Label
|
||||||
whoami:
|
whoami:
|
||||||
# A container that exposes an API to show its IP address
|
# A container that exposes an API to show its IP address
|
||||||
|
|
|
@ -18,7 +18,7 @@ PassTLSClientCert adds the selected data from the passed client TLS certificate
|
||||||
|
|
||||||
Pass the pem in the `X-Forwarded-Tls-Client-Cert` header.
|
Pass the pem in the `X-Forwarded-Tls-Client-Cert` header.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Pass the pem in the `X-Forwarded-Tls-Client-Cert` header.
|
# Pass the pem in the `X-Forwarded-Tls-Client-Cert` header.
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.pem=true"
|
- "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.pem=true"
|
||||||
|
@ -57,7 +57,7 @@ http:
|
||||||
|
|
||||||
??? example "Pass the pem in the `X-Forwarded-Tls-Client-Cert` header"
|
??? example "Pass the pem in the `X-Forwarded-Tls-Client-Cert` header"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Pass all the available info in the `X-Forwarded-Tls-Client-Cert-Info` header
|
# Pass all the available info in the `X-Forwarded-Tls-Client-Cert-Info` header
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.notafter=true"
|
- "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.notafter=true"
|
||||||
|
|
|
@ -14,7 +14,7 @@ It is based on a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) impl
|
||||||
|
|
||||||
## Configuration Example
|
## Configuration Example
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Here, an average of 100 requests per second is allowed.
|
# Here, an average of 100 requests per second is allowed.
|
||||||
# In addition, a burst of 50 requests is allowed.
|
# In addition, a burst of 50 requests is allowed.
|
||||||
labels:
|
labels:
|
||||||
|
@ -73,7 +73,7 @@ It defaults to `0`, which means no rate limiting.
|
||||||
The rate is actually defined by dividing `average` by `period`.
|
The rate is actually defined by dividing `average` by `period`.
|
||||||
So for a rate below 1 req/s, one needs to define a `period` larger than a second.
|
So for a rate below 1 req/s, one needs to define a `period` larger than a second.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# 100 reqs/s
|
# 100 reqs/s
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ratelimit.ratelimit.average=100"
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.average=100"
|
||||||
|
@ -121,7 +121,7 @@ r = average / period
|
||||||
|
|
||||||
It defaults to `1` second.
|
It defaults to `1` second.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# 6 reqs/minute
|
# 6 reqs/minute
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ratelimit.ratelimit.average=6"
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.average=6"
|
||||||
|
@ -170,7 +170,7 @@ http:
|
||||||
|
|
||||||
It defaults to `1`.
|
It defaults to `1`.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ratelimit.ratelimit.burst=100"
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.burst=100"
|
||||||
```
|
```
|
||||||
|
@ -232,7 +232,7 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and select
|
||||||
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` |
|
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` |
|
||||||
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` |
|
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` |
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.depth=2"
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.depth=2"
|
||||||
```
|
```
|
||||||
|
@ -313,7 +313,7 @@ and the first IP that is _not_ in the pool (if any) is returned.
|
||||||
| `"10.0.0.1,11.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` |
|
| `"10.0.0.1,11.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` |
|
||||||
| `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` |
|
| `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` |
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7"
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7"
|
||||||
```
|
```
|
||||||
|
@ -359,7 +359,7 @@ http:
|
||||||
|
|
||||||
Name of the header used to group incoming requests.
|
Name of the header used to group incoming requests.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requestheadername=username"
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requestheadername=username"
|
||||||
```
|
```
|
||||||
|
@ -399,7 +399,7 @@ http:
|
||||||
|
|
||||||
Whether to consider the request host as the source.
|
Whether to consider the request host as the source.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requesthost=true"
|
- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requesthost=true"
|
||||||
```
|
```
|
||||||
|
|
|
@ -16,7 +16,7 @@ The RedirectRegex redirects a request using regex matching and replacement.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Redirect with domain replacement
|
# Redirect with domain replacement
|
||||||
# Note: all dollar signs need to be doubled for escaping.
|
# Note: all dollar signs need to be doubled for escaping.
|
||||||
labels:
|
labels:
|
||||||
|
|
|
@ -25,7 +25,7 @@ The RedirectScheme middleware redirects the request if the request scheme is dif
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Redirect to https
|
# Redirect to https
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-redirectscheme.redirectscheme.scheme=https"
|
- "traefik.http.middlewares.test-redirectscheme.redirectscheme.scheme=https"
|
||||||
|
@ -75,7 +75,7 @@ http:
|
||||||
|
|
||||||
Set the `permanent` option to `true` to apply a permanent redirection.
|
Set the `permanent` option to `true` to apply a permanent redirection.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Redirect to https
|
# Redirect to https
|
||||||
labels:
|
labels:
|
||||||
# ...
|
# ...
|
||||||
|
@ -123,7 +123,7 @@ http:
|
||||||
|
|
||||||
The `scheme` option defines the scheme of the new URL.
|
The `scheme` option defines the scheme of the new URL.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Redirect to https
|
# Redirect to https
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-redirectscheme.redirectscheme.scheme=https"
|
- "traefik.http.middlewares.test-redirectscheme.redirectscheme.scheme=https"
|
||||||
|
@ -166,7 +166,7 @@ http:
|
||||||
|
|
||||||
The `port` option defines the port of the new URL.
|
The `port` option defines the port of the new URL.
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Redirect to https
|
# Redirect to https
|
||||||
labels:
|
labels:
|
||||||
# ...
|
# ...
|
||||||
|
|
|
@ -16,7 +16,7 @@ Replace the path of the request URL.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Replace the path with /foo
|
# Replace the path with /foo
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-replacepath.replacepath.path=/foo"
|
- "traefik.http.middlewares.test-replacepath.replacepath.path=/foo"
|
||||||
|
|
|
@ -16,7 +16,7 @@ The ReplaceRegex replaces the path of a URL using regex matching and replacement
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Replace path with regex
|
# Replace path with regex
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-replacepathregex.replacepathregex.regex=^/foo/(.*)"
|
- "traefik.http.middlewares.test-replacepathregex.replacepathregex.regex=^/foo/(.*)"
|
||||||
|
|
|
@ -18,7 +18,7 @@ The Retry middleware has an optional configuration to enable an exponential back
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Retry 4 times with exponential backoff
|
# Retry 4 times with exponential backoff
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-retry.retry.attempts=4"
|
- "traefik.http.middlewares.test-retry.retry.attempts=4"
|
||||||
|
|
|
@ -16,7 +16,7 @@ Remove the specified prefixes from the URL path.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Strip prefix /foobar and /fiibar
|
# Strip prefix /foobar and /fiibar
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-stripprefix.stripprefix.prefixes=/foobar,/fiibar"
|
- "traefik.http.middlewares.test-stripprefix.stripprefix.prefixes=/foobar,/fiibar"
|
||||||
|
|
|
@ -12,7 +12,7 @@ Remove the matching prefixes from the URL path.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.test-stripprefixregex.stripprefixregex.regex=/foo/[a-z0-9]+/[0-9]+/"
|
- "traefik.http.middlewares.test-stripprefixregex.stripprefixregex.regex=/foo/[a-z0-9]+/[0-9]+/"
|
||||||
```
|
```
|
||||||
|
|
|
@ -23,7 +23,7 @@ Middlewares that use the same protocol can be combined into chains to fit every
|
||||||
|
|
||||||
## Configuration Example
|
## Configuration Example
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# As a Docker Label
|
# As a Docker Label
|
||||||
whoami:
|
whoami:
|
||||||
# A container that exposes an API to show its IP address
|
# A container that exposes an API to show its IP address
|
||||||
|
|
|
@ -7,7 +7,7 @@ To proactively prevent services from being overwhelmed with high load, the numbe
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.tcp.middlewares.test-inflightconn.inflightconn.amount=10"
|
- "traefik.tcp.middlewares.test-inflightconn.inflightconn.amount=10"
|
||||||
```
|
```
|
||||||
|
|
|
@ -12,7 +12,7 @@ IPAllowList accepts / refuses connections based on the client IP.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Accepts connections from defined IP
|
# Accepts connections from defined IP
|
||||||
labels:
|
labels:
|
||||||
- "traefik.tcp.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7"
|
- "traefik.tcp.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7"
|
||||||
|
|
|
@ -12,7 +12,7 @@ Controlling connections
|
||||||
|
|
||||||
## Configuration Example
|
## Configuration Example
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# As a Docker Label
|
# As a Docker Label
|
||||||
whoami:
|
whoami:
|
||||||
# A container that exposes an API to show its IP address
|
# A container that exposes an API to show its IP address
|
||||||
|
|
|
@ -38,7 +38,7 @@ Then any router can refer to an instance of the wanted middleware.
|
||||||
|
|
||||||
!!! info "v1"
|
!!! info "v1"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.frontend.rule=Host:test.localhost;PathPrefix:/test"
|
- "traefik.frontend.rule=Host:test.localhost;PathPrefix:/test"
|
||||||
- "traefik.frontend.auth.basic.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/,test2:$$apr1$$d9hr9HBB$$4HxwgUir3HP4EsggP/QNo0"
|
- "traefik.frontend.auth.basic.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/,test2:$$apr1$$d9hr9HBB$$4HxwgUir3HP4EsggP/QNo0"
|
||||||
|
@ -100,7 +100,7 @@ Then any router can refer to an instance of the wanted middleware.
|
||||||
|
|
||||||
!!! info "v2"
|
!!! info "v2"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.router0.rule=Host(`test.localhost`) && PathPrefix(`/test`)"
|
- "traefik.http.routers.router0.rule=Host(`test.localhost`) && PathPrefix(`/test`)"
|
||||||
- "traefik.http.routers.router0.middlewares=auth"
|
- "traefik.http.routers.router0.middlewares=auth"
|
||||||
|
@ -317,7 +317,7 @@ Then, a [router's TLS field](../routing/routers/index.md#tls) can refer to one o
|
||||||
namespace: default
|
namespace: default
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
# myTLSOptions must be defined by another provider, in this instance in the File Provider.
|
# myTLSOptions must be defined by another provider, in this instance in the File Provider.
|
||||||
# see the cross provider section
|
# see the cross provider section
|
||||||
|
@ -428,7 +428,7 @@ To apply a redirection:
|
||||||
|
|
||||||
!!! info "v2"
|
!!! info "v2"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
traefik.http.routers.app.rule: Host(`example.net`)
|
traefik.http.routers.app.rule: Host(`example.net`)
|
||||||
traefik.http.routers.app.entrypoints: web
|
traefik.http.routers.app.entrypoints: web
|
||||||
|
@ -556,7 +556,7 @@ with the path `/admin` stripped, e.g. to `http://<IP>:<port>/`. In this case, yo
|
||||||
|
|
||||||
!!! info "v1"
|
!!! info "v1"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.frontend.rule=Host:example.org;PathPrefixStrip:/admin"
|
- "traefik.frontend.rule=Host:example.org;PathPrefixStrip:/admin"
|
||||||
```
|
```
|
||||||
|
@ -588,7 +588,7 @@ with the path `/admin` stripped, e.g. to `http://<IP>:<port>/`. In this case, yo
|
||||||
|
|
||||||
!!! info "v2"
|
!!! info "v2"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.admin.rule=Host(`example.org`) && PathPrefix(`/admin`)"
|
- "traefik.http.routers.admin.rule=Host(`example.org`) && PathPrefix(`/admin`)"
|
||||||
- "traefik.http.routers.admin.middlewares=admin-stripprefix"
|
- "traefik.http.routers.admin.middlewares=admin-stripprefix"
|
||||||
|
@ -1044,7 +1044,7 @@ To activate the dashboard, you can either:
|
||||||
|
|
||||||
!!! info "v2"
|
!!! info "v2"
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# dynamic configuration
|
# dynamic configuration
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.api.rule=Host(`traefik.docker.localhost`)"
|
- "traefik.http.routers.api.rule=Host(`traefik.docker.localhost`)"
|
||||||
|
|
|
@ -87,3 +87,10 @@ In v3, the InfluxDB v1 metrics provider has been removed because InfluxDB v1.x m
|
||||||
|
|
||||||
In v3 the Kubernetes CRDs API Group `traefik.containo.us` has been removed.
|
In v3 the Kubernetes CRDs API Group `traefik.containo.us` has been removed.
|
||||||
Please use the API Group `traefik.io` instead.
|
Please use the API Group `traefik.io` instead.
|
||||||
|
|
||||||
|
## Docker & Docker Swarm
|
||||||
|
|
||||||
|
In v3, the provider Docker has been split into 2 providers:
|
||||||
|
|
||||||
|
- Docker provider (without Swarm support)
|
||||||
|
- Swarm provider (Swarm support only)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Dynamic Configuration
|
# Dynamic Configuration
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.api.rule=Host(`traefik.example.com`)"
|
- "traefik.http.routers.api.rule=Host(`traefik.example.com`)"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
# Dynamic Configuration
|
# Dynamic Configuration
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
|
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
|
||||||
|
|
|
@ -12,8 +12,7 @@ A Story of Labels & Containers
|
||||||
|
|
||||||
Attach labels to your containers and let Traefik do the rest!
|
Attach labels to your containers and let Traefik do the rest!
|
||||||
|
|
||||||
Traefik works with both [Docker (standalone) Engine](https://docs.docker.com/engine/)
|
This provider works with [Docker (standalone) Engine](https://docs.docker.com/engine/).
|
||||||
and [Docker Swarm Mode](https://docs.docker.com/engine/swarm/).
|
|
||||||
|
|
||||||
!!! tip "The Quick Start Uses Docker"
|
!!! tip "The Quick Start Uses Docker"
|
||||||
|
|
||||||
|
@ -49,49 +48,6 @@ and [Docker Swarm Mode](https://docs.docker.com/engine/swarm/).
|
||||||
- traefik.http.routers.my-container.rule=Host(`example.com`)
|
- traefik.http.routers.my-container.rule=Host(`example.com`)
|
||||||
```
|
```
|
||||||
|
|
||||||
??? example "Configuring Docker Swarm & Deploying / Exposing Services"
|
|
||||||
|
|
||||||
Enabling the docker provider (Swarm Mode)
|
|
||||||
|
|
||||||
```yaml tab="File (YAML)"
|
|
||||||
providers:
|
|
||||||
docker:
|
|
||||||
# swarm classic (1.12-)
|
|
||||||
# endpoint: "tcp://127.0.0.1:2375"
|
|
||||||
# docker swarm mode (1.12+)
|
|
||||||
endpoint: "tcp://127.0.0.1:2377"
|
|
||||||
swarmMode: true
|
|
||||||
```
|
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
|
||||||
[providers.docker]
|
|
||||||
# swarm classic (1.12-)
|
|
||||||
# endpoint = "tcp://127.0.0.1:2375"
|
|
||||||
# docker swarm mode (1.12+)
|
|
||||||
endpoint = "tcp://127.0.0.1:2377"
|
|
||||||
swarmMode = true
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash tab="CLI"
|
|
||||||
# swarm classic (1.12-)
|
|
||||||
# --providers.docker.endpoint=tcp://127.0.0.1:2375
|
|
||||||
# docker swarm mode (1.12+)
|
|
||||||
--providers.docker.endpoint=tcp://127.0.0.1:2377
|
|
||||||
--providers.docker.swarmMode=true
|
|
||||||
```
|
|
||||||
|
|
||||||
Attach labels to services (not to containers) while in Swarm mode (in your docker compose file)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: "3"
|
|
||||||
services:
|
|
||||||
my-container:
|
|
||||||
deploy:
|
|
||||||
labels:
|
|
||||||
- traefik.http.routers.my-container.rule=Host(`example.com`)
|
|
||||||
- traefik.http.services.my-container-service.loadbalancer.server.port=8080
|
|
||||||
```
|
|
||||||
|
|
||||||
## Routing Configuration
|
## Routing Configuration
|
||||||
|
|
||||||
When using Docker as a [provider](./overview.md),
|
When using Docker as a [provider](./overview.md),
|
||||||
|
@ -124,14 +80,13 @@ Port detection works as follows:
|
||||||
- If a container [exposes](https://docs.docker.com/engine/reference/builder/#expose) multiple ports,
|
- If a container [exposes](https://docs.docker.com/engine/reference/builder/#expose) multiple ports,
|
||||||
or does not expose any port, then you must manually specify which port Traefik should use for communication
|
or does not expose any port, then you must manually specify which port Traefik should use for communication
|
||||||
by using the label `traefik.http.services.<service_name>.loadbalancer.server.port`
|
by using the label `traefik.http.services.<service_name>.loadbalancer.server.port`
|
||||||
(Read more on this label in the dedicated section in [routing](../routing/providers/docker.md#port)).
|
(Read more on this label in the dedicated section in [routing](../routing/providers/docker.md#services)).
|
||||||
|
|
||||||
### Host networking
|
### Host networking
|
||||||
|
|
||||||
When exposing containers that are configured with [host networking](https://docs.docker.com/network/host/),
|
When exposing containers that are configured with [host networking](https://docs.docker.com/network/host/),
|
||||||
the IP address of the host is resolved as follows:
|
the IP address of the host is resolved as follows:
|
||||||
|
|
||||||
<!-- TODO: verify and document the swarm mode case with container.Node.IPAddress coming from the API -->
|
|
||||||
- try a lookup of `host.docker.internal`
|
- try a lookup of `host.docker.internal`
|
||||||
- if the lookup was unsuccessful, try a lookup of `host.containers.internal`, ([Podman](https://docs.podman.io/en/latest/) equivalent of `host.docker.internal`)
|
- if the lookup was unsuccessful, try a lookup of `host.containers.internal`, ([Podman](https://docs.podman.io/en/latest/) equivalent of `host.docker.internal`)
|
||||||
- if that lookup was also unsuccessful, fall back to `127.0.0.1`
|
- if that lookup was also unsuccessful, fall back to `127.0.0.1`
|
||||||
|
@ -175,7 +130,6 @@ You can specify which Docker API Endpoint to use with the directive [`endpoint`]
|
||||||
- Authorization with the [Docker Authorization Plugin Mechanism](https://web.archive.org/web/20190920092526/https://docs.docker.com/engine/extend/plugins_authorization/)
|
- Authorization with the [Docker Authorization Plugin Mechanism](https://web.archive.org/web/20190920092526/https://docs.docker.com/engine/extend/plugins_authorization/)
|
||||||
- Accounting at networking level, by exposing the socket only inside a Docker private network, only available for Traefik.
|
- Accounting at networking level, by exposing the socket only inside a Docker private network, only available for Traefik.
|
||||||
- Accounting at container level, by exposing the socket on a another container than Traefik's.
|
- Accounting at container level, by exposing the socket on a another container than Traefik's.
|
||||||
With Swarm mode, it allows scheduling of Traefik on worker nodes, with only the "socket exposer" container on the manager nodes.
|
|
||||||
- Accounting at kernel level, by enforcing kernel calls with mechanisms like [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux), to only allows an identified set of actions for Traefik's process (or the "socket exposer" process).
|
- Accounting at kernel level, by enforcing kernel calls with mechanisms like [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux), to only allows an identified set of actions for Traefik's process (or the "socket exposer" process).
|
||||||
- SSH public key authentication (SSH is supported with Docker > 18.09)
|
- SSH public key authentication (SSH is supported with Docker > 18.09)
|
||||||
|
|
||||||
|
@ -192,69 +146,13 @@ You can specify which Docker API Endpoint to use with the directive [`endpoint`]
|
||||||
- [Letting Traefik run on Worker Nodes](https://blog.mikesir87.io/2018/07/letting-traefik-run-on-worker-nodes/)
|
- [Letting Traefik run on Worker Nodes](https://blog.mikesir87.io/2018/07/letting-traefik-run-on-worker-nodes/)
|
||||||
- [Docker Socket Proxy from Tecnativa](https://github.com/Tecnativa/docker-socket-proxy)
|
- [Docker Socket Proxy from Tecnativa](https://github.com/Tecnativa/docker-socket-proxy)
|
||||||
|
|
||||||
## Docker Swarm Mode
|
|
||||||
|
|
||||||
To enable Docker Swarm (instead of standalone Docker) as a configuration provider,
|
|
||||||
set the [`swarmMode`](#swarmmode) directive to `true`.
|
|
||||||
|
|
||||||
### Routing Configuration with Labels
|
|
||||||
|
|
||||||
While in Swarm Mode, Traefik uses labels found on services, not on individual containers.
|
|
||||||
|
|
||||||
Therefore, if you use a compose file with Swarm Mode, labels should be defined in the
|
|
||||||
[`deploy`](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1) part of your service.
|
|
||||||
|
|
||||||
This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/)).
|
|
||||||
|
|
||||||
### Port Detection
|
|
||||||
|
|
||||||
Docker Swarm does not provide any [port detection](#port-detection) information to Traefik.
|
|
||||||
|
|
||||||
Therefore, you **must** specify the port to use for communication by using the label `traefik.http.services.<service_name>.loadbalancer.server.port`
|
|
||||||
(Check the reference for this label in the [routing section for Docker](../routing/providers/docker.md#port)).
|
|
||||||
|
|
||||||
### Docker API Access
|
|
||||||
|
|
||||||
Docker Swarm Mode follows the same rules as Docker [API Access](#docker-api-access).
|
|
||||||
|
|
||||||
Since the Swarm API is only exposed on the [manager nodes](https://docs.docker.com/engine/swarm/how-swarm-mode-works/nodes/#manager-nodes),
|
|
||||||
these are the nodes that Traefik should be scheduled on by deploying Traefik with a constraint on the node "role":
|
|
||||||
|
|
||||||
```shell tab="With Docker CLI"
|
|
||||||
docker service create \
|
|
||||||
--constraint=node.role==manager \
|
|
||||||
#... \
|
|
||||||
```
|
|
||||||
|
|
||||||
```yml tab="With Docker Compose"
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
traefik:
|
|
||||||
# ...
|
|
||||||
deploy:
|
|
||||||
placement:
|
|
||||||
constraints:
|
|
||||||
- node.role == manager
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip "Scheduling Traefik on Worker Nodes"
|
|
||||||
|
|
||||||
Following the guidelines given in the previous section ["Docker API Access"](#docker-api-access),
|
|
||||||
if you expose the Docker API through TCP, then Traefik can be scheduled on any node if the TCP
|
|
||||||
socket is reachable.
|
|
||||||
|
|
||||||
Please consider the security implications by reading the [Security Note](#security-note).
|
|
||||||
|
|
||||||
A good example can be found on [Bret Fisher's repository](https://github.com/BretFisher/dogvscat/blob/master/stack-proxy-global.yml#L124).
|
|
||||||
|
|
||||||
## Provider Configuration
|
## Provider Configuration
|
||||||
|
|
||||||
### `endpoint`
|
### `endpoint`
|
||||||
|
|
||||||
_Required, Default="unix:///var/run/docker.sock"_
|
_Required, Default="unix:///var/run/docker.sock"_
|
||||||
|
|
||||||
See the sections [Docker API Access](#docker-api-access) and [Docker Swarm API Access](#docker-api-access_1) for more information.
|
See the [Docker API Access](#docker-api-access) section for more information.
|
||||||
|
|
||||||
??? example "Using the docker.sock"
|
??? example "Using the docker.sock"
|
||||||
|
|
||||||
|
@ -464,54 +362,6 @@ providers:
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
### `swarmMode`
|
|
||||||
|
|
||||||
_Optional, Default=false_
|
|
||||||
|
|
||||||
Enables the Swarm Mode (instead of standalone Docker).
|
|
||||||
|
|
||||||
```yaml tab="File (YAML)"
|
|
||||||
providers:
|
|
||||||
docker:
|
|
||||||
swarmMode: true
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
|
||||||
[providers.docker]
|
|
||||||
swarmMode = true
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash tab="CLI"
|
|
||||||
--providers.docker.swarmMode=true
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### `swarmModeRefreshSeconds`
|
|
||||||
|
|
||||||
_Optional, Default=15_
|
|
||||||
|
|
||||||
Defines the polling interval (in seconds) for Swarm Mode.
|
|
||||||
|
|
||||||
```yaml tab="File (YAML)"
|
|
||||||
providers:
|
|
||||||
docker:
|
|
||||||
swarmModeRefreshSeconds: 30
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
|
||||||
[providers.docker]
|
|
||||||
swarmModeRefreshSeconds = 30
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash tab="CLI"
|
|
||||||
--providers.docker.swarmModeRefreshSeconds=30
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### `httpClientTimeout`
|
### `httpClientTimeout`
|
||||||
|
|
||||||
_Optional, Default=0_
|
_Optional, Default=0_
|
||||||
|
|
|
@ -72,7 +72,7 @@ For the list of the providers names, see the [supported providers](#supported-pr
|
||||||
|
|
||||||
Using the add-foo-prefix middleware from other providers:
|
Using the add-foo-prefix middleware from other providers:
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker & Swarm"
|
||||||
your-container: #
|
your-container: #
|
||||||
image: your-docker-image
|
image: your-docker-image
|
||||||
|
|
||||||
|
|
697
docs/content/providers/swarm.md
Normal file
697
docs/content/providers/swarm.md
Normal file
|
@ -0,0 +1,697 @@
|
||||||
|
---
|
||||||
|
title: "Traefik Docker Swarm Documentation"
|
||||||
|
description: "Learn how to achieve configuration discovery in Traefik through Docker Swarm. Read the technical documentation."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Traefik & Docker Swarm
|
||||||
|
|
||||||
|
A Story of Labels & Containers
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
![Docker](../assets/img/providers/docker.png)
|
||||||
|
|
||||||
|
Attach labels to your containers and let Traefik do the rest!
|
||||||
|
|
||||||
|
This provider works with [Docker Swarm Mode](https://docs.docker.com/engine/swarm/).
|
||||||
|
|
||||||
|
!!! tip "The Quick Start Uses Docker"
|
||||||
|
|
||||||
|
If you have not already read it, maybe you would like to go through the [quick start guide](../getting-started/quick-start.md) that uses the Docker provider.
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
??? example "Configuring Docker Swarm & Deploying / Exposing Services"
|
||||||
|
|
||||||
|
Enabling the Swarm provider
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
# swarm classic (1.12-)
|
||||||
|
# endpoint: "tcp://127.0.0.1:2375"
|
||||||
|
# docker swarm mode (1.12+)
|
||||||
|
endpoint: "tcp://127.0.0.1:2377"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
# swarm classic (1.12-)
|
||||||
|
# endpoint = "tcp://127.0.0.1:2375"
|
||||||
|
# docker swarm mode (1.12+)
|
||||||
|
endpoint = "tcp://127.0.0.1:2377"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
# swarm classic (1.12-)
|
||||||
|
# --providers.swarm.endpoint=tcp://127.0.0.1:2375
|
||||||
|
# docker swarm mode (1.12+)
|
||||||
|
--providers.swarm.endpoint=tcp://127.0.0.1:2377
|
||||||
|
```
|
||||||
|
|
||||||
|
Attach labels to services (not to containers) while in Swarm mode (in your docker compose file)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
my-container:
|
||||||
|
deploy:
|
||||||
|
labels:
|
||||||
|
- traefik.http.routers.my-container.rule=Host(`example.com`)
|
||||||
|
- traefik.http.services.my-container-service.loadbalancer.server.port=8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routing Configuration
|
||||||
|
|
||||||
|
When using Docker as a [provider](./overview.md),
|
||||||
|
Traefik uses [container labels](https://docs.docker.com/engine/reference/commandline/run/#label) to retrieve its routing configuration.
|
||||||
|
|
||||||
|
See the list of labels in the dedicated [routing](../routing/providers/docker.md) section.
|
||||||
|
|
||||||
|
### Routing Configuration with Labels
|
||||||
|
|
||||||
|
By default, Traefik watches for [container level labels](https://docs.docker.com/config/labels-custom-metadata/) on a standalone Docker Engine.
|
||||||
|
|
||||||
|
When using Docker Compose, labels are specified by the directive
|
||||||
|
[`labels`](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels) from the
|
||||||
|
["services" objects](https://docs.docker.com/compose/compose-file/compose-file-v3/#service-configuration-reference).
|
||||||
|
|
||||||
|
!!! tip "Not Only Docker"
|
||||||
|
|
||||||
|
Please note that any tool like Nomad, Terraform, Ansible, etc.
|
||||||
|
that is able to define a Docker container with labels can work
|
||||||
|
with Traefik and the Swarm provider.
|
||||||
|
|
||||||
|
While in Swarm Mode, Traefik uses labels found on services, not on individual containers.
|
||||||
|
|
||||||
|
Therefore, if you use a compose file with Swarm Mode, labels should be defined in the
|
||||||
|
[`deploy`](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1) part of your service.
|
||||||
|
|
||||||
|
This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/)).
|
||||||
|
|
||||||
|
### Port Detection
|
||||||
|
|
||||||
|
Traefik retrieves the private IP and port of containers from the Docker API.
|
||||||
|
|
||||||
|
Docker Swarm does not provide any port detection information to Traefik.
|
||||||
|
|
||||||
|
Therefore, you **must** specify the port to use for communication by using the label `traefik.http.services.<service_name>.loadbalancer.server.port`
|
||||||
|
(Check the reference for this label in the [routing section for Swarm](../routing/providers/swarm.md#services)).
|
||||||
|
|
||||||
|
### Host networking
|
||||||
|
|
||||||
|
When exposing containers that are configured with [host networking](https://docs.docker.com/network/host/),
|
||||||
|
the IP address of the host is resolved as follows:
|
||||||
|
|
||||||
|
<!-- TODO: verify and document the swarm mode case with container.Node.IPAddress coming from the API -->
|
||||||
|
- try a lookup of `host.docker.internal`
|
||||||
|
- if the lookup was unsuccessful, try a lookup of `host.containers.internal`, ([Podman](https://docs.podman.io/en/latest/) equivalent of `host.docker.internal`)
|
||||||
|
- if that lookup was also unsuccessful, fall back to `127.0.0.1`
|
||||||
|
|
||||||
|
On Linux, for versions of Docker older than 20.10.0, for `host.docker.internal` to be defined, it should be provided
|
||||||
|
as an `extra_host` to the Traefik container, using the `--add-host` flag. For example, to set it to the IP address of
|
||||||
|
the bridge interface (`docker0` by default): `--add-host=host.docker.internal:172.17.0.1`
|
||||||
|
|
||||||
|
### IPv4 && IPv6
|
||||||
|
|
||||||
|
When using a docker stack that uses IPv6,
|
||||||
|
Traefik will use the IPv4 container IP before its IPv6 counterpart.
|
||||||
|
Therefore, on an IPv6 Docker stack,
|
||||||
|
Traefik will use the IPv6 container IP.
|
||||||
|
|
||||||
|
### Docker API Access
|
||||||
|
|
||||||
|
Traefik requires access to the docker socket to get its dynamic configuration.
|
||||||
|
|
||||||
|
You can specify which Docker API Endpoint to use with the directive [`endpoint`](#endpoint).
|
||||||
|
|
||||||
|
!!! warning "Security Note"
|
||||||
|
|
||||||
|
Accessing the Docker API without any restriction is a security concern:
|
||||||
|
If Traefik is attacked, then the attacker might get access to the underlying host.
|
||||||
|
{: #security-note }
|
||||||
|
|
||||||
|
As explained in the [Docker Daemon Attack Surface documentation](https://docs.docker.com/engine/security/#docker-daemon-attack-surface):
|
||||||
|
|
||||||
|
!!! quote
|
||||||
|
|
||||||
|
[...] only **trusted** users should be allowed to control your Docker daemon [...]
|
||||||
|
|
||||||
|
??? success "Solutions"
|
||||||
|
|
||||||
|
Expose the Docker socket over TCP or SSH, instead of the default Unix socket file.
|
||||||
|
It allows different implementation levels of the [AAA (Authentication, Authorization, Accounting) concepts](https://en.wikipedia.org/wiki/AAA_(computer_security)), depending on your security assessment:
|
||||||
|
|
||||||
|
- Authentication with Client Certificates as described in ["Protect the Docker daemon socket."](https://docs.docker.com/engine/security/protect-access/)
|
||||||
|
- Authorize and filter requests to restrict possible actions with [the TecnativaDocker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy).
|
||||||
|
- Authorization with the [Docker Authorization Plugin Mechanism](https://web.archive.org/web/20190920092526/https://docs.docker.com/engine/extend/plugins_authorization/)
|
||||||
|
- Accounting at networking level, by exposing the socket only inside a Docker private network, only available for Traefik.
|
||||||
|
- Accounting at container level, by exposing the socket on a another container than Traefik's.
|
||||||
|
It allows scheduling of Traefik on worker nodes, with only the "socket exposer" container on the manager nodes.
|
||||||
|
- Accounting at kernel level, by enforcing kernel calls with mechanisms like [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux), to only allows an identified set of actions for Traefik's process (or the "socket exposer" process).
|
||||||
|
- SSH public key authentication (SSH is supported with Docker > 18.09)
|
||||||
|
|
||||||
|
??? info "More Resources and Examples"
|
||||||
|
|
||||||
|
- ["Paranoid about mounting /var/run/docker.sock?"](https://medium.com/@containeroo/traefik-2-0-paranoid-about-mounting-var-run-docker-sock-22da9cb3e78c)
|
||||||
|
- [Traefik and Docker: A Discussion with Docker Captain, Bret Fisher](https://blog.traefik.io/traefik-and-docker-a-discussion-with-docker-captain-bret-fisher-7f0b9a54ff88)
|
||||||
|
- [KubeCon EU 2018 Keynote, Running with Scissors, from Liz Rice](https://www.youtube.com/watch?v=ltrV-Qmh3oY)
|
||||||
|
- [Don't expose the Docker socket (not even to a container)](https://www.lvh.io/posts/dont-expose-the-docker-socket-not-even-to-a-container/)
|
||||||
|
- [A thread on Stack Overflow about sharing the `/var/run/docker.sock` file](https://news.ycombinator.com/item?id=17983623)
|
||||||
|
- [To DinD or not to DinD](https://blog.loof.fr/2018/01/to-dind-or-not-do-dind.html)
|
||||||
|
- [Traefik issue GH-4174 about security with Docker socket](https://github.com/traefik/traefik/issues/4174)
|
||||||
|
- [Inspecting Docker Activity with Socat](https://developers.redhat.com/blog/2015/02/25/inspecting-docker-activity-with-socat/)
|
||||||
|
- [Letting Traefik run on Worker Nodes](https://blog.mikesir87.io/2018/07/letting-traefik-run-on-worker-nodes/)
|
||||||
|
- [Docker Socket Proxy from Tecnativa](https://github.com/Tecnativa/docker-socket-proxy)
|
||||||
|
|
||||||
|
Since the Swarm API is only exposed on the [manager nodes](https://docs.docker.com/engine/swarm/how-swarm-mode-works/nodes/#manager-nodes),
|
||||||
|
these are the nodes that Traefik should be scheduled on by deploying Traefik with a constraint on the node "role":
|
||||||
|
|
||||||
|
```shell tab="With Docker CLI"
|
||||||
|
docker service create \
|
||||||
|
--constraint=node.role==manager \
|
||||||
|
#... \
|
||||||
|
```
|
||||||
|
|
||||||
|
```yml tab="With Docker Compose"
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
# ...
|
||||||
|
deploy:
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.role == manager
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip "Scheduling Traefik on Worker Nodes"
|
||||||
|
|
||||||
|
Following the guidelines given in the previous section ["Docker API Access"](#docker-api-access),
|
||||||
|
if you expose the Docker API through TCP, then Traefik can be scheduled on any node if the TCP
|
||||||
|
socket is reachable.
|
||||||
|
|
||||||
|
Please consider the security implications by reading the [Security Note](#security-note).
|
||||||
|
|
||||||
|
A good example can be found on [Bret Fisher's repository](https://github.com/BretFisher/dogvscat/blob/master/stack-proxy-global.yml#L124).
|
||||||
|
|
||||||
|
### `endpoint`
|
||||||
|
|
||||||
|
_Required, Default="unix:///var/run/docker.sock"_
|
||||||
|
|
||||||
|
See the [Docker Swarm API Access](#docker-api-access) section for more information.
|
||||||
|
|
||||||
|
??? example "Using the docker.sock"
|
||||||
|
|
||||||
|
The docker-compose file shares the docker sock with the Traefik container
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.0 # The official v2 Traefik docker image
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
We specify the docker.sock in traefik's configuration file.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
endpoint = "unix:///var/run/docker.sock"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.endpoint=unix:///var/run/docker.sock
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
??? example "Using SSH"
|
||||||
|
|
||||||
|
Using Docker 18.09+ you can connect Traefik to daemon using SSH
|
||||||
|
We specify the SSH host and user in Traefik's configuration file.
|
||||||
|
Note that is server requires public keys for authentication you must have those accessible for user who runs Traefik.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
endpoint: "ssh://traefik@192.168.2.5:2022"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
endpoint = "ssh://traefik@192.168.2.5:2022"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.endpoint=ssh://traefik@192.168.2.5:2022
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
endpoint = "unix:///var/run/docker.sock"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.endpoint=unix:///var/run/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
### `useBindPortIP`
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Traefik routes requests to the IP/port of the matching container.
|
||||||
|
When setting `useBindPortIP=true`, you tell Traefik to use the IP/Port attached to the container's _binding_ instead of its inner network IP/Port.
|
||||||
|
|
||||||
|
When used in conjunction with the `traefik.http.services.<name>.loadbalancer.server.port` label (that tells Traefik to route requests to a specific port),
|
||||||
|
Traefik tries to find a binding on port `traefik.http.services.<name>.loadbalancer.server.port`.
|
||||||
|
If it cannot find such a binding, Traefik falls back on the internal network IP of the container,
|
||||||
|
but still uses the `traefik.http.services.<name>.loadbalancer.server.port` that is set in the label.
|
||||||
|
|
||||||
|
??? example "Examples of `usebindportip` in different situations."
|
||||||
|
|
||||||
|
| port label | Container's binding | Routes to |
|
||||||
|
|--------------------|----------------------------------------------------|----------------|
|
||||||
|
| - | - | IntIP:IntPort |
|
||||||
|
| - | ExtPort:IntPort | IntIP:IntPort |
|
||||||
|
| - | ExtIp:ExtPort:IntPort | ExtIp:ExtPort |
|
||||||
|
| LblPort | - | IntIp:LblPort |
|
||||||
|
| LblPort | ExtIp:ExtPort:LblPort | ExtIp:ExtPort |
|
||||||
|
| LblPort | ExtIp:ExtPort:OtherPort | IntIp:LblPort |
|
||||||
|
| LblPort | ExtIp1:ExtPort1:IntPort1 & ExtIp2:LblPort:IntPort2 | ExtIp2:LblPort |
|
||||||
|
|
||||||
|
!!! info ""
|
||||||
|
In the above table:
|
||||||
|
|
||||||
|
- `ExtIp` stands for "external IP found in the binding"
|
||||||
|
- `IntIp` stands for "internal network container's IP",
|
||||||
|
- `ExtPort` stands for "external Port found in the binding"
|
||||||
|
- `IntPort` stands for "internal network container's port."
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
useBindPortIP: true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
useBindPortIP = true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.useBindPortIP=true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `exposedByDefault`
|
||||||
|
|
||||||
|
_Optional, Default=true_
|
||||||
|
|
||||||
|
Expose containers by default through Traefik.
|
||||||
|
If set to `false`, containers that do not have a `traefik.enable=true` label are ignored from the resulting routing configuration.
|
||||||
|
|
||||||
|
For additional information, refer to [Restrict the Scope of Service Discovery](./overview.md#restrict-the-scope-of-service-discovery).
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
exposedByDefault: false
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
exposedByDefault = false
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.exposedByDefault=false
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `network`
|
||||||
|
|
||||||
|
_Optional, Default=""_
|
||||||
|
|
||||||
|
Defines a default docker network to use for connections to all containers.
|
||||||
|
|
||||||
|
This option can be overridden on a per-container basis with the `traefik.docker.network` label.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
network: test
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
network = "test"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.network=test
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `defaultRule`
|
||||||
|
|
||||||
|
_Optional, Default=```Host(`{{ normalize .Name }}`)```_
|
||||||
|
|
||||||
|
The `defaultRule` option defines what routing rule to apply to a container if no rule is defined by a label.
|
||||||
|
|
||||||
|
It must be a valid [Go template](https://pkg.go.dev/text/template/), and can use
|
||||||
|
[sprig template functions](https://masterminds.github.io/sprig/).
|
||||||
|
The container service name can be accessed with the `Name` identifier,
|
||||||
|
and the template has access to all the labels defined on this container.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
defaultRule: "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
defaultRule = "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.defaultRule=Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `swarmMode`
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Enables the Swarm Mode (instead of standalone Docker).
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
swarmMode: true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
swarmMode = true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.swarmMode=true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `swarmModeRefreshSeconds`
|
||||||
|
|
||||||
|
_Optional, Default=15_
|
||||||
|
|
||||||
|
Defines the polling interval (in seconds) for Swarm Mode.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
swarmModeRefreshSeconds: 30
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
swarmModeRefreshSeconds = 30
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.swarmModeRefreshSeconds=30
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `httpClientTimeout`
|
||||||
|
|
||||||
|
_Optional, Default=0_
|
||||||
|
|
||||||
|
Defines the client timeout (in seconds) for HTTP connections. If its value is `0`, no timeout is set.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
httpClientTimeout: 300
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
httpClientTimeout = 300
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.httpClientTimeout=300
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `watch`
|
||||||
|
|
||||||
|
_Optional, Default=true_
|
||||||
|
|
||||||
|
Watch Docker events.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
watch: false
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
watch = false
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.watch=false
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `constraints`
|
||||||
|
|
||||||
|
_Optional, Default=""_
|
||||||
|
|
||||||
|
The `constraints` option can be set to an expression that Traefik matches against the container labels to determine whether
|
||||||
|
to create any route for that container. If none of the container labels match the expression, no route for that container is
|
||||||
|
created. If the expression is empty, all detected containers are included.
|
||||||
|
|
||||||
|
The expression syntax is based on the `Label("key", "value")`, and `LabelRegex("key", "value")` functions,
|
||||||
|
as well as the usual boolean logic, as shown in examples below.
|
||||||
|
|
||||||
|
??? example "Constraints Expression Examples"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Includes only containers having a label with key `a.label.name` and value `foo`
|
||||||
|
constraints = "Label(`a.label.name`, `foo`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Excludes containers having any label with key `a.label.name` and value `foo`
|
||||||
|
constraints = "!Label(`a.label.name`, `value`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# With logical AND.
|
||||||
|
constraints = "Label(`a.label.name`, `valueA`) && Label(`another.label.name`, `valueB`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# With logical OR.
|
||||||
|
constraints = "Label(`a.label.name`, `valueA`) || Label(`another.label.name`, `valueB`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# With logical AND and OR, with precedence set by parentheses.
|
||||||
|
constraints = "Label(`a.label.name`, `valueA`) && (Label(`another.label.name`, `valueB`) || Label(`yet.another.label.name`, `valueC`))"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Includes only containers having a label with key `a.label.name` and a value matching the `a.+` regular expression.
|
||||||
|
constraints = "LabelRegex(`a.label.name`, `a.+`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
For additional information, refer to [Restrict the Scope of Service Discovery](./overview.md#restrict-the-scope-of-service-discovery).
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
constraints: "Label(`a.label.name`,`foo`)"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
constraints = "Label(`a.label.name`,`foo`)"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.constraints=Label(`a.label.name`,`foo`)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tls`
|
||||||
|
|
||||||
|
_Optional_
|
||||||
|
|
||||||
|
Defines the TLS configuration used for the secure connection to Docker.
|
||||||
|
|
||||||
|
#### `ca`
|
||||||
|
|
||||||
|
_Optional_
|
||||||
|
|
||||||
|
`ca` is the path to the certificate authority used for the secure connection to Docker,
|
||||||
|
it defaults to the system bundle.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
tls:
|
||||||
|
ca: path/to/ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm.tls]
|
||||||
|
ca = "path/to/ca.crt"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.tls.ca=path/to/ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `cert`
|
||||||
|
|
||||||
|
`cert` is the path to the public certificate used for the secure connection to Docker.
|
||||||
|
When using this option, setting the `key` option is required.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
tls:
|
||||||
|
cert: path/to/foo.cert
|
||||||
|
key: path/to/foo.key
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm.tls]
|
||||||
|
cert = "path/to/foo.cert"
|
||||||
|
key = "path/to/foo.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.tls.cert=path/to/foo.cert
|
||||||
|
--providers.swarm.tls.key=path/to/foo.key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `key`
|
||||||
|
|
||||||
|
_Optional_
|
||||||
|
|
||||||
|
`key` is the path to the private key used for the secure connection Docker.
|
||||||
|
When using this option, setting the `cert` option is required.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
tls:
|
||||||
|
cert: path/to/foo.cert
|
||||||
|
key: path/to/foo.key
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm.tls]
|
||||||
|
cert = "path/to/foo.cert"
|
||||||
|
key = "path/to/foo.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.tls.cert=path/to/foo.cert
|
||||||
|
--providers.swarm.tls.key=path/to/foo.key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `insecureSkipVerify`
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
If `insecureSkipVerify` is `true`, the TLS connection to Docker accepts any certificate presented by the server regardless of the hostnames it covers.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
tls:
|
||||||
|
insecureSkipVerify: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm.tls]
|
||||||
|
insecureSkipVerify = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.tls.insecureSkipVerify=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `allowEmptyServices`
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
If the parameter is set to `true`,
|
||||||
|
any [servers load balancer](../routing/services/index.md#servers-load-balancer) defined for Docker containers is created
|
||||||
|
regardless of the [healthiness](https://docs.docker.com/engine/reference/builder/#healthcheck) of the corresponding containers.
|
||||||
|
It also then stays alive and responsive even at times when it becomes empty,
|
||||||
|
i.e. when all its children containers become unhealthy.
|
||||||
|
This results in `503` HTTP responses instead of `404` ones,
|
||||||
|
in the above cases.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
allowEmptyServices: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
allowEmptyServices = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.swarm.allowEmptyServices=true
|
||||||
|
```
|
||||||
|
|
||||||
|
{!traefik-for-business-applications.md!}
|
|
@ -8,7 +8,7 @@ description: "Reference dynamic configuration with Docker labels in Traefik Prox
|
||||||
Dynamic configuration with Docker Labels
|
Dynamic configuration with Docker Labels
|
||||||
{: .subtitle }
|
{: .subtitle }
|
||||||
|
|
||||||
The labels are case insensitive.
|
The labels are case-insensitive.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
labels:
|
labels:
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=foobar"
|
- "traefik.docker.network=foobar"
|
||||||
- "traefik.docker.lbswarm=true"
|
|
||||||
|
|
17
docs/content/reference/dynamic-configuration/swarm.md
Normal file
17
docs/content/reference/dynamic-configuration/swarm.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
title: "Traefik Docker Swarm Configuration Documentation"
|
||||||
|
description: "Reference dynamic configuration with Docker Swarm labels in Traefik Proxy. Read the technical documentation."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Docker Swarm Configuration Reference
|
||||||
|
|
||||||
|
Dynamic configuration with Docker Labels
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
The labels are case-insensitive.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
--8<-- "content/reference/dynamic-configuration/swarm.yml"
|
||||||
|
--8<-- "content/reference/dynamic-configuration/docker-labels.yml"
|
||||||
|
```
|
3
docs/content/reference/dynamic-configuration/swarm.yml
Normal file
3
docs/content/reference/dynamic-configuration/swarm.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=foobar"
|
||||||
|
- "traefik.docker.lbswarm=true"
|
|
@ -532,7 +532,7 @@ Constraints is an expression that Traefik matches against the container's labels
|
||||||
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
||||||
|
|
||||||
`--providers.docker.endpoint`:
|
`--providers.docker.endpoint`:
|
||||||
Docker server endpoint. Can be a tcp or a unix socket endpoint. (Default: ```unix:///var/run/docker.sock```)
|
Docker server endpoint. Can be a TCP or a Unix socket endpoint. (Default: ```unix:///var/run/docker.sock```)
|
||||||
|
|
||||||
`--providers.docker.exposedbydefault`:
|
`--providers.docker.exposedbydefault`:
|
||||||
Expose containers by default. (Default: ```true```)
|
Expose containers by default. (Default: ```true```)
|
||||||
|
@ -543,12 +543,6 @@ Client timeout for HTTP connections. (Default: ```0```)
|
||||||
`--providers.docker.network`:
|
`--providers.docker.network`:
|
||||||
Default Docker network used.
|
Default Docker network used.
|
||||||
|
|
||||||
`--providers.docker.swarmmode`:
|
|
||||||
Use Docker on Swarm Mode. (Default: ```false```)
|
|
||||||
|
|
||||||
`--providers.docker.swarmmoderefreshseconds`:
|
|
||||||
Polling interval for swarm mode. (Default: ```15```)
|
|
||||||
|
|
||||||
`--providers.docker.tls.ca`:
|
`--providers.docker.tls.ca`:
|
||||||
TLS CA
|
TLS CA
|
||||||
|
|
||||||
|
@ -855,6 +849,51 @@ Enable Rest backend with default settings. (Default: ```false```)
|
||||||
`--providers.rest.insecure`:
|
`--providers.rest.insecure`:
|
||||||
Activate REST Provider directly on the entryPoint named traefik. (Default: ```false```)
|
Activate REST Provider directly on the entryPoint named traefik. (Default: ```false```)
|
||||||
|
|
||||||
|
`--providers.swarm`:
|
||||||
|
Enable Docker Swarm backend with default settings. (Default: ```false```)
|
||||||
|
|
||||||
|
`--providers.swarm.allowemptyservices`:
|
||||||
|
Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services. (Default: ```false```)
|
||||||
|
|
||||||
|
`--providers.swarm.constraints`:
|
||||||
|
Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container.
|
||||||
|
|
||||||
|
`--providers.swarm.defaultrule`:
|
||||||
|
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
||||||
|
|
||||||
|
`--providers.swarm.endpoint`:
|
||||||
|
Docker server endpoint. Can be a TCP or a Unix socket endpoint. (Default: ```unix:///var/run/docker.sock```)
|
||||||
|
|
||||||
|
`--providers.swarm.exposedbydefault`:
|
||||||
|
Expose containers by default. (Default: ```true```)
|
||||||
|
|
||||||
|
`--providers.swarm.httpclienttimeout`:
|
||||||
|
Client timeout for HTTP connections. (Default: ```0```)
|
||||||
|
|
||||||
|
`--providers.swarm.network`:
|
||||||
|
Default Docker network used.
|
||||||
|
|
||||||
|
`--providers.swarm.refreshseconds`:
|
||||||
|
Polling interval for swarm mode. (Default: ```15```)
|
||||||
|
|
||||||
|
`--providers.swarm.tls.ca`:
|
||||||
|
TLS CA
|
||||||
|
|
||||||
|
`--providers.swarm.tls.cert`:
|
||||||
|
TLS cert
|
||||||
|
|
||||||
|
`--providers.swarm.tls.insecureskipverify`:
|
||||||
|
TLS insecure skip verify (Default: ```false```)
|
||||||
|
|
||||||
|
`--providers.swarm.tls.key`:
|
||||||
|
TLS key
|
||||||
|
|
||||||
|
`--providers.swarm.usebindportip`:
|
||||||
|
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
|
||||||
|
|
||||||
|
`--providers.swarm.watch`:
|
||||||
|
Watch Docker events. (Default: ```true```)
|
||||||
|
|
||||||
`--providers.zookeeper`:
|
`--providers.zookeeper`:
|
||||||
Enable ZooKeeper backend with default settings. (Default: ```false```)
|
Enable ZooKeeper backend with default settings. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
@ -532,7 +532,7 @@ Constraints is an expression that Traefik matches against the container's labels
|
||||||
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_DOCKER_ENDPOINT`:
|
`TRAEFIK_PROVIDERS_DOCKER_ENDPOINT`:
|
||||||
Docker server endpoint. Can be a tcp or a unix socket endpoint. (Default: ```unix:///var/run/docker.sock```)
|
Docker server endpoint. Can be a TCP or a Unix socket endpoint. (Default: ```unix:///var/run/docker.sock```)
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT`:
|
`TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT`:
|
||||||
Expose containers by default. (Default: ```true```)
|
Expose containers by default. (Default: ```true```)
|
||||||
|
@ -543,12 +543,6 @@ Client timeout for HTTP connections. (Default: ```0```)
|
||||||
`TRAEFIK_PROVIDERS_DOCKER_NETWORK`:
|
`TRAEFIK_PROVIDERS_DOCKER_NETWORK`:
|
||||||
Default Docker network used.
|
Default Docker network used.
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_DOCKER_SWARMMODE`:
|
|
||||||
Use Docker on Swarm Mode. (Default: ```false```)
|
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_DOCKER_SWARMMODEREFRESHSECONDS`:
|
|
||||||
Polling interval for swarm mode. (Default: ```15```)
|
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_DOCKER_TLS_CA`:
|
`TRAEFIK_PROVIDERS_DOCKER_TLS_CA`:
|
||||||
TLS CA
|
TLS CA
|
||||||
|
|
||||||
|
@ -855,6 +849,51 @@ Enable Rest backend with default settings. (Default: ```false```)
|
||||||
`TRAEFIK_PROVIDERS_REST_INSECURE`:
|
`TRAEFIK_PROVIDERS_REST_INSECURE`:
|
||||||
Activate REST Provider directly on the entryPoint named traefik. (Default: ```false```)
|
Activate REST Provider directly on the entryPoint named traefik. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM`:
|
||||||
|
Enable Docker Swarm backend with default settings. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_ALLOWEMPTYSERVICES`:
|
||||||
|
Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_CONSTRAINTS`:
|
||||||
|
Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container.
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_DEFAULTRULE`:
|
||||||
|
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_ENDPOINT`:
|
||||||
|
Docker server endpoint. Can be a TCP or a Unix socket endpoint. (Default: ```unix:///var/run/docker.sock```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_EXPOSEDBYDEFAULT`:
|
||||||
|
Expose containers by default. (Default: ```true```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_HTTPCLIENTTIMEOUT`:
|
||||||
|
Client timeout for HTTP connections. (Default: ```0```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_NETWORK`:
|
||||||
|
Default Docker network used.
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_REFRESHSECONDS`:
|
||||||
|
Polling interval for swarm mode. (Default: ```15```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_TLS_CA`:
|
||||||
|
TLS CA
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_TLS_CERT`:
|
||||||
|
TLS cert
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_TLS_INSECURESKIPVERIFY`:
|
||||||
|
TLS insecure skip verify (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_TLS_KEY`:
|
||||||
|
TLS key
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_USEBINDPORTIP`:
|
||||||
|
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_SWARM_WATCH`:
|
||||||
|
Watch Docker events. (Default: ```true```)
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_ZOOKEEPER`:
|
`TRAEFIK_PROVIDERS_ZOOKEEPER`:
|
||||||
Enable ZooKeeper backend with default settings. (Default: ```false```)
|
Enable ZooKeeper backend with default settings. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
@ -83,9 +83,7 @@
|
||||||
defaultRule = "foobar"
|
defaultRule = "foobar"
|
||||||
exposedByDefault = true
|
exposedByDefault = true
|
||||||
useBindPortIP = true
|
useBindPortIP = true
|
||||||
swarmMode = true
|
|
||||||
network = "foobar"
|
network = "foobar"
|
||||||
swarmModeRefreshSeconds = "42s"
|
|
||||||
httpClientTimeout = "42s"
|
httpClientTimeout = "42s"
|
||||||
allowEmptyServices = true
|
allowEmptyServices = true
|
||||||
[providers.docker.tls]
|
[providers.docker.tls]
|
||||||
|
@ -93,6 +91,22 @@
|
||||||
cert = "foobar"
|
cert = "foobar"
|
||||||
key = "foobar"
|
key = "foobar"
|
||||||
insecureSkipVerify = true
|
insecureSkipVerify = true
|
||||||
|
[providers.swarm]
|
||||||
|
constraints = "foobar"
|
||||||
|
watch = true
|
||||||
|
endpoint = "foobar"
|
||||||
|
defaultRule = "foobar"
|
||||||
|
exposedByDefault = true
|
||||||
|
useBindPortIP = true
|
||||||
|
network = "foobar"
|
||||||
|
refreshSeconds = "42s"
|
||||||
|
httpClientTimeout = "42s"
|
||||||
|
allowEmptyServices = true
|
||||||
|
[providers.swarm.tls]
|
||||||
|
ca = "foobar"
|
||||||
|
cert = "foobar"
|
||||||
|
key = "foobar"
|
||||||
|
insecureSkipVerify = true
|
||||||
[providers.file]
|
[providers.file]
|
||||||
directory = "foobar"
|
directory = "foobar"
|
||||||
watch = true
|
watch = true
|
||||||
|
|
|
@ -95,9 +95,23 @@ providers:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
exposedByDefault: true
|
exposedByDefault: true
|
||||||
useBindPortIP: true
|
useBindPortIP: true
|
||||||
swarmMode: true
|
|
||||||
network: foobar
|
network: foobar
|
||||||
swarmModeRefreshSeconds: 42s
|
httpClientTimeout: 42s
|
||||||
|
allowEmptyServices: true
|
||||||
|
swarm:
|
||||||
|
constraints: foobar
|
||||||
|
watch: true
|
||||||
|
endpoint: foobar
|
||||||
|
defaultRule: foobar
|
||||||
|
tls:
|
||||||
|
ca: foobar
|
||||||
|
cert: foobar
|
||||||
|
key: foobar
|
||||||
|
insecureSkipVerify: true
|
||||||
|
exposedByDefault: true
|
||||||
|
useBindPortIP: true
|
||||||
|
network: foobar
|
||||||
|
refreshSeconds: 42s
|
||||||
httpClientTimeout: 42s
|
httpClientTimeout: 42s
|
||||||
allowEmptyServices: true
|
allowEmptyServices: true
|
||||||
file:
|
file:
|
||||||
|
|
|
@ -83,54 +83,6 @@ Attach labels to your containers and let Traefik do the rest!
|
||||||
- traefik.http.services.admin-service.loadbalancer.server.port=9000
|
- traefik.http.services.admin-service.loadbalancer.server.port=9000
|
||||||
```
|
```
|
||||||
|
|
||||||
??? example "Configuring Docker Swarm & Deploying / Exposing Services"
|
|
||||||
|
|
||||||
Enabling the docker provider (Swarm Mode)
|
|
||||||
|
|
||||||
```yaml tab="File (YAML)"
|
|
||||||
providers:
|
|
||||||
docker:
|
|
||||||
# swarm classic (1.12-)
|
|
||||||
# endpoint: "tcp://127.0.0.1:2375"
|
|
||||||
# docker swarm mode (1.12+)
|
|
||||||
endpoint: "tcp://127.0.0.1:2377"
|
|
||||||
swarmMode: true
|
|
||||||
```
|
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
|
||||||
[providers.docker]
|
|
||||||
# swarm classic (1.12-)
|
|
||||||
# endpoint = "tcp://127.0.0.1:2375"
|
|
||||||
# docker swarm mode (1.12+)
|
|
||||||
endpoint = "tcp://127.0.0.1:2377"
|
|
||||||
swarmMode = true
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash tab="CLI"
|
|
||||||
# swarm classic (1.12-)
|
|
||||||
# --providers.docker.endpoint=tcp://127.0.0.1:2375
|
|
||||||
# docker swarm mode (1.12+)
|
|
||||||
--providers.docker.endpoint=tcp://127.0.0.1:2377
|
|
||||||
--providers.docker.swarmMode=true
|
|
||||||
```
|
|
||||||
|
|
||||||
Attach labels to services (not to containers) while in Swarm mode (in your docker compose file)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: "3"
|
|
||||||
services:
|
|
||||||
my-container:
|
|
||||||
deploy:
|
|
||||||
labels:
|
|
||||||
- traefik.http.routers.my-container.rule=Host(`example.com`)
|
|
||||||
- traefik.http.services.my-container-service.loadbalancer.server.port=8080
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! important "Labels in Docker Swarm Mode"
|
|
||||||
While in Swarm Mode, Traefik uses labels found on services, not on individual containers.
|
|
||||||
Therefore, if you use a compose file with Swarm Mode, labels should be defined in the `deploy` part of your service.
|
|
||||||
This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1)).
|
|
||||||
|
|
||||||
## Routing Configuration
|
## Routing Configuration
|
||||||
|
|
||||||
!!! info "Labels"
|
!!! info "Labels"
|
||||||
|
@ -275,9 +227,6 @@ you'd add the label `traefik.http.services.<name-of-your-choice>.loadbalancer.pa
|
||||||
Registers a port.
|
Registers a port.
|
||||||
Useful when the container exposes multiples ports.
|
Useful when the container exposes multiples ports.
|
||||||
|
|
||||||
Mandatory for Docker Swarm (see the section ["Port Detection with Docker Swarm"](../../providers/docker.md#port-detection_1)).
|
|
||||||
{: #port }
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- "traefik.http.services.myservice.loadbalancer.server.port=8080"
|
- "traefik.http.services.myservice.loadbalancer.server.port=8080"
|
||||||
```
|
```
|
||||||
|
@ -675,14 +624,3 @@ otherwise it will randomly pick one (depending on how docker is returning them).
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`.
|
When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`.
|
||||||
|
|
||||||
#### `traefik.docker.lbswarm`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- "traefik.docker.lbswarm=true"
|
|
||||||
```
|
|
||||||
|
|
||||||
Enables Swarm's inbuilt load balancer (only relevant in Swarm Mode).
|
|
||||||
|
|
||||||
If you enable this option, Traefik will use the virtual IP provided by docker swarm instead of the containers IPs.
|
|
||||||
Which means that Traefik will not perform any kind of load balancing and will delegate this task to swarm.
|
|
||||||
|
|
640
docs/content/routing/providers/swarm.md
Normal file
640
docs/content/routing/providers/swarm.md
Normal file
|
@ -0,0 +1,640 @@
|
||||||
|
---
|
||||||
|
title: "Traefik Docker Swarm Routing Documentation"
|
||||||
|
description: "This guide will teach you how to attach labels to your containers, to route traffic and load balance with Traefik and Docker."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Traefik & Docker Swarm
|
||||||
|
|
||||||
|
A Story of Labels & Containers
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
![Swarm](../../assets/img/providers/docker.png)
|
||||||
|
|
||||||
|
Attach labels to your containers and let Traefik do the rest!
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
??? example "Configuring Docker Swarm & Deploying / Exposing Services"
|
||||||
|
|
||||||
|
Enabling the docker provider (Swarm Mode)
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
swarm:
|
||||||
|
# swarm classic (1.12-)
|
||||||
|
# endpoint: "tcp://127.0.0.1:2375"
|
||||||
|
# docker swarm mode (1.12+)
|
||||||
|
endpoint: "tcp://127.0.0.1:2377"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.swarm]
|
||||||
|
# swarm classic (1.12-)
|
||||||
|
# endpoint = "tcp://127.0.0.1:2375"
|
||||||
|
# docker swarm mode (1.12+)
|
||||||
|
endpoint = "tcp://127.0.0.1:2377"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
# swarm classic (1.12-)
|
||||||
|
# --providers.swarm.endpoint=tcp://127.0.0.1:2375
|
||||||
|
# docker swarm mode (1.12+)
|
||||||
|
--providers.swarm.endpoint=tcp://127.0.0.1:2377
|
||||||
|
```
|
||||||
|
|
||||||
|
Attach labels to services (not to containers) while in Swarm mode (in your docker compose file)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
my-container:
|
||||||
|
deploy:
|
||||||
|
labels:
|
||||||
|
- traefik.http.routers.my-container.rule=Host(`example.com`)
|
||||||
|
- traefik.http.services.my-container-service.loadbalancer.server.port=8080
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! important "Labels in Docker Swarm Mode"
|
||||||
|
While in Swarm Mode, Traefik uses labels found on services, not on individual containers.
|
||||||
|
Therefore, if you use a compose file with Swarm Mode, labels should be defined in the `deploy` part of your service.
|
||||||
|
This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1)).
|
||||||
|
|
||||||
|
??? example "Specifying more than one router and service per container"
|
||||||
|
|
||||||
|
Forwarding requests to more than one port on a container requires referencing the service loadbalancer port definition using the service parameter on the router.
|
||||||
|
|
||||||
|
In this example, requests are forwarded for `http://example-a.com` to `http://<private IP of container>:8000` in addition to `http://example-b.com` forwarding to `http://<private IP of container>:9000`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
my-container:
|
||||||
|
# ...
|
||||||
|
deploy:
|
||||||
|
labels:
|
||||||
|
- traefik.http.routers.www-router.rule=Host(`example-a.com`)
|
||||||
|
- traefik.http.routers.www-router.service=www-service
|
||||||
|
- traefik.http.services.www-service.loadbalancer.server.port=8000
|
||||||
|
- traefik.http.routers.admin-router.rule=Host(`example-b.com`)
|
||||||
|
- traefik.http.routers.admin-router.service=admin-service
|
||||||
|
- traefik.http.services.admin-service.loadbalancer.server.port=9000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routing Configuration
|
||||||
|
|
||||||
|
!!! info "Labels"
|
||||||
|
|
||||||
|
- Labels are case insensitive.
|
||||||
|
- The complete list of labels can be found in [the reference page](../../reference/dynamic-configuration/docker.md).
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
Traefik creates, for each container, a corresponding [service](../services/index.md) and [router](../routers/index.md).
|
||||||
|
|
||||||
|
The Service automatically gets a server per instance of the container,
|
||||||
|
and the router automatically gets a rule defined by `defaultRule` (if no rule for it was defined in labels).
|
||||||
|
|
||||||
|
#### Service definition
|
||||||
|
|
||||||
|
--8<-- "content/routing/providers/service-by-label.md"
|
||||||
|
|
||||||
|
??? example "Automatic service assignment with labels"
|
||||||
|
|
||||||
|
With labels in a compose file
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.myproxy.rule=Host(`example.net`)"
|
||||||
|
# service myservice gets automatically assigned to router myproxy
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.server.port=80"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? example "Automatic service creation and assignment with labels"
|
||||||
|
|
||||||
|
With labels in a compose file
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
# no service specified or defined and yet one gets automatically created
|
||||||
|
# and assigned to router myproxy.
|
||||||
|
- "traefik.http.routers.myproxy.rule=Host(`example.net`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Routers
|
||||||
|
|
||||||
|
To update the configuration of the Router automatically attached to the container,
|
||||||
|
add labels starting with `traefik.http.routers.<name-of-your-choice>.` and followed by the option you want to change.
|
||||||
|
|
||||||
|
For example, to change the rule, you could add the label ```traefik.http.routers.my-container.rule=Host(`example.com`)```.
|
||||||
|
|
||||||
|
!!! warning "The character `@` is not authorized in the router name `<router_name>`."
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.rule`"
|
||||||
|
|
||||||
|
See [rule](../routers/index.md#rule) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.rule=Host(`example.com`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.entrypoints`"
|
||||||
|
|
||||||
|
See [entry points](../routers/index.md#entrypoints) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.entrypoints=ep1,ep2"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.middlewares`"
|
||||||
|
|
||||||
|
See [middlewares](../routers/index.md#middlewares) and [middlewares overview](../../middlewares/overview.md) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.middlewares=auth,prefix,cb"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.service`"
|
||||||
|
|
||||||
|
See [service](../routers/index.md#service) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.service=myservice"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls`"
|
||||||
|
|
||||||
|
See [tls](../routers/index.md#tls) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.tls=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls.certresolver`"
|
||||||
|
|
||||||
|
See [certResolver](../routers/index.md#certresolver) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.tls.certresolver=myresolver"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls.domains[n].main`"
|
||||||
|
|
||||||
|
See [domains](../routers/index.md#domains) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.tls.domains[0].main=example.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls.domains[n].sans`"
|
||||||
|
|
||||||
|
See [domains](../routers/index.md#domains) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.tls.domains[0].sans=test.example.org,dev.example.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls.options`"
|
||||||
|
|
||||||
|
See [options](../routers/index.md#options) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.tls.options=foobar"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.priority`"
|
||||||
|
|
||||||
|
See [priority](../routers/index.md#priority) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.routers.myrouter.priority=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
To update the configuration of the Service automatically attached to the container,
|
||||||
|
add labels starting with `traefik.http.services.<name-of-your-choice>.`, followed by the option you want to change.
|
||||||
|
|
||||||
|
For example, to change the `passHostHeader` behavior,
|
||||||
|
you'd add the label `traefik.http.services.<name-of-your-choice>.loadbalancer.passhostheader=false`.
|
||||||
|
|
||||||
|
!!! warning "The character `@` is not authorized in the service name `<service_name>`."
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.server.port`"
|
||||||
|
|
||||||
|
Registers a port.
|
||||||
|
Useful when the container exposes multiples ports.
|
||||||
|
|
||||||
|
Mandatory for Docker Swarm (see the section ["Port Detection with Docker Swarm"](../../providers/docker.md#port-detection)).
|
||||||
|
{: #port }
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.server.port=8080"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.server.scheme`"
|
||||||
|
|
||||||
|
Overrides the default scheme.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.server.scheme=http"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.serverstransport`"
|
||||||
|
|
||||||
|
Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
|
||||||
|
See [serverstransport](../services/index.md#serverstransport) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.<service_name>.loadbalancer.serverstransport=foobar@file"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.passhostheader`"
|
||||||
|
|
||||||
|
See [pass Host header](../services/index.md#pass-host-header) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.passhostheader=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.headers.<header_name>`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.headers.X-Foo=foobar"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.hostname`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.hostname=example.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.interval`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.interval=10s"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.path`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.method`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.status`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.status=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.port`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.port=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.scheme`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.scheme=http"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.timeout`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.timeout=10s"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.followredirects`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.healthcheck.followredirects=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky.cookie`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.sticky.cookie=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky.cookie.httponly`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.sticky.cookie.httponly=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky.cookie.name`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky.cookie.secure`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.sticky.cookie.secure=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky.cookie.samesite`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.sticky.cookie.samesite=none"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.responseforwarding.flushinterval`"
|
||||||
|
|
||||||
|
See [response forwarding](../services/index.md#response-forwarding) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.responseforwarding.flushinterval=10"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
You can declare pieces of middleware using labels starting with `traefik.http.middlewares.<name-of-your-choice>.`,
|
||||||
|
followed by the middleware type/options.
|
||||||
|
|
||||||
|
For example, to declare a middleware [`redirectscheme`](../../middlewares/http/redirectscheme.md) named `my-redirect`,
|
||||||
|
you'd write `traefik.http.middlewares.my-redirect.redirectscheme.scheme=https`.
|
||||||
|
|
||||||
|
More information about available middlewares in the dedicated [middlewares section](../../middlewares/overview.md).
|
||||||
|
|
||||||
|
!!! warning "The character `@` is not authorized in the middleware name."
|
||||||
|
|
||||||
|
??? example "Declaring and Referencing a Middleware"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
my-container:
|
||||||
|
# ...
|
||||||
|
deploy:
|
||||||
|
labels:
|
||||||
|
# Declaring a middleware
|
||||||
|
- traefik.http.middlewares.my-redirect.redirectscheme.scheme=https
|
||||||
|
# Referencing a middleware
|
||||||
|
- traefik.http.routers.my-container.middlewares=my-redirect
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "Conflicts in Declaration"
|
||||||
|
|
||||||
|
If you declare multiple middleware with the same name but with different parameters, the middleware fails to be declared.
|
||||||
|
|
||||||
|
### TCP
|
||||||
|
|
||||||
|
You can declare TCP Routers and/or Services using labels.
|
||||||
|
|
||||||
|
??? example "Declaring TCP Routers and Services"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
my-container:
|
||||||
|
# ...
|
||||||
|
deploy:
|
||||||
|
labels:
|
||||||
|
- "traefik.tcp.routers.my-router.rule=HostSNI(`example.com`)"
|
||||||
|
- "traefik.tcp.routers.my-router.tls=true"
|
||||||
|
- "traefik.tcp.services.my-service.loadbalancer.server.port=4123"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "TCP and HTTP"
|
||||||
|
|
||||||
|
If you declare a TCP Router/Service, it will prevent Traefik from automatically creating an HTTP Router/Service (like it does by default if no TCP Router/Service is defined).
|
||||||
|
You can declare both a TCP Router/Service and an HTTP Router/Service for the same container (but you have to do so manually).
|
||||||
|
|
||||||
|
#### TCP Routers
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.entrypoints`"
|
||||||
|
|
||||||
|
See [entry points](../routers/index.md#entrypoints_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.mytcprouter.entrypoints=ep1,ep2"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.rule`"
|
||||||
|
|
||||||
|
See [rule](../routers/index.md#rule_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.mytcprouter.rule=HostSNI(`example.com`)"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.service`"
|
||||||
|
|
||||||
|
See [service](../routers/index.md#services) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.mytcprouter.service=myservice"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls`"
|
||||||
|
|
||||||
|
See [TLS](../routers/index.md#tls_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.mytcprouter.tls=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.certresolver`"
|
||||||
|
|
||||||
|
See [certResolver](../routers/index.md#certresolver_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.mytcprouter.tls.certresolver=myresolver"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.domains[n].main`"
|
||||||
|
|
||||||
|
See [domains](../routers/index.md#domains_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.mytcprouter.tls.domains[0].main=example.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.domains[n].sans`"
|
||||||
|
|
||||||
|
See [domains](../routers/index.md#domains_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.mytcprouter.tls.domains[0].sans=test.example.org,dev.example.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.options`"
|
||||||
|
|
||||||
|
See [options](../routers/index.md#options_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.mytcprouter.tls.options=mysoptions"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.passthrough`"
|
||||||
|
|
||||||
|
See [TLS](../routers/index.md#tls_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.mytcprouter.tls.passthrough=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.priority`"
|
||||||
|
|
||||||
|
See [priority](../routers/index.md#priority_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.routers.myrouter.priority=42"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TCP Services
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"
|
||||||
|
|
||||||
|
Registers a port of the application.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.services.mytcpservice.loadbalancer.server.port=423"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.tls`"
|
||||||
|
|
||||||
|
Determines whether to use TLS when dialing with the backend.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.services.mytcpservice.loadbalancer.server.tls=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.services.<service_name>.loadbalancer.proxyprotocol.version`"
|
||||||
|
|
||||||
|
See [PROXY protocol](../services/index.md#proxy-protocol) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.services.mytcpservice.loadbalancer.proxyprotocol.version=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.services.<service_name>.loadbalancer.serverstransport`"
|
||||||
|
|
||||||
|
Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
|
||||||
|
See [serverstransport](../services/index.md#serverstransport_2) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.tcp.services.<service_name>.loadbalancer.serverstransport=foobar@file"
|
||||||
|
```
|
||||||
|
|
||||||
|
### UDP
|
||||||
|
|
||||||
|
You can declare UDP Routers and/or Services using labels.
|
||||||
|
|
||||||
|
??? example "Declaring UDP Routers and Services"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
my-container:
|
||||||
|
# ...
|
||||||
|
deploy:
|
||||||
|
labels:
|
||||||
|
- "traefik.udp.routers.my-router.entrypoints=udp"
|
||||||
|
- "traefik.udp.services.my-service.loadbalancer.server.port=4123"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "UDP and HTTP"
|
||||||
|
|
||||||
|
If you declare a UDP Router/Service, it will prevent Traefik from automatically creating an HTTP Router/Service (like it does by default if no UDP Router/Service is defined).
|
||||||
|
You can declare both a UDP Router/Service and an HTTP Router/Service for the same container (but you have to do so manually).
|
||||||
|
|
||||||
|
#### UDP Routers
|
||||||
|
|
||||||
|
??? info "`traefik.udp.routers.<router_name>.entrypoints`"
|
||||||
|
|
||||||
|
See [entry points](../routers/index.md#entrypoints_2) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.udp.routers.myudprouter.entrypoints=ep1,ep2"
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.udp.routers.<router_name>.service`"
|
||||||
|
|
||||||
|
See [service](../routers/index.md#services_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.udp.routers.myudprouter.service=myservice"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UDP Services
|
||||||
|
|
||||||
|
??? info "`traefik.udp.services.<service_name>.loadbalancer.server.port`"
|
||||||
|
|
||||||
|
Registers a port of the application.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.udp.services.myudpservice.loadbalancer.server.port=423"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Provider Options
|
||||||
|
|
||||||
|
#### `traefik.enable`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.enable=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can tell Traefik to consider (or not) the container by setting `traefik.enable` to true or false.
|
||||||
|
|
||||||
|
This option overrides the value of `exposedByDefault`.
|
||||||
|
|
||||||
|
#### `traefik.docker.network`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.docker.network=mynetwork"
|
||||||
|
```
|
||||||
|
|
||||||
|
Overrides the default docker network to use for connections to the container.
|
||||||
|
|
||||||
|
If a container is linked to several networks, be sure to set the proper network name (you can check this with `docker inspect <container_id>`),
|
||||||
|
otherwise it will randomly pick one (depending on how docker is returning them).
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`.
|
||||||
|
|
||||||
|
#### `traefik.docker.lbswarm`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- "traefik.docker.lbswarm=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
Enables Swarm's inbuilt load balancer (only relevant in Swarm Mode).
|
||||||
|
|
||||||
|
If you enable this option, Traefik will use the virtual IP provided by docker swarm instead of the containers IPs.
|
||||||
|
Which means that Traefik will not perform any kind of load balancing and will delegate this task to swarm.
|
|
@ -75,6 +75,7 @@ nav:
|
||||||
- 'Configuration Discovery':
|
- 'Configuration Discovery':
|
||||||
- 'Overview': 'providers/overview.md'
|
- 'Overview': 'providers/overview.md'
|
||||||
- 'Docker': 'providers/docker.md'
|
- 'Docker': 'providers/docker.md'
|
||||||
|
- 'Swarm': 'providers/swarm.md'
|
||||||
- 'Kubernetes IngressRoute': 'providers/kubernetes-crd.md'
|
- 'Kubernetes IngressRoute': 'providers/kubernetes-crd.md'
|
||||||
- 'Kubernetes Ingress': 'providers/kubernetes-ingress.md'
|
- 'Kubernetes Ingress': 'providers/kubernetes-ingress.md'
|
||||||
- 'Kubernetes Gateway API': 'providers/kubernetes-gateway.md'
|
- 'Kubernetes Gateway API': 'providers/kubernetes-gateway.md'
|
||||||
|
@ -94,6 +95,7 @@ nav:
|
||||||
- 'Services': 'routing/services/index.md'
|
- 'Services': 'routing/services/index.md'
|
||||||
- 'Providers':
|
- 'Providers':
|
||||||
- 'Docker': 'routing/providers/docker.md'
|
- 'Docker': 'routing/providers/docker.md'
|
||||||
|
- 'Swarm': 'routing/providers/swarm.md'
|
||||||
- 'Kubernetes IngressRoute': 'routing/providers/kubernetes-crd.md'
|
- 'Kubernetes IngressRoute': 'routing/providers/kubernetes-crd.md'
|
||||||
- 'Kubernetes Ingress': 'routing/providers/kubernetes-ingress.md'
|
- 'Kubernetes Ingress': 'routing/providers/kubernetes-ingress.md'
|
||||||
- 'Kubernetes Gateway API': 'routing/providers/kubernetes-gateway.md'
|
- 'Kubernetes Gateway API': 'routing/providers/kubernetes-gateway.md'
|
||||||
|
@ -196,6 +198,7 @@ nav:
|
||||||
- 'Dynamic Configuration':
|
- 'Dynamic Configuration':
|
||||||
- 'File': 'reference/dynamic-configuration/file.md'
|
- 'File': 'reference/dynamic-configuration/file.md'
|
||||||
- 'Docker': 'reference/dynamic-configuration/docker.md'
|
- 'Docker': 'reference/dynamic-configuration/docker.md'
|
||||||
|
- 'Swarm': 'reference/dynamic-configuration/swarm.md'
|
||||||
- 'Kubernetes CRD': 'reference/dynamic-configuration/kubernetes-crd.md'
|
- 'Kubernetes CRD': 'reference/dynamic-configuration/kubernetes-crd.md'
|
||||||
- 'Kubernetes Gateway API': 'reference/dynamic-configuration/kubernetes-gateway.md'
|
- 'Kubernetes Gateway API': 'reference/dynamic-configuration/kubernetes-gateway.md'
|
||||||
- 'Consul Catalog': 'reference/dynamic-configuration/consul-catalog.md'
|
- 'Consul Catalog': 'reference/dynamic-configuration/consul-catalog.md'
|
||||||
|
|
|
@ -235,6 +235,7 @@ func TestHandler_Overview(t *testing.T) {
|
||||||
API: &static.API{},
|
API: &static.API{},
|
||||||
Providers: &static.Providers{
|
Providers: &static.Providers{
|
||||||
Docker: &docker.Provider{},
|
Docker: &docker.Provider{},
|
||||||
|
Swarm: &docker.SwarmProvider{},
|
||||||
File: &file.Provider{},
|
File: &file.Provider{},
|
||||||
KubernetesIngress: &ingress.Provider{},
|
KubernetesIngress: &ingress.Provider{},
|
||||||
KubernetesCRD: &crd.Provider{},
|
KubernetesCRD: &crd.Provider{},
|
||||||
|
|
1
pkg/api/testdata/overview-providers.json
vendored
1
pkg/api/testdata/overview-providers.json
vendored
|
@ -24,6 +24,7 @@
|
||||||
},
|
},
|
||||||
"providers": [
|
"providers": [
|
||||||
"Docker",
|
"Docker",
|
||||||
|
"Swarm",
|
||||||
"File",
|
"File",
|
||||||
"KubernetesIngress",
|
"KubernetesIngress",
|
||||||
"KubernetesCRD",
|
"KubernetesCRD",
|
||||||
|
|
|
@ -40,9 +40,7 @@
|
||||||
defaultRule = "foobar"
|
defaultRule = "foobar"
|
||||||
exposedByDefault = true
|
exposedByDefault = true
|
||||||
useBindPortIP = true
|
useBindPortIP = true
|
||||||
swarmMode = true
|
|
||||||
network = "foobar"
|
network = "foobar"
|
||||||
swarmModeRefreshSeconds = 42
|
|
||||||
httpClientTimeout = 42
|
httpClientTimeout = 42
|
||||||
[providers.docker.tls]
|
[providers.docker.tls]
|
||||||
ca = "foobar"
|
ca = "foobar"
|
||||||
|
|
|
@ -211,6 +211,8 @@ type Providers struct {
|
||||||
ProvidersThrottleDuration ptypes.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"`
|
ProvidersThrottleDuration ptypes.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"`
|
||||||
|
|
||||||
Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
|
Swarm *docker.SwarmProvider `description:"Enable Docker Swarm backend with default settings." json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
|
|
||||||
File *file.Provider `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"`
|
File *file.Provider `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"`
|
||||||
KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
|
@ -219,7 +221,6 @@ type Providers struct {
|
||||||
ConsulCatalog *consulcatalog.ProviderBuilder `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
ConsulCatalog *consulcatalog.ProviderBuilder `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
Nomad *nomad.ProviderBuilder `description:"Enable Nomad backend with default settings." json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
Nomad *nomad.ProviderBuilder `description:"Enable Nomad backend with default settings." json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
Ecs *ecs.Provider `description:"Enable AWS ECS backend with default settings." json:"ecs,omitempty" toml:"ecs,omitempty" yaml:"ecs,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
Ecs *ecs.Provider `description:"Enable AWS ECS backend with default settings." json:"ecs,omitempty" toml:"ecs,omitempty" yaml:"ecs,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
|
|
||||||
Consul *consul.ProviderBuilder `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
Consul *consul.ProviderBuilder `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
|
@ -265,15 +266,21 @@ func (c *Configuration) SetEffectiveConfiguration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Providers.Docker != nil {
|
if c.Providers.Docker != nil {
|
||||||
if c.Providers.Docker.SwarmModeRefreshSeconds <= 0 {
|
|
||||||
c.Providers.Docker.SwarmModeRefreshSeconds = ptypes.Duration(15 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Providers.Docker.HTTPClientTimeout < 0 {
|
if c.Providers.Docker.HTTPClientTimeout < 0 {
|
||||||
c.Providers.Docker.HTTPClientTimeout = 0
|
c.Providers.Docker.HTTPClientTimeout = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Providers.Swarm != nil {
|
||||||
|
if c.Providers.Swarm.RefreshSeconds <= 0 {
|
||||||
|
c.Providers.Swarm.RefreshSeconds = ptypes.Duration(15 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Providers.Swarm.HTTPClientTimeout < 0 {
|
||||||
|
c.Providers.Swarm.HTTPClientTimeout = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Disable Gateway API provider if not enabled in experimental.
|
// Disable Gateway API provider if not enabled in experimental.
|
||||||
if c.Experimental == nil || !c.Experimental.KubernetesGateway {
|
if c.Experimental == nil || !c.Experimental.KubernetesGateway {
|
||||||
c.Providers.KubernetesGateway = nil
|
c.Providers.KubernetesGateway = nil
|
||||||
|
|
|
@ -80,6 +80,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator {
|
||||||
p.quietAddProvider(conf.Docker)
|
p.quietAddProvider(conf.Docker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conf.Swarm != nil {
|
||||||
|
p.quietAddProvider(conf.Swarm)
|
||||||
|
}
|
||||||
|
|
||||||
if conf.Rest != nil {
|
if conf.Rest != nil {
|
||||||
p.quietAddProvider(conf.Rest)
|
p.quietAddProvider(conf.Rest)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
dockertypes "github.com/docker/docker/api/types"
|
dockertypes "github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
|
@ -17,7 +18,16 @@ import (
|
||||||
"github.com/traefik/traefik/v3/pkg/provider/constraints"
|
"github.com/traefik/traefik/v3/pkg/provider/constraints"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Provider) buildConfiguration(ctx context.Context, containersInspected []dockerData) *dynamic.Configuration {
|
type DynConfBuilder struct {
|
||||||
|
Shared
|
||||||
|
apiClient client.APIClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDynConfBuilder(configuration Shared, apiClient client.APIClient) *DynConfBuilder {
|
||||||
|
return &DynConfBuilder{Shared: configuration, apiClient: apiClient}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DynConfBuilder) build(ctx context.Context, containersInspected []dockerData) *dynamic.Configuration {
|
||||||
configurations := make(map[string]*dynamic.Configuration)
|
configurations := make(map[string]*dynamic.Configuration)
|
||||||
|
|
||||||
for _, container := range containersInspected {
|
for _, container := range containersInspected {
|
||||||
|
@ -92,7 +102,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, containersInspected [
|
||||||
return provider.Merge(ctx, configurations)
|
return provider.Merge(ctx, configurations)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.TCPConfiguration) error {
|
func (p *DynConfBuilder) buildTCPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.TCPConfiguration) error {
|
||||||
serviceName := getServiceName(container)
|
serviceName := getServiceName(container)
|
||||||
|
|
||||||
if len(configuration.Services) == 0 {
|
if len(configuration.Services) == 0 {
|
||||||
|
@ -117,7 +127,7 @@ func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, container d
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.UDPConfiguration) error {
|
func (p *DynConfBuilder) buildUDPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.UDPConfiguration) error {
|
||||||
serviceName := getServiceName(container)
|
serviceName := getServiceName(container)
|
||||||
|
|
||||||
if len(configuration.Services) == 0 {
|
if len(configuration.Services) == 0 {
|
||||||
|
@ -141,7 +151,7 @@ func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, container d
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) buildServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.HTTPConfiguration) error {
|
func (p *DynConfBuilder) buildServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.HTTPConfiguration) error {
|
||||||
serviceName := getServiceName(container)
|
serviceName := getServiceName(container)
|
||||||
|
|
||||||
if len(configuration.Services) == 0 {
|
if len(configuration.Services) == 0 {
|
||||||
|
@ -167,7 +177,7 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, container dock
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool {
|
func (p *DynConfBuilder) keepContainer(ctx context.Context, container dockerData) bool {
|
||||||
logger := log.Ctx(ctx)
|
logger := log.Ctx(ctx)
|
||||||
|
|
||||||
if !container.ExtraConf.Enable {
|
if !container.ExtraConf.Enable {
|
||||||
|
@ -193,7 +203,7 @@ func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) addServerTCP(ctx context.Context, container dockerData, loadBalancer *dynamic.TCPServersLoadBalancer) error {
|
func (p *DynConfBuilder) addServerTCP(ctx context.Context, container dockerData, loadBalancer *dynamic.TCPServersLoadBalancer) error {
|
||||||
if loadBalancer == nil {
|
if loadBalancer == nil {
|
||||||
return errors.New("load-balancer is not defined")
|
return errors.New("load-balancer is not defined")
|
||||||
}
|
}
|
||||||
|
@ -219,7 +229,7 @@ func (p *Provider) addServerTCP(ctx context.Context, container dockerData, loadB
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) addServerUDP(ctx context.Context, container dockerData, loadBalancer *dynamic.UDPServersLoadBalancer) error {
|
func (p *DynConfBuilder) addServerUDP(ctx context.Context, container dockerData, loadBalancer *dynamic.UDPServersLoadBalancer) error {
|
||||||
if loadBalancer == nil {
|
if loadBalancer == nil {
|
||||||
return errors.New("load-balancer is not defined")
|
return errors.New("load-balancer is not defined")
|
||||||
}
|
}
|
||||||
|
@ -245,7 +255,7 @@ func (p *Provider) addServerUDP(ctx context.Context, container dockerData, loadB
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) addServer(ctx context.Context, container dockerData, loadBalancer *dynamic.ServersLoadBalancer) error {
|
func (p *DynConfBuilder) addServer(ctx context.Context, container dockerData, loadBalancer *dynamic.ServersLoadBalancer) error {
|
||||||
if loadBalancer == nil {
|
if loadBalancer == nil {
|
||||||
return errors.New("load-balancer is not defined")
|
return errors.New("load-balancer is not defined")
|
||||||
}
|
}
|
||||||
|
@ -275,7 +285,7 @@ func (p *Provider) addServer(ctx context.Context, container dockerData, loadBala
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) getIPPort(ctx context.Context, container dockerData, serverPort string) (string, string, error) {
|
func (p *DynConfBuilder) getIPPort(ctx context.Context, container dockerData, serverPort string) (string, string, error) {
|
||||||
logger := log.Ctx(ctx)
|
logger := log.Ctx(ctx)
|
||||||
|
|
||||||
var ip, port string
|
var ip, port string
|
||||||
|
@ -307,7 +317,7 @@ func (p *Provider) getIPPort(ctx context.Context, container dockerData, serverPo
|
||||||
return ip, port, nil
|
return ip, port, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Provider) getIPAddress(ctx context.Context, container dockerData) string {
|
func (p *DynConfBuilder) getIPAddress(ctx context.Context, container dockerData) string {
|
||||||
logger := log.Ctx(ctx)
|
logger := log.Ctx(ctx)
|
||||||
|
|
||||||
netNotFound := false
|
netNotFound := false
|
||||||
|
@ -338,23 +348,17 @@ func (p Provider) getIPAddress(ctx context.Context, container dockerData) string
|
||||||
}
|
}
|
||||||
|
|
||||||
if container.NetworkSettings.NetworkMode.IsContainer() {
|
if container.NetworkSettings.NetworkMode.IsContainer() {
|
||||||
dockerClient, err := p.createClient()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("Unable to get IP address")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedContainer := container.NetworkSettings.NetworkMode.ConnectedContainer()
|
connectedContainer := container.NetworkSettings.NetworkMode.ConnectedContainer()
|
||||||
containerInspected, err := dockerClient.ContainerInspect(context.Background(), connectedContainer)
|
containerInspected, err := p.apiClient.ContainerInspect(context.Background(), connectedContainer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msgf("Unable to get IP address for container %s: failed to inspect container ID %s", container.Name, connectedContainer)
|
logger.Warn().Err(err).Msgf("Unable to get IP address for container %s: failed to inspect container ID %s", container.Name, connectedContainer)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check connected container for traefik.docker.network, falling back to
|
// Check connected container for traefik.docker.network,
|
||||||
// the network specified on the current container.
|
// falling back to the network specified on the current container.
|
||||||
containerParsed := parseContainer(containerInspected)
|
containerParsed := parseContainer(containerInspected)
|
||||||
extraConf, err := p.getConfiguration(containerParsed)
|
extraConf, err := p.extractLabels(containerParsed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msgf("Unable to get IP address for container %s : failed to get extra configuration for container %s", container.Name, containerInspected.Name)
|
logger.Warn().Err(err).Msgf("Unable to get IP address for container %s : failed to get extra configuration for container %s", container.Name, containerInspected.Name)
|
||||||
return ""
|
return ""
|
||||||
|
@ -379,8 +383,9 @@ func (p Provider) getIPAddress(ctx context.Context, container dockerData) string
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) getPortBinding(container dockerData, serverPort string) (*nat.PortBinding, error) {
|
func (p *DynConfBuilder) getPortBinding(container dockerData, serverPort string) (*nat.PortBinding, error) {
|
||||||
port := getPort(container, serverPort)
|
port := getPort(container, serverPort)
|
||||||
|
|
||||||
for netPort, portBindings := range container.NetworkSettings.Ports {
|
for netPort, portBindings := range container.NetworkSettings.Ports {
|
||||||
if strings.EqualFold(string(netPort), port+"/TCP") || strings.EqualFold(string(netPort), port+"/UDP") {
|
if strings.EqualFold(string(netPort), port+"/TCP") || strings.EqualFold(string(netPort), port+"/UDP") {
|
||||||
for _, p := range portBindings {
|
for _, p := range portBindings {
|
||||||
|
@ -391,36 +396,3 @@ func (p *Provider) getPortBinding(container dockerData, serverPort string) (*nat
|
||||||
|
|
||||||
return nil, fmt.Errorf("unable to find the external IP:Port for the container %q", container.Name)
|
return nil, fmt.Errorf("unable to find the external IP:Port for the container %q", container.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPort(container dockerData, serverPort string) string {
|
|
||||||
if len(serverPort) > 0 {
|
|
||||||
return serverPort
|
|
||||||
}
|
|
||||||
|
|
||||||
var ports []nat.Port
|
|
||||||
for port := range container.NetworkSettings.Ports {
|
|
||||||
ports = append(ports, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
less := func(i, j nat.Port) bool {
|
|
||||||
return i.Int() < j.Int()
|
|
||||||
}
|
|
||||||
nat.Sort(ports, less)
|
|
||||||
|
|
||||||
if len(ports) > 0 {
|
|
||||||
min := ports[0]
|
|
||||||
return min.Port()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServiceName(container dockerData) string {
|
|
||||||
serviceName := container.ServiceName
|
|
||||||
|
|
||||||
if values, err := getStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil {
|
|
||||||
serviceName = values[labelDockerComposeService] + "_" + values[labelDockerComposeProject]
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider.Normalize(serviceName)
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultRule(t *testing.T) {
|
func TestDynConfBuilder_DefaultRule(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
containers []dockerData
|
containers []dockerData
|
||||||
|
@ -376,8 +376,10 @@ func TestDefaultRule(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
p := Provider{
|
p := Provider{
|
||||||
|
Shared: Shared{
|
||||||
ExposedByDefault: true,
|
ExposedByDefault: true,
|
||||||
DefaultRule: test.defaultRule,
|
DefaultRule: test.defaultRule,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := p.Init()
|
err := p.Init()
|
||||||
|
@ -385,18 +387,20 @@ func TestDefaultRule(t *testing.T) {
|
||||||
|
|
||||||
for i := 0; i < len(test.containers); i++ {
|
for i := 0; i < len(test.containers); i++ {
|
||||||
var err error
|
var err error
|
||||||
test.containers[i].ExtraConf, err = p.getConfiguration(test.containers[i])
|
test.containers[i].ExtraConf, err = p.extractLabels(test.containers[i])
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration := p.buildConfiguration(context.Background(), test.containers)
|
builder := NewDynConfBuilder(p.Shared, nil)
|
||||||
|
|
||||||
|
configuration := builder.build(context.Background(), test.containers)
|
||||||
|
|
||||||
assert.Equal(t, test.expected, configuration)
|
assert.Equal(t, test.expected, configuration)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_buildConfiguration(t *testing.T) {
|
func TestDynConfBuilder_build(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
containers []dockerData
|
containers []dockerData
|
||||||
|
@ -3381,10 +3385,12 @@ func Test_buildConfiguration(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
p := Provider{
|
p := Provider{
|
||||||
|
Shared: Shared{
|
||||||
AllowEmptyServices: test.allowEmptyServices,
|
AllowEmptyServices: test.allowEmptyServices,
|
||||||
DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)",
|
|
||||||
ExposedByDefault: true,
|
ExposedByDefault: true,
|
||||||
UseBindPortIP: test.useBindPortIP,
|
UseBindPortIP: test.useBindPortIP,
|
||||||
|
DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
p.Constraints = test.constraints
|
p.Constraints = test.constraints
|
||||||
|
|
||||||
|
@ -3393,18 +3399,20 @@ func Test_buildConfiguration(t *testing.T) {
|
||||||
|
|
||||||
for i := 0; i < len(test.containers); i++ {
|
for i := 0; i < len(test.containers); i++ {
|
||||||
var err error
|
var err error
|
||||||
test.containers[i].ExtraConf, err = p.getConfiguration(test.containers[i])
|
test.containers[i].ExtraConf, err = p.extractLabels(test.containers[i])
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration := p.buildConfiguration(context.Background(), test.containers)
|
builder := NewDynConfBuilder(p.Shared, nil)
|
||||||
|
|
||||||
|
configuration := builder.build(context.Background(), test.containers)
|
||||||
|
|
||||||
assert.Equal(t, test.expected, configuration)
|
assert.Equal(t, test.expected, configuration)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDockerGetIPPort(t *testing.T) {
|
func TestDynConfBuilder_getIPPort_docker(t *testing.T) {
|
||||||
type expected struct {
|
type expected struct {
|
||||||
ip string
|
ip string
|
||||||
port string
|
port string
|
||||||
|
@ -3565,12 +3573,12 @@ func TestDockerGetIPPort(t *testing.T) {
|
||||||
|
|
||||||
dData := parseContainer(test.container)
|
dData := parseContainer(test.container)
|
||||||
|
|
||||||
provider := &Provider{
|
builder := NewDynConfBuilder(Shared{
|
||||||
Network: "testnet",
|
Network: "testnet",
|
||||||
UseBindPortIP: true,
|
UseBindPortIP: true,
|
||||||
}
|
}, nil)
|
||||||
|
|
||||||
actualIP, actualPort, actualError := provider.getIPPort(context.Background(), dData, test.serverPort)
|
actualIP, actualPort, actualError := builder.getIPPort(context.Background(), dData, test.serverPort)
|
||||||
if test.expected.error {
|
if test.expected.error {
|
||||||
require.Error(t, actualError)
|
require.Error(t, actualError)
|
||||||
} else {
|
} else {
|
||||||
|
@ -3582,73 +3590,7 @@ func TestDockerGetIPPort(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDockerGetPort(t *testing.T) {
|
func TestDynConfBuilder_getIPAddress_docker(t *testing.T) {
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
container docker.ContainerJSON
|
|
||||||
serverPort string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "no binding, no server port label",
|
|
||||||
container: containerJSON(name("foo")),
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "binding, no server port label",
|
|
||||||
container: containerJSON(ports(nat.PortMap{
|
|
||||||
"80/tcp": {},
|
|
||||||
})),
|
|
||||||
expected: "80",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "binding, multiple ports, no server port label",
|
|
||||||
container: containerJSON(ports(nat.PortMap{
|
|
||||||
"80/tcp": {},
|
|
||||||
"443/tcp": {},
|
|
||||||
})),
|
|
||||||
expected: "80",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "no binding, server port label",
|
|
||||||
container: containerJSON(),
|
|
||||||
serverPort: "8080",
|
|
||||||
expected: "8080",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "binding, server port label",
|
|
||||||
container: containerJSON(
|
|
||||||
ports(nat.PortMap{
|
|
||||||
"80/tcp": {},
|
|
||||||
})),
|
|
||||||
serverPort: "8080",
|
|
||||||
expected: "8080",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "binding, multiple ports, server port label",
|
|
||||||
container: containerJSON(ports(nat.PortMap{
|
|
||||||
"8080/tcp": {},
|
|
||||||
"80/tcp": {},
|
|
||||||
})),
|
|
||||||
serverPort: "8080",
|
|
||||||
expected: "8080",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
test := test
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
dData := parseContainer(test.container)
|
|
||||||
|
|
||||||
actual := getPort(dData, test.serverPort)
|
|
||||||
assert.Equal(t, test.expected, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDockerGetIPAddress(t *testing.T) {
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
container docker.ContainerJSON
|
container docker.ContainerJSON
|
||||||
|
@ -3742,24 +3684,26 @@ func TestDockerGetIPAddress(t *testing.T) {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
provider := &Provider{
|
conf := Shared{
|
||||||
Network: "webnet",
|
Network: "webnet",
|
||||||
}
|
}
|
||||||
|
|
||||||
dData := parseContainer(test.container)
|
dData := parseContainer(test.container)
|
||||||
|
|
||||||
dData.ExtraConf.Docker.Network = provider.Network
|
dData.ExtraConf.Docker.Network = conf.Network
|
||||||
if len(test.network) > 0 {
|
if len(test.network) > 0 {
|
||||||
dData.ExtraConf.Docker.Network = test.network
|
dData.ExtraConf.Docker.Network = test.network
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := provider.getIPAddress(context.Background(), dData)
|
builder := NewDynConfBuilder(conf, nil)
|
||||||
|
|
||||||
|
actual := builder.getIPAddress(context.Background(), dData)
|
||||||
assert.Equal(t, test.expected, actual)
|
assert.Equal(t, test.expected, actual)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSwarmGetIPAddress(t *testing.T) {
|
func TestDynConfBuilder_getIPAddress_swarm(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
service swarm.Service
|
service swarm.Service
|
||||||
expected string
|
expected string
|
||||||
|
@ -3810,47 +3754,13 @@ func TestSwarmGetIPAddress(t *testing.T) {
|
||||||
t.Run(strconv.Itoa(serviceID), func(t *testing.T) {
|
t.Run(strconv.Itoa(serviceID), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
provider := &Provider{
|
p := &SwarmProvider{}
|
||||||
SwarmMode: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
dData, err := provider.parseService(context.Background(), test.service, test.networks)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
actual := provider.getIPAddress(context.Background(), dData)
|
|
||||||
assert.Equal(t, test.expected, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSwarmGetPort(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
service swarm.Service
|
|
||||||
serverPort string
|
|
||||||
networks map[string]*docker.NetworkResource
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
service: swarmService(
|
|
||||||
withEndpointSpec(modeDNSSR),
|
|
||||||
),
|
|
||||||
networks: map[string]*docker.NetworkResource{},
|
|
||||||
serverPort: "8080",
|
|
||||||
expected: "8080",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for serviceID, test := range testCases {
|
|
||||||
test := test
|
|
||||||
t.Run(strconv.Itoa(serviceID), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
p := Provider{}
|
|
||||||
|
|
||||||
dData, err := p.parseService(context.Background(), test.service, test.networks)
|
dData, err := p.parseService(context.Background(), test.service, test.networks)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
actual := getPort(dData, test.serverPort)
|
builder := NewDynConfBuilder(p.Shared, nil)
|
||||||
|
actual := builder.getIPAddress(context.Background(), dData)
|
||||||
assert.Equal(t, test.expected, actual)
|
assert.Equal(t, test.expected, actual)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
35
pkg/provider/docker/data.go
Normal file
35
pkg/provider/docker/data.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
dockertypes "github.com/docker/docker/api/types"
|
||||||
|
dockercontainertypes "github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dockerData holds the need data to the provider.
|
||||||
|
type dockerData struct {
|
||||||
|
ID string
|
||||||
|
ServiceName string
|
||||||
|
Name string
|
||||||
|
Labels map[string]string // List of labels set to container or service
|
||||||
|
NetworkSettings networkSettings
|
||||||
|
Health string
|
||||||
|
Node *dockertypes.ContainerNode
|
||||||
|
ExtraConf configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkSettings holds the networks data to the provider.
|
||||||
|
type networkSettings struct {
|
||||||
|
NetworkMode dockercontainertypes.NetworkMode
|
||||||
|
Ports nat.PortMap
|
||||||
|
Networks map[string]*networkData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network holds the network data to the provider.
|
||||||
|
type networkData struct {
|
||||||
|
Name string
|
||||||
|
Addr string
|
||||||
|
Port int
|
||||||
|
Protocol string
|
||||||
|
ID string
|
||||||
|
}
|
|
@ -1,602 +0,0 @@
|
||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"github.com/docker/cli/cli/connhelper"
|
|
||||||
dockertypes "github.com/docker/docker/api/types"
|
|
||||||
dockercontainertypes "github.com/docker/docker/api/types/container"
|
|
||||||
eventtypes "github.com/docker/docker/api/types/events"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
|
||||||
"github.com/docker/docker/api/types/versions"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/docker/go-connections/nat"
|
|
||||||
"github.com/docker/go-connections/sockets"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
ptypes "github.com/traefik/paerser/types"
|
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
|
||||||
"github.com/traefik/traefik/v3/pkg/job"
|
|
||||||
"github.com/traefik/traefik/v3/pkg/logs"
|
|
||||||
"github.com/traefik/traefik/v3/pkg/provider"
|
|
||||||
"github.com/traefik/traefik/v3/pkg/safe"
|
|
||||||
"github.com/traefik/traefik/v3/pkg/types"
|
|
||||||
"github.com/traefik/traefik/v3/pkg/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use.
|
|
||||||
DockerAPIVersion = "1.24"
|
|
||||||
|
|
||||||
// SwarmAPIVersion is a constant holding the version of the Provider API traefik will use.
|
|
||||||
SwarmAPIVersion = "1.24"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultTemplateRule The default template for the default rule.
|
|
||||||
const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)"
|
|
||||||
|
|
||||||
var _ provider.Provider = (*Provider)(nil)
|
|
||||||
|
|
||||||
// Provider holds configurations of the provider.
|
|
||||||
type Provider struct {
|
|
||||||
Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"`
|
|
||||||
Watch bool `description:"Watch Docker events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"`
|
|
||||||
Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
|
||||||
DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"`
|
|
||||||
TLS *types.ClientTLS `description:"Enable Docker TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
|
|
||||||
ExposedByDefault bool `description:"Expose containers by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"`
|
|
||||||
UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network." json:"useBindPortIP,omitempty" toml:"useBindPortIP,omitempty" yaml:"useBindPortIP,omitempty" export:"true"`
|
|
||||||
SwarmMode bool `description:"Use Docker on Swarm Mode." json:"swarmMode,omitempty" toml:"swarmMode,omitempty" yaml:"swarmMode,omitempty" export:"true"`
|
|
||||||
Network string `description:"Default Docker network used." json:"network,omitempty" toml:"network,omitempty" yaml:"network,omitempty" export:"true"`
|
|
||||||
SwarmModeRefreshSeconds ptypes.Duration `description:"Polling interval for swarm mode." json:"swarmModeRefreshSeconds,omitempty" toml:"swarmModeRefreshSeconds,omitempty" yaml:"swarmModeRefreshSeconds,omitempty" export:"true"`
|
|
||||||
HTTPClientTimeout ptypes.Duration `description:"Client timeout for HTTP connections." json:"httpClientTimeout,omitempty" toml:"httpClientTimeout,omitempty" yaml:"httpClientTimeout,omitempty" export:"true"`
|
|
||||||
AllowEmptyServices bool `description:"Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
|
|
||||||
defaultRuleTpl *template.Template
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDefaults sets the default values.
|
|
||||||
func (p *Provider) SetDefaults() {
|
|
||||||
p.Watch = true
|
|
||||||
p.ExposedByDefault = true
|
|
||||||
p.Endpoint = "unix:///var/run/docker.sock"
|
|
||||||
p.SwarmMode = false
|
|
||||||
p.SwarmModeRefreshSeconds = ptypes.Duration(15 * time.Second)
|
|
||||||
p.DefaultRule = DefaultTemplateRule
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init the provider.
|
|
||||||
func (p *Provider) Init() error {
|
|
||||||
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error while parsing default rule: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.defaultRuleTpl = defaultRuleTpl
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dockerData holds the need data to the provider.
|
|
||||||
type dockerData struct {
|
|
||||||
ID string
|
|
||||||
ServiceName string
|
|
||||||
Name string
|
|
||||||
Labels map[string]string // List of labels set to container or service
|
|
||||||
NetworkSettings networkSettings
|
|
||||||
Health string
|
|
||||||
Node *dockertypes.ContainerNode
|
|
||||||
ExtraConf configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkSettings holds the networks data to the provider.
|
|
||||||
type networkSettings struct {
|
|
||||||
NetworkMode dockercontainertypes.NetworkMode
|
|
||||||
Ports nat.PortMap
|
|
||||||
Networks map[string]*networkData
|
|
||||||
}
|
|
||||||
|
|
||||||
// Network holds the network data to the provider.
|
|
||||||
type networkData struct {
|
|
||||||
Name string
|
|
||||||
Addr string
|
|
||||||
Port int
|
|
||||||
Protocol string
|
|
||||||
ID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) createClient() (client.APIClient, error) {
|
|
||||||
opts, err := p.getClientOpts()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
httpHeaders := map[string]string{
|
|
||||||
"User-Agent": "Traefik " + version.Version,
|
|
||||||
}
|
|
||||||
opts = append(opts, client.WithHTTPHeaders(httpHeaders))
|
|
||||||
|
|
||||||
apiVersion := DockerAPIVersion
|
|
||||||
if p.SwarmMode {
|
|
||||||
apiVersion = SwarmAPIVersion
|
|
||||||
}
|
|
||||||
opts = append(opts, client.WithVersion(apiVersion))
|
|
||||||
|
|
||||||
return client.NewClientWithOpts(opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) getClientOpts() ([]client.Opt, error) {
|
|
||||||
helper, err := connhelper.GetConnectionHelper(p.Endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSH
|
|
||||||
if helper != nil {
|
|
||||||
// https://github.com/docker/cli/blob/ebca1413117a3fcb81c89d6be226dcec74e5289f/cli/context/docker/load.go#L112-L123
|
|
||||||
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: helper.Dialer,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return []client.Opt{
|
|
||||||
client.WithHTTPClient(httpClient),
|
|
||||||
client.WithTimeout(time.Duration(p.HTTPClientTimeout)),
|
|
||||||
client.WithHost(helper.Host), // To avoid 400 Bad Request: malformed Host header daemon error
|
|
||||||
client.WithDialContext(helper.Dialer),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := []client.Opt{
|
|
||||||
client.WithHost(p.Endpoint),
|
|
||||||
client.WithTimeout(time.Duration(p.HTTPClientTimeout)),
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.TLS != nil {
|
|
||||||
ctx := log.With().Str(logs.ProviderName, "docker").Logger().WithContext(context.Background())
|
|
||||||
|
|
||||||
conf, err := p.TLS.CreateTLSConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to create client TLS configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostURL, err := client.ParseHostURL(p.Endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tr := &http.Transport{
|
|
||||||
TLSClientConfig: conf,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sockets.ConfigureTransport(tr, hostURL.Scheme, hostURL.Host); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = append(opts, client.WithHTTPClient(&http.Client{Transport: tr, Timeout: time.Duration(p.HTTPClientTimeout)}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
|
|
||||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
|
||||||
pool.GoCtx(func(routineCtx context.Context) {
|
|
||||||
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, "docker").Logger()
|
|
||||||
ctxLog := logger.WithContext(routineCtx)
|
|
||||||
|
|
||||||
operation := func() error {
|
|
||||||
var err error
|
|
||||||
ctx, cancel := context.WithCancel(ctxLog)
|
|
||||||
defer cancel()
|
|
||||||
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, "docker").Logger().WithContext(ctx)
|
|
||||||
|
|
||||||
dockerClient, err := p.createClient()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("Failed to create a client for docker, error")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dockerClient.Close()
|
|
||||||
|
|
||||||
serverVersion, err := dockerClient.ServerVersion(ctx)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("Failed to retrieve information of the docker client and server host")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug().Msgf("Provider connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion)
|
|
||||||
|
|
||||||
var dockerDataList []dockerData
|
|
||||||
if p.SwarmMode {
|
|
||||||
dockerDataList, err = p.listServices(ctx, dockerClient)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("Failed to list services for docker swarm mode")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dockerDataList, err = p.listContainers(ctx, dockerClient)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("Failed to list containers for docker")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration := p.buildConfiguration(ctxLog, dockerDataList)
|
|
||||||
configurationChan <- dynamic.Message{
|
|
||||||
ProviderName: "docker",
|
|
||||||
Configuration: configuration,
|
|
||||||
}
|
|
||||||
if p.Watch {
|
|
||||||
if p.SwarmMode {
|
|
||||||
errChan := make(chan error)
|
|
||||||
|
|
||||||
// TODO: This need to be change. Linked to Swarm events docker/docker#23827
|
|
||||||
ticker := time.NewTicker(time.Duration(p.SwarmModeRefreshSeconds))
|
|
||||||
|
|
||||||
pool.GoCtx(func(ctx context.Context) {
|
|
||||||
logger := log.Ctx(ctx).With().Str(logs.ProviderName, "docker").Logger()
|
|
||||||
ctx = logger.WithContext(ctx)
|
|
||||||
|
|
||||||
defer close(errChan)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
services, err := p.listServices(ctx, dockerClient)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("Failed to list services for docker swarm mode")
|
|
||||||
errChan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration := p.buildConfiguration(ctx, services)
|
|
||||||
if configuration != nil {
|
|
||||||
configurationChan <- dynamic.Message{
|
|
||||||
ProviderName: "docker",
|
|
||||||
Configuration: configuration,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
ticker.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err, ok := <-errChan; ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// channel closed
|
|
||||||
} else {
|
|
||||||
f := filters.NewArgs()
|
|
||||||
f.Add("type", "container")
|
|
||||||
options := dockertypes.EventsOptions{
|
|
||||||
Filters: f,
|
|
||||||
}
|
|
||||||
|
|
||||||
startStopHandle := func(m eventtypes.Message) {
|
|
||||||
logger.Debug().Msgf("Provider event received %+v", m)
|
|
||||||
containers, err := p.listContainers(ctx, dockerClient)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("Failed to list containers for docker")
|
|
||||||
// Call cancel to get out of the monitor
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration := p.buildConfiguration(ctx, containers)
|
|
||||||
if configuration != nil {
|
|
||||||
message := dynamic.Message{
|
|
||||||
ProviderName: "docker",
|
|
||||||
Configuration: configuration,
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case configurationChan <- message:
|
|
||||||
case <-ctx.Done():
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventsc, errc := dockerClient.Events(ctx, options)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <-eventsc:
|
|
||||||
if event.Action == "start" ||
|
|
||||||
event.Action == "die" ||
|
|
||||||
strings.HasPrefix(event.Action, "health_status") {
|
|
||||||
startStopHandle(event)
|
|
||||||
}
|
|
||||||
case err := <-errc:
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
logger.Debug().Msg("Provider event stream closed")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
notify := func(err error, time time.Duration) {
|
|
||||||
logger.Error().Err(err).Msgf("Provider error, retrying in %s", time)
|
|
||||||
}
|
|
||||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("Cannot retrieve data")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) {
|
|
||||||
containerList, err := dockerClient.ContainerList(ctx, dockertypes.ContainerListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var inspectedContainers []dockerData
|
|
||||||
// get inspect containers
|
|
||||||
for _, container := range containerList {
|
|
||||||
dData := inspectContainers(ctx, dockerClient, container.ID)
|
|
||||||
if len(dData.Name) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
extraConf, err := p.getConfiguration(dData)
|
|
||||||
if err != nil {
|
|
||||||
log.Ctx(ctx).Error().Err(err).Msgf("Skip container %s", getServiceName(dData))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dData.ExtraConf = extraConf
|
|
||||||
|
|
||||||
inspectedContainers = append(inspectedContainers, dData)
|
|
||||||
}
|
|
||||||
return inspectedContainers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func inspectContainers(ctx context.Context, dockerClient client.ContainerAPIClient, containerID string) dockerData {
|
|
||||||
containerInspected, err := dockerClient.ContainerInspect(ctx, containerID)
|
|
||||||
if err != nil {
|
|
||||||
log.Ctx(ctx).Warn().Err(err).Msgf("Failed to inspect container %s", containerID)
|
|
||||||
return dockerData{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This condition is here to avoid to have empty IP https://github.com/traefik/traefik/issues/2459
|
|
||||||
// We register only container which are running
|
|
||||||
if containerInspected.ContainerJSONBase != nil && containerInspected.ContainerJSONBase.State != nil && containerInspected.ContainerJSONBase.State.Running {
|
|
||||||
return parseContainer(containerInspected)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dockerData{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContainer(container dockertypes.ContainerJSON) dockerData {
|
|
||||||
dData := dockerData{
|
|
||||||
NetworkSettings: networkSettings{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if container.ContainerJSONBase != nil {
|
|
||||||
dData.ID = container.ContainerJSONBase.ID
|
|
||||||
dData.Name = container.ContainerJSONBase.Name
|
|
||||||
dData.ServiceName = dData.Name // Default ServiceName to be the container's Name.
|
|
||||||
dData.Node = container.ContainerJSONBase.Node
|
|
||||||
|
|
||||||
if container.ContainerJSONBase.HostConfig != nil {
|
|
||||||
dData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode
|
|
||||||
}
|
|
||||||
|
|
||||||
if container.State != nil && container.State.Health != nil {
|
|
||||||
dData.Health = container.State.Health.Status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if container.Config != nil && container.Config.Labels != nil {
|
|
||||||
dData.Labels = container.Config.Labels
|
|
||||||
}
|
|
||||||
|
|
||||||
if container.NetworkSettings != nil {
|
|
||||||
if container.NetworkSettings.Ports != nil {
|
|
||||||
dData.NetworkSettings.Ports = container.NetworkSettings.Ports
|
|
||||||
}
|
|
||||||
if container.NetworkSettings.Networks != nil {
|
|
||||||
dData.NetworkSettings.Networks = make(map[string]*networkData)
|
|
||||||
for name, containerNetwork := range container.NetworkSettings.Networks {
|
|
||||||
addr := containerNetwork.IPAddress
|
|
||||||
if addr == "" {
|
|
||||||
addr = containerNetwork.GlobalIPv6Address
|
|
||||||
}
|
|
||||||
|
|
||||||
dData.NetworkSettings.Networks[name] = &networkData{
|
|
||||||
ID: containerNetwork.NetworkID,
|
|
||||||
Name: name,
|
|
||||||
Addr: addr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dData
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) {
|
|
||||||
logger := log.Ctx(ctx)
|
|
||||||
|
|
||||||
serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
serverVersion, err := dockerClient.ServerVersion(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
networkListArgs := filters.NewArgs()
|
|
||||||
// https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06)
|
|
||||||
if versions.GreaterThanOrEqualTo(serverVersion.APIVersion, "1.29") {
|
|
||||||
networkListArgs.Add("scope", "swarm")
|
|
||||||
} else {
|
|
||||||
networkListArgs.Add("driver", "overlay")
|
|
||||||
}
|
|
||||||
|
|
||||||
networkList, err := dockerClient.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs})
|
|
||||||
if err != nil {
|
|
||||||
logger.Debug().Err(err).Msg("Failed to network inspect on client for docker")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
networkMap := make(map[string]*dockertypes.NetworkResource)
|
|
||||||
for _, network := range networkList {
|
|
||||||
networkToAdd := network
|
|
||||||
networkMap[network.ID] = &networkToAdd
|
|
||||||
}
|
|
||||||
|
|
||||||
var dockerDataList []dockerData
|
|
||||||
var dockerDataListTasks []dockerData
|
|
||||||
|
|
||||||
for _, service := range serviceList {
|
|
||||||
dData, err := p.parseService(ctx, service, networkMap)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msgf("Skip container %s", getServiceName(dData))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if dData.ExtraConf.Docker.LBSwarm {
|
|
||||||
if len(dData.NetworkSettings.Networks) > 0 {
|
|
||||||
dockerDataList = append(dockerDataList, dData)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isGlobalSvc := service.Spec.Mode.Global != nil
|
|
||||||
dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dData, networkMap, isGlobalSvc)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Send()
|
|
||||||
} else {
|
|
||||||
dockerDataList = append(dockerDataList, dockerDataListTasks...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dockerDataList, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) parseService(ctx context.Context, service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) (dockerData, error) {
|
|
||||||
logger := log.Ctx(ctx)
|
|
||||||
|
|
||||||
dData := dockerData{
|
|
||||||
ID: service.ID,
|
|
||||||
ServiceName: service.Spec.Annotations.Name,
|
|
||||||
Name: service.Spec.Annotations.Name,
|
|
||||||
Labels: service.Spec.Annotations.Labels,
|
|
||||||
NetworkSettings: networkSettings{},
|
|
||||||
}
|
|
||||||
|
|
||||||
extraConf, err := p.getConfiguration(dData)
|
|
||||||
if err != nil {
|
|
||||||
return dockerData{}, err
|
|
||||||
}
|
|
||||||
dData.ExtraConf = extraConf
|
|
||||||
|
|
||||||
if service.Spec.EndpointSpec != nil {
|
|
||||||
if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR {
|
|
||||||
if dData.ExtraConf.Docker.LBSwarm {
|
|
||||||
logger.Warn().Msgf("Ignored %s endpoint-mode not supported, service name: %s. Fallback to Traefik load balancing", swarmtypes.ResolutionModeDNSRR, service.Spec.Annotations.Name)
|
|
||||||
}
|
|
||||||
} else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP {
|
|
||||||
dData.NetworkSettings.Networks = make(map[string]*networkData)
|
|
||||||
for _, virtualIP := range service.Endpoint.VirtualIPs {
|
|
||||||
networkService := networkMap[virtualIP.NetworkID]
|
|
||||||
if networkService != nil {
|
|
||||||
if len(virtualIP.Addr) > 0 {
|
|
||||||
ip, _, _ := net.ParseCIDR(virtualIP.Addr)
|
|
||||||
network := &networkData{
|
|
||||||
Name: networkService.Name,
|
|
||||||
ID: virtualIP.NetworkID,
|
|
||||||
Addr: ip.String(),
|
|
||||||
}
|
|
||||||
dData.NetworkSettings.Networks[network.Name] = network
|
|
||||||
} else {
|
|
||||||
logger.Debug().Msgf("No virtual IPs found in network %s", virtualIP.NetworkID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.Debug().Msgf("Network not found, id: %s", virtualIP.NetworkID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string,
|
|
||||||
serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool,
|
|
||||||
) ([]dockerData, error) {
|
|
||||||
serviceIDFilter := filters.NewArgs()
|
|
||||||
serviceIDFilter.Add("service", serviceID)
|
|
||||||
serviceIDFilter.Add("desired-state", "running")
|
|
||||||
|
|
||||||
taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{Filters: serviceIDFilter})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var dockerDataList []dockerData
|
|
||||||
for _, task := range taskList {
|
|
||||||
if task.Status.State != swarmtypes.TaskStateRunning {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dData := parseTasks(ctx, task, serviceDockerData, networkMap, isGlobalSvc)
|
|
||||||
if len(dData.NetworkSettings.Networks) > 0 {
|
|
||||||
dockerDataList = append(dockerDataList, dData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dockerDataList, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTasks(ctx context.Context, task swarmtypes.Task, serviceDockerData dockerData,
|
|
||||||
networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool,
|
|
||||||
) dockerData {
|
|
||||||
dData := dockerData{
|
|
||||||
ID: task.ID,
|
|
||||||
ServiceName: serviceDockerData.Name,
|
|
||||||
Name: serviceDockerData.Name + "." + strconv.Itoa(task.Slot),
|
|
||||||
Labels: serviceDockerData.Labels,
|
|
||||||
ExtraConf: serviceDockerData.ExtraConf,
|
|
||||||
NetworkSettings: networkSettings{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if isGlobalSvc {
|
|
||||||
dData.Name = serviceDockerData.Name + "." + task.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
if task.NetworksAttachments != nil {
|
|
||||||
dData.NetworkSettings.Networks = make(map[string]*networkData)
|
|
||||||
for _, virtualIP := range task.NetworksAttachments {
|
|
||||||
if networkService, present := networkMap[virtualIP.Network.ID]; present {
|
|
||||||
if len(virtualIP.Addresses) > 0 {
|
|
||||||
// Not sure about this next loop - when would a task have multiple IP's for the same network?
|
|
||||||
for _, addr := range virtualIP.Addresses {
|
|
||||||
ip, _, _ := net.ParseCIDR(addr)
|
|
||||||
network := &networkData{
|
|
||||||
ID: virtualIP.Network.ID,
|
|
||||||
Name: networkService.Name,
|
|
||||||
Addr: ip.String(),
|
|
||||||
}
|
|
||||||
dData.NetworkSettings.Networks[network.Name] = network
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Ctx(ctx).Debug().Msgf("No IP addresses found for network %s", virtualIP.Network.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dData
|
|
||||||
}
|
|
193
pkg/provider/docker/pdocker.go
Normal file
193
pkg/provider/docker/pdocker.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
dockertypes "github.com/docker/docker/api/types"
|
||||||
|
eventtypes "github.com/docker/docker/api/types/events"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/job"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/logs"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/provider"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/safe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use.
|
||||||
|
const DockerAPIVersion = "1.24"
|
||||||
|
|
||||||
|
const dockerName = "docker"
|
||||||
|
|
||||||
|
var _ provider.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
|
// Provider holds configurations of the provider.
|
||||||
|
type Provider struct {
|
||||||
|
Shared `yaml:",inline" export:"true"`
|
||||||
|
ClientConfig `yaml:",inline" export:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values.
|
||||||
|
func (p *Provider) SetDefaults() {
|
||||||
|
p.Watch = true
|
||||||
|
p.ExposedByDefault = true
|
||||||
|
p.Endpoint = "unix:///var/run/docker.sock"
|
||||||
|
p.DefaultRule = DefaultTemplateRule
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the provider.
|
||||||
|
func (p *Provider) Init() error {
|
||||||
|
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while parsing default rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.defaultRuleTpl = defaultRuleTpl
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) createClient(ctx context.Context) (*client.Client, error) {
|
||||||
|
p.ClientConfig.apiVersion = DockerAPIVersion
|
||||||
|
return createClient(ctx, p.ClientConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
|
||||||
|
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||||
|
pool.GoCtx(func(routineCtx context.Context) {
|
||||||
|
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, dockerName).Logger()
|
||||||
|
ctxLog := logger.WithContext(routineCtx)
|
||||||
|
|
||||||
|
operation := func() error {
|
||||||
|
var err error
|
||||||
|
ctx, cancel := context.WithCancel(ctxLog)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, dockerName).Logger().WithContext(ctx)
|
||||||
|
|
||||||
|
dockerClient, err := p.createClient(ctxLog)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to create Docker API client")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = dockerClient.Close() }()
|
||||||
|
|
||||||
|
builder := NewDynConfBuilder(p.Shared, dockerClient)
|
||||||
|
|
||||||
|
serverVersion, err := dockerClient.ServerVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to retrieve information of the docker client and server host")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug().Msgf("Provider connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion)
|
||||||
|
|
||||||
|
dockerDataList, err := p.listContainers(ctx, dockerClient)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to list containers for docker")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration := builder.build(ctxLog, dockerDataList)
|
||||||
|
configurationChan <- dynamic.Message{
|
||||||
|
ProviderName: dockerName,
|
||||||
|
Configuration: configuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Watch {
|
||||||
|
f := filters.NewArgs()
|
||||||
|
f.Add("type", "container")
|
||||||
|
options := dockertypes.EventsOptions{
|
||||||
|
Filters: f,
|
||||||
|
}
|
||||||
|
|
||||||
|
startStopHandle := func(m eventtypes.Message) {
|
||||||
|
logger.Debug().Msgf("Provider event received %+v", m)
|
||||||
|
containers, err := p.listContainers(ctx, dockerClient)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to list containers for docker")
|
||||||
|
// Call cancel to get out of the monitor
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration := builder.build(ctx, containers)
|
||||||
|
if configuration != nil {
|
||||||
|
message := dynamic.Message{
|
||||||
|
ProviderName: dockerName,
|
||||||
|
Configuration: configuration,
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case configurationChan <- message:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsc, errc := dockerClient.Events(ctx, options)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-eventsc:
|
||||||
|
if event.Action == "start" ||
|
||||||
|
event.Action == "die" ||
|
||||||
|
strings.HasPrefix(event.Action, "health_status") {
|
||||||
|
startStopHandle(event)
|
||||||
|
}
|
||||||
|
case err := <-errc:
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
logger.Debug().Msg("Provider event stream closed")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
logger.Error().Err(err).Msgf("Provider error, retrying in %s", time)
|
||||||
|
}
|
||||||
|
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Cannot retrieve data")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) {
|
||||||
|
containerList, err := dockerClient.ContainerList(ctx, dockertypes.ContainerListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var inspectedContainers []dockerData
|
||||||
|
// get inspect containers
|
||||||
|
for _, container := range containerList {
|
||||||
|
dData := inspectContainers(ctx, dockerClient, container.ID)
|
||||||
|
if len(dData.Name) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
extraConf, err := p.extractLabels(dData)
|
||||||
|
if err != nil {
|
||||||
|
log.Ctx(ctx).Error().Err(err).Msgf("Skip container %s", getServiceName(dData))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dData.ExtraConf = extraConf
|
||||||
|
|
||||||
|
inspectedContainers = append(inspectedContainers, dData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inspectedContainers, nil
|
||||||
|
}
|
332
pkg/provider/docker/pswarm.go
Normal file
332
pkg/provider/docker/pswarm.go
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
dockertypes "github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/api/types/versions"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
ptypes "github.com/traefik/paerser/types"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/job"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/logs"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/provider"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/safe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SwarmAPIVersion is a constant holding the version of the Provider API traefik will use.
|
||||||
|
const SwarmAPIVersion = "1.24"
|
||||||
|
|
||||||
|
const swarmName = "swarm"
|
||||||
|
|
||||||
|
var _ provider.Provider = (*SwarmProvider)(nil)
|
||||||
|
|
||||||
|
// SwarmProvider holds configurations of the provider.
|
||||||
|
type SwarmProvider struct {
|
||||||
|
Shared `yaml:",inline" export:"true"`
|
||||||
|
ClientConfig `yaml:",inline" export:"true"`
|
||||||
|
|
||||||
|
RefreshSeconds ptypes.Duration `description:"Polling interval for swarm mode." json:"refreshSeconds,omitempty" toml:"refreshSeconds,omitempty" yaml:"refreshSeconds,omitempty" export:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values.
|
||||||
|
func (p *SwarmProvider) SetDefaults() {
|
||||||
|
p.Watch = true
|
||||||
|
p.ExposedByDefault = true
|
||||||
|
p.Endpoint = "unix:///var/run/docker.sock"
|
||||||
|
p.RefreshSeconds = ptypes.Duration(15 * time.Second)
|
||||||
|
p.DefaultRule = DefaultTemplateRule
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the provider.
|
||||||
|
func (p *SwarmProvider) Init() error {
|
||||||
|
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while parsing default rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.defaultRuleTpl = defaultRuleTpl
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SwarmProvider) createClient(ctx context.Context) (*client.Client, error) {
|
||||||
|
p.ClientConfig.apiVersion = SwarmAPIVersion
|
||||||
|
return createClient(ctx, p.ClientConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
|
||||||
|
func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||||
|
pool.GoCtx(func(routineCtx context.Context) {
|
||||||
|
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, swarmName).Logger()
|
||||||
|
ctxLog := logger.WithContext(routineCtx)
|
||||||
|
|
||||||
|
operation := func() error {
|
||||||
|
var err error
|
||||||
|
ctx, cancel := context.WithCancel(ctxLog)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger().WithContext(ctx)
|
||||||
|
|
||||||
|
dockerClient, err := p.createClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to create Docker API client")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = dockerClient.Close() }()
|
||||||
|
|
||||||
|
builder := NewDynConfBuilder(p.Shared, dockerClient)
|
||||||
|
|
||||||
|
serverVersion, err := dockerClient.ServerVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to retrieve information of the docker client and server host")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug().Msgf("Provider connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion)
|
||||||
|
|
||||||
|
dockerDataList, err := p.listServices(ctx, dockerClient)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to list services for docker swarm mode")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration := builder.build(ctxLog, dockerDataList)
|
||||||
|
configurationChan <- dynamic.Message{
|
||||||
|
ProviderName: swarmName,
|
||||||
|
Configuration: configuration,
|
||||||
|
}
|
||||||
|
if p.Watch {
|
||||||
|
errChan := make(chan error)
|
||||||
|
|
||||||
|
// TODO: This need to be change. Linked to Swarm events docker/docker#23827
|
||||||
|
ticker := time.NewTicker(time.Duration(p.RefreshSeconds))
|
||||||
|
|
||||||
|
pool.GoCtx(func(ctx context.Context) {
|
||||||
|
logger := log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger()
|
||||||
|
ctx = logger.WithContext(ctx)
|
||||||
|
|
||||||
|
defer close(errChan)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
services, err := p.listServices(ctx, dockerClient)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to list services for docker swarm mode")
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration := builder.build(ctx, services)
|
||||||
|
if configuration != nil {
|
||||||
|
configurationChan <- dynamic.Message{
|
||||||
|
ProviderName: swarmName,
|
||||||
|
Configuration: configuration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err, ok := <-errChan; ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// channel closed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
logger.Error().Err(err).Msgf("Provider error, retrying in %s", time)
|
||||||
|
}
|
||||||
|
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Cannot retrieve data")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SwarmProvider) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) {
|
||||||
|
logger := log.Ctx(ctx)
|
||||||
|
|
||||||
|
serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverVersion, err := dockerClient.ServerVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
networkListArgs := filters.NewArgs()
|
||||||
|
// https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06)
|
||||||
|
if versions.GreaterThanOrEqualTo(serverVersion.APIVersion, "1.29") {
|
||||||
|
networkListArgs.Add("scope", "swarm")
|
||||||
|
} else {
|
||||||
|
networkListArgs.Add("driver", "overlay")
|
||||||
|
}
|
||||||
|
|
||||||
|
networkList, err := dockerClient.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs})
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug().Err(err).Msg("Failed to network inspect on client for docker")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
networkMap := make(map[string]*dockertypes.NetworkResource)
|
||||||
|
for _, network := range networkList {
|
||||||
|
networkToAdd := network
|
||||||
|
networkMap[network.ID] = &networkToAdd
|
||||||
|
}
|
||||||
|
|
||||||
|
var dockerDataList []dockerData
|
||||||
|
var dockerDataListTasks []dockerData
|
||||||
|
|
||||||
|
for _, service := range serviceList {
|
||||||
|
dData, err := p.parseService(ctx, service, networkMap)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msgf("Skip container %s", getServiceName(dData))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dData.ExtraConf.Docker.LBSwarm {
|
||||||
|
if len(dData.NetworkSettings.Networks) > 0 {
|
||||||
|
dockerDataList = append(dockerDataList, dData)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isGlobalSvc := service.Spec.Mode.Global != nil
|
||||||
|
dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dData, networkMap, isGlobalSvc)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Send()
|
||||||
|
} else {
|
||||||
|
dockerDataList = append(dockerDataList, dockerDataListTasks...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerDataList, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SwarmProvider) parseService(ctx context.Context, service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) (dockerData, error) {
|
||||||
|
logger := log.Ctx(ctx)
|
||||||
|
|
||||||
|
dData := dockerData{
|
||||||
|
ID: service.ID,
|
||||||
|
ServiceName: service.Spec.Annotations.Name,
|
||||||
|
Name: service.Spec.Annotations.Name,
|
||||||
|
Labels: service.Spec.Annotations.Labels,
|
||||||
|
NetworkSettings: networkSettings{},
|
||||||
|
}
|
||||||
|
|
||||||
|
extraConf, err := p.extractLabels(dData)
|
||||||
|
if err != nil {
|
||||||
|
return dockerData{}, err
|
||||||
|
}
|
||||||
|
dData.ExtraConf = extraConf
|
||||||
|
|
||||||
|
if service.Spec.EndpointSpec != nil {
|
||||||
|
if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR {
|
||||||
|
if dData.ExtraConf.Docker.LBSwarm {
|
||||||
|
logger.Warn().Msgf("Ignored %s endpoint-mode not supported, service name: %s. Fallback to Traefik load balancing", swarmtypes.ResolutionModeDNSRR, service.Spec.Annotations.Name)
|
||||||
|
}
|
||||||
|
} else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP {
|
||||||
|
dData.NetworkSettings.Networks = make(map[string]*networkData)
|
||||||
|
for _, virtualIP := range service.Endpoint.VirtualIPs {
|
||||||
|
networkService := networkMap[virtualIP.NetworkID]
|
||||||
|
if networkService != nil {
|
||||||
|
if len(virtualIP.Addr) > 0 {
|
||||||
|
ip, _, _ := net.ParseCIDR(virtualIP.Addr)
|
||||||
|
network := &networkData{
|
||||||
|
Name: networkService.Name,
|
||||||
|
ID: virtualIP.NetworkID,
|
||||||
|
Addr: ip.String(),
|
||||||
|
}
|
||||||
|
dData.NetworkSettings.Networks[network.Name] = network
|
||||||
|
} else {
|
||||||
|
logger.Debug().Msgf("No virtual IPs found in network %s", virtualIP.NetworkID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Debug().Msgf("Network not found, id: %s", virtualIP.NetworkID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string,
|
||||||
|
serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool,
|
||||||
|
) ([]dockerData, error) {
|
||||||
|
serviceIDFilter := filters.NewArgs()
|
||||||
|
serviceIDFilter.Add("service", serviceID)
|
||||||
|
serviceIDFilter.Add("desired-state", "running")
|
||||||
|
|
||||||
|
taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{Filters: serviceIDFilter})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dockerDataList []dockerData
|
||||||
|
for _, task := range taskList {
|
||||||
|
if task.Status.State != swarmtypes.TaskStateRunning {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dData := parseTasks(ctx, task, serviceDockerData, networkMap, isGlobalSvc)
|
||||||
|
if len(dData.NetworkSettings.Networks) > 0 {
|
||||||
|
dockerDataList = append(dockerDataList, dData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dockerDataList, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTasks(ctx context.Context, task swarmtypes.Task, serviceDockerData dockerData,
|
||||||
|
networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool,
|
||||||
|
) dockerData {
|
||||||
|
dData := dockerData{
|
||||||
|
ID: task.ID,
|
||||||
|
ServiceName: serviceDockerData.Name,
|
||||||
|
Name: serviceDockerData.Name + "." + strconv.Itoa(task.Slot),
|
||||||
|
Labels: serviceDockerData.Labels,
|
||||||
|
ExtraConf: serviceDockerData.ExtraConf,
|
||||||
|
NetworkSettings: networkSettings{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if isGlobalSvc {
|
||||||
|
dData.Name = serviceDockerData.Name + "." + task.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.NetworksAttachments != nil {
|
||||||
|
dData.NetworkSettings.Networks = make(map[string]*networkData)
|
||||||
|
for _, virtualIP := range task.NetworksAttachments {
|
||||||
|
if networkService, present := networkMap[virtualIP.Network.ID]; present {
|
||||||
|
if len(virtualIP.Addresses) > 0 {
|
||||||
|
// Not sure about this next loop - when would a task have multiple IP's for the same network?
|
||||||
|
for _, addr := range virtualIP.Addresses {
|
||||||
|
ip, _, _ := net.ParseCIDR(addr)
|
||||||
|
network := &networkData{
|
||||||
|
ID: virtualIP.Network.ID,
|
||||||
|
Name: networkService.Name,
|
||||||
|
Addr: ip.String(),
|
||||||
|
}
|
||||||
|
dData.NetworkSettings.Networks[network.Name] = network
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Ctx(ctx).Debug().Msgf("No IP addresses found for network %s", virtualIP.Network.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dData
|
||||||
|
}
|
49
pkg/provider/docker/pswarm_mock_test.go
Normal file
49
pkg/provider/docker/pswarm_mock_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
dockertypes "github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeTasksClient struct {
|
||||||
|
dockerclient.APIClient
|
||||||
|
tasks []swarm.Task
|
||||||
|
container dockertypes.ContainerJSON
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeTasksClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) {
|
||||||
|
return c.tasks, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeTasksClient) ContainerInspect(ctx context.Context, container string) (dockertypes.ContainerJSON, error) {
|
||||||
|
return c.container, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeServicesClient struct {
|
||||||
|
dockerclient.APIClient
|
||||||
|
dockerVersion string
|
||||||
|
networks []dockertypes.NetworkResource
|
||||||
|
services []swarm.Service
|
||||||
|
tasks []swarm.Task
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeServicesClient) ServiceList(ctx context.Context, options dockertypes.ServiceListOptions) ([]swarm.Service, error) {
|
||||||
|
return c.services, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeServicesClient) ServerVersion(ctx context.Context) (dockertypes.Version, error) {
|
||||||
|
return dockertypes.Version{APIVersion: c.dockerVersion}, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeServicesClient) NetworkList(ctx context.Context, options dockertypes.NetworkListOptions) ([]dockertypes.NetworkResource, error) {
|
||||||
|
return c.networks, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeServicesClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) {
|
||||||
|
return c.tasks, c.err
|
||||||
|
}
|
|
@ -9,26 +9,10 @@ import (
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
dockertypes "github.com/docker/docker/api/types"
|
dockertypes "github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
dockerclient "github.com/docker/docker/client"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeTasksClient struct {
|
|
||||||
dockerclient.APIClient
|
|
||||||
tasks []swarm.Task
|
|
||||||
container dockertypes.ContainerJSON
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeTasksClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) {
|
|
||||||
return c.tasks, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeTasksClient) ContainerInspect(ctx context.Context, container string) (dockertypes.ContainerJSON, error) {
|
|
||||||
return c.container, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListTasks(t *testing.T) {
|
func TestListTasks(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
service swarm.Service
|
service swarm.Service
|
||||||
|
@ -83,7 +67,7 @@ func TestListTasks(t *testing.T) {
|
||||||
t.Run(strconv.Itoa(caseID), func(t *testing.T) {
|
t.Run(strconv.Itoa(caseID), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
p := Provider{}
|
p := SwarmProvider{}
|
||||||
dockerData, err := p.parseService(context.Background(), test.service, test.networks)
|
dockerData, err := p.parseService(context.Background(), test.service, test.networks)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -103,32 +87,7 @@ func TestListTasks(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeServicesClient struct {
|
func TestSwarmProvider_listServices(t *testing.T) {
|
||||||
dockerclient.APIClient
|
|
||||||
dockerVersion string
|
|
||||||
networks []dockertypes.NetworkResource
|
|
||||||
services []swarm.Service
|
|
||||||
tasks []swarm.Task
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeServicesClient) ServiceList(ctx context.Context, options dockertypes.ServiceListOptions) ([]swarm.Service, error) {
|
|
||||||
return c.services, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeServicesClient) ServerVersion(ctx context.Context) (dockertypes.Version, error) {
|
|
||||||
return dockertypes.Version{APIVersion: c.dockerVersion}, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeServicesClient) NetworkList(ctx context.Context, options dockertypes.NetworkListOptions) ([]dockertypes.NetworkResource, error) {
|
|
||||||
return c.networks, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeServicesClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) {
|
|
||||||
return c.tasks, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListServices(t *testing.T) {
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
services []swarm.Service
|
services []swarm.Service
|
||||||
|
@ -277,7 +236,7 @@ func TestListServices(t *testing.T) {
|
||||||
|
|
||||||
dockerClient := &fakeServicesClient{services: test.services, tasks: test.tasks, dockerVersion: test.dockerVersion, networks: test.networks}
|
dockerClient := &fakeServicesClient{services: test.services, tasks: test.tasks, dockerVersion: test.dockerVersion, networks: test.networks}
|
||||||
|
|
||||||
p := Provider{}
|
p := SwarmProvider{}
|
||||||
|
|
||||||
serviceDockerData, err := p.listServices(context.Background(), dockerClient)
|
serviceDockerData, err := p.listServices(context.Background(), dockerClient)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -293,7 +252,7 @@ func TestListServices(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSwarmTaskParsing(t *testing.T) {
|
func TestSwarmProvider_parseService_task(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
service swarm.Service
|
service swarm.Service
|
||||||
tasks []swarm.Task
|
tasks []swarm.Task
|
||||||
|
@ -396,7 +355,7 @@ func TestSwarmTaskParsing(t *testing.T) {
|
||||||
t.Run(strconv.Itoa(caseID), func(t *testing.T) {
|
t.Run(strconv.Itoa(caseID), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
p := Provider{}
|
p := SwarmProvider{}
|
||||||
|
|
||||||
dData, err := p.parseService(context.Background(), test.service, test.networks)
|
dData, err := p.parseService(context.Background(), test.service, test.networks)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
211
pkg/provider/docker/shared.go
Normal file
211
pkg/provider/docker/shared.go
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/connhelper"
|
||||||
|
dockertypes "github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/docker/go-connections/sockets"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
ptypes "github.com/traefik/paerser/types"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/provider"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/types"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultTemplateRule The default template for the default rule.
|
||||||
|
const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)"
|
||||||
|
|
||||||
|
type Shared struct {
|
||||||
|
ExposedByDefault bool `description:"Expose containers by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"`
|
||||||
|
Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"`
|
||||||
|
AllowEmptyServices bool `description:"Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
|
||||||
|
Network string `description:"Default Docker network used." json:"network,omitempty" toml:"network,omitempty" yaml:"network,omitempty" export:"true"`
|
||||||
|
UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network." json:"useBindPortIP,omitempty" toml:"useBindPortIP,omitempty" yaml:"useBindPortIP,omitempty" export:"true"`
|
||||||
|
|
||||||
|
Watch bool `description:"Watch Docker events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"`
|
||||||
|
DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"`
|
||||||
|
|
||||||
|
defaultRuleTpl *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspectContainers(ctx context.Context, dockerClient client.ContainerAPIClient, containerID string) dockerData {
|
||||||
|
containerInspected, err := dockerClient.ContainerInspect(ctx, containerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Ctx(ctx).Warn().Err(err).Msgf("Failed to inspect container %s", containerID)
|
||||||
|
return dockerData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This condition is here to avoid to have empty IP https://github.com/traefik/traefik/issues/2459
|
||||||
|
// We register only container which are running
|
||||||
|
if containerInspected.ContainerJSONBase != nil && containerInspected.ContainerJSONBase.State != nil && containerInspected.ContainerJSONBase.State.Running {
|
||||||
|
return parseContainer(containerInspected)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseContainer(container dockertypes.ContainerJSON) dockerData {
|
||||||
|
dData := dockerData{
|
||||||
|
NetworkSettings: networkSettings{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if container.ContainerJSONBase != nil {
|
||||||
|
dData.ID = container.ContainerJSONBase.ID
|
||||||
|
dData.Name = container.ContainerJSONBase.Name
|
||||||
|
dData.ServiceName = dData.Name // Default ServiceName to be the container's Name.
|
||||||
|
dData.Node = container.ContainerJSONBase.Node
|
||||||
|
|
||||||
|
if container.ContainerJSONBase.HostConfig != nil {
|
||||||
|
dData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode
|
||||||
|
}
|
||||||
|
|
||||||
|
if container.State != nil && container.State.Health != nil {
|
||||||
|
dData.Health = container.State.Health.Status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if container.Config != nil && container.Config.Labels != nil {
|
||||||
|
dData.Labels = container.Config.Labels
|
||||||
|
}
|
||||||
|
|
||||||
|
if container.NetworkSettings != nil {
|
||||||
|
if container.NetworkSettings.Ports != nil {
|
||||||
|
dData.NetworkSettings.Ports = container.NetworkSettings.Ports
|
||||||
|
}
|
||||||
|
if container.NetworkSettings.Networks != nil {
|
||||||
|
dData.NetworkSettings.Networks = make(map[string]*networkData)
|
||||||
|
for name, containerNetwork := range container.NetworkSettings.Networks {
|
||||||
|
addr := containerNetwork.IPAddress
|
||||||
|
if addr == "" {
|
||||||
|
addr = containerNetwork.GlobalIPv6Address
|
||||||
|
}
|
||||||
|
|
||||||
|
dData.NetworkSettings.Networks[name] = &networkData{
|
||||||
|
ID: containerNetwork.NetworkID,
|
||||||
|
Name: name,
|
||||||
|
Addr: addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientConfig struct {
|
||||||
|
apiVersion string
|
||||||
|
|
||||||
|
Endpoint string `description:"Docker server endpoint. Can be a TCP or a Unix socket endpoint." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||||
|
TLS *types.ClientTLS `description:"Enable Docker TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
|
||||||
|
HTTPClientTimeout ptypes.Duration `description:"Client timeout for HTTP connections." json:"httpClientTimeout,omitempty" toml:"httpClientTimeout,omitempty" yaml:"httpClientTimeout,omitempty" export:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func createClient(ctx context.Context, cfg ClientConfig) (*client.Client, error) {
|
||||||
|
opts, err := getClientOpts(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpHeaders := map[string]string{
|
||||||
|
"User-Agent": "Traefik " + version.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts,
|
||||||
|
client.WithHTTPHeaders(httpHeaders),
|
||||||
|
client.WithVersion(cfg.apiVersion))
|
||||||
|
|
||||||
|
return client.NewClientWithOpts(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClientOpts(ctx context.Context, cfg ClientConfig) ([]client.Opt, error) {
|
||||||
|
helper, err := connhelper.GetConnectionHelper(cfg.Endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSH
|
||||||
|
if helper != nil {
|
||||||
|
// https://github.com/docker/cli/blob/ebca1413117a3fcb81c89d6be226dcec74e5289f/cli/context/docker/load.go#L112-L123
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: helper.Dialer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return []client.Opt{
|
||||||
|
client.WithHTTPClient(httpClient),
|
||||||
|
client.WithTimeout(time.Duration(cfg.HTTPClientTimeout)),
|
||||||
|
client.WithHost(helper.Host), // To avoid 400 Bad Request: malformed Host header daemon error
|
||||||
|
client.WithDialContext(helper.Dialer),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := []client.Opt{
|
||||||
|
client.WithHost(cfg.Endpoint),
|
||||||
|
client.WithTimeout(time.Duration(cfg.HTTPClientTimeout)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.TLS != nil {
|
||||||
|
conf, err := cfg.TLS.CreateTLSConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create client TLS configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostURL, err := client.ParseHostURL(cfg.Endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: conf,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sockets.ConfigureTransport(tr, hostURL.Scheme, hostURL.Host); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts, client.WithHTTPClient(&http.Client{Transport: tr, Timeout: time.Duration(cfg.HTTPClientTimeout)}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPort(container dockerData, serverPort string) string {
|
||||||
|
if len(serverPort) > 0 {
|
||||||
|
return serverPort
|
||||||
|
}
|
||||||
|
|
||||||
|
var ports []nat.Port
|
||||||
|
for port := range container.NetworkSettings.Ports {
|
||||||
|
ports = append(ports, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
less := func(i, j nat.Port) bool {
|
||||||
|
return i.Int() < j.Int()
|
||||||
|
}
|
||||||
|
nat.Sort(ports, less)
|
||||||
|
|
||||||
|
if len(ports) > 0 {
|
||||||
|
min := ports[0]
|
||||||
|
return min.Port()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceName(container dockerData) string {
|
||||||
|
serviceName := container.ServiceName
|
||||||
|
|
||||||
|
if values, err := getStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil {
|
||||||
|
serviceName = values[labelDockerComposeService] + "_" + values[labelDockerComposeProject]
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.Normalize(serviceName)
|
||||||
|
}
|
|
@ -23,7 +23,7 @@ type specificConfiguration struct {
|
||||||
LBSwarm bool
|
LBSwarm bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) getConfiguration(container dockerData) (configuration, error) {
|
func (p *Shared) extractLabels(container dockerData) (configuration, error) {
|
||||||
conf := configuration{
|
conf := configuration{
|
||||||
Enable: p.ExposedByDefault,
|
Enable: p.ExposedByDefault,
|
||||||
Docker: specificConfiguration{
|
Docker: specificConfiguration{
|
112
pkg/provider/docker/shared_test.go
Normal file
112
pkg/provider/docker/shared_test.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
docker "github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_getPort_docker(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
container docker.ContainerJSON
|
||||||
|
serverPort string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "no binding, no server port label",
|
||||||
|
container: containerJSON(name("foo")),
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "binding, no server port label",
|
||||||
|
container: containerJSON(ports(nat.PortMap{
|
||||||
|
"80/tcp": {},
|
||||||
|
})),
|
||||||
|
expected: "80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "binding, multiple ports, no server port label",
|
||||||
|
container: containerJSON(ports(nat.PortMap{
|
||||||
|
"80/tcp": {},
|
||||||
|
"443/tcp": {},
|
||||||
|
})),
|
||||||
|
expected: "80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "no binding, server port label",
|
||||||
|
container: containerJSON(),
|
||||||
|
serverPort: "8080",
|
||||||
|
expected: "8080",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "binding, server port label",
|
||||||
|
container: containerJSON(
|
||||||
|
ports(nat.PortMap{
|
||||||
|
"80/tcp": {},
|
||||||
|
})),
|
||||||
|
serverPort: "8080",
|
||||||
|
expected: "8080",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "binding, multiple ports, server port label",
|
||||||
|
container: containerJSON(ports(nat.PortMap{
|
||||||
|
"8080/tcp": {},
|
||||||
|
"80/tcp": {},
|
||||||
|
})),
|
||||||
|
serverPort: "8080",
|
||||||
|
expected: "8080",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dData := parseContainer(test.container)
|
||||||
|
|
||||||
|
actual := getPort(dData, test.serverPort)
|
||||||
|
assert.Equal(t, test.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getPort_swarm(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
service swarm.Service
|
||||||
|
serverPort string
|
||||||
|
networks map[string]*docker.NetworkResource
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
service: swarmService(
|
||||||
|
withEndpointSpec(modeDNSSR),
|
||||||
|
),
|
||||||
|
networks: map[string]*docker.NetworkResource{},
|
||||||
|
serverPort: "8080",
|
||||||
|
expected: "8080",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for serviceID, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(strconv.Itoa(serviceID), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
p := SwarmProvider{}
|
||||||
|
|
||||||
|
dData, err := p.parseService(context.Background(), test.service, test.networks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
actual := getPort(dData, test.serverPort)
|
||||||
|
assert.Equal(t, test.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,7 +39,7 @@ import (
|
||||||
"github.com/traefik/traefik/v3/pkg/types"
|
"github.com/traefik/traefik/v3/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateExpected = flag.Bool("update_expected", true, "Update expected files in fixtures")
|
var updateExpected = flag.Bool("update_expected", false, "Update expected files in fixtures")
|
||||||
|
|
||||||
var fullDynConf *dynamic.Configuration
|
var fullDynConf *dynamic.Configuration
|
||||||
|
|
||||||
|
@ -591,22 +591,46 @@ func TestDo_staticConfiguration(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Providers.Docker = &docker.Provider{
|
config.Providers.Docker = &docker.Provider{
|
||||||
|
Shared: docker.Shared{
|
||||||
|
ExposedByDefault: true,
|
||||||
Constraints: `Label("foo", "bar")`,
|
Constraints: `Label("foo", "bar")`,
|
||||||
|
AllowEmptyServices: true,
|
||||||
|
Network: "MyNetwork",
|
||||||
|
UseBindPortIP: true,
|
||||||
Watch: true,
|
Watch: true,
|
||||||
Endpoint: "MyEndPoint",
|
|
||||||
DefaultRule: "PathPrefix(`/`)",
|
DefaultRule: "PathPrefix(`/`)",
|
||||||
TLS: &types.ClientTLS{
|
},
|
||||||
|
ClientConfig: docker.ClientConfig{
|
||||||
|
Endpoint: "MyEndPoint", TLS: &types.ClientTLS{
|
||||||
CA: "myCa",
|
CA: "myCa",
|
||||||
Cert: "mycert.pem",
|
Cert: "mycert.pem",
|
||||||
Key: "mycert.key",
|
Key: "mycert.key",
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
},
|
},
|
||||||
ExposedByDefault: true,
|
|
||||||
UseBindPortIP: true,
|
|
||||||
SwarmMode: true,
|
|
||||||
Network: "MyNetwork",
|
|
||||||
SwarmModeRefreshSeconds: 42,
|
|
||||||
HTTPClientTimeout: 42,
|
HTTPClientTimeout: 42,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Providers.Swarm = &docker.SwarmProvider{
|
||||||
|
Shared: docker.Shared{
|
||||||
|
ExposedByDefault: true,
|
||||||
|
Constraints: `Label("foo", "bar")`,
|
||||||
|
AllowEmptyServices: true,
|
||||||
|
Network: "MyNetwork",
|
||||||
|
UseBindPortIP: true,
|
||||||
|
Watch: true,
|
||||||
|
DefaultRule: "PathPrefix(`/`)",
|
||||||
|
},
|
||||||
|
ClientConfig: docker.ClientConfig{
|
||||||
|
Endpoint: "MyEndPoint", TLS: &types.ClientTLS{
|
||||||
|
CA: "myCa",
|
||||||
|
Cert: "mycert.pem",
|
||||||
|
Key: "mycert.key",
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
HTTPClientTimeout: 42,
|
||||||
|
},
|
||||||
|
RefreshSeconds: 42,
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Providers.KubernetesIngress = &ingress.Provider{
|
config.Providers.KubernetesIngress = &ingress.Provider{
|
||||||
|
|
|
@ -89,23 +89,40 @@
|
||||||
"providers": {
|
"providers": {
|
||||||
"providersThrottleDuration": "1m51s",
|
"providersThrottleDuration": "1m51s",
|
||||||
"docker": {
|
"docker": {
|
||||||
|
"exposedByDefault": true,
|
||||||
"constraints": "Label(\"foo\", \"bar\")",
|
"constraints": "Label(\"foo\", \"bar\")",
|
||||||
|
"allowEmptyServices": true,
|
||||||
|
"network": "MyNetwork",
|
||||||
|
"useBindPortIP": true,
|
||||||
"watch": true,
|
"watch": true,
|
||||||
"endpoint": "xxxx",
|
|
||||||
"defaultRule": "xxxx",
|
"defaultRule": "xxxx",
|
||||||
|
"endpoint": "xxxx",
|
||||||
"tls": {
|
"tls": {
|
||||||
"ca": "xxxx",
|
"ca": "xxxx",
|
||||||
"cert": "xxxx",
|
"cert": "xxxx",
|
||||||
"key": "xxxx",
|
"key": "xxxx",
|
||||||
"insecureSkipVerify": true
|
"insecureSkipVerify": true
|
||||||
},
|
},
|
||||||
"exposedByDefault": true,
|
|
||||||
"useBindPortIP": true,
|
|
||||||
"swarmMode": true,
|
|
||||||
"network": "MyNetwork",
|
|
||||||
"swarmModeRefreshSeconds": "42ns",
|
|
||||||
"httpClientTimeout": "42ns"
|
"httpClientTimeout": "42ns"
|
||||||
},
|
},
|
||||||
|
"swarm": {
|
||||||
|
"exposedByDefault": true,
|
||||||
|
"constraints": "Label(\"foo\", \"bar\")",
|
||||||
|
"allowEmptyServices": true,
|
||||||
|
"network": "MyNetwork",
|
||||||
|
"useBindPortIP": true,
|
||||||
|
"watch": true,
|
||||||
|
"defaultRule": "xxxx",
|
||||||
|
"endpoint": "xxxx",
|
||||||
|
"tls": {
|
||||||
|
"ca": "xxxx",
|
||||||
|
"cert": "xxxx",
|
||||||
|
"key": "xxxx",
|
||||||
|
"insecureSkipVerify": true
|
||||||
|
},
|
||||||
|
"httpClientTimeout": "42ns",
|
||||||
|
"refreshSeconds": "42ns"
|
||||||
|
},
|
||||||
"file": {
|
"file": {
|
||||||
"directory": "file Directory",
|
"directory": "file Directory",
|
||||||
"watch": true,
|
"watch": true,
|
||||||
|
|
6
webui/src/statics/providers/swarm.svg
Normal file
6
webui/src/statics/providers/swarm.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<circle cx="26" cy="26" r="26" fill="#007BFF"/>
|
||||||
|
<path fill="#FFF" fill-rule="nonzero" d="M22.749 13v3.94h-8.125v3.939h-4.062v3.94H7.414a.785.785 0 0 0-.787.64 8.97 8.97 0 0 0-.127 1.526c0 .55.054 1.15.152 1.748.93-.215 2.634-.539 2.336-1.6 1.603 1.8 5.449 1.255 6.423.369 1.089 1.53 7.43.945 7.87-.246 1.365 1.55 5.596 1.55 6.957 0 .441 1.19 6.757 1.775 7.845.246.346.314 1.066.575 1.904.714.283-.52.543-1.043.787-1.6 5.16-.062 6.274-3.66 6.322-3.817.089-.301-.032-.612-.28-.812-.085-.071-1.99-1.57-5.077-1.059-.866-2.736-3.097-3.982-3.199-4.038a.826.826 0 0 0-.914.074c-.082.065-2.002 1.643-1.701 4.875.076.813.295 1.53.635 2.167-.667.36-1.812.812-3.656.812h-.406v-3.94h-4.063V13H22.75zm17.238 15.216c-.098.185-.225.363-.33.542h7.337c-.882-.216-2.777-.505-2.462-1.625-1.038 1.163-3.009 1.335-4.545 1.083zm-.33.542H6.652a12.07 12.07 0 0 0 1.219 3.545c4.411 1.006 9.041-.526 9.089-.542a.8.8 0 0 1 1.016.493.78.78 0 0 1-.483 1.01c-.155.052-2.945.96-6.372.96-.68 0-1.394-.037-2.108-.124 2.133 2.743 5.748 4.9 11.298 4.9 8.778 0 15.528-3.616 19.346-10.242zm-33.005 0c-.003-.013.004-.013 0-.025-.054.012-.104.012-.152.025h.152zm17.721-14.182h2.438v2.363h-2.438v-2.363zm-8.124 3.94h2.437v2.363H16.25v-2.364zm4.062 0h2.438v2.363H20.31v-2.364zm4.062 0h2.438v2.363h-2.438v-2.364zm-12.186 3.939h2.437v2.363h-2.437v-2.363zm4.062 0h2.437v2.363H16.25v-2.363zm4.062 0h2.438v2.363H20.31v-2.363zm4.062 0h2.438v2.363h-2.438v-2.363zm4.062 0h2.438v2.363h-2.438v-2.363zM19.5 30.333c.105 0 .212.016.304.05-.098.055-.177.15-.177.27 0 .179.146.345.33.345a.367.367 0 0 0 .304-.172.7.7 0 0 1 .051.295.802.802 0 0 1-.812.788.802.802 0 0 1-.813-.788c0-.434.365-.788.813-.788z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
Loading…
Reference in a new issue