From 1a16a7275ae82f607e24ae67b27c1e65b6e76d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Mon, 5 Oct 2026 10:57:05 +0200 Subject: [PATCH] Sync with docker-starter * Serve the local production stack on port 8000 instead of 8080, which collides with the Traefik admin port of the dev stack * Let docker_exit_code() use the context's run service (php in the prod context) instead of a hardcoded builder * Do not dump the autoloader in the first composer install of the production build, it is dumped after the sources are copied * Expose the php-fpm status page (denied by nginx by default) * Pin the actions of the build-push workflow by SHA and compute the image tags in bash * Test the production images in the CI * Align .home/.gitignore, .gitignore and .dockerignore --- .castor/docker.php | 4 +- .dockerignore | 2 + .github/workflows/build-push.yml | 17 +++++---- .github/workflows/ci.yml | 37 +++++++++++++++++++ .gitignore | 1 - .home/.gitignore | 2 + README.md | 2 +- infrastructure/docker/docker-compose.prod.yml | 10 ++--- infrastructure/docker/services/php/Dockerfile | 2 +- .../services/php/nginx/conf.d/default.conf | 15 ++++++++ .../docker/services/php/php/fpm/php-fpm.conf | 1 + 11 files changed, 75 insertions(+), 18 deletions(-) diff --git a/.castor/docker.php b/.castor/docker.php index 3dc73b5..cadf130 100644 --- a/.castor/docker.php +++ b/.castor/docker.php @@ -49,7 +49,7 @@ function about(): void io()->section('Available URLs for this project:'); if (!has_router()) { - io()->listing([\sprintf('http://127.0.0.1:%s', getenv('HTTP_PORT') ?: '8080')]); + io()->listing([\sprintf('http://127.0.0.1:%s', getenv('HTTP_PORT') ?: '8000')]); return; } @@ -544,7 +544,7 @@ function docker_compose_exec( function docker_exit_code( array $params, ?Context $c = null, - string $service = 'builder', + ?string $service = null, bool $noDeps = true, ?string $workDir = null, ): int { diff --git a/.dockerignore b/.dockerignore index 0ef1e12..b43e30d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,6 +24,7 @@ foobar.sh .gitignore LICENSE phpunit.dist.xml +phpunit.xml.dist # Mirrors .gitignore: local/generated files that must never end up baked # into the image (some carry secrets, e.g. the prod secrets decryption key) @@ -34,6 +35,7 @@ phpunit.dist.xml .env.local.php .env.*.local .phpunit.cache +.phpunit.result.cache phpunit.xml config/reference.php config/secrets/prod/prod.decrypt.private.php diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index a80607c..185ae23 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -21,25 +21,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Log in to registry shell: bash run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - name: setup-castor - uses: castor-php/setup-castor@v1.1.0 + uses: castor-php/setup-castor@2a495b8c91f00be6768ad8a040ba8634c797d386 # v1.1.0 - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # Images are tagged with the short commit sha (what a deployment should # reference), "latest" on main, and the git tag when there is one - name: "Build and push the production images" - run: >- - castor docker:push - --tag="${GITHUB_SHA::7}" - ${{ github.ref_name == 'main' && '--tag=latest' || '' }} - ${{ github.ref_type == 'tag' && format('--tag={0}', github.ref_name) || '' }} + shell: bash + run: | + tags=(--tag="${GITHUB_SHA::7}") + if [ "${GITHUB_REF_NAME}" = main ]; then tags+=(--tag=latest); fi + if [ "${GITHUB_REF_TYPE}" = tag ]; then tags+=(--tag="${GITHUB_REF_NAME}"); fi + castor docker:push "${tags[@]}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72ea2d1..8646b14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,43 @@ jobs: with: dockerfile: infrastructure/docker/services/php/Dockerfile + prod-images: + name: Test production images + runs-on: ubuntu-latest + env: + CASTOR_CONTEXT: prod + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Log in to registry + shell: bash + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: setup-castor + uses: castor-php/setup-castor@2a495b8c91f00be6768ad8a040ba8634c797d386 # v1.1.0 + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: "Build and start the production images" + run: "castor start" + + - name: "Test HTTP server" + run: | + set -e + set -o pipefail + + # The whole app sits behind HTTP Basic auth (see config/packages/security.yaml) + DASHBOARD_PASSWORD=$(grep -oP '^DASHBOARD_PASSWORD=\K.*' .env) + curl --fail --silent -u "monologue:${DASHBOARD_PASSWORD}" http://127.0.0.1:8000 | grep "Monologue" + test "$(curl --silent -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/index.php)" = 404 + + - name: "Test the php image runs as a non-root user" + run: "castor builder -- id -u | grep -x 1000" + ci: name: Continuous Integration runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 9c79a6e..e307140 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ ###> infrastructure ### /.castor.stub.php -/.home/ /infrastructure/docker/docker-compose.override.yml /infrastructure/docker/services/router/certs/*.pem ###< infrastructure ### diff --git a/.home/.gitignore b/.home/.gitignore index e69de29..78d9101 100644 --- a/.home/.gitignore +++ b/.home/.gitignore @@ -0,0 +1,2 @@ +/* +!.gitignore diff --git a/README.md b/README.md index 81179ce..f6783a5 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ router), independent from the development one: castor build -c prod # builds the php and nginx images castor start -c prod # starts the stack and runs the migrations - # -> http://127.0.0.1:8080 (HTTP_PORT=18080 castor start -c prod to change the port) + # -> http://127.0.0.1:8000 (HTTP_PORT=18000 castor start -c prod to change the port) castor destroy -c prod # removes the containers and the volumes It uses dummy secrets: to test with a real Slack workspace, put the diff --git a/infrastructure/docker/docker-compose.prod.yml b/infrastructure/docker/docker-compose.prod.yml index 6fc7527..2de1713 100644 --- a/infrastructure/docker/docker-compose.prod.yml +++ b/infrastructure/docker/docker-compose.prod.yml @@ -43,8 +43,8 @@ services: <<: *php-build target: php cache_from: - - "type=registry,ref=${REGISTRY:-ghcr.io/jolicode/monologue}/php:cache" - image: "${REGISTRY:-ghcr.io/jolicode/monologue}/php:${TAG:-latest}" + - "type=registry,ref=${REGISTRY:-}/php:cache" + image: "${REGISTRY:-${PROJECT_NAME}}/php:${TAG:-latest}" environment: *app-env env_file: # Optional, gitignored: real credentials (Slack, dashboard password...) for a local test. @@ -62,10 +62,10 @@ services: <<: *php-build target: nginx cache_from: - - "type=registry,ref=${REGISTRY:-ghcr.io/jolicode/monologue}/nginx:cache" - image: "${REGISTRY:-ghcr.io/jolicode/monologue}/nginx:${TAG:-latest}" + - "type=registry,ref=${REGISTRY:-}/nginx:cache" + image: "${REGISTRY:-${PROJECT_NAME}}/nginx:${TAG:-latest}" ports: - - "${HTTP_PORT:-8080}:8080" + - "${HTTP_PORT:-8000}:8080" volumes: - php-socket:/var/run/php depends_on: diff --git a/infrastructure/docker/services/php/Dockerfile b/infrastructure/docker/services/php/Dockerfile index a33f50d..c8070dd 100644 --- a/infrastructure/docker/services/php/Dockerfile +++ b/infrastructure/docker/services/php/Dockerfile @@ -174,7 +174,7 @@ ENV COMPOSER_ALLOW_SUPERUSER=1 # Dependencies first, for better layer caching. "app" is a build context, not a stage # hadolint ignore=DL3022 COPY --from=app composer.json composer.lock symfony.lock ./ -RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts --optimize-autoloader +RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts --no-autoloader # hadolint ignore=DL3022 COPY --from=app . . diff --git a/infrastructure/docker/services/php/nginx/conf.d/default.conf b/infrastructure/docker/services/php/nginx/conf.d/default.conf index 351a874..d942a92 100644 --- a/infrastructure/docker/services/php/nginx/conf.d/default.conf +++ b/infrastructure/docker/services/php/nginx/conf.d/default.conf @@ -22,6 +22,17 @@ server { fastcgi_param SCRIPT_FILENAME /ping; } + # Remove this block if you want to access to PHP FPM monitoring + # dashboard (on URL: /php-fpm-status). WARNING: on production, you must + # secure this page (by user IP address, with a password, for example) + location = /php-fpm-status { + deny all; + include fastcgi_params; + fastcgi_pass unix:/var/run/php/php-fpm.sock; + fastcgi_param SCRIPT_NAME /php-fpm-status; + fastcgi_param SCRIPT_FILENAME /php-fpm-status; + } + location / { # try to serve file directly, fallback to index.php try_files $uri /index.php$is_args$args; @@ -36,6 +47,10 @@ server { # TLS is always terminated upstream (dev router, ingress, reverse proxy) fastcgi_param HTTPS on; fastcgi_param SERVER_NAME $http_host; + # # Uncomment if you want to use /php-fpm-status endpoint **with** + # # real request URI. It may have some side effects, that's why it's + # # commented by default + # fastcgi_param SCRIPT_NAME $request_uri; internal; } diff --git a/infrastructure/docker/services/php/php/fpm/php-fpm.conf b/infrastructure/docker/services/php/php/fpm/php-fpm.conf index 4850a81..9e25e9b 100644 --- a/infrastructure/docker/services/php/php/fpm/php-fpm.conf +++ b/infrastructure/docker/services/php/php/fpm/php-fpm.conf @@ -14,6 +14,7 @@ pm.start_servers = 2 pm.min_spare_servers = 2 pm.max_spare_servers = 3 pm.max_requests = 500 +pm.status_path = /php-fpm-status ; Used by the images HEALTHCHECK ping.path = /ping clear_env = no