Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Install Traefik v3: binary, Docker and package manager

By updated

Improve this page

Traefik logo

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.

CriterionBinary under systemdDocker container
Versiontraefik version + archive checksum@sha256: digest of the running image (detailed reading in "Which configuration does the running process really read?")
Who restarts after an incidentsystemd (Restart=on-failure)Docker (restart: unless-stopped)
Start at bootsystemctl enablerestart: + Docker itself enabled at boot
Logjournalctl -u traefikdocker compose logs / docker logs
Process userdedicated traefik accountimage user, container isolation
Docker socket accessunnecessary without a Docker providermounted in the container: see below
Updatereplace the binarychange 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.

CriterionMinimumRecommended
LinuxKernel 3.10+Kernel 5.x+
RAM128 MB256 MB+
Disk space50 MB100 MB (with logs)
Architectureamd64, arm64amd64

Traefik also runs on Windows and macOS, but this guide focuses on Linux.

Network ports

Traefik uses several ports depending on your configuration:

PortUseConfiguration
80HTTP (web entrypoint)Open to the Internet
443HTTPS (websecure entrypoint)Open to the Internet
8080Dashboard and APIInternal only

Check that these ports are available:

Terminal window
# Check the ports in use
ss -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.

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.

  1. 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 amd64
    wget https://github.com/traefik/traefik/releases/download/${TRAEFIK_VERSION}/traefik_${TRAEFIK_VERSION}_linux_amd64.tar.gz
    # Verify the checksum before extracting anything
    wget https://github.com/traefik/traefik/releases/download/${TRAEFIK_VERSION}/traefik_${TRAEFIK_VERSION}_checksums.txt
    sha256sum -c traefik_${TRAEFIK_VERSION}_checksums.txt --ignore-missing

    The --ignore-missing option limits the check to the files actually downloaded, since the checksum file covers all architectures. Without OK in the output, go no further.

    Expected result:

    traefik_v3.7.6_linux_amd64.tar.gz: OK
  2. Install the binary

    Extract it and place the binary in the system PATH:

    Terminal window
    # Extract the archive
    tar xzf traefik_${TRAEFIK_VERSION}_linux_amd64.tar.gz
    # Move to /usr/local/bin
    sudo mv traefik /usr/local/bin/
    # Check the installation
    traefik version

    Expected result:

    Version: 3.7.6
    Codename: langres
    Go version: go1.25.11
    Built: 2026-06-30T12:14:07Z
    OS/Arch: linux/amd64
  3. Create the directory structure

    The traefik account 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 directories
    sudo mkdir -p /etc/traefik/conf.d
    sudo mkdir -p /var/log/traefik
    # Create a dedicated user (security)
    sudo useradd -r -s /sbin/nologin traefik
    # Assign the permissions
    sudo chown -R traefik:traefik /etc/traefik
    sudo chown -R traefik:traefik /var/log/traefik
  4. Create the configuration file

    Create /etc/traefik/traefik.yaml with the minimal configuration:

    /etc/traefik/traefik.yaml
    # Traefik static configuration
    # Network entry points
    entryPoints:
    web:
    address: ":80"
    websecure:
    address: ":443"
    # Internal entrypoint that carries the API: bound to the loopback
    # so that it cannot be reached from another machine
    traefik:
    address: "127.0.0.1:8080"
    # Dashboard and API
    api:
    dashboard: true
    insecure: true # ❌ counter-example in production: API without authentication, tolerated here because limited to 127.0.0.1
    # Dynamic configuration providers
    providers:
    file:
    directory: /etc/traefik/conf.d
    watch: true
    # Logging
    log:
    level: INFO
    filePath: /var/log/traefik/traefik.log
    accessLog:
    filePath: /var/log/traefik/access.log

    The traefik entrypoint is the internal entrypoint that serves the API and the dashboard; it is declared here to force 127.0.0.1 on it. Without this declaration, it would listen on :8080, on all interfaces.

  5. Create the systemd service

    Two directives carry most of the hardening. ProtectSystem=strict mounts the whole file system read-only, hence the need to list the writable directories explicitly in ReadWritePaths. AmbientCapabilities=CAP_NET_BIND_SERVICE allows 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 Proxy
    Documentation=https://doc.traefik.io/traefik/
    After=network-online.target
    Wants=network-online.target
    [Service]
    Type=notify
    User=traefik
    Group=traefik
    ExecStart=/usr/local/bin/traefik --configFile=/etc/traefik/traefik.yaml
    ExecReload=/bin/kill -USR1 $MAINPID
    Restart=on-failure
    RestartSec=5
    LimitNOFILE=65536
    # Security
    NoNewPrivileges=true
    ProtectSystem=strict
    ProtectHome=true
    ReadWritePaths=/var/log/traefik /etc/traefik
    PrivateTmp=true
    # Capability for ports < 1024
    AmbientCapabilities=CAP_NET_BIND_SERVICE
    [Install]
    WantedBy=multi-user.target
  6. Start and verify

    Terminal window
    # Reload systemd
    sudo systemctl daemon-reload
    # Enable automatic startup
    sudo systemctl enable traefik
    # Start the service
    sudo systemctl start traefik
    # Check the status
    sudo systemctl status traefik

    Expected result:

    ● traefik.service - Traefik Reverse Proxy
    Loaded: 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 | jq

    Then check who is actually listening: ss lists the listening sockets and the process that holds them. Ports 80 and 443 must appear on all interfaces, port 8080 only on 127.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.

  1. Create the directory structure

    The acme.json file will store the certificates and private keys obtained from Let's Encrypt. Traefik refuses to start if its permissions are wider than 600, hence the chmod right at creation.

    Terminal window
    # Create the project directory
    mkdir -p ~/traefik-lab
    cd ~/traefik-lab
    # Create the subdirectories
    mkdir -p config/dynamic
    # Create the file for ACME certificates (Let's Encrypt)
    touch acme.json
    chmod 600 acme.json
  2. Create the static configuration

    Create config/traefik.yaml:

    config/traefik.yaml
    # Traefik static configuration
    # Entrypoints
    entryPoints:
    web:
    address: ":80"
    websecure:
    address: ":443"
    # API and Dashboard
    api:
    dashboard: true
    insecure: true # ❌ counter-example in production: API without authentication, tolerated here because port 8080 is published only on 127.0.0.1
    # Docker provider
    providers:
    docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false # Expose only containers with the label traefik.enable=true
    watch: true
    file:
    directory: /etc/traefik/dynamic
    watch: true
    # Logging
    log:
    level: INFO
    accessLog: {}
  3. Create the docker-compose.yml file

    Both configuration files are mounted read-only, which prevents the container from modifying what drives it; acme.json is the exception since Traefik must write the certificates to it. The image is designated by its tag v3.7.6 and 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:21a3d83696379bac6434bb32e1dde0aff0e84ef2abd053ed3db87d3f45e749b2
    container_name: traefik
    restart: unless-stopped
    security_opt:
    - no-new-privileges:true
    ports:
    - "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.json
    networks:
    - traefik-public
    networks:
    traefik-public:
    external: true

    The network is declared external: true because the next step creates it before docker compose up. With name: and driver: bridge instead, 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 returns network 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.

  4. Start Traefik

    Terminal window
    # Create the shared network (the Compose file declares it external)
    docker network create traefik-public 2>/dev/null || true
    # Start Traefik
    docker compose up -d
    # Check the status
    docker compose ps

    Expected result:

    NAME IMAGE COMMAND SERVICE STATUS PORTS
    traefik traefik:v3.7.6@sha256:21a3d836... "/entrypoint.sh trae…" traefik Up ... 0.0.0.0:80->80/tcp, ..., 127.0.0.1:8080->8080/tcp

    The IMAGE and STATUS columns 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 as 127.0.0.1:8080->8080/tcp.

  5. Verify the installation

    Terminal window
    # Check the logs
    docker compose logs traefik | tail -20
    # Test the API
    curl -s http://localhost:8080/api/overview | jq
    # Access the dashboard
    echo "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: 0 is 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-public

