-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile
More file actions
199 lines (170 loc) · 9.99 KB
/
Copy pathDockerfile
File metadata and controls
199 lines (170 loc) · 9.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#syntax=docker/dockerfile:1
# ─────────────────────────────────────────────────────────────────────────────
# Le Canard du Vendredi — image FrankenPHP multi-stage (§31.4).
#
# La version PHP de l'image est ALIGNÉE sur la version de développement (8.5)
# pour éviter le drift dev/prod (cf. CLAUDE.md « invariants »). Les extensions
# requises par Symfony + pdo_pgsql / intl / opcache sont fournies par
# install-php-extensions (docker-php-extension-installer, embarqué dans l'image).
#
# Stages :
# frankenphp_base — socle commun (extensions, Caddyfile, entrypoint)
# frankenphp_dev — outillage dev (Xdebug, watch), code monté
# frankenphp_prod — sans outils de dev, version exposée, HEALTHCHECK
# ─────────────────────────────────────────────────────────────────────────────
# ─── Build des assets front (Vite) ───────────────────────────────────────────
# Le front (public/build) est gitignoré ET assets/ est hors contexte en dev :
# l'image prod DOIT le compiler elle-même (§31.4 « assets compilés »), sinon un
# `git reset --hard` sur le VPS laisse l'image sans front. `@theatre/studio` est
# tree-shaké du build prod (§15.4) — vérifié par la CI front.
FROM node:22-slim AS asset_builder
WORKDIR /app
# Manifeste d'abord (couche npm cachée tant que les deps ne bougent pas).
COPY --link package.json package-lock.json ./
RUN npm ci
# Puis les sources strictement nécessaires au build.
COPY --link assets ./assets
COPY --link vite.config.ts tsconfig.json ./
RUN npm run build
FROM dunglas/frankenphp:1-php8.5 AS frankenphp_upstream
# ─── Base ────────────────────────────────────────────────────────────────────
FROM frankenphp_upstream AS frankenphp_base
SHELL ["/bin/bash", "-euxo", "pipefail", "-c"]
WORKDIR /app
# Dépendances système persistantes + extensions PHP.
# hadolint ignore=DL3008
RUN <<-EOF
apt-get update
apt-get install -y --no-install-recommends \
file \
git
install-php-extensions \
@composer \
apcu \
intl \
opcache \
opentelemetry \
pdo_pgsql \
zip
rm -rf /var/lib/apt/lists/*
EOF
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
COPY --link frankenphp/Caddyfile /etc/frankenphp/Caddyfile
ENTRYPOINT ["docker-entrypoint"]
# Health check d'infrastructure : l'endpoint d'admin Caddy (port 2019) répond
# dès que le serveur applicatif est debout. La sonde métier /health (app + base)
# est servie séparément par l'application (§31.4, Presentation/Http).
HEALTHCHECK --start-period=60s CMD php -r 'exit(false === @file_get_contents("http://localhost:2019/metrics", context: stream_context_create(["http" => ["timeout" => 5]])) ? 1 : 0);'
CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile"]
# ─── Dev ─────────────────────────────────────────────────────────────────────
FROM frankenphp_base AS frankenphp_dev
ENV APP_ENV=dev
ENV XDEBUG_MODE=off
# Pas de worker en dev : FRANKENPHP_CONFIG reste VIDE → FrankenPHP sert en mode
# classique, le kernel (routeur compris) est reconstruit à chaque requête. Aucun
# état en RAM = aucune route périmée après une modif (§22.2). Le worker n'est
# activé qu'en prod (stage frankenphp_prod).
RUN <<-EOF
mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
install-php-extensions xdebug
git config --system --add safe.directory /app
EOF
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
# CMD hérité de la base (`frankenphp run`, sans worker ni --watch).
# ─── Prod ────────────────────────────────────────────────────────────────────
# Image de production (§31.4) : sans outils de dev, version exposée, health check,
# assets compilés (stage asset_builder), config env compilée (.env.local.php).
# NOTE : le durcissement DÉFENSIF restant (non-root, base minimale, surface
# réduite) = phase suivante.
FROM frankenphp_base AS frankenphp_prod
ENV APP_ENV=prod
# Worker FrankenPHP ACTIVÉ en prod (kernel en RAM, perf). Mécanisme standard
# symfony-docker : FRANKENPHP_CONFIG alimente le bloc `frankenphp {}` du Caddyfile.
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY --link frankenphp/conf.d/20-app.prod.ini $PHP_INI_DIR/app.conf.d/
# Installer les vendors d'abord (cache des couches inchangées) puis le code.
COPY --link composer.* symfony.* ./
# Retry : codeload.github.com renvoie par vagues des HTTP 400 intermittents
# (réseau, pas un incident GitHub) ; composer abandonne au 1er échec car le
# fallback source est désactivé. Cinq tentatives espacées suffisent à franchir
# une mauvaise fenêtre et fiabilisent le build (donc le push-to-deploy).
RUN for i in 1 2 3 4 5; do \
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress && exit 0; \
echo "composer install : échec, nouvelle tentative ${i}/5…" >&2; \
sleep 5; \
done; \
echo "composer install : échec après 5 tentatives — abandon." >&2; exit 1
COPY --link . ./
# Assets compilés depuis le stage Node (entrypoints.json lu par pentatrion/vite-bundle).
COPY --link --from=asset_builder /app/public/build ./public/build
# Compile l'autoload, la config env (.env.local.php) et le cache Symfony.
#
# `composer dump-env prod` fige UNIQUEMENT APP_ENV=prod dans `.env.local.php`
# (source = .env vide) : Symfony le lit en priorité et `bootEnv()` retourne sans
# exiger de `.env` physique au runtime. Les variables réelles injectées sur le
# VPS (DATABASE_URL, APP_SECRET, MERCURE_*, …) PRIMENT sur ce fichier (populate
# ne réécrit pas une variable déjà présente). AUCUN secret, AUCUN APP_FAKE_NOW
# n'est gravé — un garde-fou de build le vérifie et casse la construction sinon.
RUN <<-EOF
mkdir -p var/cache var/log var/share
composer dump-autoload --classmap-authoritative --no-dev
# `.env` vide = placeholder de premier chargement Symfony (le projet n'en
# committe pas). En prod, `.env.local.php` court-circuite et il n'est jamais
# lu ; il évite seulement que `bootEnv()` lève une exception si l'image est
# réutilisée hors `prod` (ex. e2e en `preprod`). dump-env le prend pour source.
touch .env
composer dump-env prod
# Garde-fou : rien de sensible ni de falsifiable ne doit être figé dans l'image.
if grep -qiE 'APP_FAKE_NOW|APP_SECRET|DATABASE_URL|MERCURE_JWT|MERCURE_PUBLISHER|MERCURE_SUBSCRIBER|POSTGRES_PASSWORD' .env.local.php; then
echo 'SECURITY: a secret or APP_FAKE_NOW leaked into .env.local.php — aborting build.' >&2
cat .env.local.php >&2
exit 1
fi
# Cache prod chaud au build. Les warmers Symfony exigent que les variables
# requises soient DÉFINIES (pas résolues) : on fournit des valeurs FACTICES
# scopées à cette seule commande (jamais des ENV de couche, jamais connectées).
# Le conteneur compilé ne stocke que des placeholders %env()% résolus au
# RUNTIME — aucune de ces valeurs n'est gravée dans l'image (recette officielle
# symfony-docker). Le garde-fou ci-dessus a déjà vérifié `.env.local.php`.
APP_SECRET=__build__ \
DATABASE_URL='postgresql://build:build@127.0.0.1:5432/build?serverVersion=17&charset=utf8' \
MERCURE_URL='http://localhost/.well-known/mercure' \
MERCURE_PUBLIC_URL='http://localhost/.well-known/mercure' \
MERCURE_JWT_SECRET=__build__ \
DEFAULT_URI='http://localhost' \
MESSENGER_TRANSPORT_DSN='doctrine://default?auto_setup=0' \
php bin/console cache:clear --no-debug
chmod +x bin/console
chmod -R g=u var
sync
EOF
# ─── Durcissement : exécution non-root (Phase 9) ─────────────────────────────
# `www-data` (uid 33) existe déjà dans le socle PHP et est DÉJÀ référencé par
# opcache.preload_user (20-app.prod.ini) : compte de service tout trouvé.
#
# Le binaire frankenphp embarque une file-capability `cap_net_bind_service=ep`.
# Sous `cap_drop: ALL` + `no-new-privileges` (compose.prod.yaml), le noyau REFUSE
# d'exécuter un binaire porteur d'une capability EFFECTIVE (EPERM à l'exec). On la
# RETIRE donc (`setcap -r`) : zéro capability, le bind du port privilégié :80
# repose UNIQUEMENT sur le sysctl net.ipv4.ip_unprivileged_port_start. (« Pas de
# setcap » visait l'AJOUT d'une cap ; ici on en SUPPRIME une — même intention :
# aucune capability sur l'app.)
RUN setcap -r /usr/local/bin/frankenphp
# Chemins écrits au RUNTIME par les services SANS read-only (worker, relay) :
# var/share — pool cache.app (FilesystemAdapter, ex. Scheduler) ;
# var/log — inutilisé en prod (logs → stderr), aligné par sûreté.
# var/cache reste root + lecture seule (cache chaud baké, immuable par design).
# Le service `app` (read-only) reçoit var/share, /data et /config en tmpfs
# inscriptibles (mode=1777) — cf. compose.prod.yaml.
RUN chown -R www-data:www-data var/log var/share
USER www-data
# Version exposée (/health, Grafana). PLACÉE EN DERNIER À DESSEIN : elle change à
# chaque commit ; la garder ici évite d'invalider les couches coûteuses (vendors,
# code, cache) à chaque déploiement. Lue au RUNTIME, non requise par le build.
ARG APP_VERSION=0.0.0-dev
ENV APP_VERSION=${APP_VERSION}