
This lesson installs Traefik v3.7.6 on Linux, as a binary under systemd or as a pinned Docker container, and starts it as an entry point closed by default: only ports 80 (HTTP) and 443 (HTTPS) are open to the network, and the API and dashboard cannot be reached from another machine. It is meant for readers of the introduction to Traefik who want to go from the idea to the running process.
You will also learn to tell apart three moments that are often confused: the process startup, the dynamic reload of a modified file, and the recovery after a restart of the machine or of Docker. Routing itself (providers, routers, services) is covered in the following lessons.
Five terms come up throughout the lesson. An entrypoint is a port on which Traefik listens, for example :80 for HTTP. The internal entrypoint traefik is the reserved one that carries the API and the dashboard. A provider is the source from which Traefik reads its routes: Docker containers or files. The Docker socket (/var/run/docker.sock) is the file through which a program talks to the Docker engine. A systemd unit is the file that tells systemd how to start, monitor and restart a service; the restart policy is its equivalent on the Docker side.
Estimated time: 35 minutes of reading, then 80 minutes of practice for the two labs.
What you will learn
- Install Traefik as a binary (archive verified by checksum) or as a Docker image pinned by digest;
- Start an entry point that is closed by default and check it from an independent client;
- Find the configuration and version actually used by the running process;
- Distinguish startup, dynamic reload and recovery after a restart;
- Diagnose a failed startup and an invalid dynamic file;
- Prove with two labs that the service comes back after a restart.
Which route should you choose: binary or container?
The main route of this lesson is the container: the image pinned by digest fixes the version without ambiguity, and Docker's restart policy handles recovery. The binary under systemd remains the route for servers without Docker or environments where a container is not wanted. Both produce the same Traefik process; what changes is who supervises it, where its log goes and how you prove its version.
| Criterion | Binary under systemd | Docker container |
|---|---|---|
| Version | traefik version + archive checksum | @sha256: digest of the running image (detailed reading in "Which configuration does the running process really read?") |
| Who restarts after an incident | systemd (Restart=on-failure) | Docker (restart: unless-stopped) |
| Start at boot | systemctl enable | restart: + Docker itself enabled at boot |
| Log | journalctl -u traefik | docker compose logs / docker logs |
| Process user | dedicated traefik account | image user, container isolation |
| Docker socket access | unnecessary without a Docker provider | mounted in the container: see below |
| Update | replace the binary | change the digest and recreate the container |
Choose the container when Docker is already there and your services run on it too, the binary when Traefik protects services that do not run in containers. Mixing both on the same machine is possible, but it is the best way to wonder afterwards which one listens on port 80: sudo ss -tlnp | grep ':80' gives the answer (see "Port already in use" in the troubleshooting section).
Prerequisites
Check these points before you begin: a gap at this stage shows up much later, as an error that does not point to it.
System
Traefik is a static Go binary with no external dependency, which explains its modest hardware needs. Actual consumption depends mostly on the number of routes and the volume of logs kept, not on traffic itself.
| Criterion | Minimum | Recommended |
|---|---|---|
| Linux | Kernel 3.10+ | Kernel 5.x+ |
| RAM | 128 MB | 256 MB+ |
| Disk space | 50 MB | 100 MB (with logs) |
| Architecture | amd64, arm64 | amd64 |
Traefik also runs on Windows and macOS, but this guide focuses on Linux.
Network ports
Traefik uses several ports depending on your configuration:
| Port | Use | Configuration |
|---|---|---|
| 80 | HTTP (web entrypoint) | Open to the Internet |
| 443 | HTTPS (websecure entrypoint) | Open to the Internet |
| 8080 | Dashboard and API | Internal only |
Check that these ports are available:
# Check the ports in usess -tlnp | grep -E ':80|:443|:8080'If a port is already in use, you will have to stop the service concerned or change the Traefik port.
Recommended knowledge
Before you begin, make sure you understand:
- The basic Traefik concepts (see Introduction to Traefik)
- The Linux command line
- The basics of Docker (for the Docker method)
Method 1: Binary installation
This method gives you full control over Traefik. It is ideal for bare-metal servers or VMs without Docker.
-
Download the binary
Download the archive and the checksum file published by the project, then verify the digest before any extraction: this is the only way to detect a truncated or substituted archive.
Terminal window # wget is missing from a minimal Debian 13 image: sudo apt install -y wget# Set the version (check the latest on github.com/traefik/traefik/releases)TRAEFIK_VERSION="v3.7.6"# Download the binary for Linux amd64wget https://github.com/traefik/traefik/releases/download/${TRAEFIK_VERSION}/traefik_${TRAEFIK_VERSION}_linux_amd64.tar.gz# Verify the checksum before extracting anythingwget https://github.com/traefik/traefik/releases/download/${TRAEFIK_VERSION}/traefik_${TRAEFIK_VERSION}_checksums.txtsha256sum -c traefik_${TRAEFIK_VERSION}_checksums.txt --ignore-missingThe
--ignore-missingoption limits the check to the files actually downloaded, since the checksum file covers all architectures. WithoutOKin the output, go no further.Expected result:
traefik_v3.7.6_linux_amd64.tar.gz: OK -
Install the binary
Extract it and place the binary in the system PATH:
Terminal window # Extract the archivetar xzf traefik_${TRAEFIK_VERSION}_linux_amd64.tar.gz# Move to /usr/local/binsudo mv traefik /usr/local/bin/# Check the installationtraefik versionExpected result:
Version: 3.7.6Codename: langresGo version: go1.25.11Built: 2026-06-30T12:14:07ZOS/Arch: linux/amd64 -
Create the directory structure
The
traefikaccount is created without a login shell (/sbin/nologin) and without a home directory: it only runs the service, which limits the damage if the process is compromised.Terminal window # Create the configuration directoriessudo mkdir -p /etc/traefik/conf.dsudo mkdir -p /var/log/traefik# Create a dedicated user (security)sudo useradd -r -s /sbin/nologin traefik# Assign the permissionssudo chown -R traefik:traefik /etc/traefiksudo chown -R traefik:traefik /var/log/traefik -
Create the configuration file
Create
/etc/traefik/traefik.yamlwith the minimal configuration:/etc/traefik/traefik.yaml # Traefik static configuration# Network entry pointsentryPoints:web:address: ":80"websecure:address: ":443"# Internal entrypoint that carries the API: bound to the loopback# so that it cannot be reached from another machinetraefik:address: "127.0.0.1:8080"# Dashboard and APIapi:dashboard: trueinsecure: true # ❌ counter-example in production: API without authentication, tolerated here because limited to 127.0.0.1# Dynamic configuration providersproviders:file:directory: /etc/traefik/conf.dwatch: true# Logginglog:level: INFOfilePath: /var/log/traefik/traefik.logaccessLog:filePath: /var/log/traefik/access.logThe
traefikentrypoint is the internal entrypoint that serves the API and the dashboard; it is declared here to force127.0.0.1on it. Without this declaration, it would listen on:8080, on all interfaces. -
Create the systemd service
Two directives carry most of the hardening.
ProtectSystem=strictmounts the whole file system read-only, hence the need to list the writable directories explicitly inReadWritePaths.AmbientCapabilities=CAP_NET_BIND_SERVICEallows listening on ports 80 and 443 without running the process as root: without this line, the service fails at startup with a permission error.Create
/etc/systemd/system/traefik.service:[Unit]Description=Traefik Reverse ProxyDocumentation=https://doc.traefik.io/traefik/After=network-online.targetWants=network-online.target[Service]Type=notifyUser=traefikGroup=traefikExecStart=/usr/local/bin/traefik --configFile=/etc/traefik/traefik.yamlExecReload=/bin/kill -USR1 $MAINPIDRestart=on-failureRestartSec=5LimitNOFILE=65536# SecurityNoNewPrivileges=trueProtectSystem=strictProtectHome=trueReadWritePaths=/var/log/traefik /etc/traefikPrivateTmp=true# Capability for ports < 1024AmbientCapabilities=CAP_NET_BIND_SERVICE[Install]WantedBy=multi-user.target -
Start and verify
Terminal window # Reload systemdsudo systemctl daemon-reload# Enable automatic startupsudo systemctl enable traefik# Start the servicesudo systemctl start traefik# Check the statussudo systemctl status traefikExpected result:
● traefik.service - Traefik Reverse ProxyLoaded: loaded (/etc/systemd/system/traefik.service; enabled)Active: active (running) since ...Test access to the API from the machine itself:
Terminal window curl -s http://localhost:8080/api/overview | jqThen check who is actually listening:
sslists the listening sockets and the process that holds them. Ports 80 and 443 must appear on all interfaces, port 8080 only on127.0.0.1.Terminal window sudo ss -tlnp | grep traefik
Method 2: Docker installation
This is the natural method when Docker is already there, because Traefik is designed for containerized environments. The Docker provider reads the list of containers directly through the host's socket and builds routes from their labels, without any configuration file to modify during a deployment. In return, mounting this socket in a container amounts to giving it privileged access to the host: the read-only mount and the hardening of the service are minimums here.
-
Create the directory structure
The
acme.jsonfile will store the certificates and private keys obtained from Let's Encrypt. Traefik refuses to start if its permissions are wider than600, hence thechmodright at creation.Terminal window # Create the project directorymkdir -p ~/traefik-labcd ~/traefik-lab# Create the subdirectoriesmkdir -p config/dynamic# Create the file for ACME certificates (Let's Encrypt)touch acme.jsonchmod 600 acme.json -
Create the static configuration
Create
config/traefik.yaml:config/traefik.yaml # Traefik static configuration# EntrypointsentryPoints:web:address: ":80"websecure:address: ":443"# API and Dashboardapi:dashboard: trueinsecure: true # ❌ counter-example in production: API without authentication, tolerated here because port 8080 is published only on 127.0.0.1# Docker providerproviders:docker:endpoint: "unix:///var/run/docker.sock"exposedByDefault: false # Expose only containers with the label traefik.enable=truewatch: truefile:directory: /etc/traefik/dynamicwatch: true# Logginglog:level: INFOaccessLog: {} -
Create the docker-compose.yml file
Both configuration files are mounted read-only, which prevents the container from modifying what drives it;
acme.jsonis the exception since Traefik must write the certificates to it. The image is designated by its tagv3.7.6and by its@sha256:digest, the only identifier that cannot be reassigned to other content: if the tag were ever moved, Docker would refuse the image whose digest differs. Port 8080 is published only on the host's loopback, which keeps the API out of reach of other machines.docker-compose.yml services:traefik:image: traefik:v3.7.6@sha256:21a3d83696379bac6434bb32e1dde0aff0e84ef2abd053ed3db87d3f45e749b2container_name: traefikrestart: unless-stoppedsecurity_opt:- no-new-privileges:trueports:- "80:80"- "443:443"# API and dashboard: published on the loopback only- "127.0.0.1:8080:8080"volumes:# Docker socket (read-only)- /var/run/docker.sock:/var/run/docker.sock:ro# Static configuration- ./config/traefik.yaml:/etc/traefik/traefik.yaml:ro# Dynamic configuration- ./config/dynamic:/etc/traefik/dynamic:ro# ACME certificates- ./acme.json:/acme.jsonnetworks:- traefik-publicnetworks:traefik-public:external: trueThe network is declared
external: truebecause the next step creates it beforedocker compose up. Withname:anddriver: bridgeinstead, Compose considers that it must create this network itself: faced with a network already created by hand, Compose 2.26.1 (measured) refuses to start and returnsnetwork traefik-public was found but has incorrect label com.docker.compose.network set to "", without creating the container. More recent Compose versions were not tested here. -
Start Traefik
Terminal window # Create the shared network (the Compose file declares it external)docker network create traefik-public 2>/dev/null || true# Start Traefikdocker compose up -d# Check the statusdocker compose psExpected result:
NAME IMAGE COMMAND SERVICE STATUS PORTStraefik traefik:v3.7.6@sha256:21a3d836... "/entrypoint.sh trae…" traefik Up ... 0.0.0.0:80->80/tcp, ..., 127.0.0.1:8080->8080/tcpThe
IMAGEandSTATUScolumns and the duration vary with the Compose version (measured with Compose 2.26.1:Up ...and the full digest); what matters is that port 8080 appears only as127.0.0.1:8080->8080/tcp. -
Verify the installation
Terminal window # Check the logsdocker compose logs traefik | tail -20# Test the APIcurl -s http://localhost:8080/api/overview | jq# Access the dashboardecho "Dashboard: http://localhost:8080/dashboard/"Expected result for the API (excerpt, measured in 3.7.6 with no user route):
{"http": {"routers": { "total": 2, "warnings": 0, "errors": 0 },"services": { "total": 3, "warnings": 0, "errors": 0 },"middlewares": { "total": 2, "warnings": 0, "errors": 0 }},"tcp": { ... },"udp": { ... },"providers": ["Docker", "File"]}The two routers, three services and two middlewares already present are Traefik's internal objects (API, dashboard, internal redirects):
total: 0is therefore not what you will see before adding your routes. The same call gives"providers": ["File"]for the binary route.
Does a read-only Docker socket limit the API?
No: the :ro suffix of the /var/run/docker.sock:/var/run/docker.sock:ro mount makes the socket file read-only, not the requests that pass through it. The socket is a communication channel with the engine: a client that opens it can still send a request to create, stop or delete a container, because writing a request into a connection is not modifying the file. A process that can talk to the Docker API can therefore, for example, start a container mounted on / and take control of the host.
The :ro mount in this lesson is a minimum on principle, not a protection. What really reduces the Docker provider's power is a socket proxy that allows only read operations: the lesson Securing access to the Docker socket (in French) sets it up, Traefik with Docker explains what the provider reads, and Protecting the Traefik entry point places this choice among the other measures. Even on a private network, the risk remains that of a Traefik flaw turning into a takeover of the host, and it grows with exposure to the Internet.
Testing with a whoami service
To validate that Traefik routes traffic correctly, let us add a test service. The whoami image returns the headers of the request it receives, which lets you check at a glance that traffic went through the proxy. The four labels are the vital minimum: enable exposure, define the matching rule, choose the entrypoint and indicate the container's internal listening port. As with the Traefik image, pin this image by digest outside a test context.
# Add to docker-compose.yml whoami: image: traefik/whoami:v1.11.0@sha256:200689790a0a0ea48ca45992e0450bc26ccab5307375b41c84dfc4f2475937ab container_name: whoami labels: - "traefik.enable=true" - "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)" - "traefik.http.routers.whoami.entrypoints=web" - "traefik.http.services.whoami.loadbalancer.server.port=80" networks: - traefik-publicRestart and test:
docker compose up -dcurl -H "Host: whoami.localhost" http://localhostExpected result:
Hostname: whoamiIP: 172.18.0.3RemoteAddr: 172.18.0.2:54312GET / HTTP/1.1Host: whoami.localhost...Method 3: Package manager (optional)
Some distributions offer Traefik in their repositories, but the versions are often old. The lag is a problem for a component exposed at the front: being several minor versions behind means as many missing security fixes. On Linux, prefer the official binary or image; this section exists only for cases where internal policy requires going through the package manager.
Debian/Ubuntu (via third-party repository)
Traefik is not in the Debian/Ubuntu official repositories, and this lesson does not recommend any third-party repository: its updates do not depend on the project, which falls back into the lag problem described above. Follow Method 1 (binary) or Method 2 (Docker).
RHEL/Rocky/Alma (via binary)
Same recommendation: no package to install, follow Method 1 with the downloaded and verified binary. The Linux archive is the same whatever the distribution.
Homebrew (macOS for development)
On macOS, Homebrew remains handy for having the traefik command locally while you fine-tune a configuration. Check the version obtained, which does not necessarily match the one deployed on your servers.
brew install traefiktraefik versionConfiguration file structure
Traefik distinguishes two configurations that must never be confused. The static configuration is read only once at startup: it defines the listening ports, the configuration sources and the certificate resolvers, and any change requires a restart. The dynamic configuration describes the routes and is reloaded on the fly, with no service interruption. Confusing the two is a common mistake early on: an option placed in the wrong spot is rejected or goes unnoticed depending on the case, as the section on startup failure below shows. The vocabulary is laid out in the introduction to Traefik, and the file provider in Traefik and the file provider.
Overview
The tree below materializes this separation: a single file for the static part, a watched directory for the dynamic part. Splitting the conf.d/ directory into several files has no technical consequence, Traefik merges their content.
/etc/traefik/├── traefik.yaml # Static configuration (startup)└── conf.d/ # Dynamic configuration (hot-reload) ├── routers.yaml ├── services.yaml └── middlewares.yamlComplete static configuration
Here is an example traefik.yaml with the main options:
# =============================================================================# ENTRYPOINTS - Listening ports# =============================================================================entryPoints: web: address: ":80" http: redirections: entryPoint: to: websecure scheme: https
websecure: address: ":443" http: tls: certResolver: letsencrypt
metrics: address: ":8082"
# =============================================================================# PROVIDERS - Dynamic configuration sources# =============================================================================providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false network: traefik-public
file: directory: /etc/traefik/conf.d watch: true
# =============================================================================# API AND DASHBOARD# =============================================================================api: dashboard: true # insecure: false # In production, disable and secure
# =============================================================================# ACME CERTIFICATES (Let's Encrypt)# =============================================================================certificatesResolvers: letsencrypt: acme: email: admin@example.com storage: /etc/traefik/acme.json httpChallenge: entryPoint: web
# =============================================================================# LOGGING# =============================================================================log: level: INFO filePath: /var/log/traefik/traefik.log format: json
accessLog: filePath: /var/log/traefik/access.log format: json filters: statusCodes: - "400-599" fields: headers: names: User-Agent: keep Authorization: drop
# =============================================================================# METRICS (Prometheus)# =============================================================================metrics: prometheus: entryPoint: metrics addServicesLabels: true addRoutersLabels: trueThe paths in this example follow the binary route: storage: /etc/traefik/acme.json designates the certificate file on the machine. In this lesson's Docker route, acme.json is mounted in the container at /acme.json: the line then becomes storage: /acme.json, since the storage path is always the one seen from the Traefik process.
Supported configuration formats
Traefik accepts several formats for the static configuration, all strictly equivalent in terms of features. The choice depends on the deployment context: a YAML or TOML file is easy to version and re-read, while CLI arguments and environment variables suit orchestrators where the configuration is injected at launch. Option names always follow the same hierarchy, only the notation changes.
entryPoints: web: address: ":80" websecure: address: ":443"
api: dashboard: true
providers: docker: exposedByDefault: false[entryPoints] [entryPoints.web] address = ":80" [entryPoints.websecure] address = ":443"
[api] dashboard = true
[providers] [providers.docker] exposedByDefault = falsetraefik \ --entrypoints.web.address=:80 \ --entrypoints.websecure.address=:443 \ --api.dashboard=true \ --providers.docker.exposedByDefault=falseexport TRAEFIK_ENTRYPOINTS_WEB_ADDRESS=":80"export TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS=":443"export TRAEFIK_API_DASHBOARD="true"export TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT="false"
traefikAccessing the dashboard
The Traefik dashboard displays in real time:
- The active routers and their rules
- The backend services and their state
- The configured middlewares
- The entrypoints and their traffic
Local access (insecure mode)
The insecure mode publishes the dashboard and the administration API on port 8080 with no authentication at all. Reserve it for a development machine and never leave this port reachable from outside: the API exposes the whole configuration, host names and internal addresses included.
# Direct accesshttp://localhost:8080/dashboard/
# Via curlcurl http://localhost:8080/api/overviewSecuring access (production)
In production, you disable insecure mode and expose the dashboard like any other service: through a router declared in dynamic configuration, protected by an authentication middleware and served over HTTPS. The internal service to target is called api@internal, the reserved name Traefik gives to its own API. Authentication middlewares are detailed in Traefik middlewares: rewrite, redirect and chain and the certResolver certificate below in Traefik and automatic HTTPS: ACME resolver, redirects and TLS options.
Create a dynamic configuration file to secure the dashboard:
http: routers: dashboard: rule: "Host(`traefik.example.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))" service: api@internal entryPoints: - websecure middlewares: - dashboard-auth tls: certResolver: letsencrypt
middlewares: dashboard-auth: basicAuth: users: # admin:password (generated with htpasswd -nb admin password) - "admin:$apr1$ruca84Hq$mbjdMZBAG.KWn7vfN/SNK/"Generate the password hash:
# Install htpasswd if neededsudo apt install apache2-utils # Debian/Ubuntu# orsudo dnf install httpd-tools # RHEL/Rocky
# Generate the hashhtpasswd -nb admin YourPasswordStart, reload, resume: what is the difference?
An entry point goes through three distinct life events, and each one re-reads a different configuration: the startup reads the static configuration and opens the ports, the dynamic reload re-reads only the watched files without touching the process, the recovery restarts the process after a reboot of the machine or of Docker. Confusing these three moments leads to two classic mistakes: expecting a watch to apply a static option, and believing that a service "started" by hand will come back on its own after a reboot.
The following diagram links the three events. Look at the loopback: recovery does nothing more than trigger a new startup, so it is the static configuration that decides what comes back.
Which configuration does the running process really read?
A file written to disk is not necessarily the one the process reads. You can modify /etc/traefik/traefik.yaml for hours with no effect if the systemd unit points to another --configFile, if the container mounts another file, or if a TRAEFIK_* environment variable or a command-line argument supplies the configuration instead (the three sources are exclusive, see above). The only reliable source is the running process: query it rather than the disk.
# Binary: which unit, which arguments, which file?systemctl cat traefikps -o pid,args -C traefik
# Container: which arguments, which mounted files, which exact image?docker inspect --format '{{.Config.Cmd}}' traefikdocker inspect --format '{{json .Mounts}}' traefik | jqdocker inspect --format '{{.Image}}' traefiksystemctl cat displays the unit as systemd reads it, overrides (drop-ins) included; ps shows the actual command line; docker inspect lists the mounts (source on the host, destination in the container). For the version, do not trust a tag: docker inspect --format '{{.Image}}' gives the identifier of the image actually launched. With the containerd image store (measured with Docker 29.1.3), it equals the pinned digest; with the legacy store, it is the image's configuration identifier, which differs from the digest: compare docker image inspect --format '{{json .RepoDigests}}' <image> with the pinned digest instead. Then traefik version (or docker exec traefik traefik version) queries the binary itself.
What happens when startup fails?
An unknown option inside a known section of the static configuration prevents Traefik from starting: the process stops, and the log's error message names the faulty option. With Restart=on-failure and RestartSec=5, systemd restarts the unit every five seconds, so the visible symptom is a unit that oscillates between activating and failed, not a clean stop.
Trigger the error to recognize it. Make a typo in an option of a known section (here dashboard written dashbord under api:), then restart the service:
sudo sed -i 's/^ dashboard: true/ dashbord: true/' /etc/traefik/traefik.yamlsudo systemctl restart traefiksudo journalctl -u traefik -n 20 --no-pager -o catIn 3.7.6 (measured), the log contains this line, and systemctl is-active traefik answers activating in a loop:
{"level":"error","error":"command traefik error: field not found, node: dashbord","time":"...","message":"Command error"}The exact wording depends on the version; what is stable is the pattern field not found, node: <option>. Put dashboard back, then run sudo systemctl restart traefik again before going on.
With log.filePath set (as in the binary route's configuration), Traefik's operating messages go to that file, not to the systemd journal; the error above, emitted before this option is read, does appear in the journal. With Docker, the same approach gives docker compose logs traefik and a container that restarts in a loop (docker compose ps shows Restarting).
What happens when a dynamic file is invalid?
An error in a dynamic file does not bring the process down: Traefik logs the error and keeps running, because the file provider's watch re-reads the file on the fly, without a restart. This is exactly what separates the two configurations: a static mistake shows immediately (the process stops), a dynamic mistake shows in the log, while the service keeps answering.
# Drop an invalid file in the watched directoryecho 'http: {routers: {cassee: {rule: Host(' | sudo tee /etc/traefik/conf.d/invalide.yaml
# The process is still activesystemctl is-active traefik
# The error is in the binary route's log file (log.filePath)sudo grep -i -E 'error|invalid' /var/log/traefik/traefik.log | tail -5
# Put things back in ordersudo rm /etc/traefik/conf.d/invalide.yamlWhere to read the error depends on log.filePath, as for the startup failure seen above: the binary route's configuration sets it, so its messages go to /var/log/traefik/traefik.log and not to the systemd journal. Without this option, sudo journalctl -u traefik -n 20 --no-pager (binary) or docker logs traefik --tail 50 (container) shows the same line; for a container, also drop the file in the mounted directory. The right check is not "is the route applied?" but "is the error readable and the process intact?": compare the process start time (systemctl show traefik -p ActiveEnterTimestamp) before and after the injection, it must not have changed. File-based configuration is detailed in Traefik and the file provider.
What does ExecReload mean in the systemd unit?
The line ExecReload=/bin/kill -USR1 $MAINPID does not reload the static configuration. The USR1 signal asks Traefik to close and reopen the access log file (accessLog.filePath), which is used for log rotation by an external tool such as logrotate. The documentation states that it has no effect on Traefik's own log file (log.filePath): for that one, rotation by signal does not work. This ExecReload line therefore reloads nothing of the configuration, and systemctl reload traefik does not take a change to traefik.yaml into account. A change to traefik.yaml requires a systemctl restart traefik, hence a short interruption: only dynamic files reload by themselves.
| Event | What is re-read | Interruption | Test command |
|---|---|---|---|
| Startup | static configuration, then initial dynamic | yes, for the duration of the startup | systemctl restart traefik |
| Dynamic reload | watched files only | no | modify a file in conf.d/ |
| Recovery after restart | everything, as at startup | yes, for the duration of the reboot | sudo systemctl restart docker or sudo reboot |
How does the entry point come back after a reboot?
The entry point comes back by itself if, and only if, someone other than you takes care of restarting it. For the binary, it is systemd: systemctl enable traefik creates the link that starts the unit at boot of the machine (systemctl is-enabled traefik must answer enabled). For the container, it is Docker (or systemd, in the variant described below): the restart: unless-stopped key of the Compose file (equivalent to the --restart option of docker run) restarts the container when the engine starts, provided that Docker itself starts at boot (systemctl is-enabled docker).
The policies are not equivalent. unless-stopped restarts the container after a Docker or machine restart, unless you stopped it yourself; always also restarts it after a deliberate stop, but only at the next Docker startup (a manual docker stop is not undone until the engine restarts); no (the default) never restarts it. A fourth policy, on-failure, restarts only after an exit with an error. The complete table of policies is in Docker Compose (in French).
Measured with Docker 26.1.5: an on-failure container that exits cleanly (code 0) when the engine stops stays exited after systemctl restart docker, but a container that exits with an error at that moment, or that is killed, comes back. So do not count on on-failure for recovery: choose unless-stopped or always.
Still on Docker 26.1.5, a sudo systemctl reboot of the host gives the same result as systemctl restart docker: the unless-stopped container and the always container come back, a container with no policy (no) stays exited. A container's states and exit codes are detailed in Running and troubleshooting a container (in French).
Read the policy actually applied to the container, not the one you think you wrote:
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' traefikThen prove the recovery with an independent client, without replaying any start command. Restart the Docker engine (the service must come back without intervention), then, for the whole machine, sudo reboot and a curl from another host once the machine is back. A curl run on the machine itself does not prove that the port is reachable from outside.
# On the machine: restart the Docker engine, without touching the containersudo systemctl restart docker
# From another host: does the entry point answer?curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 -H "Host: whoami.localhost" http://MACHINE_ADDRESS/A 200 answer (or 404 if no route matches) proves that port 80 is served again; a connection refused or a timeout signals an entry point that has not recovered. Wait a few seconds before concluding: the engine and the container do not restart instantly.
Running the container under systemd (lab variant)
A container can also be driven by a systemd unit that runs docker run: this is the form requested by the first lab, useful when you want a single tool (systemctl, journalctl) for all the machine's services. The unit then replaces Compose's restart: key: systemd does the restarting, and ExecStartPre first removes any leftover container of the same name to avoid a name conflict.
[Unit]Description=Traefik (conteneur)After=docker.service network-online.targetRequires=docker.service
[Service]Restart=on-failureRestartSec=5ExecStartPre=-/usr/bin/docker rm -f traefikExecStart=/usr/bin/docker run --rm --name traefik \ -p 80:80 -p 127.0.0.1:8080:8080 \ -v /etc/traefik/traefik.yaml:/etc/traefik/traefik.yaml:ro \ -v /etc/traefik/conf.d:/etc/traefik/conf.d:ro \ traefik:v3.7.6@sha256:21a3d83696379bac6434bb32e1dde0aff0e84ef2abd053ed3db87d3f45e749b2ExecStop=/usr/bin/docker stop traefik
[Install]WantedBy=multi-user.targetThe dash in ExecStartPre=- tells systemd to ignore the command's failure (no container to remove on first launch). On first launch, the journal does display Error response from daemon: No such container: traefik, which is normal and does not block startup. This template must be adapted to your directory tree.
Warning: if the mounted file is the one from the binary route, its traefik entrypoint listens on 127.0.0.1:8080 inside the container, where the host's 127.0.0.1:8080:8080 publication does not reach it. Measured with Docker 26.1.5: curl http://127.0.0.1:8080/api/overview from the machine then returns 000 (exit code 56, connection reset). Leave the default address (:8080) in the mounted file (address: ":8080"): the same curl then returns 200 from the machine, and 000 from another host, the loopback restriction being carried by the port publication.
Measured behavior of this unit (Docker 26.1.5, Debian 13):
| Situation | Result |
|---|---|
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' traefik | no: the unit runs docker run without --restart, recovery is not carried by Docker |
docker kill traefik (exit 137) | the unit restarts after 5 s (Restart=on-failure), the container is recreated: --rm and ExecStartPre=- do not contradict each other |
docker stop traefik from outside | the unit also restarts the container: to stop it, use systemctl stop traefik-conteneur |
systemctl stop traefik-conteneur | unit inactive, no container left (--rm) |
systemctl restart docker | Requires=docker.service makes the unit restart with the engine (active after about 15 s) |
| Leftover container of the same name | removed by ExecStartPre, no name conflict |
systemctl reboot with the unit enabled | container back, external curl on port 80: 404; port 8080: 000 |
Check it like the other routes: systemctl enable --now traefik-conteneur, then docker inspect for the image actually launched and ss -tlnp for the ports. With log.filePath in the mounted file, docker logs traefik stays empty: the log is written inside the container.
This unit sets no restart policy on the container (the no row of the table): it is systemd, not Docker, that restarts it. The second lab of the "Practice" section requires the opposite, a policy carried by Docker: read what distinguishes it there before starting it.
Validating the installation
A started service does not prove that it works: Traefik starts without error even when no provider reports any configuration back to it. Three checks complement each other: reading the logs at startup, querying the API to count the loaded routes, then a real call through the proxy. Run them in this order, since a configuration error always shows up first in the logs.
Checking the logs
At startup, Traefik states the version launched and the providers enabled. ERROR-level messages repeated in a loop often signal an invalid dynamic configuration file, which the proxy re-reads each time a change is detected.
# Binary with systemd: the lesson's configuration sets log.filePath,# so the messages are in this filesudo tail -f /var/log/traefik/traefik.log# (without log.filePath: sudo journalctl -u traefik -f)
# Dockerdocker compose logs -f traefik
# Look for errorsdocker compose logs traefik 2>&1 | grep -i errorTesting with curl
These requests query the administration API and return JSON, which jq formats. A router count of zero while containers are running points to the missing traefik.enable=true label, a direct consequence of the exposedByDefault: false setting.
# API overviewcurl -s http://localhost:8080/api/overview | jq
# List of HTTP routerscurl -s http://localhost:8080/api/http/routers | jq
# List of servicescurl -s http://localhost:8080/api/http/services | jq
# Service healthcurl -s http://localhost:8080/pingChecking from an independent client
A check made on the machine itself lies by omission: curl localhost:8080 succeeds even when the port is open to the whole network. To prove that an entry point is closed by default, query it from another machine on the same network (replace MACHINE_ADDRESS): port 80 must answer, the API on 8080 must not answer.
# From another host: the web entry point answers (404 without a route is normal)curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 http://MACHINE_ADDRESS/
# From that same remote host: the API must not be reachablecurl -s -o /dev/null -w '%{http_code}\n' --max-time 3 http://MACHINE_ADDRESS:8080/api/overviewThe first call must return 404 or 200. The second must fail (connection refused or timeout, 000 for curl): a 200 means the API is exposed without authentication, and you must go back to the listening address (127.0.0.1:8080) or to the port publication.
Useful API endpoints
These routes are served by the internal API, hence subject to the same protection as the dashboard. One exception is worth knowing: /ping is available only if the ping option is in the static configuration, otherwise the request returns an error even though Traefik is running. This is the endpoint you will wire to an availability probe.
| Endpoint | Description |
|---|---|
/api/overview | Overview of the resources |
/api/http/routers | List of HTTP routers |
/api/http/services | List of HTTP services |
/api/http/middlewares | List of middlewares |
/api/entrypoints | Entrypoint configuration |
/ping | Simple health check |
Troubleshooting
Incidents met at installation fall into four families: an occupied port, a denied access to the Docker socket, an unreachable dashboard and an invalid configuration. The first three are diagnosed from the host system, the last from Traefik's logs.
Port already in use
On a server that has already hosted a site, a web server often occupies port 80. Traefik stops immediately in this case, without starting partially.
Symptom: "bind: address already in use" error
# Identify the process using the portsudo ss -tlnp | grep :80# orsudo lsof -i :80
# Stop the competing servicesudo systemctl stop nginx # or apache2Docker socket permissions
The socket belongs to the docker group and is readable only by its members. Adding the service account to this group solves the problem but in effect gives it privileges equivalent to root on the host, hence the second solution offered.
Symptom: "permission denied" error on /var/run/docker.sock
# Check the permissionsls -la /var/run/docker.sock
# Solution 1: Add the user to the docker groupsudo usermod -aG docker traefiksudo systemctl restart traefik
# Solution 2: Use a secure socket proxy (recommended in production)# See: https://github.com/Tecnativa/docker-socket-proxyDashboard unreachable
Three causes follow one another in the order in which traffic meets them: the API is not enabled, the port is not published by the container, or the host firewall blocks the connection. Check them in this order rather than at random.
Symptom: "Connection refused" on port 8080
-
Check that the API is enabled (
insecure: trueis a ❌ counter-example outside local diagnosis, see the section on the dashboard):api:dashboard: trueinsecure: true # ❌ local diagnosis only -
Check that the port is published (Docker). From the machine, the form
127.0.0.1:8080:8080is enough; from another host, the dashboard is deliberately not reachable (see the check from an independent client):ports:- "127.0.0.1:8080:8080" -
Check the firewall:
Terminal window sudo ufw statussudo firewall-cmd --list-all
Configuration errors
Running Traefik in the foreground with the DEBUG log level displays the exact error, often truncated in the service journal. Many messages come from options inherited from version 2, several of which were renamed in v3.
Symptom: Traefik does not start
# Validate the YAML syntaxyamllint /etc/traefik/traefik.yaml
# Run in the foreground to read the exact error: Traefik has no# configuration check outside execution, unlike nginx -ttraefik --configFile=/etc/traefik/traefik.yaml --log.level=DEBUG 2>&1 | head -50| Common error | Solution |
|---|---|
yaml: unmarshal errors | Check the YAML indentation |
field not found, node: <option> | Check the name of the option cited (typo, v3 vs v2); message measured in 3.7.6 |
invalid port | Correct format: ":80" with the quotes |
For routing symptoms (404, 502, 504, certificate), the lesson Troubleshooting a Traefik entry point takes over: this troubleshooting stops at startup.
Practice
The first lab has you install Traefik under systemd from a pinned image provided in /opt/pub-installation/image-ref, with the web entrypoint on port 80 as the only entry point. Validation looks at the actual state of the machine: the running container must match the expected digest, the API on 8080 must stay unreachable from poste.lab, an invalid dynamic file must be logged without restarting the process, and the entry point must come back after a reboot of edge.lab. It follows the "Running the container under systemd" variant, described above.
The second lab deals with recovery in isolation, and uses no systemd unit: a containerized web service must answer on http://edge.lab:18080/ before and after a Docker engine restart, without intervention. Validation reads the container's restart policy (docker inspect), which must be always or unless-stopped, then restarts Docker and calls the real HTTP endpoint within 30 seconds.
The two mechanisms answer two different questions. Docker's --restart policy answers "does the container come back when the engine or machine restarts?". A systemd unit answers "must systemd supervise this service, with a start order and dependencies?". The unit in the "Running the container under systemd" variant does not replace the policy: it runs docker run without --restart, so docker inspect returns no there, and this second lab's test would fail (an on-failure policy would not pass it either). For this lab, set the policy on the container (docker run --restart unless-stopped ... or restart: unless-stopped in Compose) and leave systemd aside.
Key points
- A version is verified: archive checksum and
traefik versionfor the binary,@sha256:digest of the image actually launched for the container. - Closed by default: only ports 80 and 443 are open; the API (8080) stays on the loopback and
insecure: trueis never a production setting. - Checked from outside: a
curlon the machine itself proves nothing, you need an independent client. - Static configuration: read at startup only, it requires a
restart; the dynamic one (conf.d) reloads on the fly. - A dynamic error does not kill the process: it is read in the log, whereas an unknown option in a known section of the static configuration prevents startup.
- The file written is not necessarily the file read:
systemctl cat,psanddocker inspecttell you what the running process uses. - Recovery is configured:
systemctl enablefor the binary,restart: unless-stopped(and Docker enabled at boot) for the container, then proof by a restart. :roon the Docker socket does not limit the API: only a socket proxy restricts the operations.--restartand a systemd unit are two distinct mechanisms: a unit that runsdocker runwithout--restartleaves the policy atno, andon-failureis not meant for recovery after a Docker restart.
Knowledge check
Check that you have mastered the essentials of this guide. The questions cover only what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Traefik with Docker: publishing and health-checking containers
A new full quiz with random questions
Work again only on the questions you missed