Traefik reverse proxy: 5 застосунків у Docker Compose
Налаштуйте Traefik v3 для п’яти застосунків на одній IP-адресі: правила Host, автоматичний TLS Let's Encrypt і помилка acme.json, що блокує запуск.
Одна IP-адреса, п’ять застосунків, один порт 443
Ваш VPS має одну публічну IPv4-адресу та один TCP-порт 443. Ви хочете розмістити на ньому Gitea, staging-копію застосунку, внутрішню панель, сторінку стану та приймач webhook: п’ять імен хостів на одному сервері. Reverse proxy — це процес, який обробляє :80 і :443, читає заголовок Host у кожному запиті та передає його потрібному контейнеру. Traefik виконує цю роботу, а також отримує й поновлює сертифікат для кожного імені хоста, тому вам не потрібно вручну запускати certbot. Nginx і Caddy так само коректно оброблятимуть ці п’ять імен хостів, тому, якщо вибір ще не зроблено, варто порівняти три reverse proxy за роботою із сертифікатами та вартістю конфігурації для кожного застосунку перед тим, як підключати всі сервіси до одного з них.
Traefik відрізняється від блоку nginx server {} джерелом конфігурації. У nginx ви редагуєте файл і виконуєте reload, а життєвий цикл сертифікатів залишається окремим завданням. Саме такий підхід використовується, коли ви отримуєте сертифікати Let's Encrypt за допомогою certbot у nginx: таймер поновлення працює окремо від веб-сервера. Docker provider у Traefik відстежує потік подій Docker і читає labels контейнерів: запустіть контейнер із label правила Host(), і протягом секунди він стане доступним через маршрутизацію; зупиніть його — і маршрут зникне. У цьому полягає і головна проблема. Конфігурація в labels зберігається одразу в п’яти місцях, а неправильний label не спричиняє явної помилки: контейнер просто не маршрутизується, і Traefik нічого не повідомляє.
Чотири терміни
- Точки входу — це сокети, що прослуховують з’єднання. Ви визначите дві:
webна:80іwebsecureна:443. - Маршрутизатори зіставляють запит (
Host(...)) і прив’язують його до сервісу. Сертифікати запитуються для кожного маршрутизатора черезtls.certresolver. - Сервіси — це бекенд: контейнер і порт, який він прослуховує усередині Docker network.
- Проміжні обробники розташовані між маршрутизатором і сервісом: basic auth, списки дозволених IP-адрес, переписування заголовків і редиректи.
Ці чотири терміни — назви Traefik для компонентів, які інакше довелося б налаштовувати вручну: маршрутизатор — це server_name, сервіс — ціль proxy_pass, а проміжні обробники — це директиви для заголовків і автентифікації, які ви налаштовуєте самостійно, коли крок за кроком створюєте server block reverse proxy в nginx.
Статична конфігурація (точки входу, providers, ACME) передається Traefik у командному рядку або у traefik.yml. Її зміна потребує перезапуску Traefik. Динамічна конфігурація (маршрутизатори, сервіси, проміжні обробники) надходить із labels контейнерів і перезавантажується без зупинки сервісу. Плутанина між цими типами конфігурації зазвичай спричиняє проблему «мій flag нічого не робить».
Файл compose
Спільна Docker network з назвою proxy є основою цієї схеми. Traefik може звертатися до контейнера лише тоді, коли обидва підключені до неї.
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.5
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.network=proxy
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
# while you iterate, point at staging so a mistake costs nothing:
# - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --api.dashboard=true
- --log.level=INFO
- --accesslog=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=le
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS
gitea:
image: gitea/gitea:1 # major-only pin keeps this demo copy-pasteable; pin an exact release in production
restart: unless-stopped
volumes:
- ./gitea:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000Спочатку docker compose up -d, потім docker compose logs -f traefik. Кожен додатковий застосунок додається копіюванням блоку gitea із власною назвою роутера, власним Host() і власним внутрішнім портом. Інсталяція Nextcloud у Docker із TLS і резервними копіями підключається так само: приберіть опубліковані порти, підключіть його до proxy, а маршрутизацію за іменем хоста й сертифікат оброблятимуть labels роутера.
Важливі п’ять деталей.
exposedByDefault=false робить контейнер невидимим для Traefik, доки контейнер не має traefik.enable=true. Якщо не вказати цей параметр, маршрут буде створено для кожного контейнера, який ви коли-небудь запустите, зокрема для тимчасового postgres, використаного для перевірки.
providers.docker.network=proxy вказує Traefik, яку network використовувати, якщо контейнер підключений до кількох мереж. Якщо не вказати цей параметр, Traefik може вибрати неправильну IP-адресу контейнера. У результаті з’явиться помилка 502, яка виглядатиме як несправність застосунку.
loadbalancer.server.port=3000 — це порт усередині контейнера; Gitea прослуховує там порт 3000. Зверніть увагу: жоден контейнер застосунку не публікує портів. Це робить лише Traefik.
Перенаправлення на entrypoint web перетворює незашифровані запити на HTTPS-відповідь 308. Порт 80 все одно має залишатися відкритим: він потрібен для HTTP-перевірки ACME, а також користувачам, які вводять ім’я хоста без протоколу.
Подвоєний $$ у хеші basic-auth — це екранування Compose, а не помилка. Згенеруйте хеш за допомогою htpasswd -nbB admin 'your-password' (пакет apache2-utils), а потім подвойте кожен $.
Сертифікат і пастка з acme.json
tlschallenge=true вибирає TLS-ALPN-01: Let's Encrypt підключається до вашого сервера через 443, а Traefik відповідає на challenge під час TLS handshake. Альтернатива — HTTP-01 через порт 80. Замініть рядок tlschallenge у списку command: Traefik на ці два:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webПрацюють обидва варіанти. Для обох потрібно, щоб публічний DNS hostname уже вказував на ваш VPS, а certificate authority могла розв’язати це ім’я та підключитися ззовні. Спочатку створіть запис A (і AAAA), перевірте його за допомогою dig +short git.example.com, а потім запустіть Traefik.
Тепер про пастку, яка може забрати цілий вечір. Traefik зберігає ключ ACME account і кожен виданий сертифікат в одному acme.json. Якщо цей файл доступний для читання групі або всім користувачам, Traefik виводить приблизно такий рядок і зупиняється:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600Найкраще виправлення наведене вище: підключіть через bind mount каталог і дозвольте Traefik самостійно створити файл із правильними правами. Якщо ви створили acme.json за допомогою touch, umask встановив для нього права 644. Виправте це на хості:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikДодайте резервну копію цього каталогу до резервних копій томів застосунків. Втрата каталогу не є критичною: сертифікати буде випущено повторно. Але повторний випуск для п’яти hostname одночасно може спричинити перевищення rate limits.
Під час ітерацій використовуйте staging CA. Розкоментуйте рядок caserver, налаштуйте всі маршрути, потім закоментуйте його та видаліть acme.json, щоб production-сертифікати було запитано повторно. Production Let's Encrypt дозволяє п’ять дубльованих сертифікатів на тиждень для ідентичного набору hostname і обмежує кількість повторних невдалих перевірок для того самого імені. Staging видає недовірені сертифікати, тому браузер показує попередження. Це попередження означає, що перевірка пройшла, а ліміти staging значно вищі.
Панель керування — це інтерфейс адміністрування, а не демонстраційна сторінка
У більшості quickstart-конфігурацій встановлено --api.insecure=true. Це відкриває панель керування на порту 8080 без автентифікації. На сервері з публічною IP-адресою така конфігурація розкриває будь-кому, хто сканує сервер, топологію маршрутизації, імена хостів, назви middleware і порти бекендів.
Мітки на сервісі traefik вище показують альтернативу: панель керування маршрутизується як будь-який інший застосунок — через реальне ім’я хоста, TLS і за basicauth. service=api@internal підключає маршрутизатор до вбудованого API Traefik. Захист можна посилити, послідовно додавши список дозволених IP-адрес, який застосовується зліва направо. Якщо офісна адреса динамічна, укажіть діапазон підмережі, яку видає WireGuard VPN, розгорнутий вами на тому самому VPS, і підключайтеся до панелі керування лише через тунель:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authОдин спільний пароль basicauth перестає бути прийнятним, коли п’ять застосунків потребують окремих облікових записів. У тому самому слоті middleware можна вказати forwardauth, який передає рішення до Authentik, self-hosted сервера єдиного входу. Тоді панель керування та всі сусідні маршрути захищені одним входом, який можна відкликати в одному місці.
Сокет Docker дає права root
/var/run/docker.sock — це API, за допомогою якого можна створити контейнер, що монтує / з хоста. Доступ до нього еквівалентний правам root на машині, а Traefik потребує цей доступ, щоб читати labels.
Залиште :ro для монтування, але чітко розумійте, що саме він забезпечує: файл сокета стає доступним лише для читання. Це не забороняє POST запити до API Docker, які проходять через цей сокет. Справжній захист полягає в тому, щоб не передавати Traefik доступ до сокета, а розмістити між ними фільтрувальний проксі:
dockerproxy:
image: tecnativa/docker-socket-proxy # pin the current tag
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxyВидаліть том із сокетом із Traefik і вкажіть проксі як джерело для provider:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik зберігає доступ на читання контейнерів і мереж, але втрачає можливість щось створювати.
Firewall, порти та правило, на якому всі помиляються
Відкрито два порти та SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableОпубліковані порти Docker обходять ufw. Docker додає власні правила iptables, які обробляються перед ланцюжками ufw. Тому контейнер, запущений із ports: ["3000:3000"], доступний з інтернету, навіть якщо для цього порту налаштовано ufw deny. Захист має бути структурним, а не обмежуватися конфігурацією firewall: публікуйте порти лише з Traefik, а для кожного іншого контейнера задавайте networks: [proxy] і нічого більше. Якщо певному сервісу справді потрібно отримувати доступ до host, прив’яжіть його до loopback, "127.0.0.1:3000:3000".
Усунення несправностей: помилки, які ви справді побачите
Сторінку 404 не знайдено, її повернув Traefik. Жоден router не підійшов. Найімовірніші причини: у контейнері немає traefik.enable=true (за налаштованого exposedByDefault=false); правило Host() не відповідає введеному імені; ім’я router в одному label відрізняється від імені в іншому (routers.gitea.rule і routers.gitea.entrypoints мають бути однаковим словом); або hostname взято в лапки замість backticks. Traefik v3 вимагає backticks усередині matchers.
502 Bad Gateway. Router підійшов, але backend недоступний. Майже завжди контейнер не під’єднаний до мережі proxy; перевірте docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. Інша можлива причина — неправильний loadbalancer.server.port: ви вказали опублікований порт або застосунок слухає інший порт. У журналі зазначено спробу підключення: dial tcp 172.18.0.5:8080: connect: connection refused.
Браузер показує попередження, а сертифікат видано для TRAEFIK DEFAULT CERT. Для цього hostname немає сертифіката, і Traefik надіслав свій самопідписаний сертифікат-заглушку. Перегляньте рядки ACME:
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comDNS ще не вказує на цей сервер. Виправте запис, дочекайтеся завершення TTL і перезапустіть Traefik.
Invalid response from http://git.example.com/.well-known/acme-challenge/... під час HTTP challenge: порт 80 недоступний для Traefik із зовнішньої мережі. Зазвичай це firewall на рівні провайдера перед VPS, а не ufw.
Сертифікати не випускаються, а DNS працює через Cloudflare з увімкненою помаранчевою хмарою. Cloudflare завершує TLS на своєму edge, тому TLS-ALPN-01 не може пройти через нього. На час випуску сертифіката встановіть для запису режим DNS-only або перейдіть на challenge DNS-01 з API token. DNS-01 також єдиний challenge, який випускає wildcard-сертифікати.
Цикл перенаправлень. Щось перед Traefik уже завершує TLS і передає незашифрований трафік на :80; redirect для entrypoint надсилає його назад на HTTPS. Приберіть один із двох redirect.
Підтримка роботи
Unit Docker має бути увімкнений для запуску під час завантаження системи (systemctl is-enabled docker), а restart: unless-stopped відновлює стек після перезавантаження. Для явного керування створіть невеликий unit systemd, який запускає docker compose -f /srv/edge/compose.yml up -d із параметром RemainAfterExit=yes. Це забезпечує systemctl status edge і контроль порядку запуску.
Зафіксуйте тег Traefik (traefik:v3.5, ніколи не використовуйте latest). Оновлення з v2 до v3 змінило синтаксис правил і назви провайдерів. Неконтрольований latest без проблем повторно завантажить конфігурацію, яку Traefik більше не розуміє. Оновлюйте навмисно: прочитайте примітки щодо міграції, змініть тег, виконайте docker compose up -d traefik і моніторте журнал. Якщо ви все ще використовуєте тег v2, посібник з міграції Traefik з v2 до v3 описує всі перейменування, режим сумісності та відкат зі збереженням сертифікатів.
Створюйте резервну копію ./letsencrypt і тому даних кожного застосунку. Traefik не зберігає іншого стану, який неможливо відновити з compose-файлу.
Що ламається під час масштабування
Перша межа — не пропускна здатність, а один сервер: один Traefik на одному VPS є єдиною точкою відмови для п’яти застосунків, а acme.json — це файлове сховище у звичайному текстовому файлі. Два екземпляри Traefik, які записують у нього одночасно, пошкодять файл. Масштабування означає винесення сховища сертифікатів із файлу або завершення TLS в іншому місці.
Друга проблема — довготривалі з’єднання. Server-sent events, великі завантаження та повільні клієнти стикаються з тайм-аутами відповіді entrypoint; --entryPoints.websecure.transport.respondingTimeouts.readTimeout, а також його споріднені параметри writeTimeout і idleTimeout, керують цими значеннями. WebSockets проходять без додаткової конфігурації.
Третя проблема — диск. --accesslog=true записує дані в stdout, а Docker json-file driver зберігає їх безстроково, якщо не встановити обмеження. Налаштуйте logging.options.max-size для сервісу Traefik або записуйте access log у файл і виконуйте його ротацію.
Для цього не потрібен оркестратор. Потрібен сервер під вашим контролем, із реальною IP-адресою та відкритими для всього світу портами 80 і 443. Один невеликий VPS — це весь список залежностей.
FAQ
Чи потрібен certbot, якщо я використовую Traefik?
Ні. ACME-резолвер Traefik запитує та поновлює сертифікат для кожного hostname, який він маршрутизує, і зберігає їх усі в acme.json. Certbot залишається правильним інструментом, якщо nginx або інший сервер самостійно завершує TLS; одночасний запуск обох засобів для тих самих hostname лише витрачає ліміти Let's Encrypt.
Чому мій контейнер повертає 404 через Traefik?
Відповідь 404 від Traefik означає, що жоден router взагалі не зіставився із запитом. Перевірте, чи містить контейнер traefik.enable=true (цей параметр обов’язковий, якщо задано exposedByDefault=false), чи відповідає значення Host() введеному вами імені та чи однакове ім’я router у всіх labels цього застосунку. Traefik v3 також очікує backticks усередині matcher, а не лапки.
У чому тут різниця між 404 і 502?
404 означає, що маршрутизація не відбулася; 502 означає, що router зіставився із запитом, але backend відхилив з’єднання. Зазвичай 502 спричиняють контейнер, не підключений до мережі proxy, або loadbalancer.server.port, який вказує на опублікований порт, а не на порт, який застосунок прослуховує всередині контейнера. У access log зазначено точну адресу, до якої Traefik намагався підключитися.
Чи достатньо змонтувати Docker socket у режимі read-only?
Прапорець :ro робить read-only файл socket, але не API за ним. Запити POST і далі передаються через цей socket, а доступ до Docker API еквівалентний root на хості. Надійніший варіант — контейнер docker-socket-proxy, наведений вище: він надає Traefik доступ лише до операцій читання контейнерів і мереж та повністю блокує операції запису.
Чи може Traefik видати wildcard-сертифікат?
Лише через challenge DNS-01 і з API token вашого DNS-провайдера. TLS-ALPN-01 та HTTP-01 перевіряють один hostname і не можуть видати wildcard-сертифікат. DNS-01 також потрібен, коли CDN, наприклад Cloudflare, завершує TLS перед вашим VPS, а два інші challenge не проходять до кінця.