SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-09-25

Запуск Tailscale в Docker Compose через sidecar

Узнайте, как изолировать контейнеры в Docker Compose с помощью Tailscale sidecar. Вы исключите публикацию портов на хосте и сделаете сервис доступным только внутри tailnet.

Запуск Tailscale в стеке Docker Compose

Для запуска Tailscale в Docker Compose требуется два сервиса. Первый — это контейнер tailscale/tailscale, который подключается к вашей tailnet. Второй — это контейнер вашего приложения, который использует сетевое пространство имен (network namespace) первого контейнера вместо публикации порта на хосте. В результате сервис становится доступен с вашего ноутбука по имени, при этом оставаясь полностью недоступным из публичного интернета.

Tailnet — это частная сеть, которую Tailscale создает между устройствами, в которые вы вошли. Если этот термин вам незнаком, сначала прочитайте что такое Tailscale и как он соединяет две машины. В этом руководстве предполагается, что Docker Engine и плагин Compose v2 уже работают, как описано в первом стеке Docker Compose на VPS.

Пример от вендора и открытый порт

В официальном руководстве Tailscale по Compose приводится стек, близкий к этому.

services:
  tailscale:
    image: tailscale/tailscale:latest
    container_name: tailscale
    hostname: tailscale-nginx
    environment:
      - TS_AUTHKEY=tskey-auth-REPLACE-ME
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ./tailscale-state:/var/lib/tailscale
    cap_add:
      - net_admin
      - net_raw
    restart: unless-stopped

  nginx:
    image: nginx:latest
    container_name: nginx_server
    ports:
      - "8080:80"
    depends_on:
      - tailscale
    restart: unless-stopped

Каждая строка сервиса tailscale корректна. TS_AUTHKEY выполняет аутентификацию узла. TS_STATE_DIR указывает tailscaled путь для записи состояния, а bind mount сохраняет это состояние на диске. Проблема заключается во втором сервисе.

Эти два контейнера находятся в стандартной bridge-сети Compose, каждый со своим адресом. Это обычное поведение, описанное в как сети Compose соединяют контейнеры по имени сервиса. Контейнер tailscale присоединился к tailnet только для себя и не перенаправляет трафик в контейнер nginx. Поэтому единственный путь к nginx — это порт 8080 на хосте.

Опубликованный порт привязывается к 0.0.0.0, если вы не укажете адрес перед ним, поэтому на VPS этот порт отвечает на публичном IP-адресе. Приложение находится в вашей tailnet только номинально, а фактически доступно из интернета. Хостовый файрвол вас не спасет, так как Docker вставляет свои правила перенаправления перед цепочкой ufw. Это ловушка, описанная в почему правило deny в ufw не закрывает опубликованный порт Docker.

Еще одна деталь, которую стоит отметить. В примере предоставлены net_admin и net_raw, но /dev/net/tun не проброшен. TS_USERSPACE по умолчанию имеет значение true, поэтому контейнер использует сетевой стек пользовательского пространства, и эти две возможности (capabilities) не имеют никакого значения.

Sidecar: один namespace, без публикации портов

Поместите приложение в сетевой namespace контейнера Tailscale с помощью network_mode: service:tailscale. Оба процесса будут видеть один и тот же loopback и один и тот же адрес в tailnet, даже если они запущены в разных контейнерах.

services:
  tailscale:
    image: tailscale/tailscale:v1.102.3
    container_name: ts-nginx
    hostname: nginx-demo
    environment:
      - TS_AUTHKEY=${TS_AUTHKEY}
      - TS_HOSTNAME=nginx-demo
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ./ts-state:/var/lib/tailscale
    restart: unless-stopped

  nginx:
    image: nginx:1.30.4-alpine
    network_mode: service:tailscale
    depends_on:
      - tailscale
    restart: unless-stopped

Ключ размещается в файле .env рядом с compose-файлом, а не внутри YAML, поэтому файл, который вы отправляете в репозиторий, не содержит секретов. Одна строка: TS_AUTHKEY=tskey-auth-.... В Хранение секретов вне compose-файла в репозитории описаны остальные детали этого подхода.

Запустите конфигурацию и проверьте обе части.

docker compose up -d
docker compose exec tailscale tailscale status
docker compose logs --tail 20 tailscale

