Docker-сервисы, которые принимают HTTP-запросы, обычно слушают внутренний порт контейнера: например, 3000, 8080, 5000 или 8000. Этот порт используется внутри Docker-сети, а внешний доступ к сервису лучше организовать через домен, HTTPS и reverse proxy.
В production-среде пользователь открывает сервис по адресу:
https://app.example.com
Домен указывает на публичный IP VPS, Traefik принимает запрос и передаёт его во внутренний Docker-сервис.
Схема выглядит так:
User → https://app.example.com → Traefik → Docker service
1. Направьте домен на VPS
Сначала создайте DNS-запись для домена или поддомена.
Пример:
Type: A
Name: app
Value: SERVER_IP
Если домен example.com, сервис будет открываться по адресу:
app.example.com
Проверить DNS можно командой:
dig app.example.com +short
Ожидаемый результат:
SERVER_IP
2. Подготовьте Docker-сервис
Допустим, приложение работает внутри контейнера на порту 3000.
Пример базового сервиса:
services:
app:
image: your-app-image:latest
container_name: app
restart: unless-stopped
expose:
- "3000"
Здесь используется expose, а не ports, потому что приложение не нужно открывать напрямую в интернет. Оно будет доступно только внутри Docker-сети для Traefik.
Плохо:
ports:
- "3000:3000"
Лучше:
expose:
- "3000"
Важно
Не публикуйте внутренний порт приложения наружу, если доступ к нему должен идти только через HTTPS и reverse proxy.
3. Добавьте Traefik как reverse proxy
Ниже пример docker-compose.yml, где Traefik публикует приложение через HTTPS.
Замените:
app.example.com
admin@example.com
your-app-image:latest
на свои значения.
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.email=admin@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"
- "./letsencrypt:/letsencrypt"
networks:
- proxy
app:
image: your-app-image:latest
container_name: app
restart: unless-stopped
expose:
- "3000"
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls=true"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=3000"
networks:
- proxy
networks:
proxy:
name: proxy
Traefik использует routers, services и entrypoints для обработки HTTP-запросов. В Docker-сценарии эти параметры обычно задаются через labels контейнера.
4. Что делает эта конфигурация
В этом примере Traefik:
- слушает порты 80 и 443;
- получает информацию о контейнерах через Docker socket;
- не публикует все контейнеры автоматически;
- ищет только контейнеры с traefik.enable=true;
- принимает запросы на app.example.com;
- выпускает HTTPS-сертификат через Let’s Encrypt;
- проксирует запросы на контейнер app:3000.
Ключевой блок для приложения:
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls=true"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=3000"
Что это означает:
traefik.enable=true
Traefik должен обработать этот контейнер.
Host(`app.example.com`)
Запросы на этот домен направляются в контейнер.
entrypoints=websecure
Сервис работает через HTTPS entrypoint, то есть через порт 443.
tls.certresolver=letsencrypt
Для этого домена нужно получить сертификат через Let’s Encrypt.
loadbalancer.server.port=3000
Внутренний порт приложения внутри контейнера — 3000.
Traefik поддерживает ACME/Let’s Encrypt для автоматического получения сертификатов. В официальном примере Traefik для Docker Compose с Let’s Encrypt показана схема, где сертификат выпускается через HTTP challenge для сервиса, опубликованного через Traefik.
5. Запустите сервисы
Создайте папку для сертификатов:
mkdir -p letsencrypt
Запустите контейнеры:
docker compose up -d
Проверьте статус:
docker ps
Ожидаемо должны быть запущены:
traefik
app
Проверьте логи Traefik:
docker logs traefik
Если DNS настроен правильно и порты 80/443 доступны, Traefik сможет выпустить HTTPS-сертификат.
6. Откройте порты в firewall
На VPS должны быть открыты порты:
80/tcp
443/tcp
Если используется UFW:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status
SSH лучше оставить доступным только с доверенного IP:
sudo ufw allow from YOUR_TRUSTED_IP to any port 22 proto tcp
7. Проверьте работу HTTPS
Откройте в браузере:
https://app.example.com
Также можно проверить через curl:
curl -I https://app.example.com
Ожидаемый результат:
HTTP/2 200
или другой успешный ответ приложения.
Если открыть:
http://app.example.com
Traefik должен перенаправить запрос на HTTPS, потому что в конфигурации включён redirect с web на websecure:
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
Итог
Для публикации Docker-сервиса через HTTPS на VPS можно использовать Traefik как reverse proxy.
В простом варианте схема выглядит так:
Domain → VPS → Traefik → Docker service
Приложение не нужно открывать напрямую через:
SERVER_IP:PORT
Достаточно оставить его внутри Docker-сети, а наружу публиковать только 80 и 443 через Traefik.
Traefik удобен для Docker-сервисов, потому что маршрутизация описывается прямо в docker-compose.yml через labels. Это особенно полезно, если на одном VPS нужно публиковать несколько сервисов:
app.example.com → app:3000
api.example.com → api:8080
panel.example.com → panel:9000
Такой подход безопаснее и удобнее для production-схемы: пользователь открывает сервис по домену с HTTPS, а внутренний порт приложения остаётся скрытым.