Le problème
Le service order, sous Symfony 7.4, tourne en
développement avec Docker Compose et l'image officielle de
FrankenPHP, comme dans le gabarit symfony-docker. Le code
est monté depuis l'hôte (bind mount), et le conteneur de
développement tourne en root.
Sous Linux, avec Docker en mode root (l'installation par
défaut), chaque fichier créé dans le conteneur appartient
donc à root sur l'hôte. Après
bin/console make:entity Order, l'éditeur ne
peut plus modifier src/Entity/Order.php sans
sudo. Le gabarit propose un
chown -R après la première installation : il
faudrait le relancer après chaque génération.
La solution
Nous ajoutons à l'étape frankenphp_dev un
utilisateur qui porte l'UID et le GID du développeur. Sur un
bind mount, le noyau ne connaît que ces numéros : le nom de
l'utilisateur ne compte pas.
# Dockerfile (service order)
FROM frankenphp_base AS frankenphp_dev
# ... contenu de l'étape dev du gabarit (php.ini, Xdebug) ...
# Identifiants de l'utilisateur de l'hôte, 1000 par défaut.
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN <<EOF
set -eux
# Le GID peut déjà exister dans l'image.
if ! getent group "${GROUP_ID}" > /dev/null; then
groupadd --gid "${GROUP_ID}" app
fi
# L'UID aussi.
if ! getent passwd "${USER_ID}" > /dev/null; then
useradd --uid "${USER_ID}" --gid "${GROUP_ID}" --create-home --shell /bin/bash app
fi
# Écouter sur 80 et 443 sans être root.
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp
# Dossiers où Caddy et Symfony écrivent.
mkdir -p /data/caddy /config/caddy /app/var
chown -R "${USER_ID}:${GROUP_ID}" /data /config /app/var
EOF
USER ${USER_ID}:${GROUP_ID}
Les raisons de ces choix :
-
set -euxarrête le build à la première erreur et affiche chaque commande. Le gabarit l'impose déjà avecSHELL, mais le bloc reste sûr s'il est copié ailleurs. -
getentévite ungroupaddou unuseraddqui échouerait sur un identifiant pris. Sur macOS,id -grenvoie en général 20, le GID du groupe Debiandialout. L'étape dev du gabarit crée aussi un utilisateurnonroot, qui reçoit l'UID et le GID 1000. -
USERreçoit les numéros, pas le nomapp: si l'UID existait déjà,appn'a pas été créé. -
setcapdonne au binaire la capacitéCAP_NET_BIND_SERVICE, qui autorise l'écoute sur les ports inférieurs à 1024. L'image officielle la pose déjà, aveclibcap2-binqui fournitsetcap. Docker Engine ouvre aussi ces ports aux utilisateurs non root, mais pas tous les moteurs : nous la réappliquons, comme la documentation de FrankenPHP. -
/dataet/configreçoivent les certificats et la configuration de Caddy. Le gabarit place/app/vardans un volume anonyme, que Docker remplit à partir du dossier de l'image. Sansmkdir -p, ce dossier n'existe pas dans l'image et le volume appartient à root.
Le compose.override.yaml cible cette étape et
lui passe les identifiants :
# compose.override.yaml
services:
php:
# ... reste du service du gabarit ...
build:
context: .
target: frankenphp_dev
args:
USER_ID: ${USER_ID:-1000}
GROUP_ID: ${GROUP_ID:-1000}
volumes:
- ./:/app
- /app/var
Sur un projet existant, ce qui a été créé en root le reste :
Docker ne remplit qu'un volume vide.
sudo chown -R "$(id -u):$(id -g)" . rend les
fichiers, et docker compose up -V recrée les
volumes anonymes.
docker compose down -v supprime aussi
caddy_data et caddy_config, donc
les certificats locaux de Caddy.
Le piège
Le premier réflexe est d'écrire ${UID} et
${GID} dans le fichier Compose. Dans bash,
UID est une variable en lecture seule et non
exportée : Compose ne la voit pas. GID n'existe
pas dans bash, et zsh définit les deux sans les exporter.
Compose affiche alors un avertissement et remplace la
variable par une chaîne vide. L'argument vide écrase la
valeur par défaut de ARG, et le build s'arrête
sur groupadd: invalid group ID ''. Avec
${UID:-1000}, le repli s'applique sans un mot :
un poste dont l'UID vaut 1001 reçoit une image construite
pour 1000.
Nous exportons donc nos propres variables depuis le Makefile
du projet. Elles ne s'appellent pas UID, pour
qu'un développeur puisse aussi les exporter depuis son shell
: dans bash, export UID=1000 échoue.
# Makefile
export USER_ID := $(shell id -u)
export GROUP_ID := $(shell id -g)
build:
docker compose build --pull
make build passe les bons identifiants à
Compose. Un docker compose build lancé à la
main retombe sur 1000, l'UID habituel du premier compte sous
Debian et Ubuntu.
Sources
- FrankenPHP, Running as a non-root user et Dockerfile de l'image officielle.
-
symfony-docker,
Dockerfile,
compose.override.yaml
et
Troubleshooting
: utilisateur
nonroot, volume/app/var,chownsous Linux. - Docker, référence Dockerfile, Compose, build et interpolation : variables absentes.
-
Docker,
volumes,
docker compose up
et
docker compose down
: volume vide, options
-Vet-v. - Docker Engine, ports privilégiés sans capacité.
- Linux, capabilities(7) et getent(1) ; Debian, libcap2-bin.
- Shells et make, Bash Variables, paramètres de zsh et export dans GNU make.