Restart and test:

Terminal window
docker compose up -d
curl -H "Host: whoami.localhost" http://localhost

Expected result:

Hostname: whoami
IP: 172.18.0.3
RemoteAddr: 172.18.0.2:54312
GET / HTTP/1.1
Host: 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.

Terminal window
brew install traefik
traefik version

Configuration 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.yaml

Complete static configuration

Here is an example traefik.yaml with the main options:

/etc/traefik/traefik.yaml
# =============================================================================
# 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: true

The 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.

traefik.yaml
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
api:
dashboard: true
providers:
docker:
exposedByDefault: false

Accessing 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.

Terminal window
# Direct access
http://localhost:8080/dashboard/
# Via curl
curl http://localhost:8080/api/overview

Securing 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:

/etc/traefik/conf.d/dashboard.yaml
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:

Terminal window
# Install htpasswd if needed
sudo apt install apache2-utils # Debian/Ubuntu
# or
sudo dnf install httpd-tools # RHEL/Rocky
# Generate the hash
htpasswd -nb admin YourPassword

Start, 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.

Three life events of a Traefik entry point: startup reads the static configuration then opens the ports; dynamic reload re-reads a modified conf.d file without restarting, and an invalid file produces an error in the log while the process stays active; recovery after a reboot of the machine or of Docker restarts the process through systemd or the restart policy, which triggers a new startup.Recovery after restartDynamic reloadProcess startinvalid fileStatic configurationis readPorts opened,providers startedFile in conf.d modifiedFile reread,no restartError in the log,process still runningMachine or Dockerrestartssystemd enable orrestart policy restarts the process
Recovery is just a startup triggered by systemd or Docker: anything that is not in the static configuration or in a watched file disappears with the process. Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

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.

