SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor

Traefik: 5 застосунків в одному Compose-файлі

Налаштуйте Traefik v3 для 5 застосунків на одній IP: Host-маршрути, Let's Encrypt TLS і помилка з 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.7
    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 із власною назвою router, власним Host() і власним внутрішнім портом. Встановлення Nextcloud у Docker із TLS і резервними копіями підключається так само: приберіть опубліковані порти, підключіть його до proxy, а маршрутизацію за hostname і сертифікат обробляйте через labels router.

У цьому фрагменті важливі п’ять деталей.

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 за замовчуванням є постійним, тому звичайний GET-запит без шифрування, який надсилає браузер, отримує відповідь 301 на HTTPS. Для інших методів, наприклад POST, Traefik повертає 308, щоб зберегти метод під час переходу. Порт 80 все одно має залишатися відкритим: він потрібен для HTTP challenge ACME, а також користувачам, які вводять hostname без схеми.

Подвоєний $$ у hash для 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 та кожен виданий сертифікат в одному acme.json. Якщо цей файл доступний для читання групі або всім користувачам, Traefik записує в журнал повідомлення, дуже подібне до цього, і вимикає resolver. Він продовжує працювати та маршрутизувати запити, але сертифікат більше не запитується, тому для кожного hostname використовується TRAEFIK DEFAULT CERT:

ERR The ACME resolve is skipped from the resolvers list error="unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600" resolver=le

Правильне виправлення наведене вище: підключіть через bind mount каталог і дозвольте Traefik самостійно створити файл із правильними правами. Якщо ви створили acme.json за допомогою touch, umask встановив для нього права 644. Виправте це на хості:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

Додайте резервну копію цього каталогу до резервних копій томів застосунку. Втрата каталогу один раз не є критичною: сертифікати буде видано повторно. Але його видалення під час кожного redeploy призведе до обмеження — не більше п’яти сертифікатів для точного набору hostname протягом 7 днів.

Під час налаштування використовуйте 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:2375

Traefik зберігає доступ на читання контейнерів і мереж, але втрачає можливість щось створювати.

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 page not found, відповідь надає Traefik. Жоден router не відповідає запиту. Найімовірніші причини: у контейнері немає traefik.enable=true (з установленим exposedByDefault=false); правило Host() не відповідає введеному імені; ім’я router в одному label відрізняється від імені в іншому (routers.gitea.rule і routers.gitea.entrypoints мають містити те саме слово); або hostname взято в одинарні лапки. Traefik v3 приймає backticks або екрановані подвійні лапки (\") усередині matcher і відхиляє одинарні лапки.

502 Bad Gateway. Router відповідає запиту, але бекенд недоступний. Майже завжди контейнер не підключений до мережі 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.com

DNS ще не вказує на цей сервер. Виправте запис, дочекайтеся завершення 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.7, але не latest) і виберіть гілку, для якої ще випускаються релізи. Станом на 2026-10-12 Traefik вказує v3.7 як єдину підтримувану гілку v3. Останній реліз v3.5, v3.5.6, вийшов 7 November 2025, а його Docker provider жорстко використовує API version 1.24. Docker Engine 29 і новіші версії відхиляють її з client version 1.24 is too old, тому жоден контейнер не маршрутизується. У Traefik 3.6.1 додано узгодження версії Docker API, тому наведений вище compose file використовує v3.7. Оновлення з v2 до v3 змінило синтаксис правил і назви провайдерів, а необдуманий latest без проблем перезавантажить конфігурацію, яку Traefik більше не розуміє. Оновлюйте навмисно: прочитайте примітки щодо міграції, змініть тег, docker compose up -d traefik і стежте за журналом. Якщо ви ще використовуєте тег v2, посібник з міграції Traefik з v2 до v3 описує кожне перейменування, режим сумісності та відкат зі збереженням сертифікатів.

Створюйте резервні копії ./letsencrypt і тому даних кожного застосунку. Traefik не зберігає іншого стану, який не можна відновити з compose file.

Що ламається під час масштабування

Перша межа — не пропускна здатність, а один сервер: один 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-resolver 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 у режимі лише для читання?

Прапорець :ro робить файл 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 не проходять до кінця.