Попросил chatgpt проанализировать мой php.dockerfile и дать советы по улучшению.
В текущем php.dockerfile есть одна ошибка сборки и несколько проблем с правами, размером и воспроизводимостью.
В строках 41–44 отсутствуют &&:
&& chown ... /app/var && chmod ... /app/var \
mkdir -p /data/private
Shell передаст mkdir как аргумент команде chmod. Исправленный вариант:
RUN install -d -o "${UID}" -g "${GID}" -m 0775 \
/app/var/cache \
/app/var/log \
&& install -d -o "${UID}" -g "${GID}" -m 0770 \
/data/private \
/data/public
install -d одновременно создаёт каталог и назначает владельца/режим, поэтому лучше цепочки mkdir + chown + chmod.
После подключения:
- ./data/private:/data/private
- ./data/public:/data/public
владелец каталогов из образа скрывается владельцем каталогов на хосте. Поэтому chown при сборке гарантирует права только:
На хосте каталоги нужно создать под тем же UID/GID:
mkdir -p data/private data/public
chmod 0770 data/private data/public
Если PHP работает с UID/GID текущего разработчика, дополнительный chown не потребуется. Например:
LOCAL_UID="$(id -u)" LOCAL_GID="$(id -g)" docker compose build php
В Compose:
services:
php:
build:
context: .
dockerfile: php.dockerfile
target: dev
args:
UID: ${LOCAL_UID:-1000}
GID: ${LOCAL_GID:-1000}
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
volumes:
- ./data/private:/data/private
- ./data/public:/data/public
Для production с фиксированным UID, например 33:33, каталоги должен заранее подготовить деплой:
install -d -o 33 -g 33 -m 0770 /путь/data/private /путь/data/public
Это действие выполняется на хосте при развёртывании, а не контейнером при каждом старте. PHP-контейнер при этом остаётся полностью non-root.
Если /data/public читает отдельный nginx-контейнер, nginx должен состоять в общей группе либо каталог должен иметь подходящие групповые права и setgid-бит:
chmod 2770 data/public
Сейчас ARG UID/GID меняет только:
USER.Он не создаёт пользователя с таким UID и не изменяет www-data. При значениях, отличных от 33:33, процесс получает только числовую учётную запись, а /etc/passwd и конфигурация пула FPM продолжают ссылаться на www-data.
Возможны два подхода.
www-dataЕсли везде используется Debian UID/GID 33:33, лучше убрать настраиваемость и явно написать:
RUN install -d -o www-data -g www-data -m 0775 /app/var/cache /app/var/log \
&& install -d -o www-data -g www-data -m 0770 /data/private /data/public
USER www-data:www-data
Это наиболее предсказуемый production-вариант.
Для локальной разработки изменяйте UID/GID существующего www-data, а не запускайте процесс под безымянным числовым пользователем:
ARG UID=1000
ARG GID=1000
RUN groupmod --gid "${GID}" www-data \
&& usermod --uid "${UID}" --gid "${GID}" www-data
USER www-data:www-data
Нужно учитывать возможное совпадение заданных идентификаторов с другими пользователями базового образа.
После установки в конечном образе:
USER www-data:www-data
обычная команда docker compose exec автоматически использует пользователя образа:
docker compose exec php php bin/console cache:clear --env=prod --no-debug
docker compose exec php php bin/console cache:warmup --env=prod --no-debug
docker compose exec php composer install
Для явной гарантии:
docker compose exec --user www-data:www-data php \
php bin/console cache:warmup --env=prod --no-debug
Одноразовая команда:
docker compose run --rm --no-deps --user www-data:www-data php \
php bin/console doctrine:migrations:migrate --no-interaction
Проверка:
docker compose exec php id
docker compose exec php php -r 'printf("%s:%s\n", posix_geteuid(), posix_getegid());'
Не следует использовать:
docker compose exec --user root php ...
иначе Composer или Symfony создадут в vendor, var/cache и var/log файлы, недоступные PHP-FPM.
Установка через PECL в целом правильная, но:
pecl install --onlyreqdeps --force redis
всегда берёт текущую версию. Это делает сборку невоспроизводимой. Версию лучше зафиксировать:
ARG PHP_REDIS_VERSION=<проверенная-версия>
RUN pecl install "redis-${PHP_REDIS_VERSION}" \
&& docker-php-ext-enable redis \
&& rm -rf /tmp/pear
После сборки стоит выполнить:
docker run --rm your-image php --ri redis
Если composer.json использует Redis напрямую, добавьте платформенное требование:
{
"require": {
"ext-redis": "*"
}
}
PECL redis — это PHP-расширение. Оно не заменяет Composer-пакеты вроде symfony/cache, если приложение использует их API.
Сейчас системные пакеты устанавливаются в пяти слоях, а списки APT вообще не удаляются. Кроме размера, apt-get install git без собственного apt-get update зависит от закешированного списка предыдущего слоя.
Минимально:
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libicu-dev \
libzip-dev \
libonig-dev \
unzip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" \
gd \
intl \
mbstring \
opcache \
pdo_mysql \
zip \
&& rm -rf /var/lib/apt/lists/*
Дополнительно:
pdo обычно уже присутствует в официальном PHP-образе — перед удалением из сборки проверить через php -m;iconv;--no-install-recommends;git устанавливать только в dev;unzip, компиляторы и *-dev не должны оставаться в production runtime.Практичный вариант автоматической установки и очистки зависимостей — install-php-extensions, но внешний installer-образ необходимо фиксировать по версии или digest:
COPY --from=mlocati/php-extension-installer:<фиксированная-версия> \
/usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions \
gd intl mbstring opcache pdo_mysql redis zip
vendor внутри Composer-стадииСейчас vendor копируется с хоста. Это создаёт риски:
Лучше:
FROM composer:2 AS vendor-prod
WORKDIR /app
COPY composer.json composer.lock symfony.lock* ./
RUN composer install \
--no-dev \
--prefer-dist \
--no-interaction \
--no-progress \
--no-scripts
После копирования исходников можно запустить нужные Composer scripts и прогрев кеша под прикладным пользователем. В финальный образ:
COPY --from=vendor-prod --chown=www-data:www-data /app/vendor /app/vendor
Для кеширования BuildKit ещё лучше:
RUN --mount=type=cache,target=/tmp/composer-cache \
COMPOSER_CACHE_DIR=/tmp/composer-cache \
composer install ...
COPY --chownВместо копирования файлов от root с последующим рекурсивным chown:
COPY --chown=www-data:www-data ./src /app/src
COPY --chown=www-data:www-data ./config /app/config
Исходный код можно оставить только для чтения, а права записи дать исключительно:
/app/var/cache;/app/var/log;/data/private;/data/public..env в productionСейчас .env попадает внутрь образа. Если там находятся пароли или токены, они останутся в слоях образа. Production-настройки лучше передавать через:
VOLUME из DockerfileДирективы:
VOLUME ["/app", ...]
не гарантируют права и могут создавать неожиданные анонимные volumes. Особенно нежелателен VOLUME ["/app"], который способен скрыть содержимое приложения.
Лучше описывать все mounts в Compose/Kubernetes. В Dockerfile оставить только создание каталогов.
chmod 550 -R /usr/local/etc/php
назначает исполняемый бит файлам .ini. Это не требуется. Подходящий режим:
RUN find /usr/local/etc/php -type d -exec chmod 0755 {} + \
&& find /usr/local/etc/php -type f -exec chmod 0444 {} +
Либо вообще оставить стандартные права после COPY.
Оптимальная схема:
php-runtime — только PHP, runtime-библиотеки и расширения.composer-prod — установка production-зависимостей.dev — Composer, Git, отладочные настройки и bind-mount /app.prod — исходники и production vendor, без Git/Composer/compiler packages.prod переключение на USER www-data:www-data.www-data, если он не требует production-секретов или доступной БД.Также нужен .dockerignore, исключающий как минимум:
.git
data/
var/cache/
var/log/
node_modules/
.env.local
.env.*.local
vendor/
Главное ограничение: гарантировать запись в подключённые /data/* только средствами Dockerfile невозможно. Гарантия появляется из сочетания одинакового UID/GID, заранее подготовленных каталогов/volumes и запуска PHP-FPM, Composer и Symfony Console под одним non-root пользователем.
Не, неинтерпретировал, это в контекста chatGPT это была ссылка на файл