diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c8b35b6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,49 @@ +# Git files +.git +.gitignore +.gitattributes + +# IDE files +.idea +.vscode +*.swp +*.swo + +# Documentation +README.md +*.MD +HELPER.MD +NOTIFICATION.MD + +# Test files +tests +phpunit.xml.dist + +# CI/CD +.github +.gitlab-ci.yml + +# Platform.sh +.platform +.platform.app.yaml + +# Docker files (don't copy Docker files into the image) +Dockerfile +docker-compose*.yml +compose*.yaml +.dockerignore + +# Environment files (will be provided at runtime) +.env.local +.env.*.local + +# Development dependencies +.php-cs-fixer.cache + +# Cache and logs (will be created at runtime) +var/cache/* +var/log/* +var/sessions/* + +# Vendor (will be installed during build) +# vendor is installed during the build process, so we don't ignore it diff --git a/.env b/.env index 31d9655..f8deabf 100644 --- a/.env +++ b/.env @@ -17,6 +17,7 @@ ###> symfony/framework-bundle ### APP_ENV=prod APP_SECRET='kjuusshgvk35434judshfgvkusd224444hvg' +APPLICATION=EasyPortal ###< symfony/framework-bundle ### ###> doctrine/doctrine-bundle ### @@ -43,8 +44,8 @@ MAILER_DSN=null://null TRUSTED_PROXY='185.116.130.121','10.8.34.21' ###> league/oauth2-server-bundle ### -OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.pem -OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key +OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4 OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066 ###< league/oauth2-server-bundle ### @@ -66,4 +67,7 @@ MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!" ###> aws/aws-sdk-php-symfony ### AWS_KEY=not-a-real-key AWS_SECRET=@@not-a-real-secret +AWS_REGION=us-east-1 +AWS_ENDPOINT=https://s3.amazonaws.com +AWS_S3_PORTAL_URL=https://s3.amazonaws.com/portal ###< aws/aws-sdk-php-symfony ### diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..7b41dfb --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,302 @@ +stages: + - build + - deploy + - notify + +variables: + # OVH Container Registry URL (without https://) + # Image name and tag (format: registry/project/image) + IMAGE_NAME: "easyportal" + IMAGE_TAG: "${OVH_REGISTRY_URL}/${IMAGE_NAME}:${CI_COMMIT_REF_SLUG}-${CI_PIPELINE_ID}" + IMAGE_LATEST: "${OVH_REGISTRY_URL}/${IMAGE_NAME}:latest" + +build: + stage: build + image: docker:20.10.16 + rules: + - if: '$CI_COMMIT_BRANCH == "develop"' + when: always + - when: manual + before_script: + # Use host Docker daemon via socket + - docker info + # Login to OVH Container Registry + - echo "Logging into OVH Container Registry..." + - echo "$OVH_REGISTRY_PASSWORD" | docker login -u "$OVH_REGISTRY_USERNAME" "$OVH_REGISTRY_URL" --password-stdin + script: + # Build the FrankenPHP image + - echo "Building Docker image..." + - docker build --build-arg APP_ENV=prod --target frankenphp_prod -t $IMAGE_TAG -t $IMAGE_LATEST . + + # Push both tags to OVH registry + - echo "Pushing image to OVH registry..." + - docker push $IMAGE_TAG + - docker push $IMAGE_LATEST + + # Display image info + - echo "Successfully pushed:" + - echo " - $IMAGE_TAG" + - echo " - $IMAGE_LATEST" + after_script: + - docker logout $OVH_REGISTRY_URL || true + +deploy: + stage: deploy + image: alpine:latest + needs: + - build + rules: + - if: '$CI_COMMIT_BRANCH == "develop"' + when: on_success + - when: manual + before_script: + - apk add --no-cache openssh-client + - mkdir -p ~/.ssh + - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - echo "Host *\n StrictHostKeyChecking no\n UserKnownHostsFile=/dev/null" > ~/.ssh/config + - chmod 600 ~/.ssh/config + - ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts || true + script: + - | + ssh $SSH_USER@$SERVER_IP << ENDSSH + set -e + cd /mnt/external-disk/easy-monitor/easyportal + + # ===== PRE-DEPLOYMENT DOCKER CLEANUP ===== + echo '===== Starting pre-deployment Docker cleanup =====' + + # Show current Docker space usage + echo 'Current Docker space usage:' + docker system df + + # Clean build cache + echo 'Cleaning Docker build cache...' + docker builder prune -f || true + + # Clean stopped containers + echo 'Cleaning stopped containers...' + docker container prune -f || true + + # Clean unused networks + echo 'Cleaning unused networks...' + docker network prune -f || true + + # Show space usage after cleanup + echo 'Docker space usage after initial cleanup:' + docker system df + + # Login to OVH registry + echo "$OVH_REGISTRY_PASSWORD" | docker login -u "$OVH_REGISTRY_USERNAME" "$OVH_REGISTRY_URL" --password-stdin + + # Pull latest image + echo 'Pulling latest image...' + docker pull $IMAGE_LATEST + + # Clean unused images before deployment + echo 'Cleaning unused Docker images...' + docker image prune -f || true + + # Update .env.compose with new image tag + echo 'Updating .env.compose with new image...' + echo "EASYPORTAL_IMAGE=$IMAGE_LATEST" > .env.compose + + # Recreate php and messenger containers with new image + echo 'Restarting EasyPortal services with new image...' + docker compose --env-file .env.compose up -d --force-recreate --no-deps php messenger + + # Wait for service to be ready + sleep 15 + + # Run migrations + echo 'Running database migrations...' + docker compose --env-file .env.compose exec -T php php bin/console doctrine:migrations:migrate --no-interaction + + # Clear cache + echo 'Clearing Symfony cache...' + docker compose --env-file .env.compose exec -T php php bin/console cache:clear --env=prod --no-debug + + # ===== POST-DEPLOYMENT CLEANUP ===== + echo '===== Final Docker cleanup =====' + + docker builder prune -f || true + docker container prune -f || true + + # Keep last 3 image versions + echo 'Cleaning old image versions (keeping last 3)...' + docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}' | grep -v REPOSITORY | sort -k1,1 -k4,4r | awk ' + { + repo_tag = \$1":" \$2 + if (repo_count[\$1]++ >= 3 && \$2 != "latest" && \$1 != "") { + print \$3 + } + }' | xargs -r docker rmi -f || true + + echo 'Final Docker space usage:' + docker system df + + echo '===== Deployment completed successfully! =====' + ENDSSH + after_script: + - rm -f ~/.ssh/id_rsa + environment: + name: production + url: https://testportail.solutions-easy.com + +notify_success: + stage: notify + image: curlimages/curl:latest + needs: + - deploy + rules: + - when: on_success + - when: manual + allow_failure: true + script: + - | + COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8) + DEPLOYMENT_TYPE="Déploiement Beta" + + if [ "$CI_COMMIT_BRANCH" != "develop" ]; then + DEPLOYMENT_TYPE="Déploiement Beta" + fi + + curl -H "Content-Type: application/json" -d "{ + \"type\": \"message\", + \"attachments\": [ + { + \"contentType\": \"application/vnd.microsoft.card.adaptive\", + \"content\": { + \"type\": \"AdaptiveCard\", + \"body\": [ + { + \"type\": \"TextBlock\", + \"text\": \"$DEPLOYMENT_TYPE\", + \"weight\": \"Bolder\", + \"size\": \"Large\" + }, + { + \"type\": \"TextBlock\", + \"text\": \"**App:** EasyPortal\", + \"wrap\": true + }, + { + \"type\": \"TextBlock\", + \"text\": \"**Version:** [$CI_COMMIT_REF_NAME - $COMMIT_SHORT_SHA]($CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA)\", + \"wrap\": true, + \"markdown\": true + }, + { + \"type\": \"TextBlock\", + \"text\": \"**Pipeline:** [Voir le pipeline]($CI_PIPELINE_URL)\", + \"wrap\": true, + \"markdown\": true + }, + { + \"type\": \"TextBlock\", + \"text\": \"**Auteur:** $GITLAB_USER_LOGIN\", + \"wrap\": true + }, + { + \"type\": \"TextBlock\", + \"text\": \"**Statut:** Succès ✓\", + \"wrap\": true, + \"color\": \"Good\" + } + ], + \"actions\": [ + { + \"type\": \"Action.OpenUrl\", + \"title\": \"Voir le pipeline\", + \"url\": \"$CI_PIPELINE_URL\" + }, + { + \"type\": \"Action.OpenUrl\", + \"title\": \"Voir le commit\", + \"url\": \"$CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA\" + } + ], + \"\$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\", + \"version\": \"1.0\" + } + } + ] + }" "$TEAMS_WEBHOOK_URL" + +notify_failure: + stage: notify + image: curlimages/curl:latest + needs: + - deploy + rules: + - when: on_failure + script: + - | + COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8) + DEPLOYMENT_TYPE="Déploiement Beta" + + if [ "$CI_COMMIT_BRANCH" != "develop" ]; then + DEPLOYMENT_TYPE="Déploiement Beta" + fi + + curl -H "Content-Type: application/json" -d "{ + \"type\": \"message\", + \"attachments\": [ + { + \"contentType\": \"application/vnd.microsoft.card.adaptive\", + \"content\": { + \"type\": \"AdaptiveCard\", + \"body\": [ + { + \"type\": \"TextBlock\", + \"text\": \"$DEPLOYMENT_TYPE\", + \"weight\": \"Bolder\", + \"size\": \"Large\" + }, + { + \"type\": \"TextBlock\", + \"text\": \"**App:** EasyPortal\", + \"wrap\": true + }, + { + \"type\": \"TextBlock\", + \"text\": \"**Version:** [$CI_COMMIT_REF_NAME - $COMMIT_SHORT_SHA]($CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA)\", + \"wrap\": true, + \"markdown\": true + }, + { + \"type\": \"TextBlock\", + \"text\": \"**Pipeline:** [Voir le pipeline]($CI_PIPELINE_URL)\", + \"wrap\": true, + \"markdown\": true + }, + { + \"type\": \"TextBlock\", + \"text\": \"**Auteur:** $GITLAB_USER_LOGIN\", + \"wrap\": true + }, + { + \"type\": \"TextBlock\", + \"text\": \"**Statut:** Échec ✗\", + \"wrap\": true, + \"color\": \"Attention\" + } + ], + \"actions\": [ + { + \"type\": \"Action.OpenUrl\", + \"title\": \"Voir le pipeline\", + \"url\": \"$CI_PIPELINE_URL\" + }, + { + \"type\": \"Action.OpenUrl\", + \"title\": \"Voir le commit\", + \"url\": \"$CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA\" + } + ], + \"\$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\", + \"version\": \"1.0\" + } + } + ] + }" "$TEAMS_WEBHOOK_URL" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c37d005 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,104 @@ +#syntax=docker/dockerfile:1 + +# Versions +FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream + +# The different stages of this Dockerfile are meant to be built into separate images +# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage +# https://docs.docker.com/compose/compose-file/#target + + +# Base FrankenPHP image +FROM frankenphp_upstream AS frankenphp_base + +WORKDIR /app + +VOLUME /app/var/ + +# persistent / runtime deps +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + acl \ + file \ + gettext \ + git \ + && rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + install-php-extensions \ + @composer \ + apcu \ + intl \ + opcache \ + zip \ + ; + +# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser +ENV COMPOSER_ALLOW_SUPERUSER=1 + +# Transport to use by Mercure (default to Bolt) +ENV MERCURE_TRANSPORT_URL=bolt:///data/mercure.db + +ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d" + +###> recipes ### +###> doctrine/doctrine-bundle ### +RUN install-php-extensions pdo_pgsql +###< doctrine/doctrine-bundle ### +###< recipes ### + +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.prod /etc/frankenphp/Caddyfile + +ENTRYPOINT ["docker-entrypoint"] + +HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1 +CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile" ] + +# Dev FrankenPHP image +FROM frankenphp_base AS frankenphp_dev + +ENV APP_ENV=dev +ENV XDEBUG_MODE=off +ENV FRANKENPHP_WORKER_CONFIG=watch + +RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" + +RUN set -eux; \ + install-php-extensions \ + xdebug \ + ; + +COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/ + +CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--watch" ] + +# Prod FrankenPHP image +FROM frankenphp_base AS frankenphp_prod + +ENV APP_ENV=prod + +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/ + +# prevent the reinstallation of vendors at every changes in the source code +COPY --link composer.* symfony.* ./ +RUN set -eux; \ + composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress + +# copy sources +COPY --link . ./ +RUN rm -Rf frankenphp/ + +RUN set -eux; \ + mkdir -p var/cache var/log; \ + composer dump-autoload --classmap-authoritative --no-dev; \ + composer dump-env prod; \ + chmod +x bin/console; \ + # Install JavaScript vendor assets and compile assets for production \ + php bin/console importmap:install; \ + php bin/console asset-map:compile; \ + php bin/console assets:install public; \ + sync; diff --git a/composer.json b/composer.json index 3ab5270..63d7d8a 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ "symfony/property-info": "7.2.*", "symfony/rate-limiter": "7.2.*", "symfony/runtime": "7.2.*", + "runtime/frankenphp-symfony": "^0.2.0", "symfony/security-bundle": "7.2.*", "symfony/serializer": "7.2.*", "symfony/stimulus-bundle": "^2.24", diff --git a/composer.lock b/composer.lock index 4c2dcff..ecc326a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9c6693b9e0508ab0c1ff3ee95823be7d", + "content-hash": "8868acbf6d26a43a2ba21777976b0844", "packages": [ { "name": "aws/aws-crt-php", @@ -3765,6 +3765,58 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "runtime/frankenphp-symfony", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-runtime/frankenphp-symfony.git", + "reference": "56822c3631d9522a3136a4c33082d006bdfe4bad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-runtime/frankenphp-symfony/zipball/56822c3631d9522a3136a4c33082d006bdfe4bad", + "reference": "56822c3631d9522a3136a4c33082d006bdfe4bad", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/runtime": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Runtime\\FrankenPhpSymfony\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.dev" + } + ], + "description": "FrankenPHP runtime for Symfony", + "support": { + "issues": "https://github.com/php-runtime/frankenphp-symfony/issues", + "source": "https://github.com/php-runtime/frankenphp-symfony/tree/0.2.0" + }, + "funding": [ + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-12-12T12:06:11+00:00" + }, { "name": "symfony/asset", "version": "v7.2.0", @@ -6972,7 +7024,7 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -7035,7 +7087,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -7046,6 +7098,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -7055,7 +7111,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -7116,7 +7172,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -7127,6 +7183,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -7136,7 +7196,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -7197,7 +7257,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -7208,6 +7268,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -7217,16 +7281,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -7273,7 +7337,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -7284,12 +7348,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/polyfill-php84", @@ -8389,16 +8457,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -8452,7 +8520,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -8463,12 +8531,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/stimulus-bundle", @@ -12444,7 +12516,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -12453,6 +12525,6 @@ "ext-iconv": "*", "ext-openssl": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" -} +} \ No newline at end of file diff --git a/frankenphp/Caddyfile b/frankenphp/Caddyfile new file mode 100644 index 0000000..62d420a --- /dev/null +++ b/frankenphp/Caddyfile @@ -0,0 +1,79 @@ +{ + # Global options + frankenphp { + # Number of workers for better performance + num_threads {$NUM_THREADS:4} + } + + # Order directives properly + order mercure after encode + order php_server before file_server +} + +# HTTP server - HTTPS is handled by caddy-proxy +{$SERVER_NAME:80} { + # Root directory + root * /app/public + + # Enable compression + encode zstd gzip + + # Mercure hub configuration (built-in) + mercure { + # Publisher JWT key + publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} { + algorithm hs256 + } + # Subscriber JWT key + subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} { + algorithm hs256 + } + # Allow anonymous subscribers + anonymous + # CORS configuration + cors_origins * + } + + # Client max body size (for uploads) + request_body { + max_size 20MB + } + + # Security: Deny access to sensitive directories + @forbidden { + path /bin/* /config/* /src/* /templates/* /tests/* /translations/* /var/* /vendor/* + } + handle @forbidden { + respond "Access Denied" 404 + } + + # Security: Deny access to dot files (except .well-known for Mercure) + @dotfiles { + path */.* + not path /.well-known/* + } + handle @dotfiles { + respond "Access Denied" 404 + } + + # Cache static assets (30 days) + @static { + path *.jpg *.jpeg *.png *.gif *.ico *.css *.js *.svg *.woff *.woff2 *.ttf *.eot *.xlsx + } + handle @static { + header Cache-Control "public, max-age=2592000, no-transform" + file_server + } + + # PHP FrankenPHP handler + php_server { + # Resolve symlinks + resolve_root_symlink + } + + # Logging + log { + output file /var/log/caddy/access.log + format json + } +} diff --git a/frankenphp/Caddyfile.prod b/frankenphp/Caddyfile.prod new file mode 100644 index 0000000..e2b2f91 --- /dev/null +++ b/frankenphp/Caddyfile.prod @@ -0,0 +1,104 @@ +{ + # Global options + frankenphp { + # Number of workers for better performance + num_threads {$NUM_THREADS:4} + } + + # Order directives properly + order mercure after encode + order php_server before file_server +} + +# HTTP - redirect to HTTPS +http://{$SERVER_NAME:localhost} { + redir https://{host}{uri} permanent +} + +# HTTPS server +https://{$SERVER_NAME:localhost} { + # Root directory + root * /app/public + + # TLS configuration - Caddy will automatically obtain and renew Let's Encrypt certificates + tls { + protocols tls1.2 tls1.3 + } + + # Enable compression + encode zstd gzip + + # Mercure hub configuration (built-in) + mercure { + # Publisher JWT key + publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} { + algorithm hs256 + } + # Subscriber JWT key + subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} { + algorithm hs256 + } + # Allow anonymous subscribers + anonymous + # Enable subscriptions + subscriptions + # CORS configuration + cors_origins * + } + + # Client max body size (for uploads) + request_body { + max_size 20MB + } + + # Security: Deny access to sensitive directories + @forbidden { + path /bin/* /config/* /src/* /templates/* /tests/* /translations/* /var/* /vendor/* + } + handle @forbidden { + respond "Access Denied" 404 + } + + # Security: Deny access to dot files (except .well-known for Mercure) + @dotfiles { + path */.* + not path /.well-known/* + } + handle @dotfiles { + respond "Access Denied" 404 + } + + # Cache static assets (30 days) + @static { + path *.jpg *.jpeg *.png *.gif *.ico *.css *.js *.svg *.woff *.woff2 *.ttf *.eot *.xlsx *.pdf + file + } + handle @static { + header Cache-Control "public, max-age=2592000, no-transform" + file_server + } + + # Serve files from /assets directory + handle /assets/* { + root * /app/public + file_server + } + + # PHP FrankenPHP handler + php_server { + # Resolve symlinks + resolve_root_symlink + } + + # Logging + log { + output file /app/var/log/access.log + format json + # Redact sensitive data + format filter { + request>uri query { + replace authorization REDACTED + } + } + } +} diff --git a/frankenphp/conf.d/10-app.ini b/frankenphp/conf.d/10-app.ini new file mode 100644 index 0000000..79a17dd --- /dev/null +++ b/frankenphp/conf.d/10-app.ini @@ -0,0 +1,13 @@ +expose_php = 0 +date.timezone = UTC +apc.enable_cli = 1 +session.use_strict_mode = 1 +zend.detect_unicode = 0 + +; https://symfony.com/doc/current/performance.html +realpath_cache_size = 4096K +realpath_cache_ttl = 600 +opcache.interned_strings_buffer = 16 +opcache.max_accelerated_files = 20000 +opcache.memory_consumption = 256 +opcache.enable_file_override = 1 diff --git a/frankenphp/conf.d/20-app.dev.ini b/frankenphp/conf.d/20-app.dev.ini new file mode 100644 index 0000000..e50f43d --- /dev/null +++ b/frankenphp/conf.d/20-app.dev.ini @@ -0,0 +1,5 @@ +; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host +; See https://github.com/docker/for-linux/issues/264 +; The `client_host` below may optionally be replaced with `discover_client_host=yes` +; Add `start_with_request=yes` to start debug session on each request +xdebug.client_host = host.docker.internal diff --git a/frankenphp/conf.d/20-app.prod.ini b/frankenphp/conf.d/20-app.prod.ini new file mode 100644 index 0000000..b716441 --- /dev/null +++ b/frankenphp/conf.d/20-app.prod.ini @@ -0,0 +1,5 @@ +; https://symfony.com/doc/current/performance.html#use-the-opcache-class-preloading +opcache.preload_user = root +opcache.preload = /app/config/preload.php +; https://symfony.com/doc/current/performance.html#don-t-check-php-files-timestamps +opcache.validate_timestamps = 0 diff --git a/frankenphp/docker-entrypoint.sh b/frankenphp/docker-entrypoint.sh new file mode 100644 index 0000000..82cb1ff --- /dev/null +++ b/frankenphp/docker-entrypoint.sh @@ -0,0 +1,66 @@ +#!/bin/sh +set -e + +if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then + # Install the project the first time PHP is started + # After the installation, the following block can be deleted + if [ ! -f composer.json ]; then + rm -Rf tmp/ + composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install + + cd tmp + cp -Rp . .. + cd - + rm -Rf tmp/ + + composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony + composer config --json extra.symfony.docker 'true' + + if grep -q ^DATABASE_URL= .env; then + echo 'To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build --wait' + sleep infinity + fi + fi + + if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then + composer install --prefer-dist --no-progress --no-interaction + fi + + # Display information about the current project + # Or about an error in project initialization + php bin/console -V + + if grep -q ^DATABASE_URL= .env; then + echo 'Waiting for database to be ready...' + ATTEMPTS_LEFT_TO_REACH_DATABASE=60 + until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do + if [ $? -eq 255 ]; then + # If the Doctrine command exits with 255, an unrecoverable error occurred + ATTEMPTS_LEFT_TO_REACH_DATABASE=0 + break + fi + sleep 1 + ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1)) + echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left." + done + + if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then + echo 'The database is not up or not reachable:' + echo "$DATABASE_ERROR" + exit 1 + else + echo 'The database is now ready and reachable' + fi + + if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then + php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing + fi + fi + + setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var + setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var + + echo 'PHP app ready!' +fi + +exec docker-php-entrypoint "$@"