Traefik с нуля: от пустого VPS до контейнера с HTTPS

Каждый новый сервис на сервере — это лишний блок в nginx. Добавил контейнер — пошёл руками писать конфиг, перезагружать nginx, проверять, не сломал ли что-то в соседних доменах. Работает, но плохо масштабируется, когда сервисов становится больше двух-трёх.
Traefik решает эту задачу иначе: он смотрит на Docker socket и сам видит, какие контейнеры запущены и что у них написано в labels или в отдельном конфиге.
Добавил или изменил правило — Traefik сам обновляет маршруты без своего перезапуска.
Что понадобится
- VPS на Debian 12 или 13 (Ubuntu 24.04)
- Домен, у которого A-запись указывает на IP сервера
Ставим Docker
На чистой VPS Docker не установлен вообще — ни из репозитория, ни тем более последней.
Ставим из официального репозитория Docker:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginПроверка:
docker --version
docker compose versionДобавьте пользователя в группу docker, чтобы не писать каждую команду docker через sudo:
sudo usermod -aG docker $USERПосле этого нужно перелогиниться, чтобы группа применилась.
Структура проекта и сеть
Заводим рабочую директорию и сразу создаём отдельную Docker-сеть — через неё Traefik будет видеть остальные контейнеры:
mkdir -p ~/traefik-demo/dynamic
cd ~/traefik-demo
docker network create proxyЕсли Traefik и целевой контейнер не в одной сети, роутинг просто не будет работать. Traefik физически не сможет достучаться до контейнера, даже если лейблы выставлены правильно.
Готовим директорию под сертификаты.
touch ~/traefik-demo/acme.json
chmod 600 ~/traefik-demo/acme.jsonTraefik не будет работать с ACME, если у файла права шире чем 600.
Способ 1 — конфигурация целиком через файлы
Статика в traefik.yml, динамика — в отдельном yml-файле, который Traefik умеет перечитывать сам, без единого рестарта.
# ~/traefik-demo/traefik.yml
api:
dashboard: true
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
log:
level: INFO
providers:
docker:
exposedByDefault: false
network: proxy
file:
directory: /etc/traefik/dynamic
watch: true
certificatesResolvers:
letsencrypt:
acme:
email: your-email@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: webКлючевой момент — providers.file.watch: true. Он показывает Traefik директорию, за изменениями в которой нужно следить. Все изменения подхватываются сразу, без перезапуска
Динамическая конфигурация, роутинг на сервис whoami:
# ~/traefik-demo/dynamic/dynamic.yml
http:
routers:
whoami:
rule: "Host(`whoami.example.com`)"
entryPoints:
- web
service: whoami
services:
whoami:
loadBalancer:
servers:
- url: "http://whoami:80"Здесь нет никаких labels на контейнере whoami — Traefik находит его напрямую по имени сервиса в Docker-сети (http://whoami:80), потому что оба контейнера сидят в одной сети proxy, а Docker резолвит имена контейнеров через встроенный DNS.
Docker Compose:
# ~/traefik-demo/docker-compose.yml
services:
traefik:
image: traefik:v3.7.8
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./dynamic/:/etc/traefik/dynamic/:ro
- ./acme.json:/letsencrypt/acme.json
networks:
- proxy
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: true
Версия образа зафиксирована — v3.7.8, актуальная стабильная на момент публикации. Фиксировать версию, а не брать latest, необходимо чтобы через полгода конфиг не сломался из-за минорного обновления с несовместимыми изменениями. Docker socket монтируется в режиме :ro — Traefik читает список контейнеров, но не должен иметь возможность их менять.
Поднимаем стек:
docker compose up -d
docker compose logs traefik
Проверяем роутинг:
curl http://whoami.example.comДолжен прийти ответ whoami с hostname контейнера и заголовками запроса.
Теперь — живой reload. Смотрим, что смонтировано внутри контейнера Traefik:
docker exec traefik cat /etc/traefik/dynamic/dynamic.yml
Меняем правило на хосте — например, меняем сам Host — не трогая ни Traefik, ни whoami:
nano ~/traefik-demo/dynamic/dynamic.yml
# меняем Host(`whoami.example.com`) на Host(`whoami2.example.com`)Снова смотрим файл внутри контейнера — он уже другой, хотя примонтирован :ro и никто ничего не перезапускал:
docker exec traefik cat /etc/traefik/dynamic/dynamic.ymlTraefik не может писать в этот файл — но само изменение на хосте всё равно долетает до контейнера через bind mount, потому что это просто файл на диске, куда контейнер смотрит. Traefik замечает изменение и перечитывает файл, обновляя роутинг без единого рестарта.
Способ 2 — вообще без файлов конфигурации: command + Docker labels
Это полная противоположность первому способу. Статика задаётся аргументами прямо в command у сервиса Traefik, а роутинг — labels на контейнере whoami. Весь стек описывается одним docker-compose.yml, без сторонних конфигов.
# ~/traefik-demo/docker-compose.yml
services:
traefik:ы
image: traefik:v3.7.8
container_name: traefik
restart: unless-stopped
command:
- "--api.dashboard=true"
- "--log.level=INFO"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./acme.json:/letsencrypt/acme.json
networks:
- proxy
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
- "traefik.http.routers.whoami.entrypoints=web"
- "traefik.http.services.whoami.loadbalancer.server.port=80"
networks:
proxy:
external: trueРоутинг читается только через docker-провайдер, по labels на контейнере. Traefik видит их в момент создания контейнера, через Docker API.
Какой способ выбрать — вопрос вкуса и удобства. Разница проявляется только в том, как каждый из них ведёт себя при изменениях.
Если поменять правило в label (например, домен) и выполнить docker compose restart whoami — ничего не изменится. Traefik узнаёт о labels в момент, когда видит Docker-событие создания контейнера, а обычный restart не создаёт контейнер заново, он просто перезапускает существующий процесс внутри него. Чтобы Traefik подхватил новые labels, контейнер нужно пересоздать:
docker compose up -d whoamiup -d увидит, что конфигурация сервиса изменилась, снесёт старый контейнер и создаст новый — на это событие Traefik и отреагирует.
Это и есть ключевая разница между способами на практике: файл (способ 1) Traefik перечитывает сам по изменению на диске, без каких-либо рестартов; labels (способ 2) требуют пересоздания контейнера, обычного restart недостаточно.
HTTPS через Let's Encrypt (HTTP challenge)
HTTP challenge — самый простой вариант получения сертификата: Let's Encrypt обращается на 80-й порт вашего сервера, Traefik сам отвечает на запрос подтверждения владения доменом. Не забудьте сделать A-запись в доменной зоне на белый IP-адрес VPS — без неё Let's Encrypt просто не достучится до сервера в момент проверки.
Резолвер сертификатов уже описан осталось для каждого способа сделать две вещи: включить редирект с web на websecure и указать настройка роута, что он должен слушать websecure и использовать этот резолвер.
Способ 1 — правим оба файла.
Редирект — в traefik.yml:
# ~/traefik-demo/traefik.yml
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"А привязку к HTTPS — в dynamic.yml, у route whoami:
# ~/traefik-demo/dynamic/dynamic.yml
http:
routers:
whoami:
rule: "Host(`whoami.example.com`)"
entryPoints:
- websecure
service: whoami
tls:
certResolver: letsencrypt
services:
whoami:
loadBalancer:
servers:
- url: "http://whoami:80"Поменяли entryPoints с web на websecure и добавили блок tls.certResolve. Поскольку providers.file уже с watch: true, Traefik подхватит это без рестарта.
Способ 2 — правим command и labels.
Редирект — двумя аргументами в command у Traefik:
command:
...
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"Привязку к HTTPS — в labels у whoami:
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
- "traefik.http.services.whoami.loadbalancer.server.port=80"Traefik подхватывает labels только при пересоздании контейнера. Нужно выполнить docker compose up -d whoami, чтобы контейнер пересоздался и Traefik увидел новые labels.
Перезапускаем стек:
docker compose up -dПервый запрос на HTTPS может занять несколько секунд — Traefik в этот момент обращается в Let's Encrypt, проходит challenge и сохраняет сертификат в acme.json. Вот как это выглядит в логах при удачном запросе:
2026-08-04T07:10:11Z INF Obtaining bundled SAN certificate. domains=whoami.example.com lib=lego
2026-08-04T07:10:12Z INF Use solver. domain=whoami.example.com lib=lego type=http-01
2026-08-04T07:10:12Z INF http01: Trying to solve HTTP-01. domain=whoami.example.com lib=lego
2026-08-04T07:10:25Z INF The server validated our request. domain=whoami.example.com lib=lego
2026-08-04T07:10:25Z INF Validations succeeded; requesting certificates. domains=whoami.example.com lib=lego
2026-08-04T07:10:27Z INF Server responded with a certificate. domains=whoami.example.com lib=legoОт первой попытки до полученного сертификата в этом прогоне ушло около 16 секунд — большую часть времени занимает не сам обмен с Let's Encrypt, а проверка HTTP-01: сервер Let's Encrypt должен успеть постучаться на ваш домен по 80-му порту и получить ответ.
Проверить, что сертификат выпустился, можно так:
curl -v https://whoami.example.com 2>&1 | grep -i "issuer\|subject"Если в acme.json к этому моменту появилась запись с вашим доменом — сертификат получен и будет обновляться автоматически, без дополнительных действий.
Что может пойти не так:
- домен не резолвится на IP сервера в момент запроса — Let's Encrypt не сможет пройти challenge
- порт 80 закрыт файрволом облака или iptables на самом сервере — challenge не пройдёт, даже если Traefik настроен верно
- неверные права на acme.json — Traefik откажется писать в файл и будет молча пропускать резолвер
Итог
На этом стенде — один Traefik и один тестовый контейнер, но принцип масштабируется без изменений в конфигурации самого Traefik: добавляете новый сервис в сеть proxy, вешаете на него нужные labels (или строку в dynamic.yml) — и он появляется в роутинге сам.
Для такого стенда не нужен мощный сервер — VPS с 1-2 vCPU и парой гигабайт RAM хватает и на сам Traefik, и на десяток легких контейнеров за ним.
Протестировать или изучить как работает Traefik удобно на Serverum: берёте минимальный тариф на Debian или Ubuntu, прогоняете весь стек из статьи.