При всех своих достоинствах Traefik не умеет работать с FPM, следовательно, для работы с приложением на PHP ему требуется веб-сервер. Считается, что Apache не слишком хорош при обслуживании статики, поэтому раньше была (а может и до сих пор) популярна его связка с Nginx. С последним я не рекомендую связываться в связи с появлением на рынке Caddy/FrankenPHP, но ставить их позади Traefik своего рода масло масляное. В поисках компромисса я открыл для себя (а теперь делаю это и для вас) Lighttpd - оказывается, наряду с большой двойкой давно живет и здравствует мощный, быстрый и при этом легковесный веб-сервер.

Прежде чем мы начнем, наверное стоит обговорить, что не так в данном контексте с Nginx и FrankenPHP. Первый мне никогда не нравился своей крайне сложной настройкой, казалось бы, простых вещей: перенаправление вызовов на index.php для обработки fpm’ом занимает строк этак 100 – 500. Шучу? На самом деле конфигурация сервера для фреймворка где-то в 30 укладывается, если комментарии почистить, но все равно. Caddy в этом плане просто работает на 5-10 строках конфига, поэтому Nginx у меня, скажу по секрету, остался только для GitLab, и то он там настроен умными людьми как реверс-прокси (один из трех, получается).

Благодаря же FrankenPHP вы якобы получаете непревзойденную производительность, но работать по HTTP/3 с автоматическими сертификатами наверное будет только один из них (причем скорее всего Traefik, а ведь это одна из главных фишек Caddy тоже). Если не нужно обнаружение сервисов и/или масштабирование, то по мне проще сразу выставить Доктора наружу.

Lighttpd же сочетает в себе компактность, скорость и простоту настройки, что идеально подходит для работы в паре с Traefik.

Traefik

В рамках статьи я буду работать в виртуальной машине – это старая знакомая Arch Linux с Docker. Traefik в ней бессовестным образом работает на сети хоста под root, чтобы не переусложнять настройку.

compose.yaml:

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

На «продуктовом» складе сервере так делать не стоит, пример изоляции сокета Докера и сетей как ни странно есть в вышеупомянутой статье про Лабораторию.

config/traefik.yaml:

providers:
  docker:
    exposedByDefault: false

api:
  insecure: true

entryPoints:
  web:
    address: ":80"

Для разработки – самое оно, но на боевом сервере API будет отключен или защищен и как минимум будет настроен HTTPS.

jitesoft/lighttpd

Официального образа похоже не существует, так что jitesoft/lighttpd пожалуй один из наиболее адекватный из готовых. По крайней мере из тех, что ищутся довольно быстро и при этом поддерживаются в актуальном состоянии и имеют хорошую документацию. За исходным кодом тоже далеко ходить не надо, все ссылки опубликованы. Образ делается на базе Alpine Linux, поэтому он еще и весьма компактный. За производительность (типа musl и все такое), наверное, не стоит переживать, поскольку образы Traefik и Caddy точно так же основаны на Alpine.

Итак, в очередной раз мы будем разворачивать нашего подопытного кролика, то есть демо-приложение Symfony. Для определенности я буду работать в каталоге /srv/docker/jitesoft. Первоначальный compose.yaml может выглядеть примерно так:

services:
  fpm:
    image: php:8.4-fpm
    restart: always
    volumes:
      - ./app:/var/www/html
  web:
    image: jitesoft/lighttpd:cgi
    restart: always
    depends_on:
      - fpm
    environment:
      SERVER_ROOT: /var/www/html/public
    expose:
      - '80'
    labels:
      - traefik.enable=true
      - traefik.http.routers.jitesoft.rule=Host(`jitesoft.lighttpd.local`)
    volumes:
      - ./app/public:/var/www/html/public