Terminal window
# Binary: which unit, which arguments, which file?
systemctl cat traefik
ps -o pid,args -C traefik
# Container: which arguments, which mounted files, which exact image?
docker inspect --format '{{.Config.Cmd}}' traefik
docker inspect --format '{{json .Mounts}}' traefik | jq
docker inspect --format '{{.Image}}' traefik

systemctl 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:

Terminal window
sudo sed -i 's/^ dashboard: true/ dashbord: true/' /etc/traefik/traefik.yaml
sudo systemctl restart traefik
sudo journalctl -u traefik -n 20 --no-pager -o cat

In 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.

Terminal window
# Drop an invalid file in the watched directory
echo 'http: {routers: {cassee: {rule: Host(' | sudo tee /etc/traefik/conf.d/invalide.yaml
# The process is still active
systemctl 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 order
sudo rm /etc/traefik/conf.d/invalide.yaml

Where 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.

EventWhat is re-readInterruptionTest command
Startupstatic configuration, then initial dynamicyes, for the duration of the startupsystemctl restart traefik
Dynamic reloadwatched files onlynomodify a file in conf.d/
Recovery after restarteverything, as at startupyes, for the duration of the rebootsudo 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:

Terminal window
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' traefik

Then 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.

Terminal window
# On the machine: restart the Docker engine, without touching the container
sudo 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.

/etc/systemd/system/traefik-conteneur.service
[Unit]
Description=Traefik (conteneur)
After=docker.service network-online.target
Requires=docker.service
[Service]
Restart=on-failure
RestartSec=5
ExecStartPre=-/usr/bin/docker rm -f traefik
ExecStart=/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:21a3d83696379bac6434bb32e1dde0aff0e84ef2abd053ed3db87d3f45e749b2
ExecStop=/usr/bin/docker stop traefik
[Install]
WantedBy=multi-user.target

The 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):