tailscale status должен вывести строку для этого узла с адресом 100.x, а затем остальные машины в вашей tailnet. С ноутбука, который также авторизован в сети, curl http://nginx-demo/ вернет приветственную страницу nginx. На самом VPS sudo ss -lntp | grep 8080 ничего не вернет, так как порт не был опубликован.

Почему порт 80, а не 8080: в режиме userspace tailscaled направляет входящее туннельное соединение на тот же порт на localhost. nginx слушает порт 80 внутри общего namespace, поэтому tailnet обращается к нему по порту 80. Измените порт, на котором слушает приложение, и порт в tailnet изменится вместе с ним. Этот трюк с общим namespace не является специфичным для Tailscale, и те же вопросы о доступе к хосту и остальному стеку возникают в контейнере Gluetun, который управляет сетью соседних контейнеров.

Какой ключ аутентификации нужен контейнеру?

Тип ключа определяет поведение при повторном запуске, поэтому выберите его до развертывания. Сгенерируйте ключ на странице Keys в панели администратора. Диалоговое окно отобразит его только один раз.

  • Одноразовые ключи (One-off keys) аутентифицируют одно устройство. Стек, пересозданный без директории состояния, не сможет подключиться снова.
  • Многоразовые ключи (Reusable keys) аутентифицируют любое количество устройств. Это стандартный выбор для стека Compose.
  • Эфемерные ключи (Ephemeral keys) помечают узел для автоматической очистки. Tailscale удаляет эфемерное устройство через 30–60 минут после его последней активности.
  • Предварительно одобренные ключи (Pre-approved keys) позволяют пропустить ручное одобрение устройства; это важно, только если в вашей tailnet включена функция одобрения устройств.
  • Тегированные ключи (Tagged keys) применяют ACL-тег, например tag:container, во время аутентификации. Устройство перестает быть привязанным к конкретному пользователю, а срок действия его ключа по умолчанию отключается.

Последний пункт — самый важный для эксплуатации. Ключ узла по умолчанию истекает через 180 дней, после чего узел отключается от tailnet, пока пользователь не авторизует его снова. Тегированный ключ отключает этот «таймер», поэтому теги используются для серверов и контейнеров.

Истечение срока действия ключа аутентификации — это отдельное событие, которое часто путают с истечением ключа узла. Ключи аутентификации действуют от 1 до 90 дней (по умолчанию 90). Истечение срока действия ключа аутентификации не отключает устройства, которые он уже успел аутентифицировать. Он лишь перестает добавлять новые. Для долгоживущего сервиса используйте многоразовый тегированный ключ, который не является эфемерным. Для стека, который вы постоянно удаляете (например, среда предварительного просмотра), эфемерный ключ поможет поддерживать чистоту в панели администратора без ручного удаления записей.

Почему контейнер каждый раз определяется как новая машина?

Потому что tailscaled записывает свое состояние в записываемый слой контейнера, а docker compose down удаляет контейнер.

Идентификатор узла хранится в этом каталоге состояния. Обеспечьте его постоянство, и контейнер сохранит свое имя и 100.x адрес после перезагрузок, наряду со всей конфигурацией сервиса. Если потерять эти данные, следующий запуск будет считаться первым: контейнер проходит аутентификацию заново с тем же ключом, и в консоли администратора появляется вторая машина. Обе претендуют на имя хоста nginx-demo, поэтому MagicDNS присваивает новой машине пронумерованный суффикс, а все сохраненные вами ссылки ведут на нерабочий узел.

Должны выполняться два условия. Параметр TS_STATE_DIR=/var/lib/tailscale должен быть задан, так как вне Kubernetes у него нет значения по умолчанию. Этот путь должен быть смонтирован с помощью bind mount или именованного тома — выбор между ними описан в сравнении bind mounts и именованных томов. Настройка одного без другого — распространенная ошибка, которая приводит к скрытому сбою: стек работает корректно до первого down.

Проверяйте это, а не полагайтесь на предположения.

docker compose down
ls -l ./ts-state
docker compose up -d
docker compose exec tailscale tailscale status

./ts-state должен содержать tailscaled.state еще до второго up, и узел должен вернуться с тем же адресом, который у него был ранее. Другой адрес означает, что монтирование не работает должным образом.

Закрепите образ и укажите использованный тег

tailscale/tailscale:latest соответствует новейшей стабильной сборке. docker compose pull через шесть месяцев незаметно заменит tailscaled на другую версию, и после следующего перезапуска будет выполняться код, который вы не выбирали. Стек выше закрепляет v1.102.3, стабильный релиз на сентябрь 2026 года. Docker Hub также публикует v1.102 для ветки исправлений и тег unstable, который не следует использовать на сервере.

Обновляйте намеренно.

docker compose pull tailscale
docker compose up -d
docker compose exec tailscale tailscale version

Изменение тега и выполнение up -d пересоздает контейнер, что не то же самое, что его перезапуск. Разница между restart, up и rebuild стоит прочитать перед отладкой изменения версии, которое не вступило в силу.

Сетевое взаимодействие в пространстве пользователя и его стоимость

TS_USERSPACE по умолчанию имеет значение true. В этом режиме контейнер запускает стек TCP/IP в пространстве пользователя и не взаимодействует с /dev/net/tun. Это позволяет работать на хостах, которые не предоставляют контейнеру устройство TUN. Входящие соединения работают, так как они перенаправляются на соответствующий порт localhost. Именно поэтому боковому контейнеру (sidecar) выше не требуются специальные устройства или права доступа.

Исходящий трафик — это то, за что приходится платить. В режиме пространства пользователя приложение не может просто открыть сокет к другому узлу tailnet. Вместо этого tailscaled предоставляет SOCKS5-прокси и HTTP-прокси. Вам нужно настроить TS_SOCKS5_SERVER=localhost:1055 для службы tailscale и ALL_PROXY=socks5://localhost:1055 для приложения, при этом приложение должно учитывать эти настройки. Любое ПО, игнорирующее переменные окружения прокси, не сможет подключиться к tailnet.

Сам стек имеет ограничения, о которых следует знать. Передаются только протоколы TCP и UDP, поэтому другие IP-протоколы, такие как SCTP, не проходят. ICMP ограничен только эхо-запросами (ping), которые воссоздает демон, что добавляет небольшую видимую задержку. Соединения завершаются на узле и устанавливаются заново до цели, поэтому они не являются сквозными (end-to-end). Узел в пространстве пользователя также не может использовать выходной узел (exit node) или маршрут подсети, анонсированный кем-то другим, хотя сам он может их анонсировать.

Если вам нужен прозрачный исходящий трафик, переключитесь на сетевой стек ядра, добавив три параметра в службу tailscale.

    environment:
      - TS_USERSPACE=false
    devices:
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin

Сначала убедитесь, что хост может предоставить такую возможность с помощью test -c /dev/net/tun && echo ok. В KVM устройство присутствует. В контейнерной виртуализации, использующей общее ядро хоста, оно может отсутствовать, и тогда пространство пользователя останется единственным вариантом. Предоставьте контейнеру устройство TUN, если он должен выполнять роль маршрутизатора подсети, анонсирующего частный диапазон или выходного узла для других ваших устройств, так как именно эти роли больше всего ограничены при работе в пространстве пользователя.

Доступ к сервису: serve или обычное имя MagicDNS

Самый простой путь — использование имени MagicDNS. С любого устройства в tailnet работает http://nginx-demo/, а также полное имя http://nginx-demo.your-tailnet.ts.net/. Обычный HTTP в данном случае не является открытым текстом в сети, так как WireGuard шифрует трафик между двумя узлами, а информация о том, что сервер координации может и не может видеть определяет границы этой гарантии. Сертификат отсутствует, поэтому браузер помечает источник как небезопасный, и любые веб-функции, требующие безопасного контекста, отказываются работать.

Другой путь — использование Tailscale Serve, запущенного внутри контейнера.

docker compose exec tailscale tailscale serve --bg localhost:80
docker compose exec tailscale tailscale serve status

Эта команда публикует приложение по адресу https://nginx-demo.your-tailnet.ts.net с сертификатом, который предоставляет Tailscale. И MagicDNS, и HTTPS-сертификаты должны быть включены на странице DNS в панели администратора, иначе не будет имени, которое можно указать в сертификате. --bg записывает конфигурацию в состояние tailscaled, которое вы сохранили, поэтому она восстанавливается вместе с контейнером, а tailscale serve reset удаляет её. TS_SERVE_CONFIG указывает на JSON-файл, если вы предпочитаете хранить конфигурацию в репозитории, а не в командной строке. Serve работает только внутри tailnet. Funnel — это отдельная команда, которая выставляет тот же сервис в публичный интернет, поэтому ознакомьтесь с разницей между Serve и Funnel перед выполнением любой из них.

Типичные сбои и сообщения об ошибках