PHP я решил взять Дебиановский, тогда как Lighttpd – Альпиновский. Когда-то давно на Альпиновском PHP демка не разворачивалась из-за зависимого от платформы Dart Sass, однако похоже что туда или добавили поддержку musl, или еще как-то починили, так что можно было бы PHP тоже взять 8.4-fpm-alpine. Однако стандартный вариант вроде как шустрее и проблем в нем меньше (например с intl), но за счет размера. С другой стороны получается, что веб-сервер и PHP будут работать под разными www-data: 82 и 33 соответственно. Для демонстрации это не принципиально (по идее веб-серверу достаточно доступа на чтение), но в реальности возможно стоило бы привести их к общему знаменателю.

Еще из нюансов – сервис PHP я сразу назвал fpm, чтобы не переопределять CGI_HOST для Lighttpd (кстати надо брать именно cgi-образ, в нем преднастроен mod_fastcgi для работы с php-fpm), а еще надо принудительно открыть 80-й порт (в контейнере изначально вроде нет открытых портов, что немного странно). Переопределяем SERVER_ROOT, в случае демо-приложения (и вообще Symfony по умолчанию) это подкаталог public. Кстати в контейнер веб-сервера я пробросил только ее – если в ней нет символических ссылок на вышестоящее содержимое (а их по идее и не должно быть), то этого достаточно.

«Сайт» я назвал jitesoft.lighttpd.local, его надо будет сопоставить с IP виртуалки на хосте. Например в Windows 10 это файл C:\Windows\System32\drivers\etc\hosts (редактировать с правами администратора):

192.168.56.103 jitesoft.lighttpd.local

Чтобы развернуть приложение, нужно немного похимичить. Запускаем контейнер PHP:

docker compose run -it --rm fpm bash

Флаг --rm говорит нам об удалении контейнера после выхода. В контейнере устанавливаем unzip, composer (надеюсь, curl-метод у вас отработает, у меня к сожалению иногда нет доступа) и саму демку, после чего меняем владельца на www-data (опять же подчеркну, внутри контейнера):

apt-get update
apt-get install unzip
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
cd /var/www
composer create-project symfony/symfony-demo html
cd html
php bin/console asset-map:compile
chown -R www-data:www-data /var/www/html
exit

Поднимаем приложение:

docker compose up -d

И заходим строго на http://jitesoft.lighttpd.local/index.php/ru (имеется в виду с index.php в URL) поскольку у нас не настроена перезапись (rewrite).

symfony_demo.png

До боли знакомая картина, отличающаяся лишь адресом и версией Symfony (сейчас у нас 7.3.2).

Настроим эту самую перезапись. Создадим файл, например, lighttpd/001-rewrite.conf (расширение .conf обязательно, имя с учетом сортировки по алфавиту):

# index.php expects original URL in PATH_INFO
url.rewrite-if-not-file = ( "" => "/index.php${url.path}${qsa}" )

Модуль дополнительно включать не надо, поскольку он и так указан в базовых настройках (/etc/lighttpd/000-default.conf контейнера). Кстати названия модулей очень напоминают Apache, но этим наверное сходство и заканчивается – пусть подсветка синтаксиса не вводит вас в заблуждение.

Гасим приложение – docker compose down, в compose.yaml добавляем проброс директории:

services:
  # ...
  web:
    # ...
    volumes:
      - ./app/public:/var/www/html/public
      - ./lighttpd:/usr/local/lighttpd.d

И запускаем обратно. Теперь можно открыть просто http://jitesoft.lighttpd.local/, но и URL’ы с index.php продолжат работать. Убрать их проще всего (а может быть это и вовсе единственный способ) на уровне Traefik. Добавим метки:

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

И перезапустим приложение.

Итого compose.yaml после доработок:

services:
  fpm:
    image: php:8.4-fpm
    restart: always
    volumes:
      - ./app:/var/www/html
  web:
    image: jitesoft/lighttpd:cgi
    restart: always
    depends_on:
      - fpm
    environment:
      SERVER_ROOT: /var/www/html/public
    expose:
      - '80'
    labels:
      - traefik.enable=true
      - traefik.http.routers.jitesoft.rule=Host(`jitesoft.lighttpd.local`)
      - traefik.http.routers.jitesoft.middlewares=rmindexphp
      - traefik.http.middlewares.rmindexphp.redirectregex.regex=/index\.php(?:/(.*)|$)
      - traefik.http.middlewares.rmindexphp.redirectregex.replacement=/$${1}
    volumes:
      - ./app/public:/var/www/html/public
      - ./lighttpd:/usr/local/lighttpd.d

