Merge branch 'dockerize-portal' into 'develop'

Dockerize portal

See merge request easy-solutions/apps/easyportal!2
This commit is contained in:
Qada 2026-01-21 15:33:51 +00:00
commit 64ebaa4e05
12 changed files with 828 additions and 24 deletions

49
.dockerignore Normal file
View File

@ -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

8
.env
View File

@ -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 ###

302
.gitlab-ci.yml Normal file
View File

@ -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 != "<none>") {
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"

104
Dockerfile Normal file
View File

@ -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;

View File

@ -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",

114
composer.lock generated
View File

@ -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"
}

79
frankenphp/Caddyfile Normal file
View File

@ -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
}
}

104
frankenphp/Caddyfile.prod Normal file
View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 "$@"