Error response from daemon: conflicting options: port publishing and the container type network mode. Вы оставили блок ports: в описании sidecar-сервиса. Публиковать порты может только контейнер, владеющий пространством имен, а приложение, работающее только внутри tailnet, не должно публиковать ничего. Удалите этот блок.

Контейнер приложения запущен, но запросы до него не доходят. Вы пересоздали сервис tailscale отдельно. Пространство имен, которым он владел, было уничтожено вместе с ним, и приложение теперь привязано к несуществующему объекту. Пересоздайте пару целиком с помощью docker compose up -d --force-recreate.

Узел не появляется в консоли администратора. Прочитайте docker compose logs tailscale. Там отображаются отклоненные ключи. Использование одноразового ключа, который уже был применен, или ключа с истекшим сроком действия останавливает узел до того, как он подключится к tailnet.

Узел запущен, tailscale status выглядит корректно, а curl http://nginx-demo/ зависает. Приложение слушает не тот интерфейс, который вы ожидаете. Проверьте его изнутри общего пространства имен с помощью docker compose exec nginx wget -qO- http://localhost/. Если проверка не проходит, проблема в приложении, а не в Tailscale. Если проходит, значит, приложение привязано к конкретному интерфейсу, а не ко всем доступным.

Машина исчезла из консоли через час после остановки стека. Ключ был эфемерным. Удаление происходит через 30–60 минут после последней активности, это штатная работа функции.

Начиная с версии 1.78 образ может открывать неаутентифицированный endpoint /healthz: установите TS_ENABLE_HEALTH_CHECK=true, который слушает TS_LOCAL_ADDR_PORT, по умолчанию [::]:9002. Настройте healthcheck в Compose на этот адрес, чтобы узел, не прошедший аутентификацию, помечался как неработоспособный, а не отображался как активный. Если вы не хотите зависеть от серверов координации Tailscale, тот же файл compose может взаимодействовать с вашим собственным сервером управления через TS_EXTRA_ARGS=--login-server=https://headscale.example.com, что является отправной точкой для запуска Headscale в качестве собственного сервера управления.

FAQ

Почему другие устройства в моей tailnet не могут получить доступ к моему контейнеру с приложением?

Потому что контейнер Tailscale присоединился к tailnet только для самого себя. Если приложение работает в стандартной сети bridge Docker Compose со своим собственным адресом, узел Tailscale не пересылает на него трафик, и единственный путь доступа — это опубликованный порт хоста. Назначьте приложению network_mode: service:tailscale, чтобы оно использовало сетевое пространство имен контейнера Tailscale, затем удалите блок ports:. После этого приложение станет доступно в tailnet на порту, который оно прослушивает.

Нужен ли /dev/net/tun для запуска Tailscale в Docker Compose?

Для входящих соединений — нет. Параметр TS_USERSPACE по умолчанию имеет значение true, и в этом режиме tailscaled использует собственный сетевой стек, перенаправляя входящие туннельные соединения на тот же порт на localhost. Таким образом, sidecar-контейнер работает без устройства и дополнительных привилегий. Вам потребуются /dev/net/tun, TS_USERSPACE=false и net_admin только в том случае, если контейнер должен прозрачно открывать исходящие соединения в tailnet или выступать в роли маршрутизатора подсети (subnet router) или выходного узла (exit node).

Что лучше использовать для стека Compose: эфемерный ключ или многоразовый ключ авторизации?

Для любых долгоживущих сервисов используйте многоразовый ключ с тегами, который не является эфемерным. Тегирование отключает истечение срока действия ключа узла, поэтому контейнер не будет удален из tailnet через 180 дней в ожидании повторной авторизации. Выбирайте эфемерные ключи только для стеков, которые вы часто удаляете (например, для сред предварительного просмотра), так как Tailscale удаляет эфемерное устройство через 30–60 минут после его последней активности, что позволяет поддерживать консоль администратора в чистоте.

Почему мой контейнер отображается как новое устройство при каждом перезапуске?

Директория состояния не сохраняется, поэтому tailscaled запускается без идентификационных данных и проходит аутентификацию как совершенно новый узел. Укажите TS_STATE_DIR=/var/lib/tailscale (у этого параметра нет значения по умолчанию вне Kubernetes) и примонтируйте этот путь к bind mount или именованному тому. Настройка одного без другого выглядит корректно до первого docker compose down. Убедитесь, что tailscaled.state существует в примонтированной директории, пока стек остановлен.