В принципе все замечательно работает и на этом можно было бы завершить статью, но так просто вы от меня не отделаетесь! acute

Если серьезно, то мне не совсем нравится как бы лишний контейнер, например FrankenPHP самодостаточен. В случае единого контейнера мы могли бы взаимодействовать с PHP-FPM через сокет, а ориентируясь на использование позади прокси – отключить журналы доступа. Еще мне не совсем нравится HEALTHCHECK – там происходит head-запрос к главной. Способ вполне рабочий (и конечно же отключаемый), но недостаточно гибкий, да и есть опасения, что он может вызывать лишнюю нагрузку. Так что сейчас будем изобретать велосипед, а именно…

Мой вариант на базе fpm

Для начала необходимо решить проблему запуска нескольких программ в контейнере. В общем случае для этого используется система инициализации типа Supervisor, как предложено в документации Docker, или намного более компактный s6-overlay, которым воспользовались к примеру serversideup/php для разработки своих образов Apahce и Nginx. Если что, рекомендую к ним присмотреться, многое оттуда почерпнул для себя.

Однако в нашем конкретном случае мы можем воспользоваться умением Lighttpd самостоятельно запускать процесс FastCGI благодаря параметру bin-path. Ограничение (или наоборот, преимущество) в том, что придется сразу создавать rootless-контейнер, иными словами принудительно устанавливать пользователя www-data для запуска.

Мы же не хотим запускать веб-сервер под root? Дело в том, что если контейнер запускается под ним, а www-data прописан в настройках Лайти, то php-fpm не стартует по причине недоступности stderr. Так что либо запускаем Lighttpd под root и тогда мастер-процесс нормально запустится под ним же, либо изначально работаем под непривилегированным пользователем.

Итак, потихоньку начинаем формировать Dockerfile на базе Дебиановского официального образа.

Загрузка исходников

Схема отработана на примере создания образа FrankenPHP для FreeBSD, правда в отличие от статьи я вынес хэш в .env:

PHP_VERSION=8.4.13
LIGHTTPD_VERSION=1.4.82
LIGHTTPD_SHA256=abfe74391f9cbd66ab154ea07e64f194dbe7e906ef4ed47eb3b0f3b46246c962

Собирать образ предлагаю с помощью docker compose build, поэтому параллельно набрасываем compose.yaml:

services:
  lighttpd:
    build:
      context: .
      args:
        - PHP_VERSION=${PHP_VERSION}
        - LIGHTTPD_VERSION=${LIGHTTPD_VERSION}
        - LIGHTTPD_SHA256=${LIGHTTPD_SHA256}

Начало Dockerfile:

ARG PHP_VERSION=8
FROM php:${PHP_VERSION}-fpm-trixie

# Fetch sources
ARG LIGHTTPD_VERSION LIGHTTPD_SHA256
RUN set -eux; \
    cd /usr/src; \
    curl -s -o lighttpd.tar.xz https://download.lighttpd.net/lighttpd/releases-1.4.x/lighttpd-${LIGHTTPD_VERSION}.tar.xz; \
    echo "$LIGHTTPD_SHA256 lighttpd.tar.xz" | sha256sum -c -

Компиляция

В архиве с исходниками есть файл install, в котором описывается порядок сборки. Дело немного осложняется тем, что предварительно нужно установить несколько пакетов (а затем их удалить, чтобы не засорять образ). Кстати компиляция для образа от Jitesoft происходит где-то за кадром (на самом деле в рамках CI/CD), а хочется это видеть прямо в Dockerfile.

В официальном образе PHP для временной установки и последующего удаления пакетов применяется паттерн с apt-mark. Не совсем понятно, от какого Linux перечислены пакеты в install, поэтому под Debian Trixie пришлось их адаптировать. Пишу пока в виде shell-скрипта:

savedAptMark="$(apt-mark showmanual)"
apt-get update
apt-get install -y --no-install-recommends \
    automake \
    libtool \
    libpcre2-dev \
    zlib1g-dev