SituationResult
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' traefikno: 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 outsidethe unit also restarts the container: to stop it, use systemctl stop traefik-conteneur
systemctl stop traefik-conteneurunit inactive, no container left (--rm)
systemctl restart dockerRequires=docker.service makes the unit restart with the engine (active after about 15 s)
Leftover container of the same nameremoved by ExecStartPre, no name conflict
systemctl reboot with the unit enabledcontainer 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.

Terminal window
# Binary with systemd: the lesson's configuration sets log.filePath,
# so the messages are in this file
sudo tail -f /var/log/traefik/traefik.log
# (without log.filePath: sudo journalctl -u traefik -f)
# Docker
docker compose logs -f traefik
# Look for errors
docker compose logs traefik 2>&1 | grep -i error

Testing 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.

Terminal window
# API overview
curl -s http://localhost:8080/api/overview | jq
# List of HTTP routers
curl -s http://localhost:8080/api/http/routers | jq
# List of services
curl -s http://localhost:8080/api/http/services | jq
# Service health
curl -s http://localhost:8080/ping

Checking 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.

Terminal window
# 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 reachable
curl -s -o /dev/null -w '%{http_code}\n' --max-time 3 http://MACHINE_ADDRESS:8080/api/overview

The 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.

EndpointDescription
/api/overviewOverview of the resources
/api/http/routersList of HTTP routers
/api/http/servicesList of HTTP services
/api/http/middlewaresList of middlewares
/api/entrypointsEntrypoint configuration
/pingSimple 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

Terminal window
# Identify the process using the port
sudo ss -tlnp | grep :80
# or
sudo lsof -i :80
# Stop the competing service
sudo systemctl stop nginx # or apache2

Docker 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

Terminal window
# Check the permissions
ls -la /var/run/docker.sock
# Solution 1: Add the user to the docker group
sudo usermod -aG docker traefik
sudo systemctl restart traefik
# Solution 2: Use a secure socket proxy (recommended in production)
# See: https://github.com/Tecnativa/docker-socket-proxy

Dashboard 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

  1. Check that the API is enabled (insecure: true is a ❌ counter-example outside local diagnosis, see the section on the dashboard):

    api:
    dashboard: true
    insecure: true # ❌ local diagnosis only
  2. Check that the port is published (Docker). From the machine, the form 127.0.0.1:8080:8080 is enough; from another host, the dashboard is deliberately not reachable (see the check from an independent client):

    ports:
    - "127.0.0.1:8080:8080"
  3. Check the firewall:

    Terminal window
    sudo ufw status
    sudo 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

Terminal window
# Validate the YAML syntax
yamllint /etc/traefik/traefik.yaml
# Run in the foreground to read the exact error: Traefik has no
# configuration check outside execution, unlike nginx -t
traefik --configFile=/etc/traefik/traefik.yaml --log.level=DEBUG 2>&1 | head -50
Common errorSolution
yaml: unmarshal errorsCheck 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 portCorrect 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

  1. A version is verified: archive checksum and traefik version for the binary, @sha256: digest of the image actually launched for the container.
  2. Closed by default: only ports 80 and 443 are open; the API (8080) stays on the loopback and insecure: true is never a production setting.
  3. Checked from outside: a curl on the machine itself proves nothing, you need an independent client.
  4. Static configuration: read at startup only, it requires a restart; the dynamic one (conf.d) reloads on the fly.
  5. 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.
  6. The file written is not necessarily the file read: systemctl cat, ps and docker inspect tell you what the running process uses.
  7. Recovery is configured: systemctl enable for the binary, restart: unless-stopped (and Docker enabled at boot) for the container, then proof by a restart.
  8. :ro on the Docker socket does not limit the API: only a socket proxy restricts the operations.
  9. --restart and a systemd unit are two distinct mechanisms: a unit that runs docker run without --restart leaves the policy at no, and on-failure is 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

8 questions
8 min
70% to pass

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

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn