Что такое Traefik? Сами себя они позиционируют как Edge Router или Application Proxy. Суть в том, чтобы связать внешний запрос с конкретным сервисом, который его обслужит. В сочетании с Докером задача обнаружения сервисов и масштабирования решается весьма элегантно за счет меток. Из коробки поддерживается Let's Encrypt, а что не поддерживается, так это PHP-FPM: приложения должны сами предоставлять веб-сервер.

Установка

Как это ни странно, программа, предназначенная в немалой степени для работы с Docker, поставляется в его образе. Таким образом, вам нужна по сути любая система с установленным Докером, например мой новый фаворит – Arch Linux. Создание виртуальной машины VirtualBox я описал в соответствующей статье, в аналогичной я и буду работать далее.

В той статье я не создавал пользователей, давайте сделаем какого-нибудь dev'а:

useradd -m -G docker dev
passwd dev
mkdir /srv/docker
chown dev:dev /srv/docker

Я бессовестным образом добавил его в группу docker, чтобы не надо было каждый раз использовать sudo (которого я даже и не установил), ну и папку создал. Вообще может вместо обычной директории стоило бы создать подтом btrfs, но не суть.

Перезаходим под новым пользователем и создаем к примеру /srv/docker/traefik/compose.yaml как по учебнику:

services:
  traefik:
    image: traefik:v3.4
    restart: unless-stopped
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
    ports:
      - "80:80"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Я правда restart еще добавил. Запускаем:

docker compose up -d

И по идее уже можно зайти в панель управления «по IP» на порт 8080, например http://192.168.56.103:8080/

dashboard.png

Недостатком такой конфигурации является обнаружение Трафиком лишних сервисов, поэтому я предпочитаю явно включать нужные. Поменяем параметр командной строки --providers.docker=true на --providers.docker.exposedbydefault=false и перезапустим приложение. На скриншоте выше вы можете видеть 4 сервиса, теперь их станет 3 (Трафик не создаст сервис для самого себя).

Также хотелось бы обратить внимание на то, что API (и, как следствие, панель управления) не защищены (--api.insecure=true), что и позволяет обращаться к 8080-му порту. В виртуальной машине это не имеет значения и даже удобно, но на «продуктовом» сервере API нужно или отключать совсем, или защищать к примеру базовой HTTP-аутентификацией (об этом позже).

В документации предполагают единый compose-файл. Если же у нас несколько приложений, то без настройки сети Traefik не получит доступ к сервисам. Простейшее решение – перевести сеть Traefik'а в режим хоста. Проброс портов в таком случае не требуется.

services:
  traefik:
    image: traefik:v3.4
    restart: unless-stopped
    network_mode: host
    command:
      - "--api.insecure=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Хотя такой трюк наверное не стоит повторять на боевом сервере.

Демо-приложение

Давайте развернем какой-нибудь «Hello World» на современном фреймворке – пусть будет Symfony. На данный момент – версия 7.x для PHP 8.2. В принципе у них есть свой образ с FrankenPHP и PostgreSQL (что весьма круто само по себе), но поскольку для демки вторая программа не нужна, сделаем все сами на базе первой.

Упомянутый FrankenPHP есть ни что иное, как Caddy (о которой я уже писал неоднократно, например в качестве веб-сервера на FreeBSD) со встроенной поддержкой PHP, что, по заверениям разработчиков, выводит производительность на космический уровень. Не то, чтобы я жаловался на скорость PHP-FPM…

Создадим директорию, например /srv/docker/demo. Dockerfile:

FROM dunglas/frankenphp

# Install PHP extensions
RUN install-php-extensions \
        intl