# работаем работу...
apt-mark auto '.*' > /dev/null
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false
apt-get dist-clean

Компиляция по умолчанию достаточно простая (в директории с распакованными исходниками):

./autogen.sh
./configure -C
make check
make install

Исходя из принципа наименьшего удивления, я думаю что добавлять какие-то нестандартные параметры в ./configure не стоит (хотя вот шведы добавили --with-lua --with-openssl --with-ldap --with-brotli). По аналогии с компиляцией PHP мы еще добавим strip и make clean, а также традиционный дымовой тест. Прежде чем удалять исходники, возьмем образцово-показательную настройку mime-файлов, она там явно более обширная, чем у Jitesoft. В идеале бы скопировать все конфиги, но как править их потом sed’ом, боюсь даже представлять.

Стоит заметить, что исполняемые файлы make install кладет в /usr/local/sbin. После FreeBSD это кажется немного неправильным, но так как php-fpm оказался там же, куда-то перемещать их не будем.

Собираем все вышесказанное в Dockerfile, получается изрядная простыня:

# Compile
RUN set -eux; \
    # Install build tools
    savedAptMark="$(apt-mark showmanual)"; \
    apt-get update; \
    apt-get install -y --no-install-recommends \
        automake \
        libtool \
        libpcre2-dev \
        zlib1g-dev; \
    # Extract and compile
    tar xJf /usr/src/lighttpd.tar.xz -C /usr/src; \
    cd /usr/src/lighttpd-${LIGHTTPD_VERSION}; \
    ./autogen.sh; \
    ./configure -C; \
    make check; \
    make install; \
    find /usr/local/lib \
        -type f \
        -name '*.so' \
        -exec sh -euxc ' \
            strip --strip-all "$@" || : \
        ' -- '{}' + \
    ; \
    strip --strip-all /usr/local/sbin/lighttpd; \
    strip --strip-all /usr/local/sbin/lighttpd-angel; \
    # smoke test
    lighttpd -v; \
    # pre-configure
    mkdir -p /etc/lighttpd/conf.d /usr/local/etc/lighttpd/conf.d; \
    cp doc/config/conf.d/mime.conf /etc/lighttpd/conf.d/001-mime.conf; \
    # cleanup
    make clean; \
    cd /; \
    rm -rf /usr/src/lighttpd-${LIGHTTPD_VERSION}; \
    # reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
    apt-mark auto '.*' > /dev/null; \
    [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \
    apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
    apt-get dist-clean

Предлагаю локальную директорию конфигурации назвать более длинно по сравнению с Jitesoft – /usr/local/etc/lighttpd/conf.d, зато более единообразно с тем же PHP, да и FreeBSD опять же напоминает.

Базовая конфигурация

В официальном образе PHP конфигурационные файлы зачастую создаются сочетанием echo и tee, но на мой взгляд лучше подготовить файлы заранее и поместить их в образ директивой COPY. Начнем с lighttpd.conf, в котором по аналогии с Jitesoft просто импортируем конфигурационные файлы из созданных директорий:

include "/etc/lighttpd/conf.d/*.conf"
include "/usr/local/etc/lighttpd/conf.d/*.conf"

Далее возникает вопрос, насколько мелко надо нарезать файлы конфигурации. Можно было бы каждый модуль подключать и настраивать отдельно, но наверное проще и нагляднее все же вновь сделать по аналогии с Jitesoft в виде conf.d/000-default.conf. Однако само содержимое у меня будет отличаться:

##  Modules to load
## -----------------
##
## Load only the modules needed in order to keep things simple.
##
## NOTE: The order of modules in server.modules is important.
server.modules += ( "mod_access", "mod_accesslog", "mod_auth", "mod_rewrite" )

##  Basic Configuration
## ---------------------
server.port = env.LIGHTTPD_PORT
server.document-root = env.LIGHTTPD_DOCUMENT_ROOT

## The value for the "Server:" response field.
## Disable for security reasons.
server.tag = ""

##  Filename/File handling
## ------------------------

## files to check for if .../ is requested
index-file.names += ( "index.xhtml", "index.html", "index.htm", "default.htm", "index.php" )

## deny access the file-extensions
##
## ~    is for backupfiles from vi, emacs, joe, ...
## .inc is often used for code includes which should in general not be part
##      of the document-root
url.access-deny = ( "~", ".inc" )

## which extensions should not be handled via static-file transfer
##
## .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi
static-file.exclude-extensions = ( ".php", ".pl", ".cgi", ".fcgi", ".scgi" )

Я добавил комментарии из doc/config/lighttpd.annotated.conf, как и некоторые значения по умолчанию. Несмотря на то, что я вроде как не собирался использовать логи доступа, модуль подключил на случай, если вдруг они все же кому-то потребуются. Тогда можно будет подкинуть отдельный конфигурационный файл, при этом сам модуль будет уже загружен с правильным приоритетом.

Подключаем conf.d/500-fastcgi.conf:

##  FastCGI Module
## ---------------
##
## https://wiki.lighttpd.net/mod_fastcgi
##
server.modules += ( "mod_fastcgi" )

## PHP
##
## If you want to use PATH_INFO and PHP_SELF in you PHP scripts you have to
## configure php and lighttpd. The php.ini needs the option:
##
## cgi.fix_pathinfo = 1 
##
## and the option "broken-scriptfilename" in your fastcgi.server config.
##
## Why this? The "cgi.fix_pathinfo = 0" would give you a working PATH_INFO but
## no PHP_SELF. If you enable it, it turns around. To fix the
## PATH_INFO `--enable-discard-path` needs a SCRIPT_FILENAME which is against the
## CGI spec, a broken-scriptfilename. With "cgi.fix_pathinfo = 1" in php.ini
## and "broken-scriptfilename => "enable"" you get both.
##
fastcgi.server = ( ".php" =>
    ((
        "socket" => "/tmp/www.sock",
        "broken-scriptfilename" => "enable",
        "bin-path" => "/usr/local/sbin/php-fpm",
        "max-procs" => 1,
        "kill-signal" => 3
    ))
)

В отличие от какой-то древней уязвимости Nginx, менять значение cgi.fix_pathinfo (1 по умолчанию) не надо.

Как я анонсировал ранее, будем работать через сокет. Выбор директории tmp для него обусловлен правами доступа – в run, например, под обычным пользователем не попасть. Да и вообще во FreeBSD там сокет PostgreSQL создается. crazy Ограничиваем количество мастер-процессов php-fpm до одного, иначе Lighttpd запустит 4 (по умолчанию).

Для «плавного» завершения пыхапэ стоп-сигнал должен быть SIGQUIT, или 3 в числовом выражении. Однако меня все же терзают смутные сомнения – а не надо ли было все-таки оставить SIGTERM (по умолчанию). Есть подозрение, что контейнер останавливается прежде чем отработает это самое завершение (в логах или на экране не вижу прощальной строки).

Возвращаемся к Dockerfile. В него нужно добавить копирование созданных файлов, подправить настройки PHP-FPM и объявить переменные окружения, используемые в конфигурации.

# Configure
COPY lighttpd.conf /etc/lighttpd/
COPY conf.d/*.conf /etc/lighttpd/conf.d/

RUN set -eux; \
    # disable PHP access log
    sed -i -r 's#^access.log = /proc/self/fd/2$#;access.log = /proc/self/fd/2#g' /usr/local/etc/php-fpm.d/docker.conf; \
    # avoid warning about user and group
    sed -i -r 's#^user = www-data$#;user = www-data#g' /usr/local/etc/php-fpm.d/www.conf; \
    sed -i -r 's#^group = www-data$#;group = www-data#g' /usr/local/etc/php-fpm.d/www.conf; \
    # listen socket
    sed -i -r 's#^listen = 9000$#listen = /tmp/www.sock-0#g' /usr/local/etc/php-fpm.d/zz-docker.conf

ENV LIGHTTPD_PORT=9000
ENV LIGHTTPD_DOCUMENT_ROOT=/var/www/html

STOPSIGNAL SIGINT
USER www-data
CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"]

С сокетом интересная ситуация: Lighttpd подразумевает добавление суффикса к его имени. Скорее всего это индекс процесса (число которых мы ограничивали), но отмечу, что в общем случае может использоваться несколько FastCGI-серверов для балансировки нагрузки.

Я решил занять ранее открытый для PHP-FPM 9000-й порт. Немного странно для веб-сервера, но зато не надо лишний раз «дырявить» контейнер. Стоп-сигнал для самого Lighttpd – SIGINT. Наконец, переключаемся в пользователя www-data и запускаем веб-сервер «явно» (-D).

Проверка

Дополнил compose.yaml:

services:
  lighttpd:
    build:
      context: .
      args:
        - PHP_VERSION=${PHP_VERSION}
        - LIGHTTPD_VERSION=${LIGHTTPD_VERSION}
        - LIGHTTPD_SHA256=${LIGHTTPD_SHA256}
    labels:
      - traefik.enable=true
      - traefik.http.routers.lightrix.rule=Host(`trixie.lighttpd.local`)
    environment:
      LIGHTTPD_DOCUMENT_ROOT: /var/www/html/public
    volumes:
      - ./demo/app:/var/www/html
      - ./demo/config:/usr/local/etc/lighttpd/conf.d

Прописал хост trixie.lighttpd.local для Traefik, переопределил корневую директорию веб-сервера и сопоставил каталоги демо-приложения и конфигурации Lighttpd. В demo/app проще всего скопировать результат развертывания в контейнере Джайтов, а заодно убедиться, что есть доступ пользователю 33. Файл demo/config/rewrite.conf, как и раньше, определяет правило перезаписи URL:

# index.php expects original URL in PATH_INFO
url.rewrite-if-not-file = ( "" => "/index.php${url.path}${qsa}" )

Стартуем, на сей раз, с принудительной сборкой образа:

docker compose up -d --build

И чуть погодя можно заходить. Скорее всего, внешне ничего по сравнению с предыдущим примером не изменилось, что и требовалось доказать.

Установка ПО

Поскольку у нас получился «бесправный» контейнер, что-либо установить в него мы не можем. Разве что локально composer.phar в папку проекта, но толку от него без unzip или git или PHP-расширения zip немного. Установим composer и unzip, а заодно добавим запрет на установку пакетов lighttpd:

# install composer from Composer's official Docker image
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

RUN set -eux; \
    # prevent Debian's Lighttpd packages from being installed
    { \
        echo 'Package: lighttpd*'; \
        echo 'Pin: release *'; \
        echo 'Pin-Priority: -1'; \
    } > /etc/apt/preferences.d/no-debian-lighttpd; \
    # install unzip for composer
    apt-get update; \
    apt-get install -y --no-install-recommends unzip; \
    apt-get dist-clean; \
    # composer working directory
    mkdir /var/www/.composer; \
    chown www-data:www-data /var/www/.composer

Вставил этот фрагмент до раздела конфигурирования.

Проверка состояния

Она же HEALTHCHECK. Ранее я писал, что опрос главной страницы мне не очень понравился, хочется большей гибкости. Допустим, сайт имеет специальную «ручку» (как это стало модно говорить) для проверки состояния. Например, она выполняет запрос к БД SELECT 200; и возвращает HTTP-код 200 если БД это вернула обратно. Или скажем FrankenPHP проверяет статистику веб-сервера как такового.

Чем Lighttpd хуже? Да ничем, подключим модуль статуса – файл conf.d/900-status.conf:

##  Status Module
## ---------------
##
## https://wiki.lighttpd.net/mod_status
##
server.modules += ( "mod_status" )

$HTTP["remoteip"] == "127.0.0.1" {
    status.status-url = "/_webserver-status"
}

$HTTP["url"] =~ "^/_webserver-status" {
    accesslog.filename = ""
}

Разрешаем локально запрашивать состояние сервера по адресу /_webserver-status, при этом исключаем такие URL из логов доступа, если они вдруг используются. Изнутри контейнера можно проверить так:

curl 127.0.0.1:9000/_webserver-status?json

Пример ответа:

{
        "RequestsTotal": 5,
        "TrafficTotal": 1116,
        "Uptime": 95,
        "BusyServers": 1,
        "IdleServers": 1364,
        "RequestAverage5s":0,
        "TrafficAverage5s":0
}

Напишем скрипт healthcheck. В зависимости от URL мы будем парсить статус сервера (впрочем текстовый, а не json) или же проверять определенную конечную точку веб-приложения:

#!/bin/sh
if [ "${HEALTHCHECK_PATH}" = "/_webserver-status?auto" ]; then
    (curl -fks http://127.0.0.1:${LIGHTTPD_PORT}/_webserver-status?auto | grep -P "^Uptime: \d+$") || exit 1
else
    (curl -fksIL http://127.0.0.1:${LIGHTTPD_PORT}${HEALTHCHECK_PATH}) || exit 1
fi

Где curl -fksIL:

  • -f, --fail – быстрый выход при отсутствии ответа или ошибках HTTP
  • -k, --insecure - разрешать небезопасные подключения
  • -s, --silent - «тихий» режим
  • -I, --head - запрос информации о документе, т.е. только заголовков
  • -L, --location - переходить по редиректам

Добавляем фрагмент в Dockerfile (разместил до переключения пользователя):

# Configure health check
ENV HEALTHCHECK_PATH="/_webserver-status?auto"
COPY --chmod=755 healthcheck /usr/local/bin/
HEALTHCHECK --interval=1m --timeout=5s --start-period=30s CMD healthcheck

docker compose up -d --build и проверяем состояние контейнера через docker ps, например:

CONTAINER ID   IMAGE                             COMMAND                  CREATED          STATUS                    PORTS      NAMES
1d8a6568629b   trixie-lighttpd                   "docker-php-entrypoi…"   16 seconds ago   Up 15 seconds (healthy)   9000/tcp   trixie-lighttpd-1

На этом предлагаю остановиться, вроде довольно неплохой контейнер получился.

Использование

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

https://git.dmkos.ru/containers/php/-/tree/main/linux/lighttpd

В зависимости от того, когда вы читаете статью, возможно там уже все сто раз поменялось smile, поэтому вот отсечка на момент публикации:

https://git.dmkos.ru/containers/php/-/tree/8.4.13-lighttpd-1.4.82-trixie/linux/lighttpd

По аналогии с официальными образами я наплодил все возможные комбинации тегов, наиболее востребованным из которых наверное будет 8.4-lighttpd:

dmkos/php:8.4-lighttpd
ghcr.io/dmkos/php:8.4-lighttpd
glcr.dmkos.ru/containers/php:8.4-lighttpd

Хотя вообще-то скоро выйдет новая версия, тогда 8.5-lighttpd.

Переменные окружения

Их немного:

  • LIGHTTPD_PORT – порт веб-сервера, по умолчанию 9000 (вместо php-fpm);
  • LIGHTTPD_DOCUMENT_ROOT – корневой каталог веб-сервера, по умолчанию /var/www/html. Переопределите в зависимости от архитектуры сайта;
  • HEALTHCHECK_PATH – путь (URL) для проверки состояния. Начальный слэш обязателен. Запрашиваемый ресурс должен возвратить HTTP-код 200. По умолчанию опрашивается статус Lighttpd. Рекомендую определить специальную конечную точку сайта, например /up для Laravel.

Конфигурация Lighttpd

У вас есть два три пути:

  1. Примонтировать директорию с локальными конфигами на /usr/local/etc/lighttpd/conf.d, как это делал я выше в примере для правил перезаписи URL. Думаю в большинстве случаев этого будет достаточно.
  2. Прокинуть конкретные файлы в /etc/lighttpd/conf.d, особенно если нужно включить какие-то модули до fastcgi.
  3. Заменить всю директорию /etc/lighttpd/conf.d написанной с нуля конфигурацией. Надеюсь, обращаться к столь радикальному способу не потребуется.

И четвертый в виде комбинации из первых двух. smile

Если пользователи сайта могут отправлять файлы, то помимо rewrite я рекомендую запретить доступ к файлам *.php в директории загрузок, например:

$HTTP["url"] =~ "^/upload" {
    url.access-deny = ( ".php" )
}

Или разрешить только конкретные типы:

$HTTP["url"] =~ "^/upload" {
    url.access-allow = ( ".jpg", ".png" )
}

Включить логи доступа можно так:

accesslog.filename = "/proc/self/fd/2"

Хотя рекомендую все же делать это на уровне прокси. На нем же советую настраивать и сжатие, но при необходимости (например если трафик между серверами проходит через сеть) можно включить старый добрый gzip:

##  Output Compression
## --------------------
##
## https://wiki.lighttpd.net/mod_deflate
##
server.modules += ( "mod_deflate" )

deflate.mimetypes = ( "text/*" )
deflate.allowed-encodings = ( "gzip", "deflate" )

Конфигурация и расширения PHP

Поскольку образ основан на официальном php:fpm и в этом плане подвергся изменениям только в части логов, пользователя и сокета, то и все болячки благополучно унаследовал. Дело осложняется пользователем www-data, придется переключаться. Например:

FROM dmkos/php:8.4-lighttpd

# Switch to configure PHP
USER root

# E.g. install PHP extension
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions \
        intl

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

# Switch back to unprivileged user (required)
USER www-data

Совместно с Traefik

Пример compose.yaml:

services:
  app:
    build:
      context: .
    restart: always
    labels:
      - traefik.enable=true
      - traefik.http.routers.example.rule=Host(`example.com`)
    environment:
      LIGHTTPD_DOCUMENT_ROOT: /var/www/html/public
      SYMFONY_TRUSTED_PROXIES: private_ranges
    volumes:
      - ./config:/usr/local/etc/lighttpd/conf.d
      - ./src:/var/www/html
  proxy:
    image: traefik:v3.5
    restart: always
    # this is very basic configuration, please do not use in production
    command:
      - --api.insecure=true
      - --providers.docker.exposedbydefault=false
    ports:
      - "80:80"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

Получилось немного не согласовано – конфигурация Traefik в лучшем случае для среды разработки, а в PHP выше подкинул «продуктовую», но главное что не наоборот.

Запуск под произвольным пользователем

Если по каким-либо причинам не устраивает www-data, а в моем случае обычно это связано с правами доступа к исходному коду, то можно создать нужного пользователя примерно так:

FROM dmkos/php:8.4-lighttpd

# Switch to create user
USER root

ARG WWWUSER=1000 WWWNAME=lighty
RUN set -eux; \
        useradd -m -c 'World Wide Web Owner' -u $WWWUSER $WWWNAME; \
        chown ${WWWUSER}:${WWWUSER} /var/www/html

# Switch to newly created user
USER $WWWUSER

Заключение

Надеюсь, я смог заинтересовать вас своим открытием – веб-сервером Lighttpd. Согласитесь, его конфигурация куда проще, чем Nginx, а скорость работы вряд ли уступает. В сочетании с Traefik получаем систему, которая в свою очередь элементарно настраивается на работу по HTTP/2 и /3 с автоматической сертификацей, да еще и масштабированием при желании. На всякий случай упомяну, что в документации Лайти описывается настройка Let’s Encrypt, но она уже не столь очевидна, да и поддержки HTTP/3 на данный момент нет. Именно поэтому я предлагаю решать эти вопросы на уровне прокси.

В планах у меня сделать версию контейнера на s6-overlay, чтобы PHP-FPM запускался и останавливался правильно, да и вообще bin-path – «грязный хак», как я это называю. С другой стороны, будут потеряны преимущества обычного пользователя с точки зрения безопасности: систему инициализации запускает root. Впрочем теоретически может и обычный (как у тех же Серверсайдов), но с оговорками.

Постараюсь сделать еще и Альпиновские варианты, возможно они пригодятся для сред разработки с точки зрения экономии места. Хотя по сути Lighttpd с composer’ом добавляют всего где-то 8 мегабайт к Дебиановскому.


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

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

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

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


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