# Install composer and unzip
RUN set -eux; \
        curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \
        # smoke test
        composer --version; \
        apt-get update; \
        apt-get install -y --no-install-recommends \
                unzip \
        ; \
        rm -rf /var/lib/apt/lists/*

# Use the default production configuration
RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

На момент написания статьи у нас уже FrankenPHP v1.7.0 и PHP 8.4.10. Почему-то в комплекте не идет расширение intl – доустановил. Также решил «затащить» в сборку composer, которым мы и развернем проект, и unzip ему в помощь. Наконец, подкидываем «боевой» php.ini.

compose.yaml:

services:
  php:
    build: .
    restart: unless-stopped
    labels:
      - traefik.enable=true
      - traefik.http.routers.demo.rule=PathPrefix(`/`)
    environment:
      CADDY_GLOBAL_OPTIONS: "auto_https off"
      SERVER_NAME: "http://"
    volumes:
#      - ./app:/app
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

Меткой traefik.http.routers.demo.rule=PathPrefix(`/`) мы создали (объявили) HTTP-роутер с правилом роутинга по префиксу пути, т.е. в случае HTTP-запроса, если путь начинается с / (а это все возможные пути в URL), перенаправляем этот запрос данному сервису.

Забегая вперед, схему можно проиллюстрировать скриншотами админ-панели Traefik. Вот только зачем нужен такой здоровый промежуток между верхними и нижними блоками?..

traefik_demo.png
demo_service.png

В данном случае Docker назначил контейнеру IP 172.20.0.2. Любопытно, что не потребовалось указывать порт:

If a container exposes a single port, then Traefik uses this port.
If a container exposes multiple ports, then Traefik uses the lowest port. E.g. if 80 and 8080 are exposed, Traefik will use 80.

На заметку:

If a container does not expose any port, or the selection from multiple ports does not fit, then you must manually specify which port Traefik should use for communication by using the label traefik.http.services.<service_name>.loadbalancer.server.port

Возвращаемся к разбору compose.yaml. Следующая секция – переменные окружения:

    environment:
      CADDY_GLOBAL_OPTIONS: "auto_https off"
      SERVER_NAME: "http://"

Ими я бессовестным образом отключаю чуть ли не главную фишку Caddy – автоматический HTTPS (этим в дальнейшем займется сам Traefik), и, как следствие, перевожу веб-сервер на обслуживание всего поступающего http-трафика. В образе dunglas/frankenphp предусмотрена гибкая настройка переменными окружения, так что в большинстве случаев собственный Caddyfile может и не потребоваться.

Пока что я закомментировал проброс директории, но сразу же прописал хранение внутренних данных и конфигурации в соответствии с общей инструкцией. Запустим:

docker compose up -d

И через некоторое время можно будет зайти на виртуальную машину, например http://192.168.56.103/, и увидеть сведения о PHP от «Франкенштейна».

Собственно, к приложению. Погасим сервис (docker compose down), раскомментируем проброс директории и запустим обратно. Зайдем в контейнер, например:

docker exec -it demo-php-1 bash

Где demo-php-1 - автоматически назначенное имя контейнера. Разворачиваем приложение:

cd /
composer create-project symfony/symfony-demo app
cd app
openssl rand -hex 32 | php bin/console secrets:set APP_SECRET -
exit

По идее composer должен отработать гладко (здесь у нас не FreeBSD или Alpine Linux какой), а далее мы хитрой командой генерируем ключ приложения (без него не работает). Есть контакт!

demo_homepage.png

Масштабирование

Это неотъемлемая часть любой статьи про Traefik – придется делать. smile Создадим, к примеру, 3 реплики нашего сервиса.

services:
  php:
    deploy:
      replicas: 3
# остальные настройки...

down/up…

[+] Running 4/4
 ✔ Network demo_default  Created                                           0.0s
 ✔ Container demo-php-3  Started                                           0.6s
 ✔ Container demo-php-1  Started                                           0.4s
 ✔ Container demo-php-2  Started                                           1.0s
demo_replicas.png

В панели разработчика Symfony можно вызвать просмотр информации о PHP (/_profiler/phpinfo). Если спуститься к разделу Environment и несколько раз обновить страницу, то HOSTNAME будет изменяться.

php_hostname.png

Сессии (авторизация)

Все бы ничего, но ломается авторизация (если не взводить флаг «Оставаться в системе»).

demo_login.png

К счастью, починить легко – включаем «липкие» (sticky) сессии / куки.

      - traefik.http.services.demo.loadbalancer.sticky.cookie=true

При желании можно установить имя этой самой «липкой печеньки», но особого смысла в этом я не вижу. Первоначально я подумал, что нужно перечислить те cookie, которые ставит сайт, но нет – это всего лишь служебная кука для самого Traefik, на основе которой он сопоставляет посетителя с конкретным контейнером.

Демо Symfony ставит куки с признаками HttpOnly и SameSite:"Lax", можем определить те же свойства:

      - traefik.http.services.demo.loadbalancer.sticky.cookie.httponly=true
      - traefik.http.services.demo.loadbalancer.sticky.cookie.samesite=lax

URL без index.php

Вот она – истинная цель установки Traefik! А не эти ваши обнаружения сервисов с масштабированием.

В чем смысл. Понятно, что так скорее всего никто не будет делать, но если явно прописать index.php в URL (http://192.168.56.103/index.php), то он там так и останется. Это с точки зрения SEO (на локалхосте biggrin) как бы не есть хорошо. Добавляем промежуточное «ПО» (назвал rmindexphp) для редиректа по регулярке такими метками (в compose.yaml демки):

      - traefik.http.routers.demo.middlewares=rmindexphp
      - traefik.http.middlewares.rmindexphp.redirectregex.regex=/index\.php(?:/(.*)|$)
      - traefik.http.middlewares.rmindexphp.redirectregex.replacement=/$${1}

В последней метке двойной знак $ для экранирования. Регулярка - «апачевская» /index\.php(?:/(.*)|$) (только начинается со слэша вместо символа начала строки). Стало быть:

  • (?: – объявление группы без «захвата»
    • 1-я альтернатива:
      • литерал /
      • (.*) захватывает любые символы после слэша
    • 2-я альтернатива:
      • $ – конец строки
  • ) – конец группы

Таким образом:

URL Редирект
http://192.168.56.103/index.php http://192.168.56.103/
http://192.168.56.103/index.php/ http://192.168.56.103/
http://192.168.56.103/index.php/ru/blog/posts/in-hac-habitasse-platea-dictumst http://192.168.56.103/ru/blog/posts/in-hac-habitasse-platea-dictumst
http://192.168.56.103/index.php?a=b Не соответствует шаблону, поэтому редирект на /index.php/ru осуществляет Symfony, а Traefik, в свою очередь, перенаправляет на /ru.

Схема (в случае чего – масштабирование я выключил):

redirectregex_schema.png

Выход из пещеры

Заказываем самый дешевый VPS (ну или нормальный wink), прописываем для него, допустим, A/AAAA записи в DNS (далее будут фигурировать хосты demo.example.com и traefik.example.com), ставим обновляться систему (на VPS Arch Linux встречается довольно редко, поэтому далее я буду ориентироваться на Debian 12) и идем обедать. Потом наверное можно будет установить Docker. smile Проект удобно залить сочетанием tar и scp. Для определенности пути пусть будут такими же – /srv/docker/traefik и /srv/docker/demo.

Демо-приложение

Добавим фрагмент в Dockerfile, чтобы оно работало под пользователем www-data (33:33):

FROM dunglas/frankenphp

# Install PHP extensions
RUN install-php-extensions \
        intl

# Install composer and unzip
RUN set -eux; \
        curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \
        # smoke test
        composer --version; \
        apt-get update; \
        apt-get install -y --no-install-recommends \
                unzip \
        ; \
        rm -rf /var/lib/apt/lists/*

# Use the default production configuration
RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

# Running as a Non-Root User
RUN set -eux; \
        # Add additional capability to bind to port 80
        setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
        # Give write access to /data/caddy and /config/caddy
        chown -R www-data:www-data /data/caddy && chown -R www-data:www-data /config/caddy

USER www-data

На хосте (Debian) уже должен быть такой же пользователь, так что:

chown -R www-data:www-data /srv/docker/demo/app

В compose.yaml нам нужно поменять метку правила маршрутизации – теперь это будет конкретный хост. Также, забегая вперед, указываем точку входа websecure:

services:
  php:
    build: .
    restart: unless-stopped
    labels:
      - traefik.enable=true
      - traefik.http.routers.demo.rule=Host(`demo.example.com`)
      - traefik.http.routers.demo.entrypoints=websecure
    environment:
      CADDY_GLOBAL_OPTIONS: "auto_https off"
      SERVER_NAME: "http://"
    volumes:
      - ./app:/app
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

И уже пора настроить доверенный прокси, иначе, к примеру, не загрузится панель отладки из-за смешанного содержимого. В app/config/packages/framework.yaml добавляем такой фрагмент:

framework:
    # ...

    # you can use the 'PRIVATE_SUBNETS' string, which is replaced at
    # runtime by the IpUtils::PRIVATE_SUBNETS constant
    trusted_proxies: '127.0.0.1,PRIVATE_SUBNETS'

Как вариант, вместо этого можно объявить переменную окружения прямо в compose.yaml:

services:
  php:
    # ...
    environment:
      # ...
      SYMFONY_TRUSTED_PROXIES: "private_ranges"

Конфигурация Traefik в файле

Далее предлагаю вместо параметров командной строки сделать эквивалентный конфигурационный файл /srv/docker/traefik/config/traefik.yaml:

providers:
  docker:
    exposedByDefault: false

api:
  insecure: true

entryPoints:
  web:
    address: ":80"

Соответственно в compose.yaml убираем параметры командной строки и пробрасываем директорию:

services:
  traefik:
    image: traefik:v3.4
    restart: unless-stopped
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config:/etc/traefik

Заодно перевел сокет докера в режим только для чтения.

Let's Encrypt

Сделаем автоматический https с редиректом. Для этого дополним раздел entryPoints в настройках Traefik и добавим соответствующий обработчик сертификатов:

providers:
  docker:
    exposedByDefault: false

api:
  insecure: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entrypoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /etc/traefik/acme.json
      tlsChallenge: {}

Точка web на 80-м порту перенаправляет весь трафик в websecure с изменением схемы URL на https. В свою очередь, websecure использует Let's Encrypt (ACME) для выпуска сертификатов. По традиции для сертификации предлагается указать свой e-mail, а в качестве «испытания» (challenge) проще всего использовать TLS-ALPN-01: Traefik сам сначала выпустит самоподписанный сертификат, а потом уже получит доверенный. Поскольку мы пробросили директорию /etc/traefik, то данные о сертификатах будут сохранятся на постоянной основе.

Кстати если не хочется сразу же получать «боевые» сертификаты, можно использовать тестовый (staging) сервер:

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      caServer: https://acme-staging-v02.api.letsencrypt.org/directory
      storage: /etc/traefik/acme.json
      tlsChallenge: {}

Разумеется, на такие сертификаты браузер и/или антивирус будут очень сильно ругаться.

Защита API и панели Traefik

Как и в виртуальной машине, сейчас панель доступна всем желающим на порту 8080. Защитим ее базовой HTTP-аутентификацией. Для хэширования пароля алгоритмом BCrypt по идее требуется утилита htpasswd. Альтернативы в виде Python или PHP такие себе – зачем они на хосте (хотя пых вроде как есть в контейнере)? MD5 и SHA-1 мы однозначно говорим «нет»! Так что придется установить…

apt install apache2-utils

В traefik.yaml «обнуляем» настройки API:

api: {}

Генерируем хэш пароля и сразу сохраняем результат в файл:

htpasswd -cbBC 10 /srv/docker/traefik/config/usersfile admin "P@ssw0rd"

Где:

  • -c – создать новый файл;
  • -b – пароль будет указан в командной строке;
  • -B – хэширование алгоритмом BCrypt;
  • -C – время работы (computing time) BCrypt. Увеличил значение с 5 (по умолчанию) до 10.

Прописываем метки в compose.yaml (почему-то через traefik.yaml не заработало, скорее всего потому что указан только Докер как провайдер):

services:
  traefik:
    image: traefik:v3.4
    restart: unless-stopped
    network_mode: host
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.middlewares=apiauth"
      - "traefik.http.middlewares.apiauth.basicauth.usersfile=/etc/traefik/usersfile"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config:/etc/traefik

В настройках базовой аутентификации как раз указываем сформированный файл соответствия пользователей и паролей. Также хочу обратить ваше внимание на принудительное указание внутреннего сервиса Traefik'а в настройках роутера.

Root или не root

Вот в чем вопрос, как говорится. Сейчас, очевидно, Traefik работает под root'ом, и это наверное не очень хорошо. К сожалению, все дело упирается в доступ к сокету Докера. Есть в немалой степени специально для этого написанный wollomatic/socket-proxy, таким образом сокет пробрасывается в контейнер прокси, а Трафик опосредованно обращается к Докеру по TCP.

Позволю себе выказать некоторый скепсис по поводу такой системы, хотя должен признать, что в изоляции сетей, а их предлагается создать две – «сервисную» Трафика и внутреннюю для прокси, определенный смысл есть. К тому же в самой документации предлагается именно изолировать сокет Докера через прокси, правда другой – tecnativa/docker-socket-proxy, заменой которому и является рассматриваемый.

На хосте создадим пользователя traefik (в зависимости от того, предполагается ли интерактивная работа под ним и какая, скорректируйте команду создания):

useradd -c 'Traefik user' -M -s /usr/sbin/nologin traefik

Возможно это не очень красиво, но чтобы не создавать локальных групп и пользователей в контейнерах, мы можем ограничиться директивами user в compose.yaml, который обретет примерно такой вид:

services:
  dockerproxy:
    image: wollomatic/socket-proxy:1
    restart: unless-stopped
    user: "65534:${DOCKERGID:-994}"
    mem_limit: 64M
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    command:
      - '-loglevel=error'
      - '-listenip=0.0.0.0'
      - '-allowfrom=traefik' # allow only hostname "traefik" to connect
      - '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'
      - '-allowbindmountfrom=/var/log,/tmp' # restrict bind mounts to specific directories
      - '-watchdoginterval=3600' # check once per hour for socket availability
      - '-stoponwatchdog' # halt program on error and let compose restart it
      - '-shutdowngracetime=5' # wait 5 seconds before shutting down
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - docker-proxynet    # NEVER EVER expose this to the public internet!
                           # this is a private network only for traefik and socket-proxy
  traefik:
    image: traefik:v3.4
    restart: unless-stopped
    read_only: true
    depends_on:
      - dockerproxy
    security_opt:
      - no-new-privileges:true
    ports:
      - "80:10080"  # use high ports inside the container so
      - "443:10443" # we don't need to be root to bind the ports
    networks:
      - default
      - docker-proxynet
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.middlewares=apiauth"
      - "traefik.http.middlewares.apiauth.basicauth.usersfile=/etc/traefik/usersfile"
    volumes:
      - ./config/traefik.yaml:/etc/traefik/traefik.yaml:ro    # static configuration
      - ./config/usersfile:/etc/traefik/usersfile:ro          # dashboard basic auth
      - ./config/acme.json:/etc/traefik/acme.json             # certificate storage
    user: "${TRAEFIKUID:-1000}"

networks:
  default:
    name: traefik
  docker-proxynet:
    internal: true

Добавился сервис dockerproxy, различные защиты, две сети, как мы говорили выше, и т.д. Обратите внимание, что файлы конфигурации пробрасываем по одному в режиме «только чтение», за исключением хранилища сертификатов. И вновь перерабатываем traefik.yaml – явно прописываем доступ к Докеру по TCP и используемую сеть, а также повышаем номера портов:

providers:
  docker:
    exposedByDefault: false
    endpoint: 'tcp://dockerproxy:2375'
    network: traefik

api: {}

entryPoints:
  web:
    address: ":10080" # will be routed to port 80
    http:
      redirections:
        entrypoint:
          to: ":443"
          scheme: https
  websecure:
    address: ":10443" # will be routed to port 443
    http:
      tls:
        certResolver: letsencrypt
    http3:
      advertisedPort: 443

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /etc/traefik/acme.json
      tlsChallenge: {}

Если файла acme.json еще не существует, создадим его, чтобы при запуске приложения не возникла директория, и назначим права 600 (это требование Traefik):

touch /srv/docker/traefik/config/acme.json
chmod 600 /srv/docker/traefik/config/acme.json

Рекурсивно изменим владельца директории конфигурации:

chown -R traefik:traefik /srv/docker/traefik/config

Traefik можно было бы запустить, однако есть важный нюанс в виде реальных UID и GID. Особенно непредсказуемой в этом плане является группа docker. Проще, конечно, если что подправить значения по умолчанию в compose.yaml или создать файл .env, но все же:

TRAEFIKUID="$(id -u traefik)" DOCKERGID="$(getent group docker | cut -d: -f3)" docker compose up -d

Теперь необходимо подключить демо-приложение к «сервисной» сети – /srv/docker/demo/compose.yaml:

services:
  php:
    build: .
    restart: unless-stopped
    labels:
      - traefik.enable=true
      - traefik.http.routers.demo.rule=Host(`demo.example.com`)
      - traefik.http.routers.demo.entrypoints=websecure
    environment:
      CADDY_GLOBAL_OPTIONS: "auto_https off"
      SERVER_NAME: "http://"
	  SYMFONY_TRUSTED_PROXIES: "private_ranges"
    volumes:
      - ./app:/app
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

networks:
  default:
    name: traefik
    external: true

Наконец, можно сделать docker compose up -d и тут.

Заключение

Скорее всего у вас все это время вертелся в голове вопрос – зачем ставить Traefik перед Caddy (или FrankenPHP в данном случае), если у них очень много общего? Поддержка современных протоколов HTTP/2 и HTTP/3, автоматический HTTPS, даже балансировщик вообще говоря у Caddy тоже есть (хотя схема с метками, подобная Traefik, реализуется через некий плагин, на первый взгляд очень неочевидно в случае реплик сервисов). Причем Traefik даже хуже как бы, поскольку не умеет общаться с PHP.

Действительно, во многих случаях можно использовать Caddy с «ручным» обнаружением сервисов на основе хостов. Преимущества Traefik проявляются в случае достаточно сложных конфигураций с масштабированием и/или множества приложений (файлов compose.yaml) – достаточно добавить несколько меток, и ваш сервис опубликован.

Разумеется вместо Caddy в качестве веб-сервера для PHP можно использовать старый добрый Apache (особенно для Drupal фактически безальтернативно) или тоже старый, но злой Nginx, а для разработки вообще собственные сервера фреймворков Symfony (который как раз мог бы обслуживать демо-приложениие, но я решил представить вам FrankenPHP) или Laravel. Однако я хотел бы анонсировать вариант с Lighttpd – оказывается, есть и такая весьма интересная альтернатива всему вышеперечисленному. На мой взгляд, этот веб-сервер совершенно незаслуженно находится как бы в тени гигантов, а в качестве бэка для Трафика подходит идеально.


Категория: Программирование, веб | Опубликовано 01.08.2025 | Редакция от 03.08.2025

Похожие материалы

Перенос GitLab на другой сервер в Docker

Примерно год я мучался с GitLab на сервере с двумя гигабайтами оперативки. Когда оплаченный период закончился, решил взять более мощный VDS по формуле 4/4/30. До этого сам GitLab был установлен непосредственно из репозитория, но для экспериментов с Pages и т.д. нужен Docker. А раз он и так есть, почему бы не завернуть GitLab в контейнер? Заодно на сервер можно будет установить что-нибудь еще.


Комментарии, обсуждение