├── .gitignore ├── deployment ├── supervisord.conf ├── grafana │ └── provisioning │ │ ├── datasources │ │ └── datasource.yml │ │ └── dashboards │ │ ├── dashboard.yml │ │ ├── caddy.json │ │ ├── frankenphp.json │ │ └── traefik.json ├── prometheus │ └── prometheus.yml ├── supervisord.worker.conf ├── supervisord.horizon.conf ├── supervisord.reverb.conf ├── octane │ ├── RoadRunner │ │ ├── .rr.prod.yaml │ │ └── supervisord.roadrunner.conf │ ├── FrankenPHP │ │ ├── Caddyfile │ │ └── supervisord.frankenphp.conf │ └── Swoole │ │ └── supervisord.swoole.conf ├── supervisord.scheduler.conf ├── php.ini ├── healthcheck └── start-container ├── .dockerignore ├── LICENSE ├── .env.production ├── .github └── workflows │ ├── compose-test.yaml │ ├── swoole-test.yml │ ├── frankenphp-test.yml │ └── roadrunner-test.yml ├── Makefile ├── Swoole.Alpine.Dockerfile ├── static-build.Dockerfile ├── RoadRunner.Alpine.Dockerfile ├── Swoole.Dockerfile ├── RoadRunner.Dockerfile ├── FrankenPHP.Alpine.Dockerfile ├── FrankenPHP.Dockerfile ├── README.md └── compose.production.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | **/.DS_Store 4 | .config 5 | .data -------------------------------------------------------------------------------- /deployment/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon = true 3 | user = %(ENV_USER)s 4 | 5 | [supervisorctl] 6 | 7 | [inet_http_server] 8 | port = 127.0.0.1:9001 9 | 10 | [rpcinterface:supervisor] 11 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface -------------------------------------------------------------------------------- /deployment/grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | access: proxy 7 | orgId: 1 8 | url: http://prometheus:9090 9 | isDefault: true 10 | editable: false 11 | basicAuth: false 12 | uid: prometheus -------------------------------------------------------------------------------- /deployment/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s 3 | evaluation_interval: 5s 4 | 5 | scrape_configs: 6 | - job_name: 'frankenphp' 7 | static_configs: 8 | - targets: [ 'app:2019' ] 9 | 10 | - job_name: 'traefik' 11 | static_configs: 12 | - targets: [ 'traefik:8190' ] 13 | -------------------------------------------------------------------------------- /deployment/grafana/provisioning/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'Prometheus' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: false 10 | allowUiUpdates: true 11 | options: 12 | path: /etc/grafana/provisioning/dashboards -------------------------------------------------------------------------------- /deployment/supervisord.worker.conf: -------------------------------------------------------------------------------- 1 | [program:worker] 2 | process_name = %(program_name)s_%(process_num)s 3 | command = %(ENV_WORKER_COMMAND)s 4 | user = %(ENV_USER)s 5 | autostart = true 6 | autorestart = true 7 | stopasgroup = true 8 | killasgroup = true 9 | stdout_logfile = /dev/stdout 10 | stdout_logfile_maxbytes = 0 11 | stderr_logfile = /dev/stderr 12 | stderr_logfile_maxbytes = 0 13 | 14 | [include] 15 | files = /etc/supervisord.conf 16 | -------------------------------------------------------------------------------- /deployment/supervisord.horizon.conf: -------------------------------------------------------------------------------- 1 | [program:horizon] 2 | process_name = %(program_name)s_%(process_num)s 3 | command = php %(ENV_ROOT)s/artisan horizon 4 | user = %(ENV_USER)s 5 | autostart = true 6 | autorestart = true 7 | stopasgroup = true 8 | killasgroup = true 9 | stdout_logfile = /dev/stdout 10 | stdout_logfile_maxbytes = 0 11 | stderr_logfile = /dev/stderr 12 | stderr_logfile_maxbytes = 0 13 | stopwaitsecs = 3600 14 | 15 | [include] 16 | files = /etc/supervisord.conf 17 | -------------------------------------------------------------------------------- /deployment/supervisord.reverb.conf: -------------------------------------------------------------------------------- 1 | [program:reverb] 2 | process_name = %(program_name)s_%(process_num)s 3 | command = php %(ENV_ROOT)s/artisan reverb:start 4 | user = %(ENV_USER)s 5 | autostart = true 6 | autorestart = true 7 | stopasgroup = true 8 | killasgroup = true 9 | stdout_logfile = /dev/stdout 10 | stdout_logfile_maxbytes = 0 11 | stderr_logfile = /dev/stderr 12 | stderr_logfile_maxbytes = 0 13 | minfds = 10000 14 | 15 | [include] 16 | 17 | files = /etc/supervisord.conf 18 | -------------------------------------------------------------------------------- /deployment/octane/RoadRunner/.rr.prod.yaml: -------------------------------------------------------------------------------- 1 | version: '2.7' 2 | rpc: 3 | listen: 'tcp://127.0.0.1:6001' 4 | server: 5 | relay: pipes 6 | http: 7 | middleware: [ "static", "gzip", "headers" ] 8 | max_request_size: 20 9 | static: 10 | dir: "public" 11 | forbid: [ ".php", ".htaccess" ] 12 | uploads: 13 | forbid: [".php", ".exe", ".bat", ".sh"] 14 | pool: 15 | allocate_timeout: 10s 16 | destroy_timeout: 10s 17 | supervisor: 18 | max_worker_memory: 128 19 | exec_ttl: 60s 20 | logs: 21 | mode: production 22 | level: debug 23 | encoding: json 24 | status: 25 | address: localhost:2114 26 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /storage/app/public 8 | /storage/debugbar 9 | /storage/ssr 10 | /storage/clockwork 11 | /storage/logs 12 | /storage/pail 13 | .phpunit.result.cache 14 | Homestead.json 15 | Homestead.yaml 16 | npm-debug.log 17 | yarn-error.log 18 | /vendor 19 | .env.backup 20 | /.idea/sonarlint 21 | .phpstorm.meta.php 22 | _ide_helper_models.php 23 | _ide_helper.php 24 | .php-cs-fixer.cache 25 | .husky 26 | .vscode 27 | **/.DS_Store 28 | /public/page-cache 29 | .phpunit.database.checksum 30 | .phpunit.cache 31 | rr 32 | .rr.yaml 33 | frankenphp 34 | .config 35 | .data 36 | .git 37 | -------------------------------------------------------------------------------- /deployment/supervisord.scheduler.conf: -------------------------------------------------------------------------------- 1 | [program:scheduler] 2 | process_name = %(program_name)s_%(process_num)s 3 | command = supercronic -overlapping /etc/supercronic/laravel 4 | user = %(ENV_USER)s 5 | autostart = true 6 | autorestart = true 7 | stopasgroup = true 8 | killasgroup = true 9 | stdout_logfile = /dev/stdout 10 | stdout_logfile_maxbytes = 0 11 | stderr_logfile = /dev/stderr 12 | stderr_logfile_maxbytes = 0 13 | 14 | [program:clear-scheduler-cache] 15 | process_name = %(program_name)s_%(process_num)s 16 | command = php %(ENV_ROOT)s/artisan schedule:clear-cache 17 | user = %(ENV_USER)s 18 | autostart = true 19 | autorestart = false 20 | startsecs = 0 21 | startretries = 1 22 | stdout_logfile = /dev/stdout 23 | stdout_logfile_maxbytes = 0 24 | stderr_logfile = /dev/stderr 25 | stderr_logfile_maxbytes = 0 26 | 27 | [include] 28 | files = /etc/supervisord.conf 29 | -------------------------------------------------------------------------------- /deployment/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 420M 3 | upload_max_filesize = 400M 4 | expose_php = 0 5 | realpath_cache_size = 32M 6 | realpath_cache_ttl = 720 7 | memory_limit = 256M 8 | max_input_time = 5 9 | register_argc_argv = 0 10 | date.timezone = ${TZ:-UTC} 11 | 12 | [Opcache] 13 | opcache.enable = 1 14 | opcache.enable_cli = 1 15 | opcache.memory_consumption = 256 16 | opcache.use_cwd = 0 17 | opcache.save_comments = 1 18 | opcache.max_file_size = 0 19 | opcache.max_accelerated_files = 32531 20 | opcache.validate_timestamps = 0 21 | opcache.file_update_protection = 0 22 | opcache.interned_strings_buffer = 16 23 | opcache.enable_file_override = 1 24 | opcache.file_cache_consistency_checks = 0 25 | opcache.file_cache = /tmp/opcache-file-cache 26 | 27 | [JIT] 28 | opcache.jit_buffer_size = 128M 29 | opcache.jit = tracing 30 | opcache.jit_hot_loop = 16 31 | opcache.jit_hot_func = 32 32 | opcache.jit_hot_return = 4 33 | opcache.jit_max_root_traces = 2048 34 | opcache.jit_max_side_traces = 256 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Exa Company 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /deployment/healthcheck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | container_mode=${CONTAINER_MODE:-"http"} 6 | 7 | if [ "${container_mode}" = "http" ]; then 8 | php "${ROOT}/artisan" octane:status 9 | elif [ "${container_mode}" = "horizon" ]; then 10 | php "${ROOT}/artisan" horizon:status 11 | elif [ "${container_mode}" = "scheduler" ]; then 12 | if [ "$(supervisorctl status scheduler:scheduler_0 | awk '{print tolower($2)}')" = "running" ]; then 13 | exit 0 14 | else 15 | echo "Healthcheck failed." 16 | exit 1 17 | fi 18 | elif [ "${container_mode}" = "reverb" ]; then 19 | if [ "$(supervisorctl status reverb:reverb_0 | awk '{print tolower($2)}')" = "running" ]; then 20 | exit 0 21 | else 22 | echo "Healthcheck failed." 23 | exit 1 24 | fi 25 | elif [ "${container_mode}" = "worker" ]; then 26 | if [ "$(supervisorctl status worker:worker_0 | awk '{print tolower($2)}')" = "running" ]; then 27 | exit 0 28 | else 29 | echo "Healthcheck failed." 30 | exit 1 31 | fi 32 | else 33 | echo "Container mode mismatched." 34 | exit 1 35 | fi 36 | -------------------------------------------------------------------------------- /deployment/octane/FrankenPHP/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | {$CADDY_GLOBAL_OPTIONS} 3 | 4 | admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT} 5 | 6 | auto_https off 7 | 8 | skip_install_trust 9 | 10 | frankenphp { 11 | worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT} 12 | } 13 | 14 | metrics { 15 | per_host 16 | } 17 | 18 | servers { 19 | protocols h1 20 | } 21 | } 22 | 23 | {$CADDY_EXTRA_CONFIG} 24 | 25 | {$CADDY_SERVER_SERVER_NAME} { 26 | log { 27 | level WARN 28 | 29 | format filter { 30 | wrap {$CADDY_SERVER_LOGGER} 31 | fields { 32 | uri query { 33 | replace authorization REDACTED 34 | } 35 | } 36 | } 37 | } 38 | 39 | route { 40 | root * "{$APP_PUBLIC_PATH}" 41 | encode zstd br gzip 42 | 43 | {$CADDY_SERVER_EXTRA_DIRECTIVES} 44 | 45 | request_body { 46 | max_size 500MB 47 | } 48 | 49 | @static { 50 | file 51 | path *.js *.css *.jpg *.jpeg *.webp *.weba *.webm *.gif *.png *.ico *.cur *.gz *.svg *.svgz *.mp4 *.mp3 *.ogg *.ogv *.htc *.woff2 *.woff 52 | } 53 | 54 | @staticshort { 55 | file 56 | path *.json *.xml *.rss 57 | } 58 | 59 | header @static Cache-Control "public, immutable, stale-while-revalidate, max-age=31536000" 60 | 61 | header @staticshort Cache-Control "no-cache, max-age=3600" 62 | 63 | @rejected `path('*.bak', '*.conf', '*.dist', '*.fla', '*.ini', '*.inc', '*.inci', '*.log', '*.orig', '*.psd', '*.sh', '*.sql', '*.swo', '*.swp', '*.swop', '*/.*') && !path('*/.well-known/*')` 64 | error @rejected 401 65 | 66 | php_server { 67 | try_files {path} frankenphp-worker.php 68 | resolve_root_symlink 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | APP_NAME= 2 | VITE_APP_NAME="${APP_NAME}" 3 | APP_ENV=production 4 | APP_KEY=base64:pKFQK9r60jaB/4CB5UOz1sIabob9yF1qt9rIrsS/UUw= 5 | APP_DEBUG=false 6 | APP_HOST=localhost 7 | APP_URL=https://${APP_HOST} 8 | APP_TIMEZONE=UTC 9 | ASSET_URL="/" # for relative asset path 10 | 11 | OCTANE_HTTPS=true 12 | 13 | HASH_DRIVER=argon2id 14 | 15 | LOG_CHANNEL=stack 16 | LOG_STACK=daily,errorlog 17 | LOG_DEPRECATIONS_CHANNEL=null 18 | LOG_LEVEL=debug 19 | 20 | DB_CONNECTION=pgsql 21 | DB_HOST=pgbouncer 22 | DB_PORT=6432 23 | DB_DATABASE=userdb 24 | DB_USERNAME=user 25 | DB_PASSWORD="123456" 26 | 27 | PGADMIN_DEFAULT_EMAIL=user@localhost.test 28 | PGADMIN_DEFAULT_PASSWORD="123456" 29 | 30 | BROADCAST_CONNECTION=reverb 31 | 32 | CACHE_STORE=failover 33 | CACHE_PREFIX="${APP_NAME}_" 34 | 35 | QUEUE_CONNECTION=failover 36 | 37 | SESSION_DRIVER=redis 38 | SESSION_LIFETIME=180 39 | SESSION_COOKIE="_${APP_NAME}" 40 | 41 | MEMCACHED_HOST=127.0.0.1 42 | 43 | REDIS_HOST=redis 44 | REDIS_PASSWORD="123456" 45 | REDIS_PORT=6379 46 | 47 | MAIL_MAILER=smtp 48 | MAIL_DRIVER=smtp 49 | MAIL_HOST=smtp.googlemail.com 50 | MAIL_PORT=465 51 | MAIL_USERNAME= 52 | MAIL_PASSWORD= 53 | MAIL_ENCRYPTION=ssl 54 | MAIL_FROM_ADDRESS= 55 | MAIL_FROM_NAME="${APP_NAME}" 56 | 57 | MINIO_ROOT_USER=user 58 | MINIO_ROOT_PASSWORD="123456789" 59 | 60 | FILESYSTEM_DISK=s3 61 | AWS_ACCESS_KEY_ID=${MINIO_ROOT_USER} 62 | AWS_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD} 63 | AWS_DEFAULT_REGION=us-east-1 64 | AWS_BUCKET=assets 65 | AWS_ENDPOINT=http://minio:9000 66 | AWS_USE_PATH_STYLE_ENDPOINT=true 67 | AWS_URL=${APP_URL}/${AWS_BUCKET} 68 | 69 | SCOUT_DRIVER=typesense 70 | 71 | TYPESENSE_HOST=typesense 72 | TYPESENSE_PORT=8108 73 | TYPESENSE_PROTOCOL=http 74 | TYPESENSE_API_KEY="123456" 75 | 76 | REVERB_APP_ID=123456 77 | REVERB_APP_KEY=user 78 | REVERB_APP_SECRET=123456 79 | REVERB_HOST=${APP_HOST} 80 | REVERB_PORT=8080 81 | REVERB_SCHEME=https 82 | 83 | VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" 84 | VITE_REVERB_HOST="${REVERB_HOST}" 85 | VITE_REVERB_PORT="${REVERB_PORT}" 86 | VITE_REVERB_SCHEME="${REVERB_SCHEME}" 87 | -------------------------------------------------------------------------------- /deployment/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | 4 | container_mode=${CONTAINER_MODE:-"http"} 5 | octane_server=${OCTANE_SERVER} 6 | running_migrations_and_seeders=${RUNNING_MIGRATIONS_AND_SEEDERS:-"false"} 7 | 8 | initialStuff() { 9 | echo "Container mode: $container_mode" 10 | 11 | if [ "${running_migrations_and_seeders}" = "true" ]; then 12 | echo "Running migrations and seeding database ..." 13 | php artisan migrate --isolated --seed --force || php artisan migrate --seed --force 14 | fi 15 | 16 | php artisan storage:link; \ 17 | php artisan optimize; 18 | } 19 | 20 | if [ "$1" != "" ]; then 21 | exec "$@" 22 | elif [ "${container_mode}" = "http" ]; then 23 | initialStuff 24 | echo "Octane Server: $octane_server" 25 | if [ "${octane_server}" = "frankenphp" ]; then 26 | exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.frankenphp.conf 27 | elif [ "${octane_server}" = "swoole" ]; then 28 | exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.swoole.conf 29 | elif [ "${octane_server}" = "roadrunner" ]; then 30 | exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.roadrunner.conf 31 | else 32 | echo "Invalid Octane server supplied." 33 | exit 1 34 | fi 35 | elif [ "${container_mode}" = "horizon" ]; then 36 | initialStuff 37 | exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.horizon.conf 38 | elif [ "${container_mode}" = "reverb" ]; then 39 | initialStuff 40 | exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.reverb.conf 41 | elif [ "${container_mode}" = "scheduler" ]; then 42 | initialStuff 43 | exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.scheduler.conf 44 | elif [ "${container_mode}" = "worker" ]; then 45 | if [ -z "${WORKER_COMMAND}" ]; then 46 | echo "WORKER_COMMAND is undefined." 47 | exit 1 48 | fi 49 | initialStuff 50 | exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.worker.conf 51 | else 52 | echo "Container mode mismatched." 53 | exit 1 54 | fi 55 | -------------------------------------------------------------------------------- /deployment/octane/Swoole/supervisord.swoole.conf: -------------------------------------------------------------------------------- 1 | [program:octane] 2 | process_name = %(program_name)s_%(process_num)s 3 | command = php %(ENV_ROOT)s/artisan octane:swoole --host=0.0.0.0 --port=8000 4 | user = %(ENV_USER)s 5 | priority = 1 6 | autostart = true 7 | autorestart = true 8 | stopasgroup = true 9 | killasgroup = true 10 | environment = LARAVEL_OCTANE = "1" 11 | stdout_logfile = /dev/stdout 12 | stdout_logfile_maxbytes = 0 13 | stderr_logfile = /dev/stderr 14 | stderr_logfile_maxbytes = 0 15 | 16 | [program:horizon] 17 | process_name = %(program_name)s_%(process_num)s 18 | command = php %(ENV_ROOT)s/artisan horizon 19 | user = %(ENV_USER)s 20 | priority = 3 21 | autostart = %(ENV_WITH_HORIZON)s 22 | autorestart = true 23 | stopasgroup = true 24 | killasgroup = true 25 | stdout_logfile = %(ENV_ROOT)s/storage/logs/horizon.log 26 | stdout_logfile_maxbytes = 200MB 27 | stderr_logfile = %(ENV_ROOT)s/storage/logs/horizon.log 28 | stderr_logfile_maxbytes = 200MB 29 | stopwaitsecs = 3600 30 | 31 | [program:scheduler] 32 | process_name = %(program_name)s_%(process_num)s 33 | command = supercronic -overlapping /etc/supercronic/laravel 34 | user = %(ENV_USER)s 35 | autostart = %(ENV_WITH_SCHEDULER)s 36 | autorestart = true 37 | stopasgroup = true 38 | killasgroup = true 39 | stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 40 | stdout_logfile_maxbytes = 200MB 41 | stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 42 | stderr_logfile_maxbytes = 200MB 43 | 44 | [program:clear-scheduler-cache] 45 | process_name = %(program_name)s_%(process_num)s 46 | command = php %(ENV_ROOT)s/artisan schedule:clear-cache 47 | user = %(ENV_USER)s 48 | autostart = %(ENV_WITH_SCHEDULER)s 49 | autorestart = false 50 | startsecs = 0 51 | startretries = 1 52 | stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 53 | stdout_logfile_maxbytes = 200MB 54 | stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 55 | stderr_logfile_maxbytes = 200MB 56 | 57 | [program:reverb] 58 | process_name = %(program_name)s_%(process_num)s 59 | command = php %(ENV_ROOT)s/artisan reverb:start 60 | user = %(ENV_USER)s 61 | priority = 2 62 | autostart = %(ENV_WITH_REVERB)s 63 | autorestart = true 64 | stopasgroup = true 65 | killasgroup = true 66 | stdout_logfile = %(ENV_ROOT)s/storage/logs/reverb.log 67 | stdout_logfile_maxbytes = 200MB 68 | stderr_logfile = %(ENV_ROOT)s/storage/logs/reverb.log 69 | stderr_logfile_maxbytes = 200MB 70 | minfds = 10000 71 | 72 | [include] 73 | files = /etc/supervisord.conf 74 | -------------------------------------------------------------------------------- /deployment/octane/RoadRunner/supervisord.roadrunner.conf: -------------------------------------------------------------------------------- 1 | [program:octane] 2 | process_name = %(program_name)s_%(process_num)s 3 | command = php %(ENV_ROOT)s/artisan octane:roadrunner --host=0.0.0.0 --port=8000 --rpc-port=6001 --rr-config=%(ENV_ROOT)s/.rr.yaml 4 | user = %(ENV_USER)s 5 | priority = 1 6 | autostart = true 7 | autorestart = true 8 | stopasgroup = true 9 | killasgroup = true 10 | environment = LARAVEL_OCTANE = "1" 11 | stdout_logfile = /dev/stdout 12 | stdout_logfile_maxbytes = 0 13 | stderr_logfile = /dev/stderr 14 | stderr_logfile_maxbytes = 0 15 | 16 | [program:horizon] 17 | process_name = %(program_name)s_%(process_num)s 18 | command = php %(ENV_ROOT)s/artisan horizon 19 | user = %(ENV_USER)s 20 | priority = 3 21 | autostart = %(ENV_WITH_HORIZON)s 22 | autorestart = true 23 | stopasgroup = true 24 | killasgroup = true 25 | stdout_logfile = %(ENV_ROOT)s/storage/logs/horizon.log 26 | stdout_logfile_maxbytes = 200MB 27 | stderr_logfile = %(ENV_ROOT)s/storage/logs/horizon.log 28 | stderr_logfile_maxbytes = 200MB 29 | stopwaitsecs = 3600 30 | 31 | [program:scheduler] 32 | process_name = %(program_name)s_%(process_num)s 33 | command = supercronic -overlapping /etc/supercronic/laravel 34 | user = %(ENV_USER)s 35 | autostart = %(ENV_WITH_SCHEDULER)s 36 | autorestart = true 37 | stopasgroup = true 38 | killasgroup = true 39 | stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 40 | stdout_logfile_maxbytes = 200MB 41 | stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 42 | stderr_logfile_maxbytes = 200MB 43 | 44 | [program:clear-scheduler-cache] 45 | process_name = %(program_name)s_%(process_num)s 46 | command = php %(ENV_ROOT)s/artisan schedule:clear-cache 47 | user = %(ENV_USER)s 48 | autostart = %(ENV_WITH_SCHEDULER)s 49 | autorestart = false 50 | startsecs = 0 51 | startretries = 1 52 | stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 53 | stdout_logfile_maxbytes = 200MB 54 | stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 55 | stderr_logfile_maxbytes = 200MB 56 | 57 | [program:reverb] 58 | process_name = %(program_name)s_%(process_num)s 59 | command = php %(ENV_ROOT)s/artisan reverb:start 60 | user = %(ENV_USER)s 61 | priority = 2 62 | autostart = %(ENV_WITH_REVERB)s 63 | autorestart = true 64 | stopasgroup = true 65 | killasgroup = true 66 | stdout_logfile = %(ENV_ROOT)s/storage/logs/reverb.log 67 | stdout_logfile_maxbytes = 200MB 68 | stderr_logfile = %(ENV_ROOT)s/storage/logs/reverb.log 69 | stderr_logfile_maxbytes = 200MB 70 | minfds = 10000 71 | 72 | [include] 73 | files = /etc/supervisord.conf 74 | -------------------------------------------------------------------------------- /deployment/octane/FrankenPHP/supervisord.frankenphp.conf: -------------------------------------------------------------------------------- 1 | [program:octane] 2 | process_name = %(program_name)s_%(process_num)s 3 | command = php %(ENV_ROOT)s/artisan octane:frankenphp --host=0.0.0.0 --port=8000 --admin-host=0.0.0.0 --admin-port=2019 --caddyfile=%(ENV_ROOT)s/deployment/octane/FrankenPHP/Caddyfile 4 | user = %(ENV_USER)s 5 | priority = 1 6 | autostart = true 7 | autorestart = true 8 | stopwaitsecs = 30 9 | stopasgroup = true 10 | killasgroup = true 11 | environment = LARAVEL_OCTANE = "1" 12 | stdout_logfile = /dev/stdout 13 | stdout_logfile_maxbytes = 0 14 | stderr_logfile = /dev/stderr 15 | stderr_logfile_maxbytes = 0 16 | 17 | [program:horizon] 18 | process_name = %(program_name)s_%(process_num)s 19 | command = php %(ENV_ROOT)s/artisan horizon 20 | user = %(ENV_USER)s 21 | priority = 3 22 | autostart = %(ENV_WITH_HORIZON)s 23 | autorestart = true 24 | stopasgroup = true 25 | killasgroup = true 26 | stdout_logfile = %(ENV_ROOT)s/storage/logs/horizon.log 27 | stdout_logfile_maxbytes = 200MB 28 | stderr_logfile = %(ENV_ROOT)s/storage/logs/horizon.log 29 | stderr_logfile_maxbytes = 200MB 30 | stopwaitsecs = 3600 31 | 32 | [program:scheduler] 33 | process_name = %(program_name)s_%(process_num)s 34 | command = supercronic -overlapping /etc/supercronic/laravel 35 | user = %(ENV_USER)s 36 | autostart = %(ENV_WITH_SCHEDULER)s 37 | autorestart = true 38 | stopasgroup = true 39 | killasgroup = true 40 | stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 41 | stdout_logfile_maxbytes = 200MB 42 | stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 43 | stderr_logfile_maxbytes = 200MB 44 | 45 | [program:clear-scheduler-cache] 46 | process_name = %(program_name)s_%(process_num)s 47 | command = php %(ENV_ROOT)s/artisan schedule:clear-cache 48 | user = %(ENV_USER)s 49 | autostart = %(ENV_WITH_SCHEDULER)s 50 | autorestart = false 51 | startsecs = 0 52 | startretries = 1 53 | stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 54 | stdout_logfile_maxbytes = 200MB 55 | stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log 56 | stderr_logfile_maxbytes = 200MB 57 | 58 | [program:reverb] 59 | process_name = %(program_name)s_%(process_num)s 60 | command = php %(ENV_ROOT)s/artisan reverb:start 61 | user = %(ENV_USER)s 62 | priority = 2 63 | autostart = %(ENV_WITH_REVERB)s 64 | autorestart = true 65 | stopasgroup = true 66 | killasgroup = true 67 | stdout_logfile = %(ENV_ROOT)s/storage/logs/reverb.log 68 | stdout_logfile_maxbytes = 200MB 69 | stderr_logfile = %(ENV_ROOT)s/storage/logs/reverb.log 70 | stderr_logfile_maxbytes = 200MB 71 | minfds = 10000 72 | 73 | [include] 74 | files = /etc/supervisord.conf 75 | -------------------------------------------------------------------------------- /.github/workflows/compose-test.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Compose test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - '**.md' 11 | pull_request: 12 | paths-ignore: 13 | - '**.md' 14 | 15 | jobs: 16 | build: 17 | name: Build and Run Docker Compose 18 | runs-on: ubuntu-24.04 19 | timeout-minutes: 30 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | 24 | - name: Setup PHP with Composer and extensions 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | extensions: dom, curl, libxml, mbstring, zip 28 | tools: composer:v2 29 | coverage: none 30 | 31 | - name: Get Composer cache directory 32 | id: composer-cache 33 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 34 | 35 | - name: Cache PHP dependencies 36 | uses: actions/cache@v3 37 | with: 38 | path: ${{ steps.composer-cache.outputs.dir }} 39 | key: "${{ runner.os }}-composer" 40 | restore-keys: "${{ runner.os }}-composer-" 41 | 42 | - name: Create app directory 43 | run: mkdir -p /var/www 44 | 45 | - name: Install a fresh Laravel app 46 | run: sudo composer create-project laravel/laravel app 47 | working-directory: /var/www 48 | 49 | - name: Install Laravel Octane 50 | run: sudo script -e -c "composer require --no-interaction laravel/octane laravel/horizon" 51 | working-directory: /var/www/app 52 | 53 | - name: Install Laravel Reverb 54 | run: sudo script -e -c "php artisan install:broadcasting --reverb --no-interaction --force" 55 | working-directory: /var/www/app 56 | 57 | - name: Copy required content 58 | run: sudo cp -R FrankenPHP.Dockerfile compose.production.yaml Makefile .env.production .dockerignore deployment/ /var/www/app/ 59 | 60 | - name: Prepare the environment 61 | run: sudo mkdir -p storage/framework/{sessions,views,cache,testing} storage/logs && sudo chmod -R a+rw storage 62 | working-directory: /var/www/app 63 | 64 | - name: Run the Docker Compose 65 | run: sudo make down:with-volumes && sudo make build && sudo make up 66 | working-directory: /var/www/app 67 | 68 | - name: Wait for the container 69 | run: sleep 60s 70 | 71 | - name: Print the container logs 72 | run: sudo docker ps -a && docker logs app-app-1 73 | 74 | - name: Check application health 75 | run: curl -f -s -o /dev/null -w "%{http_code}" --insecure https://localhost/up 76 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make 2 | # Makefile readme (en): 3 | 4 | SHELL = /bin/bash 5 | DC_RUN_ARGS = --env-file ./.env.production --profile app --profile administration -f compose.production.yaml 6 | HOST_UID = $(shell if [ $$(id -u) -eq 0 ] && [ $$(id -g) -eq 0 ]; then echo 1000; else id -u; fi) 7 | HOST_GID = $(shell if [ $$(id -u) -eq 0 ] && [ $$(id -g) -eq 0 ]; then echo 1000; else id -g; fi) 8 | 9 | .PHONY: help up down stop shell\:app stop-all ps update build restart down-up images\:list images\:clean logs\:app logs containers\:health command\:app 10 | .DEFAULT_GOAL: help 11 | 12 | # This will output the help for each task. thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 13 | help: ## Show this help 14 | @printf "\033[33m%s:\033[0m\n" 'Available commands' 15 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf " \033[32m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 16 | 17 | up: ## Up containers 18 | HOST_UID=${HOST_UID} HOST_GID=${HOST_GID} docker compose ${DC_RUN_ARGS} up -d --remove-orphans 19 | 20 | logs: ## Tail all containers logs 21 | docker compose ${DC_RUN_ARGS} logs -f 22 | 23 | logs\:app: ## Tail app container logs 24 | docker compose ${DC_RUN_ARGS} logs -f app 25 | 26 | down: ## Stop and remove containers and networks 27 | docker compose ${DC_RUN_ARGS} down 28 | 29 | stop: ## Stop containers 30 | docker compose ${DC_RUN_ARGS} stop 31 | 32 | down\:with-volumes: ## Stop and remove containers and networks and remove volumes 33 | docker compose ${DC_RUN_ARGS} down -v 34 | 35 | shell\:app: ## Start shell into app container 36 | docker compose ${DC_RUN_ARGS} exec app sh 37 | 38 | command\:app: ## Run a command in the app container 39 | docker compose ${DC_RUN_ARGS} exec app sh -c "$(command)" 40 | 41 | stop-all: ## Stop all containers 42 | docker stop $(shell docker ps -a -q) 43 | 44 | ps: ## Containers status 45 | docker compose ${DC_RUN_ARGS} ps 46 | 47 | build: ## Build images 48 | HOST_UID=${HOST_UID} HOST_GID=${HOST_GID} docker compose ${DC_RUN_ARGS} build 49 | 50 | update: ## Update containers 51 | HOST_UID=${HOST_UID} HOST_GID=${HOST_GID} docker compose ${DC_RUN_ARGS} up -d --no-deps --build --remove-orphans 52 | 53 | restart: ## Restart all containers 54 | HOST_UID=${HOST_UID} HOST_GID=${HOST_GID} docker compose ${DC_RUN_ARGS} restart 55 | 56 | down-up: down up ## Down all containers, then up 57 | 58 | images\:list: ## Sort Docker images by size 59 | docker images --format "{{.ID}}\t{{.Size}}\t{{.Repository}}" | sort -k 2 -h 60 | 61 | images\:clean: ## Remove all dangling images and images not referenced by any container 62 | docker image prune -a 63 | 64 | containers\:health: ## Check all containers health 65 | docker compose ${DC_RUN_ARGS} ps --format "table {{.Name}}\t{{.Service}}\t{{.Status}}" 66 | -------------------------------------------------------------------------------- /Swoole.Alpine.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.4 2 | ARG COMPOSER_VERSION=2.8 3 | 4 | FROM composer:${COMPOSER_VERSION} AS vendor 5 | 6 | FROM php:${PHP_VERSION}-cli-alpine 7 | 8 | LABEL maintainer="Mortexa " 9 | LABEL org.opencontainers.image.title="Laravel Docker Setup" 10 | LABEL org.opencontainers.image.description="Production-ready Docker Setup for Laravel" 11 | LABEL org.opencontainers.image.source=https://github.com/exaco/laravel-docktane 12 | LABEL org.opencontainers.image.licenses=MIT 13 | 14 | ARG USER_ID=1000 15 | ARG GROUP_ID=1000 16 | ARG TZ=UTC 17 | 18 | ENV TERM=xterm-color \ 19 | OCTANE_SERVER=swoole \ 20 | TZ=${TZ} \ 21 | USER=laravel \ 22 | ROOT=/var/www/html \ 23 | APP_ENV=production \ 24 | COMPOSER_FUND=0 \ 25 | COMPOSER_MAX_PARALLEL_HTTP=48 \ 26 | WITH_HORIZON=false \ 27 | WITH_SCHEDULER=false \ 28 | WITH_REVERB=false 29 | 30 | WORKDIR ${ROOT} 31 | 32 | SHELL ["/bin/sh", "-eou", "pipefail", "-c"] 33 | 34 | RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ 35 | && echo ${TZ} > /etc/timezone 36 | 37 | ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ 38 | 39 | RUN apk update; \ 40 | apk upgrade; \ 41 | apk add --no-cache \ 42 | curl \ 43 | wget \ 44 | vim \ 45 | tzdata \ 46 | ncdu \ 47 | procps \ 48 | unzip \ 49 | ca-certificates \ 50 | supervisor \ 51 | bash \ 52 | libsodium-dev \ 53 | && curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash \ 54 | && install-php-extensions \ 55 | apcu \ 56 | bz2 \ 57 | pcntl \ 58 | mbstring \ 59 | bcmath \ 60 | sockets \ 61 | pdo_pgsql \ 62 | opcache \ 63 | exif \ 64 | pdo_mysql \ 65 | zip \ 66 | uv \ 67 | intl \ 68 | gd \ 69 | redis \ 70 | rdkafka \ 71 | ffi \ 72 | ldap \ 73 | swoole \ 74 | && docker-php-source delete \ 75 | && rm -rf /var/cache/apk/* /tmp/* /var/tmp/* 76 | 77 | RUN arch="$(apk --print-arch)" \ 78 | && case "$arch" in \ 79 | armhf) _cronic_fname='supercronic-linux-arm' ;; \ 80 | aarch64) _cronic_fname='supercronic-linux-arm64' ;; \ 81 | x86_64) _cronic_fname='supercronic-linux-amd64' ;; \ 82 | x86) _cronic_fname='supercronic-linux-386' ;; \ 83 | *) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \ 84 | esac \ 85 | && wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.38/${_cronic_fname}" \ 86 | -O /usr/bin/supercronic \ 87 | && chmod +x /usr/bin/supercronic \ 88 | && mkdir -p /etc/supercronic \ 89 | && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel 90 | 91 | RUN addgroup -g ${GROUP_ID} ${USER} \ 92 | && adduser -D -G ${USER} -u ${USER_ID} -s /bin/sh ${USER} 93 | 94 | RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini 95 | 96 | COPY --link --from=vendor /usr/bin/composer /usr/bin/composer 97 | COPY --link deployment/supervisord.conf /etc/ 98 | COPY --link deployment/octane/Swoole/supervisord.swoole.conf /etc/supervisor/conf.d/ 99 | COPY --link deployment/supervisord.*.conf /etc/supervisor/conf.d/ 100 | COPY --link deployment/php.ini ${PHP_INI_DIR}/conf.d/99-php.ini 101 | COPY --link deployment/start-container /usr/local/bin/start-container 102 | COPY --link deployment/healthcheck /usr/local/bin/healthcheck 103 | COPY --link composer.* ./ 104 | 105 | RUN composer install \ 106 | --no-dev \ 107 | --no-interaction \ 108 | --no-autoloader \ 109 | --no-ansi \ 110 | --no-scripts \ 111 | --no-progress \ 112 | --audit 113 | 114 | COPY --link package.json bun.lock* ./ 115 | 116 | RUN bun install --frozen-lockfile 117 | 118 | COPY --link . . 119 | 120 | RUN mkdir -p \ 121 | storage/framework/sessions \ 122 | storage/framework/views \ 123 | storage/framework/cache \ 124 | storage/framework/testing \ 125 | storage/logs \ 126 | bootstrap/cache \ 127 | && chown -R ${USER_ID}:${GROUP_ID} ${ROOT} \ 128 | && chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck 129 | 130 | RUN composer dump-autoload \ 131 | --optimize \ 132 | --apcu \ 133 | --no-dev 134 | 135 | RUN bun run build 136 | 137 | USER ${USER} 138 | 139 | EXPOSE 8000 140 | EXPOSE 8080 141 | 142 | ENTRYPOINT ["start-container"] 143 | 144 | HEALTHCHECK --start-period=5s --interval=1s --timeout=3s --retries=10 CMD healthcheck || exit 1 -------------------------------------------------------------------------------- /static-build.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.4 2 | ARG FRANKENPHP_VERSION=1.8 3 | ARG COMPOSER_VERSION=2.8 4 | 5 | FROM composer:${COMPOSER_VERSION} AS vendor 6 | 7 | FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-builder-php${PHP_VERSION} AS upstream 8 | 9 | COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy 10 | 11 | RUN CGO_ENABLED=1 \ 12 | XCADDY_SETCAP=1 \ 13 | XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ 14 | CGO_CFLAGS=$(php-config --includes) \ 15 | CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ 16 | xcaddy build \ 17 | --output /usr/local/bin/frankenphp \ 18 | --with github.com/dunglas/frankenphp=./ \ 19 | --with github.com/dunglas/frankenphp/caddy=./caddy/ \ 20 | --with github.com/dunglas/caddy-cbrotli 21 | 22 | FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} 23 | 24 | COPY --from=upstream /usr/local/bin/frankenphp /usr/local/bin/frankenphp 25 | 26 | LABEL maintainer="Mortexa " 27 | LABEL org.opencontainers.image.title="Laravel Docker Setup" 28 | LABEL org.opencontainers.image.description="Production-ready Docker Setup for Laravel" 29 | LABEL org.opencontainers.image.source=https://github.com/exaco/laravel-docktane 30 | LABEL org.opencontainers.image.licenses=MIT 31 | 32 | ARG USER_ID=1000 33 | ARG GROUP_ID=1000 34 | ARG TZ=UTC 35 | 36 | ENV DEBIAN_FRONTEND=noninteractive \ 37 | TERM=xterm-color \ 38 | OCTANE_SERVER=frankenphp \ 39 | TZ=${TZ} \ 40 | USER=laravel \ 41 | ROOT=/var/www/html \ 42 | APP_ENV=production \ 43 | COMPOSER_FUND=0 \ 44 | COMPOSER_MAX_PARALLEL_HTTP=48 45 | 46 | ENV XDG_CONFIG_HOME=${ROOT}/.config XDG_DATA_HOME=${ROOT}/.data 47 | 48 | WORKDIR ${ROOT} 49 | 50 | SHELL ["/bin/bash", "-eou", "pipefail", "-c"] 51 | 52 | RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ 53 | && echo ${TZ} > /etc/timezone 54 | 55 | RUN echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ 56 | echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom 57 | 58 | RUN apt-get update; \ 59 | apt-get upgrade -yqq; \ 60 | apt-get install -yqq --no-install-recommends --show-progress \ 61 | apt-utils \ 62 | curl \ 63 | wget \ 64 | vim \ 65 | unzip \ 66 | ncdu \ 67 | procps \ 68 | ca-certificates \ 69 | supervisor \ 70 | libsodium-dev \ 71 | && curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash \ 72 | && install-php-extensions \ 73 | apcu \ 74 | bz2 \ 75 | pcntl \ 76 | mbstring \ 77 | bcmath \ 78 | sockets \ 79 | pdo_pgsql \ 80 | opcache \ 81 | exif \ 82 | pdo_mysql \ 83 | zip \ 84 | uv \ 85 | intl \ 86 | gd \ 87 | redis \ 88 | rdkafka \ 89 | ffi \ 90 | ldap \ 91 | && apt-get -y autoremove \ 92 | && apt-get clean \ 93 | && docker-php-source delete \ 94 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/lastlog /var/log/faillog 95 | 96 | RUN userdel --remove --force www-data \ 97 | && groupadd --force -g ${GROUP_ID} ${USER} \ 98 | && useradd -ms /bin/bash --no-log-init --no-user-group -g ${GROUP_ID} -u ${USER_ID} ${USER} \ 99 | && setcap -r /usr/local/bin/frankenphp 100 | 101 | RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini 102 | 103 | USER ${USER} 104 | 105 | COPY --link --from=vendor /usr/bin/composer /usr/bin/composer 106 | COPY --link deployment/php.ini ${PHP_INI_DIR}/conf.d/99-php.ini 107 | COPY --link composer.* ./ 108 | 109 | RUN composer install \ 110 | --no-dev \ 111 | --no-interaction \ 112 | --no-autoloader \ 113 | --no-ansi \ 114 | --no-scripts \ 115 | --no-progress \ 116 | --audit 117 | 118 | COPY --link package.json bun.lock* ./ 119 | 120 | RUN bun install --frozen-lockfile 121 | 122 | COPY --link . . 123 | 124 | RUN mkdir -p \ 125 | storage/framework/{sessions,views,cache,testing} \ 126 | storage/logs \ 127 | bootstrap/cache \ 128 | && chown -R ${USER_ID}:${GROUP_ID} ${ROOT} 129 | 130 | RUN composer dump-autoload \ 131 | --optimize \ 132 | --apcu \ 133 | --no-dev 134 | 135 | RUN bun run build 136 | 137 | ########################################### 138 | 139 | FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-${FRANKENPHP_VERSION} AS static 140 | 141 | WORKDIR /go/src/app/dist/app 142 | 143 | COPY --link --from=runner . . 144 | 145 | RUN rm -Rf tests/ 146 | 147 | ENV NO_COMPRESS=true 148 | 149 | WORKDIR /go/src/app/ 150 | 151 | RUN EMBED=dist/app/ ./build-static.sh 152 | -------------------------------------------------------------------------------- /.github/workflows/swoole-test.yml: -------------------------------------------------------------------------------- 1 | name: Swoole test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - '**.md' 11 | pull_request: 12 | paths-ignore: 13 | - '**.md' 14 | 15 | jobs: 16 | debian-build: 17 | name: Build and Run Debian-based Docker image 18 | runs-on: ubuntu-24.04 19 | strategy: 20 | fail-fast: true 21 | matrix: 22 | php: [ 8.3, 8.4 ] 23 | timeout-minutes: 15 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v2 27 | 28 | - name: Setup PHP with Composer and extensions 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: "${{ matrix.php }}" 32 | extensions: dom, curl, libxml, mbstring, zip 33 | tools: composer:v2 34 | coverage: none 35 | 36 | - name: Get Composer cache directory 37 | id: composer-cache 38 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 39 | 40 | - name: Cache PHP dependencies 41 | uses: actions/cache@v3 42 | with: 43 | path: ${{ steps.composer-cache.outputs.dir }} 44 | key: "${{ runner.os }}-composer-${{ matrix.setup }}" 45 | restore-keys: "${{ runner.os }}-composer-" 46 | 47 | - name: Create app directory 48 | run: mkdir -p /var/www 49 | 50 | - name: Install a fresh Laravel app 51 | run: sudo composer create-project laravel/laravel app 52 | working-directory: /var/www 53 | 54 | - name: Install Laravel Octane 55 | run: sudo composer require laravel/octane 56 | working-directory: /var/www/app 57 | 58 | - name: Copy required content to dockerize the app 59 | run: sudo cp -R Swoole.Dockerfile .dockerignore deployment/ /var/www/app/ 60 | 61 | - name: Build image 62 | run: docker build -t app:local --build-arg PHP_VERSION=${{ matrix.php }} -f Swoole.Dockerfile . 63 | working-directory: /var/www/app 64 | 65 | - name: Run the Docker container 66 | run: docker run -d --name app --rm -p 8000:8000 app:local 67 | working-directory: /var/www/app 68 | 69 | - name: Wait for the container 70 | run: sleep 30s 71 | 72 | - name: Print the container logs 73 | run: docker logs app 74 | 75 | - name: Check application health 76 | run: curl -f -s -o /dev/null -w "%{http_code}" http://localhost:8000/up 77 | 78 | alpine-build: 79 | name: Build and Run Alpine-based Docker image 80 | runs-on: ubuntu-24.04 81 | strategy: 82 | fail-fast: true 83 | matrix: 84 | php: [ 8.3, 8.4 ] 85 | timeout-minutes: 15 86 | steps: 87 | - name: Checkout code 88 | uses: actions/checkout@v2 89 | 90 | - name: Setup PHP with Composer and extensions 91 | uses: shivammathur/setup-php@v2 92 | with: 93 | php-version: "${{ matrix.php }}" 94 | extensions: dom, curl, libxml, mbstring, zip 95 | tools: composer:v2 96 | coverage: none 97 | 98 | - name: Get Composer cache directory 99 | id: composer-cache 100 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 101 | 102 | - name: Cache PHP dependencies 103 | uses: actions/cache@v3 104 | with: 105 | path: ${{ steps.composer-cache.outputs.dir }} 106 | key: "${{ runner.os }}-composer-${{ matrix.setup }}" 107 | restore-keys: "${{ runner.os }}-composer-" 108 | 109 | - name: Create app directory 110 | run: mkdir -p /var/www 111 | 112 | - name: Install a fresh Laravel app 113 | run: sudo composer create-project laravel/laravel app 114 | working-directory: /var/www 115 | 116 | - name: Install Laravel Octane 117 | run: sudo composer require laravel/octane 118 | working-directory: /var/www/app 119 | 120 | - name: Copy required content to dockerize the app 121 | run: sudo cp -R Swoole.Alpine.Dockerfile .dockerignore deployment/ /var/www/app/ 122 | 123 | - name: Build image 124 | run: docker build -t app:local --build-arg PHP_VERSION=${{ matrix.php }} -f Swoole.Alpine.Dockerfile . 125 | working-directory: /var/www/app 126 | 127 | - name: Run the Docker container 128 | run: docker run -d --name app --rm -p 8000:8000 app:local 129 | working-directory: /var/www/app 130 | 131 | - name: Wait for the container 132 | run: sleep 30s 133 | 134 | - name: Print the container logs 135 | run: docker logs app 136 | 137 | - name: Check application health 138 | run: curl -f -s -o /dev/null -w "%{http_code}" http://localhost:8000/up -------------------------------------------------------------------------------- /.github/workflows/frankenphp-test.yml: -------------------------------------------------------------------------------- 1 | name: FrankenPHP test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - '**.md' 11 | pull_request: 12 | paths-ignore: 13 | - '**.md' 14 | 15 | jobs: 16 | debian-build: 17 | name: Build and Run Debian-based Docker image 18 | runs-on: ubuntu-24.04 19 | strategy: 20 | fail-fast: true 21 | matrix: 22 | php: [ 8.3, 8.4 ] 23 | timeout-minutes: 15 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v2 27 | 28 | - name: Setup PHP with Composer and extensions 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: "${{ matrix.php }}" 32 | extensions: dom, curl, libxml, mbstring, zip 33 | tools: composer:v2 34 | coverage: none 35 | 36 | - name: Get Composer cache directory 37 | id: composer-cache 38 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 39 | 40 | - name: Cache PHP dependencies 41 | uses: actions/cache@v3 42 | with: 43 | path: ${{ steps.composer-cache.outputs.dir }} 44 | key: "${{ runner.os }}-composer-${{ matrix.setup }}" 45 | restore-keys: "${{ runner.os }}-composer-" 46 | 47 | - name: Create app directory 48 | run: mkdir -p /var/www 49 | 50 | - name: Install a fresh Laravel app 51 | run: sudo composer create-project laravel/laravel app 52 | working-directory: /var/www 53 | 54 | - name: Install Laravel Octane 55 | run: sudo composer require laravel/octane 56 | working-directory: /var/www/app 57 | 58 | - name: Copy required content to dockerize the app 59 | run: sudo cp -R FrankenPHP.Dockerfile .dockerignore deployment/ /var/www/app/ 60 | 61 | - name: Build image 62 | run: docker build -t app:local --build-arg PHP_VERSION=${{ matrix.php }} -f FrankenPHP.Dockerfile . 63 | working-directory: /var/www/app 64 | 65 | - name: Run the Docker container 66 | run: docker run -d --name app --rm -p 8000:8000 app:local 67 | working-directory: /var/www/app 68 | 69 | - name: Wait for the container 70 | run: sleep 30s 71 | 72 | - name: Print the container logs 73 | run: docker logs app 74 | 75 | - name: Check application health 76 | run: curl -f -s -o /dev/null -w "%{http_code}" http://localhost:8000/up 77 | 78 | alpine-build: 79 | name: Build and Run Alpine-based Docker image 80 | runs-on: ubuntu-24.04 81 | strategy: 82 | fail-fast: true 83 | matrix: 84 | php: [ 8.3, 8.4 ] 85 | timeout-minutes: 15 86 | steps: 87 | - name: Checkout code 88 | uses: actions/checkout@v2 89 | 90 | - name: Setup PHP with Composer and extensions 91 | uses: shivammathur/setup-php@v2 92 | with: 93 | php-version: "${{ matrix.php }}" 94 | extensions: dom, curl, libxml, mbstring, zip 95 | tools: composer:v2 96 | coverage: none 97 | 98 | - name: Get Composer cache directory 99 | id: composer-cache 100 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 101 | 102 | - name: Cache PHP dependencies 103 | uses: actions/cache@v3 104 | with: 105 | path: ${{ steps.composer-cache.outputs.dir }} 106 | key: "${{ runner.os }}-composer-${{ matrix.setup }}" 107 | restore-keys: "${{ runner.os }}-composer-" 108 | 109 | - name: Create app directory 110 | run: mkdir -p /var/www 111 | 112 | - name: Install a fresh Laravel app 113 | run: sudo composer create-project laravel/laravel app 114 | working-directory: /var/www 115 | 116 | - name: Install Laravel Octane 117 | run: sudo composer require laravel/octane 118 | working-directory: /var/www/app 119 | 120 | - name: Copy required content to dockerize the app 121 | run: sudo cp -R FrankenPHP.Alpine.Dockerfile .dockerignore deployment/ /var/www/app/ 122 | 123 | - name: Build image 124 | run: docker build -t app:local --build-arg PHP_VERSION=${{ matrix.php }} -f FrankenPHP.Alpine.Dockerfile . 125 | working-directory: /var/www/app 126 | 127 | - name: Run the Docker container 128 | run: docker run -d --name app --rm -p 8000:8000 app:local 129 | working-directory: /var/www/app 130 | 131 | - name: Wait for the container 132 | run: sleep 30s 133 | 134 | - name: Print the container logs 135 | run: docker logs app 136 | 137 | - name: Check application health 138 | run: curl -f -s -o /dev/null -w "%{http_code}" http://localhost:8000/up -------------------------------------------------------------------------------- /RoadRunner.Alpine.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.4 2 | ARG COMPOSER_VERSION=2.8 3 | 4 | FROM composer:${COMPOSER_VERSION} AS vendor 5 | 6 | FROM php:${PHP_VERSION}-cli-alpine 7 | 8 | LABEL maintainer="Mortexa " 9 | LABEL org.opencontainers.image.title="Laravel Docker Setup" 10 | LABEL org.opencontainers.image.description="Production-ready Docker Setup for Laravel" 11 | LABEL org.opencontainers.image.source=https://github.com/exaco/laravel-docktane 12 | LABEL org.opencontainers.image.licenses=MIT 13 | 14 | ARG USER_ID=1000 15 | ARG GROUP_ID=1000 16 | ARG TZ=UTC 17 | 18 | ENV TERM=xterm-color \ 19 | OCTANE_SERVER=roadrunner \ 20 | TZ=${TZ} \ 21 | USER=laravel \ 22 | ROOT=/var/www/html \ 23 | APP_ENV=production \ 24 | COMPOSER_FUND=0 \ 25 | COMPOSER_MAX_PARALLEL_HTTP=48 \ 26 | WITH_HORIZON=false \ 27 | WITH_SCHEDULER=false \ 28 | WITH_REVERB=false 29 | 30 | WORKDIR ${ROOT} 31 | 32 | SHELL ["/bin/sh", "-eou", "pipefail", "-c"] 33 | 34 | RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ 35 | && echo ${TZ} > /etc/timezone 36 | 37 | ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ 38 | 39 | RUN apk update; \ 40 | apk upgrade; \ 41 | apk add --no-cache \ 42 | curl \ 43 | wget \ 44 | vim \ 45 | tzdata \ 46 | ncdu \ 47 | procps \ 48 | unzip \ 49 | ca-certificates \ 50 | bash \ 51 | supervisor \ 52 | libsodium-dev \ 53 | && curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash \ 54 | && install-php-extensions \ 55 | apcu \ 56 | bz2 \ 57 | pcntl \ 58 | mbstring \ 59 | bcmath \ 60 | sockets \ 61 | pdo_pgsql \ 62 | opcache \ 63 | exif \ 64 | pdo_mysql \ 65 | zip \ 66 | uv \ 67 | intl \ 68 | gd \ 69 | redis \ 70 | rdkafka \ 71 | ffi \ 72 | ldap \ 73 | && docker-php-source delete \ 74 | && rm -rf /var/cache/apk/* /tmp/* /var/tmp/* 75 | 76 | RUN arch="$(apk --print-arch)" \ 77 | && case "$arch" in \ 78 | armhf) _cronic_fname='supercronic-linux-arm' ;; \ 79 | aarch64) _cronic_fname='supercronic-linux-arm64' ;; \ 80 | x86_64) _cronic_fname='supercronic-linux-amd64' ;; \ 81 | x86) _cronic_fname='supercronic-linux-386' ;; \ 82 | *) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \ 83 | esac \ 84 | && wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.38/${_cronic_fname}" \ 85 | -O /usr/bin/supercronic \ 86 | && chmod +x /usr/bin/supercronic \ 87 | && mkdir -p /etc/supercronic \ 88 | && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel 89 | 90 | RUN addgroup -g ${GROUP_ID} ${USER} \ 91 | && adduser -D -G ${USER} -u ${USER_ID} -s /bin/sh ${USER} 92 | 93 | RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini 94 | 95 | COPY --link --from=vendor /usr/bin/composer /usr/bin/composer 96 | COPY --link deployment/supervisord.conf /etc/ 97 | COPY --link deployment/octane/RoadRunner/supervisord.roadrunner.conf /etc/supervisor/conf.d/ 98 | COPY --link deployment/supervisord.*.conf /etc/supervisor/conf.d/ 99 | COPY --link deployment/php.ini ${PHP_INI_DIR}/conf.d/99-php.ini 100 | COPY --link deployment/octane/RoadRunner/.rr.prod.yaml ./.rr.yaml 101 | COPY --link deployment/start-container /usr/local/bin/start-container 102 | COPY --link deployment/healthcheck /usr/local/bin/healthcheck 103 | COPY --link composer.* ./ 104 | 105 | RUN composer install \ 106 | --no-dev \ 107 | --no-interaction \ 108 | --no-autoloader \ 109 | --no-ansi \ 110 | --no-scripts \ 111 | --no-progress \ 112 | --audit 113 | 114 | COPY --link package.json bun.lock* ./ 115 | 116 | RUN bun install --frozen-lockfile 117 | 118 | COPY --link . . 119 | 120 | RUN mkdir -p \ 121 | storage/framework/sessions \ 122 | storage/framework/views \ 123 | storage/framework/cache \ 124 | storage/framework/testing \ 125 | storage/logs \ 126 | bootstrap/cache \ 127 | && chown -R ${USER_ID}:${GROUP_ID} ${ROOT} \ 128 | && chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck 129 | 130 | RUN composer dump-autoload \ 131 | --optimize \ 132 | --apcu \ 133 | --no-dev 134 | 135 | RUN if composer show | grep spiral/roadrunner-cli >/dev/null; then \ 136 | ./vendor/bin/rr get-binary --quiet && chmod +x rr; else \ 137 | echo "`spiral/roadrunner-cli` package is not installed. Exiting..."; exit 1; \ 138 | fi 139 | 140 | RUN bun run build 141 | 142 | USER ${USER} 143 | 144 | EXPOSE 8000 145 | EXPOSE 6001 146 | EXPOSE 8080 147 | 148 | ENTRYPOINT ["start-container"] 149 | 150 | HEALTHCHECK --start-period=5s --interval=1s --timeout=3s --retries=10 CMD healthcheck || exit 1 -------------------------------------------------------------------------------- /Swoole.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.4 2 | ARG COMPOSER_VERSION=2.8 3 | 4 | FROM composer:${COMPOSER_VERSION} AS vendor 5 | 6 | FROM php:${PHP_VERSION}-cli-bookworm 7 | 8 | LABEL maintainer="Mortexa " 9 | LABEL org.opencontainers.image.title="Laravel Docker Setup" 10 | LABEL org.opencontainers.image.description="Production-ready Docker Setup for Laravel" 11 | LABEL org.opencontainers.image.source=https://github.com/exaco/laravel-docktane 12 | LABEL org.opencontainers.image.licenses=MIT 13 | 14 | ARG USER_ID=1000 15 | ARG GROUP_ID=1000 16 | ARG TZ=UTC 17 | 18 | ENV DEBIAN_FRONTEND=noninteractive \ 19 | TERM=xterm-color \ 20 | OCTANE_SERVER=swoole \ 21 | TZ=${TZ} \ 22 | USER=laravel \ 23 | ROOT=/var/www/html \ 24 | APP_ENV=production \ 25 | COMPOSER_FUND=0 \ 26 | COMPOSER_MAX_PARALLEL_HTTP=48 \ 27 | WITH_HORIZON=false \ 28 | WITH_SCHEDULER=false \ 29 | WITH_REVERB=false 30 | 31 | WORKDIR ${ROOT} 32 | 33 | SHELL ["/bin/bash", "-eou", "pipefail", "-c"] 34 | 35 | RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ 36 | && echo ${TZ} > /etc/timezone 37 | 38 | ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ 39 | 40 | RUN echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ 41 | echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom 42 | 43 | RUN apt-get update; \ 44 | apt-get upgrade -yqq; \ 45 | apt-get install -yqq --no-install-recommends --show-progress \ 46 | apt-utils \ 47 | curl \ 48 | wget \ 49 | vim \ 50 | git \ 51 | unzip \ 52 | ncdu \ 53 | procps \ 54 | ca-certificates \ 55 | supervisor \ 56 | libsodium-dev \ 57 | && curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash \ 58 | && install-php-extensions \ 59 | apcu \ 60 | bz2 \ 61 | pcntl \ 62 | mbstring \ 63 | bcmath \ 64 | sockets \ 65 | pdo_pgsql \ 66 | opcache \ 67 | exif \ 68 | pdo_mysql \ 69 | zip \ 70 | uv \ 71 | intl \ 72 | gd \ 73 | redis \ 74 | rdkafka \ 75 | ffi \ 76 | ldap \ 77 | swoole \ 78 | && apt-get -y autoremove \ 79 | && apt-get clean \ 80 | && docker-php-source delete \ 81 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/lastlog /var/log/faillog 82 | 83 | RUN arch="$(uname -m)" \ 84 | && case "$arch" in \ 85 | armhf) _cronic_fname='supercronic-linux-arm' ;; \ 86 | aarch64) _cronic_fname='supercronic-linux-arm64' ;; \ 87 | x86_64) _cronic_fname='supercronic-linux-amd64' ;; \ 88 | x86) _cronic_fname='supercronic-linux-386' ;; \ 89 | *) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \ 90 | esac \ 91 | && wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.38/${_cronic_fname}" \ 92 | -O /usr/bin/supercronic \ 93 | && chmod +x /usr/bin/supercronic \ 94 | && mkdir -p /etc/supercronic \ 95 | && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel 96 | 97 | RUN userdel --remove --force www-data \ 98 | && groupadd --force -g ${GROUP_ID} ${USER} \ 99 | && useradd -ms /bin/bash --no-log-init --no-user-group -g ${GROUP_ID} -u ${USER_ID} ${USER} 100 | 101 | RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini 102 | 103 | COPY --link --from=vendor /usr/bin/composer /usr/bin/composer 104 | COPY --link deployment/supervisord.conf /etc/ 105 | COPY --link deployment/octane/Swoole/supervisord.swoole.conf /etc/supervisor/conf.d/ 106 | COPY --link deployment/supervisord.*.conf /etc/supervisor/conf.d/ 107 | COPY --link deployment/php.ini ${PHP_INI_DIR}/conf.d/99-php.ini 108 | COPY --link deployment/start-container /usr/local/bin/start-container 109 | COPY --link deployment/healthcheck /usr/local/bin/healthcheck 110 | COPY --link composer.* ./ 111 | 112 | RUN composer install \ 113 | --no-dev \ 114 | --no-interaction \ 115 | --no-autoloader \ 116 | --no-ansi \ 117 | --no-scripts \ 118 | --no-progress \ 119 | --audit 120 | 121 | COPY --link package.json bun.lock* ./ 122 | 123 | RUN bun install --frozen-lockfile 124 | 125 | COPY --link . . 126 | 127 | RUN mkdir -p \ 128 | storage/framework/{sessions,views,cache,testing} \ 129 | storage/logs \ 130 | bootstrap/cache \ 131 | && chown -R ${USER_ID}:${GROUP_ID} ${ROOT} \ 132 | && chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck 133 | 134 | RUN composer dump-autoload \ 135 | --optimize \ 136 | --apcu \ 137 | --no-dev 138 | 139 | RUN bun run build 140 | 141 | USER ${USER} 142 | 143 | EXPOSE 8000 144 | EXPOSE 8080 145 | 146 | ENTRYPOINT ["start-container"] 147 | 148 | HEALTHCHECK --start-period=5s --interval=1s --timeout=3s --retries=10 CMD healthcheck || exit 1 -------------------------------------------------------------------------------- /.github/workflows/roadrunner-test.yml: -------------------------------------------------------------------------------- 1 | name: RoadRunner test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - '**.md' 11 | pull_request: 12 | paths-ignore: 13 | - '**.md' 14 | 15 | jobs: 16 | debian-build: 17 | name: Build and Run Debian-based Docker image 18 | runs-on: ubuntu-24.04 19 | strategy: 20 | fail-fast: true 21 | matrix: 22 | php: [ 8.3, 8.4 ] 23 | timeout-minutes: 15 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v2 27 | 28 | - name: Setup PHP with Composer and extensions 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: "${{ matrix.php }}" 32 | extensions: dom, curl, libxml, mbstring, zip 33 | tools: composer:v2 34 | coverage: none 35 | 36 | - name: Get Composer cache directory 37 | id: composer-cache 38 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 39 | 40 | - name: Cache PHP dependencies 41 | uses: actions/cache@v3 42 | with: 43 | path: ${{ steps.composer-cache.outputs.dir }} 44 | key: "${{ runner.os }}-composer-${{ matrix.setup }}" 45 | restore-keys: "${{ runner.os }}-composer-" 46 | 47 | - name: Create app directory 48 | run: mkdir -p /var/www 49 | 50 | - name: Install a fresh Laravel app 51 | run: sudo composer create-project laravel/laravel app 52 | working-directory: /var/www 53 | 54 | - name: Install Laravel Octane 55 | run: sudo composer require laravel/octane spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies 56 | working-directory: /var/www/app 57 | 58 | - name: Copy required content to dockerize the app 59 | run: sudo cp -R RoadRunner.Dockerfile .dockerignore deployment/ /var/www/app/ 60 | 61 | - name: Build image 62 | run: docker build -t app:local --build-arg PHP_VERSION=${{ matrix.php }} -f RoadRunner.Dockerfile . 63 | working-directory: /var/www/app 64 | 65 | - name: Run the Docker container 66 | run: docker run -d --name app --rm -p 8000:8000 app:local 67 | working-directory: /var/www/app 68 | 69 | - name: Wait for the container 70 | run: sleep 30s 71 | 72 | - name: Print the container logs 73 | run: docker logs app 74 | 75 | - name: Check application health 76 | run: curl -f -s -o /dev/null -w "%{http_code}" http://localhost:8000/up 77 | 78 | alpine-build: 79 | name: Build and Run Alpine-based Docker image 80 | runs-on: ubuntu-24.04 81 | strategy: 82 | fail-fast: true 83 | matrix: 84 | php: [ 8.3, 8.4 ] 85 | timeout-minutes: 15 86 | steps: 87 | - name: Checkout code 88 | uses: actions/checkout@v2 89 | 90 | - name: Setup PHP with Composer and extensions 91 | uses: shivammathur/setup-php@v2 92 | with: 93 | php-version: "${{ matrix.php }}" 94 | extensions: dom, curl, libxml, mbstring, zip 95 | tools: composer:v2 96 | coverage: none 97 | 98 | - name: Get Composer cache directory 99 | id: composer-cache 100 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 101 | 102 | - name: Cache PHP dependencies 103 | uses: actions/cache@v3 104 | with: 105 | path: ${{ steps.composer-cache.outputs.dir }} 106 | key: "${{ runner.os }}-composer-${{ matrix.setup }}" 107 | restore-keys: "${{ runner.os }}-composer-" 108 | 109 | - name: Create app directory 110 | run: mkdir -p /var/www 111 | 112 | - name: Install a fresh Laravel app 113 | run: sudo composer create-project laravel/laravel app 114 | working-directory: /var/www 115 | 116 | - name: Install Laravel Octane 117 | run: sudo composer require laravel/octane spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies 118 | working-directory: /var/www/app 119 | 120 | - name: Copy required content to dockerize the app 121 | run: sudo cp -R RoadRunner.Alpine.Dockerfile .dockerignore deployment/ /var/www/app/ 122 | 123 | - name: Build image 124 | run: docker build -t app:local --build-arg PHP_VERSION=${{ matrix.php }} -f RoadRunner.Alpine.Dockerfile . 125 | working-directory: /var/www/app 126 | 127 | - name: Run the Docker container 128 | run: docker run -d --name app --rm -p 8000:8000 app:local 129 | working-directory: /var/www/app 130 | 131 | - name: Wait for the container 132 | run: sleep 30s 133 | 134 | - name: Print the container logs 135 | run: docker logs app 136 | 137 | - name: Check application health 138 | run: curl -f -s -o /dev/null -w "%{http_code}" http://localhost:8000/up -------------------------------------------------------------------------------- /RoadRunner.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.4 2 | ARG COMPOSER_VERSION=2.8 3 | 4 | FROM composer:${COMPOSER_VERSION} AS vendor 5 | 6 | FROM php:${PHP_VERSION}-cli-bookworm 7 | 8 | LABEL maintainer="Mortexa " 9 | LABEL org.opencontainers.image.title="Laravel Docker Setup" 10 | LABEL org.opencontainers.image.description="Production-ready Docker Setup for Laravel" 11 | LABEL org.opencontainers.image.source=https://github.com/exaco/laravel-docktane 12 | LABEL org.opencontainers.image.licenses=MIT 13 | 14 | ARG USER_ID=1000 15 | ARG GROUP_ID=1000 16 | ARG TZ=UTC 17 | 18 | ENV DEBIAN_FRONTEND=noninteractive \ 19 | TERM=xterm-color \ 20 | OCTANE_SERVER=roadrunner \ 21 | TZ=${TZ} \ 22 | USER=laravel \ 23 | ROOT=/var/www/html \ 24 | APP_ENV=production \ 25 | COMPOSER_FUND=0 \ 26 | COMPOSER_MAX_PARALLEL_HTTP=48 \ 27 | WITH_HORIZON=false \ 28 | WITH_SCHEDULER=false \ 29 | WITH_REVERB=false 30 | 31 | WORKDIR ${ROOT} 32 | 33 | SHELL ["/bin/bash", "-eou", "pipefail", "-c"] 34 | 35 | RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ 36 | && echo ${TZ} > /etc/timezone 37 | 38 | ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ 39 | 40 | RUN echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ 41 | echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom 42 | 43 | RUN apt-get update; \ 44 | apt-get upgrade -yqq; \ 45 | apt-get install -yqq --no-install-recommends --show-progress \ 46 | apt-utils \ 47 | curl \ 48 | wget \ 49 | vim \ 50 | git \ 51 | unzip \ 52 | ncdu \ 53 | procps \ 54 | ca-certificates \ 55 | supervisor \ 56 | libsodium-dev \ 57 | && curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash \ 58 | && install-php-extensions \ 59 | apcu \ 60 | bz2 \ 61 | pcntl \ 62 | mbstring \ 63 | bcmath \ 64 | sockets \ 65 | pdo_pgsql \ 66 | opcache \ 67 | exif \ 68 | pdo_mysql \ 69 | zip \ 70 | uv \ 71 | intl \ 72 | gd \ 73 | redis \ 74 | rdkafka \ 75 | ffi \ 76 | ldap \ 77 | && apt-get -y autoremove \ 78 | && apt-get clean \ 79 | && docker-php-source delete \ 80 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/lastlog /var/log/faillog 81 | 82 | RUN arch="$(uname -m)" \ 83 | && case "$arch" in \ 84 | armhf) _cronic_fname='supercronic-linux-arm' ;; \ 85 | aarch64) _cronic_fname='supercronic-linux-arm64' ;; \ 86 | x86_64) _cronic_fname='supercronic-linux-amd64' ;; \ 87 | x86) _cronic_fname='supercronic-linux-386' ;; \ 88 | *) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \ 89 | esac \ 90 | && wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.38/${_cronic_fname}" \ 91 | -O /usr/bin/supercronic \ 92 | && chmod +x /usr/bin/supercronic \ 93 | && mkdir -p /etc/supercronic \ 94 | && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel 95 | 96 | RUN userdel --remove --force www-data \ 97 | && groupadd --force -g ${GROUP_ID} ${USER} \ 98 | && useradd -ms /bin/bash --no-log-init --no-user-group -g ${GROUP_ID} -u ${USER_ID} ${USER} 99 | 100 | RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini 101 | 102 | COPY --link --from=vendor /usr/bin/composer /usr/bin/composer 103 | COPY --link deployment/supervisord.conf /etc/ 104 | COPY --link deployment/octane/RoadRunner/supervisord.roadrunner.conf /etc/supervisor/conf.d/ 105 | COPY --link deployment/supervisord.*.conf /etc/supervisor/conf.d/ 106 | COPY --link deployment/php.ini ${PHP_INI_DIR}/conf.d/99-php.ini 107 | COPY --link deployment/octane/RoadRunner/.rr.prod.yaml ./.rr.yaml 108 | COPY --link deployment/start-container /usr/local/bin/start-container 109 | COPY --link deployment/healthcheck /usr/local/bin/healthcheck 110 | COPY --link composer.* ./ 111 | 112 | RUN composer install \ 113 | --no-dev \ 114 | --no-interaction \ 115 | --no-autoloader \ 116 | --no-ansi \ 117 | --no-scripts \ 118 | --no-progress \ 119 | --audit 120 | 121 | COPY --link package.json bun.lock* ./ 122 | 123 | RUN bun install --frozen-lockfile 124 | 125 | COPY --link . . 126 | 127 | RUN mkdir -p \ 128 | storage/framework/{sessions,views,cache,testing} \ 129 | storage/logs \ 130 | bootstrap/cache \ 131 | && chown -R ${USER_ID}:${GROUP_ID} ${ROOT} \ 132 | && chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck 133 | 134 | RUN composer dump-autoload \ 135 | --optimize \ 136 | --apcu \ 137 | --no-dev 138 | 139 | RUN if composer show | grep spiral/roadrunner-cli >/dev/null; then \ 140 | ./vendor/bin/rr get-binary --quiet && chmod +x rr; else \ 141 | echo "`spiral/roadrunner-cli` package is not installed. Exiting..."; exit 1; \ 142 | fi 143 | 144 | RUN bun run build 145 | 146 | USER ${USER} 147 | 148 | EXPOSE 8000 149 | EXPOSE 6001 150 | EXPOSE 8080 151 | 152 | ENTRYPOINT ["start-container"] 153 | 154 | HEALTHCHECK --start-period=5s --interval=1s --timeout=3s --retries=10 CMD healthcheck || exit 1 -------------------------------------------------------------------------------- /FrankenPHP.Alpine.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.4 2 | ARG FRANKENPHP_VERSION=1.9 3 | ARG COMPOSER_VERSION=2.8 4 | 5 | FROM composer:${COMPOSER_VERSION} AS vendor 6 | 7 | FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-builder-php${PHP_VERSION}-alpine AS upstream 8 | 9 | COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy 10 | 11 | RUN CGO_ENABLED=1 \ 12 | XCADDY_SETCAP=1 \ 13 | XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ 14 | CGO_CFLAGS=$(php-config --includes) \ 15 | CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ 16 | xcaddy build \ 17 | --output /usr/local/bin/frankenphp \ 18 | --with github.com/dunglas/frankenphp=./ \ 19 | --with github.com/dunglas/frankenphp/caddy=./caddy/ \ 20 | --with github.com/dunglas/caddy-cbrotli 21 | 22 | FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION}-alpine 23 | 24 | COPY --from=upstream /usr/local/bin/frankenphp /usr/local/bin/frankenphp 25 | 26 | LABEL maintainer="Mortexa " 27 | LABEL org.opencontainers.image.title="Laravel Docker Setup" 28 | LABEL org.opencontainers.image.description="Production-ready Docker Setup for Laravel" 29 | LABEL org.opencontainers.image.source=https://github.com/exaco/laravel-docktane 30 | LABEL org.opencontainers.image.licenses=MIT 31 | 32 | ARG USER_ID=1000 33 | ARG GROUP_ID=1000 34 | ARG TZ=UTC 35 | 36 | ENV TERM=xterm-color \ 37 | OCTANE_SERVER=frankenphp \ 38 | TZ=${TZ} \ 39 | USER=laravel \ 40 | ROOT=/var/www/html \ 41 | APP_ENV=production \ 42 | COMPOSER_FUND=0 \ 43 | COMPOSER_MAX_PARALLEL_HTTP=48 \ 44 | WITH_HORIZON=false \ 45 | WITH_SCHEDULER=false \ 46 | WITH_REVERB=false 47 | 48 | ENV XDG_CONFIG_HOME=${ROOT}/.config XDG_DATA_HOME=${ROOT}/.data 49 | 50 | WORKDIR ${ROOT} 51 | 52 | SHELL ["/bin/sh", "-eou", "pipefail", "-c"] 53 | 54 | RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ 55 | && echo ${TZ} > /etc/timezone 56 | 57 | RUN apk update; \ 58 | apk upgrade; \ 59 | apk add --no-cache \ 60 | curl \ 61 | wget \ 62 | vim \ 63 | tzdata \ 64 | ncdu \ 65 | procps \ 66 | unzip \ 67 | ca-certificates \ 68 | bash \ 69 | supervisor \ 70 | libsodium-dev \ 71 | && curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash \ 72 | && install-php-extensions \ 73 | apcu \ 74 | bz2 \ 75 | pcntl \ 76 | mbstring \ 77 | bcmath \ 78 | sockets \ 79 | pdo_pgsql \ 80 | opcache \ 81 | exif \ 82 | pdo_mysql \ 83 | zip \ 84 | uv \ 85 | intl \ 86 | gd \ 87 | redis \ 88 | rdkafka \ 89 | ffi \ 90 | ldap \ 91 | && docker-php-source delete \ 92 | && rm -rf /var/cache/apk/* /tmp/* /var/tmp/* 93 | 94 | RUN arch="$(apk --print-arch)" \ 95 | && case "$arch" in \ 96 | armhf) _cronic_fname='supercronic-linux-arm' ;; \ 97 | aarch64) _cronic_fname='supercronic-linux-arm64' ;; \ 98 | x86_64) _cronic_fname='supercronic-linux-amd64' ;; \ 99 | x86) _cronic_fname='supercronic-linux-386' ;; \ 100 | *) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \ 101 | esac \ 102 | && wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.38/${_cronic_fname}" \ 103 | -O /usr/bin/supercronic \ 104 | && chmod +x /usr/bin/supercronic \ 105 | && mkdir -p /etc/supercronic \ 106 | && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel 107 | 108 | RUN addgroup -g ${GROUP_ID} ${USER} \ 109 | && adduser -D -G ${USER} -u ${USER_ID} -s /bin/sh ${USER} 110 | 111 | RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini 112 | 113 | COPY --link --from=vendor /usr/bin/composer /usr/bin/composer 114 | COPY --link deployment/supervisord.conf /etc/ 115 | COPY --link deployment/octane/FrankenPHP/supervisord.frankenphp.conf /etc/supervisor/conf.d/ 116 | COPY --link deployment/supervisord.*.conf /etc/supervisor/conf.d/ 117 | COPY --link deployment/start-container /usr/local/bin/start-container 118 | COPY --link deployment/healthcheck /usr/local/bin/healthcheck 119 | COPY --link deployment/php.ini ${PHP_INI_DIR}/conf.d/99-php.ini 120 | COPY --link composer.* ./ 121 | 122 | RUN composer install \ 123 | --no-dev \ 124 | --no-interaction \ 125 | --no-autoloader \ 126 | --no-ansi \ 127 | --no-scripts \ 128 | --no-progress \ 129 | --audit 130 | 131 | COPY --link package.json bun.lock* ./ 132 | 133 | RUN bun install --frozen-lockfile 134 | 135 | COPY --link . . 136 | 137 | RUN mkdir -p \ 138 | storage/framework/sessions \ 139 | storage/framework/views \ 140 | storage/framework/cache \ 141 | storage/framework/testing \ 142 | storage/logs \ 143 | bootstrap/cache \ 144 | && chown -R ${USER_ID}:${GROUP_ID} ${ROOT} \ 145 | && chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck 146 | 147 | RUN composer dump-autoload \ 148 | --optimize \ 149 | --apcu \ 150 | --no-dev 151 | 152 | RUN bun run build 153 | 154 | USER ${USER} 155 | 156 | EXPOSE 8000 157 | EXPOSE 2019 158 | EXPOSE 8080 159 | 160 | ENTRYPOINT ["start-container"] 161 | 162 | HEALTHCHECK --start-period=5s --interval=1s --timeout=3s --retries=10 CMD healthcheck || exit 1 -------------------------------------------------------------------------------- /FrankenPHP.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.4 2 | ARG FRANKENPHP_VERSION=1.9 3 | ARG COMPOSER_VERSION=2.8 4 | 5 | FROM composer:${COMPOSER_VERSION} AS vendor 6 | 7 | FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-builder-php${PHP_VERSION} AS upstream 8 | 9 | COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy 10 | 11 | RUN CGO_ENABLED=1 \ 12 | XCADDY_SETCAP=1 \ 13 | XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ 14 | CGO_CFLAGS=$(php-config --includes) \ 15 | CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ 16 | xcaddy build \ 17 | --output /usr/local/bin/frankenphp \ 18 | --with github.com/dunglas/frankenphp=./ \ 19 | --with github.com/dunglas/frankenphp/caddy=./caddy/ \ 20 | --with github.com/dunglas/caddy-cbrotli 21 | 22 | FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} 23 | 24 | COPY --from=upstream /usr/local/bin/frankenphp /usr/local/bin/frankenphp 25 | 26 | LABEL maintainer="Mortexa " 27 | LABEL org.opencontainers.image.title="Laravel Docker Setup" 28 | LABEL org.opencontainers.image.description="Production-ready Docker Setup for Laravel" 29 | LABEL org.opencontainers.image.source=https://github.com/exaco/laravel-docktane 30 | LABEL org.opencontainers.image.licenses=MIT 31 | 32 | ARG USER_ID=1000 33 | ARG GROUP_ID=1000 34 | ARG TZ=UTC 35 | 36 | ENV DEBIAN_FRONTEND=noninteractive \ 37 | TERM=xterm-color \ 38 | OCTANE_SERVER=frankenphp \ 39 | TZ=${TZ} \ 40 | USER=laravel \ 41 | ROOT=/var/www/html \ 42 | APP_ENV=production \ 43 | COMPOSER_FUND=0 \ 44 | COMPOSER_MAX_PARALLEL_HTTP=48 \ 45 | WITH_HORIZON=false \ 46 | WITH_SCHEDULER=false \ 47 | WITH_REVERB=false 48 | 49 | ENV XDG_CONFIG_HOME=${ROOT}/.config XDG_DATA_HOME=${ROOT}/.data 50 | 51 | WORKDIR ${ROOT} 52 | 53 | SHELL ["/bin/bash", "-eou", "pipefail", "-c"] 54 | 55 | RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ 56 | && echo ${TZ} > /etc/timezone 57 | 58 | RUN echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ 59 | echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom 60 | 61 | RUN apt-get update; \ 62 | apt-get upgrade -yqq; \ 63 | apt-get install -yqq --no-install-recommends --show-progress \ 64 | apt-utils \ 65 | curl \ 66 | wget \ 67 | vim \ 68 | git \ 69 | unzip \ 70 | ncdu \ 71 | procps \ 72 | ca-certificates \ 73 | supervisor \ 74 | libsodium-dev \ 75 | && curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash \ 76 | && install-php-extensions \ 77 | apcu \ 78 | bz2 \ 79 | pcntl \ 80 | mbstring \ 81 | bcmath \ 82 | sockets \ 83 | pdo_pgsql \ 84 | opcache \ 85 | exif \ 86 | pdo_mysql \ 87 | zip \ 88 | uv \ 89 | intl \ 90 | gd \ 91 | redis \ 92 | rdkafka \ 93 | ffi \ 94 | ldap \ 95 | && apt-get -y autoremove \ 96 | && apt-get clean \ 97 | && docker-php-source delete \ 98 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/lastlog /var/log/faillog 99 | 100 | RUN arch="$(uname -m)" \ 101 | && case "$arch" in \ 102 | armhf) _cronic_fname='supercronic-linux-arm' ;; \ 103 | aarch64) _cronic_fname='supercronic-linux-arm64' ;; \ 104 | x86_64) _cronic_fname='supercronic-linux-amd64' ;; \ 105 | x86) _cronic_fname='supercronic-linux-386' ;; \ 106 | *) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \ 107 | esac \ 108 | && wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.38/${_cronic_fname}" \ 109 | -O /usr/bin/supercronic \ 110 | && chmod +x /usr/bin/supercronic \ 111 | && mkdir -p /etc/supercronic \ 112 | && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel 113 | 114 | RUN userdel --remove --force www-data \ 115 | && groupadd --force -g ${GROUP_ID} ${USER} \ 116 | && useradd -ms /bin/bash --no-log-init --no-user-group -g ${GROUP_ID} -u ${USER_ID} ${USER} 117 | 118 | RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini 119 | 120 | COPY --link --from=vendor /usr/bin/composer /usr/bin/composer 121 | COPY --link deployment/supervisord.conf /etc/ 122 | COPY --link deployment/octane/FrankenPHP/supervisord.frankenphp.conf /etc/supervisor/conf.d/ 123 | COPY --link deployment/supervisord.*.conf /etc/supervisor/conf.d/ 124 | COPY --link deployment/start-container /usr/local/bin/start-container 125 | COPY --link deployment/healthcheck /usr/local/bin/healthcheck 126 | COPY --link deployment/php.ini ${PHP_INI_DIR}/conf.d/99-php.ini 127 | COPY --link composer.* ./ 128 | 129 | RUN composer install \ 130 | --no-dev \ 131 | --no-interaction \ 132 | --no-autoloader \ 133 | --no-ansi \ 134 | --no-scripts \ 135 | --no-progress \ 136 | --audit 137 | 138 | COPY --link package.json bun.lock* ./ 139 | 140 | RUN bun install --frozen-lockfile 141 | 142 | COPY --link . . 143 | 144 | RUN mkdir -p \ 145 | storage/framework/{sessions,views,cache,testing} \ 146 | storage/logs \ 147 | bootstrap/cache \ 148 | && chown -R ${USER_ID}:${GROUP_ID} ${ROOT} \ 149 | && chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck 150 | 151 | RUN composer dump-autoload \ 152 | --optimize \ 153 | --apcu \ 154 | --no-dev 155 | 156 | RUN bun run build 157 | 158 | USER ${USER} 159 | 160 | EXPOSE 8000 161 | EXPOSE 2019 162 | EXPOSE 8080 163 | 164 | ENTRYPOINT ["start-container"] 165 | 166 | HEALTHCHECK --start-period=5s --interval=1s --timeout=3s --retries=10 CMD healthcheck || exit 1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Laravel Docker Setup

3 |
4 |
5 | License 6 | GitHub release (latest by date) 7 | GitHub closed pull requests 8 |
9 | GitHub Workflow Status 10 | GitHub Workflow Status 11 | GitHub Workflow Status 12 | GitHub Workflow Status 13 |
14 |
15 | 16 | A production-ready Docker setup for running high-performance Laravel applications with Laravel Octane. This repository provides Dockerfiles and a comprehensive Docker Compose configuration for various Octane drivers, including Swoole, RoadRunner, and FrankenPHP. 17 | 18 | The setup is optimized for performance and includes multi-stage builds to create lean final images. 19 | 20 | ## Key Features 21 | 22 | - **Multiple Drivers:** Dockerfiles available for Swoole, RoadRunner, and FrankenPHP. 23 | - **Production-Ready:** Optimized for a production environment with best practices. 24 | - **Multi-Stage Builds:** Creates smaller, more secure Docker images by separating build dependencies from the final runtime image. 25 | - **Container Modes:** Easily run your container in different modes for handling web requests (`http`), queues (`horizon`), scheduled tasks (`scheduler`), custom worker (`worker`), or WebSocket server (`reverb`). 26 | - **Extensible:** Simple to customize for your specific application needs. 27 | - **Comprehensive Docker Compose:** Includes a production-ready `compose.production.yaml` to orchestrate the full application stack. 28 | 29 | 30 | ## Container modes 31 | 32 | Easily launch your container in different modes to handle specific tasks: 33 | 34 | 35 | | Mode | `CONTAINER_MODE` value | Description 36 | | --------------------- | ---------------- | ---------------- | 37 | | HTTP Server (default) | `http` | Runs your Laravel Octane application. | 38 | | Horizon | `horizon` | Manages your queued jobs efficiently. | 39 | | Scheduler | `scheduler` | Executes scheduled tasks at defined intervals. | 40 | | Worker | `worker` | A dedicated worker for background processing. | 41 | | Reverb | `reverb` | Facilitates real-time communication with Laravel Echo. | 42 | 43 | ## Production-Ready Docker Compose 44 | 45 | For a complete production environment, this repository includes a `compose.production.yaml` file to orchestrate a full stack of services. This setup is security-hardened and provides a comprehensive solution for deploying and managing your application. 46 | 47 | The orchestrated containers include: 48 | 49 | - **Application:** Your Laravel Octane application running in http mode to serve web requests. 50 | - **Horizon:** A dedicated container for running Laravel Horizon to manage your Redis queues. 51 | - **Scheduler:** A container responsible for executing Laravel's scheduled tasks. 52 | - **Database:** A PostgreSQL container for your application's database. 53 | - **PgBouncer:** Lightweight connection pooler for PostgreSQL. 54 | - **Redis:** An in-memory data store used for caching and as a message broker for Laravel Horizon. 55 | - **Minio:** An S3-compatible object storage service, perfect for handling file uploads and storage. 56 | - **Typesense:** A fast, typo-tolerant, and open-source search engine for building powerful search functionality into your application. 57 | - **pgAdmin & pghero:** Web-based tools for managing your PostgreSQL database and monitoring its performance. 58 | - **Backup Service:** A container that performs automated backups of your database to ensure data safety. 59 | - **System Monitoring:** Includes Netdata containers to provide real-time insights and monitoring for your entire infrastructure. 60 | - **Prometheus:** A powerful time-series database used for collecting metrics from your application and the host system. 61 | - **Grafana:** A leading open-source platform for monitoring and observability, used to visualize the metrics collected by Prometheus in beautiful dashboards. 62 | 63 | This comprehensive stack provides a robust and observable environment for your production application. 64 | 65 | ## Prerequisites 66 | 67 | - Docker installed on your system 68 | - Docker Compose installed on your system 69 | - Setup Laravel Octane, Laravel Horizon and Laravel Reverb 70 | 71 | ## Usage 72 | 73 | ### Building Docker image 74 | 75 | 1. Clone the repository: 76 | ``` 77 | git clone --depth 1 git@github.com:exaco/laravel-docktane.git 78 | ``` 79 | 2. Copy the contents of the cloned directory, including the following items, into your Laravel project powered by Octane: 80 | - `deployment` directory 81 | - `.Dockerfile` 82 | - `.dockerignore` 83 | 84 | 1. Change the directory to your Laravel project 85 | 2. Build your image: 86 | ``` 87 | docker build -t : -f .Dockerfile . 88 | ``` 89 | 90 | ### Running Docker container 91 | 92 | ```bash 93 | # HTTP mode 94 | docker run -p :8000 --rm : 95 | 96 | # Horizon mode 97 | docker run -e CONTAINER_MODE=horizon --rm : 98 | 99 | # Scheduler mode 100 | docker run -e CONTAINER_MODE=scheduler --rm : 101 | 102 | # Reverb mode 103 | docker run -e CONTAINER_MODE=reverb --rm : 104 | 105 | # HTTP mode with Horizon 106 | docker run -e WITH_HORIZON=true -p :8000 --rm : 107 | 108 | # HTTP mode with Scheduler 109 | docker run -e WITH_SCHEDULER=true -p :8000 --rm : 110 | 111 | # HTTP mode with Scheduler and Horizon 112 | docker run \ 113 | -e WITH_SCHEDULER=true \ 114 | -e WITH_HORIZON=true \ 115 | -p :8000 \ 116 | --rm : 117 | 118 | # HTTP mode with Scheduler, Horizon and Reverb 119 | docker run \ 120 | -e WITH_SCHEDULER=true \ 121 | -e WITH_HORIZON=true \ 122 | -e WITH_REVERB=true \ 123 | -p :8000 \ 124 | --rm : 125 | 126 | # Worker mode 127 | docker run \ 128 | -e CONTAINER_MODE=worker \ 129 | -e WORKER_COMMAND="php /var/www/html/artisan foo:bar" \ 130 | --rm : 131 | 132 | # Running a single command 133 | docker run --rm : php artisan about 134 | ``` 135 | 136 | ### Docker Compose 137 | 138 | To deploy your application stack with Docker Compose: 139 | 1. Copy the following items to your code base: 140 | - `compose.production.yaml` 141 | - `.env.production` 142 | - `Makefile` 143 | 2. Edit `.env.production` and populate it with the appropriate values for your production environment variables (e.g., database credentials, API keys). 144 | 3. Run the command `make up` to start the containers. 145 | 146 | > [!NOTE] 147 | > The included `Makefile` offers a range of additional commands for managing your deployment, including options for rebuilding, stopping, and restarting services. 148 | 149 | > [!CAUTION] 150 | > Do not forget to edit `.env.production`! 151 | 152 | ## Configuration and Customization 153 | 154 | * You can use the `APP_ENV` build argument to specify a different environment file. 155 | 156 | ### Recommended options in `octane.php` 157 | 158 | ```php 159 | // config/octane.php 160 | 161 | return [ 162 | 'swoole' => [ 163 | 'options' => [ 164 | 'http_compression' => true, 165 | 'http_compression_level' => 6, // 1 - 9 166 | 'compression_min_length' => 20, 167 | 'package_max_length' => 2 * 1024 * 1024, // 2MB 168 | 'upload_max_filesize' => 20 * 1024 * 1024, // 20MB 169 | 'open_http2_protocol' => true, 170 | 'document_root' => public_path(), 171 | 'enable_static_handler' => true, 172 | ] 173 | ], 174 | 175 | // https://github.com/laravel/octane/pull/853#issuecomment-1999530137 176 | 'state_file' => base_path('bootstrap/octane-server-state.json'), 177 | 178 | // https://github.com/laravel/octane/pull/902/files 179 | 'usleep_between_writing_server_output' => 1, 180 | ]; 181 | ``` 182 | 183 | ## Essential Notes 184 | 185 | * Some configurations are highly opinionated, so please make sure they align with your needs. 186 | * Laravel Octane logs request information only in the `local` environment. 187 | * Be mindful of the contents of the `.dockerignore` file. 188 | 189 | ## ToDo 190 | - [x] Add Docker Compose 191 | - [x] Add support for PHP 8.4 192 | - [x] Add support for worker mode 193 | - [x] Build assets with Bun 194 | - [x] Install more Caddy modules 195 | - [x] Create standalone and self-executable app 196 | - [x] Add support for Horizon 197 | - [x] Add support for RoadRunner 198 | - [x] Add support for FrankenPHP 199 | - [x] Add support for Laravel Reverb 200 | - [x] Add support for the full-stack apps (Front-end assets) 201 | - [ ] Add support `testing` environment and CI 202 | - [x] Add support for the Laravel scheduler 203 | - [ ] Add support for Laravel Dusk 204 | - [x] Support more PHP extensions 205 | - [x] Add tests 206 | - [x] Add Alpine-based images 207 | 208 | ## Contributing 209 | 210 | Thank you for considering contributing! If you find an issue, or have a better way to do something, feel free to open an 211 | issue, or a PR. 212 | 213 | ## Credits 214 | 215 | - [Mortexa](https://github.com/smortexa) 216 | - [All contributors](https://github.com/exaco/laravel-docktane/graphs/contributors) 217 | 218 | ## License 219 | 220 | This repository is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 221 | -------------------------------------------------------------------------------- /compose.production.yaml: -------------------------------------------------------------------------------- 1 | x-logging: &default-logging 2 | driver: 'json-file' 3 | options: 4 | max-size: "50m" 5 | max-file: "10" 6 | compress: "true" 7 | x-healthcheck: &default-healthcheck 8 | interval: 1s 9 | retries: 10 10 | timeout: 3s 11 | x-base: &base 12 | profiles: [app] 13 | depends_on: 14 | pgbouncer: 15 | condition: service_started 16 | redis: 17 | condition: service_healthy 18 | minio: 19 | condition: service_healthy 20 | build: 21 | context: . 22 | dockerfile: FrankenPHP.Dockerfile 23 | cache_from: 24 | - 'laravel/app:latest' 25 | args: 26 | USER_ID: ${HOST_UID:-1000} 27 | GROUP_ID: ${HOST_GID:-1000} 28 | image: 'laravel/app:latest' 29 | user: "${HOST_UID:-1000}:${HOST_GID:-1000}" 30 | ulimits: 31 | nofile: 32 | soft: 65536 33 | hard: 65536 34 | security_opt: 35 | - no-new-privileges:true 36 | networks: 37 | - stack 38 | volumes: 39 | - './storage/app/public:/var/www/html/storage/app/public' 40 | - './storage/logs:/var/www/html/storage/logs' 41 | logging: *default-logging 42 | restart: unless-stopped 43 | services: 44 | traefik: 45 | profiles: [app] 46 | image: traefik:v3.6 47 | restart: unless-stopped 48 | stop_grace_period: 35s 49 | ulimits: 50 | nofile: 51 | soft: 65536 52 | hard: 65536 53 | security_opt: 54 | - no-new-privileges:true 55 | command: 56 | - "--log.level=ERROR" 57 | - "--log.format=common" 58 | - "--ping=false" 59 | - "--api=true" 60 | - "--accesslog=true" 61 | - "--accesslog.format=common" 62 | - "--metrics.prometheus=true" 63 | - "--providers.docker=true" 64 | - "--providers.docker.exposedByDefault=false" 65 | - "--providers.docker.network=stack" 66 | - "--entryPoints.traefik.address=:8190" 67 | - "--entryPoints.app.address=:80" 68 | - "--entryPoints.app.http.redirections.entryPoint.to=app-secure" 69 | - "--entryPoints.app.http.redirections.entryPoint.scheme=https" 70 | - "--entryPoints.app-secure.address=:443" 71 | - "--entryPoints.app-secure.http3=true" 72 | - "--entryPoints.app-secure.transport.lifeCycle.graceTimeOut=30s" 73 | - "--entryPoints.pgadmin.address=:6053" 74 | - "--entryPoints.pghero.address=:6660" 75 | - "--entryPoints.minio-console.address=:8900" 76 | - "--entryPoints.netdata.address=:19999" 77 | - "--entryPoints.prometheus.address=:9090" 78 | - "--entryPoints.grafana.address=:3000" 79 | ports: 80 | - "127.0.0.1:8190:8190" # Traefik 81 | - "80:80" # HTTP 82 | - "443:443" # HTTPS 83 | - "443:443/udp" # HTTP/3 84 | - "127.0.0.1:6053:6053" # pgAdmin 85 | - "127.0.0.1:6660:6660" # PgHero 86 | - "127.0.0.1:8900:8900" # MinIO console 87 | - "127.0.0.1:19999:19999" # NetData 88 | - "127.0.0.1:9090:9090" # Prometheus 89 | - "127.0.0.1:3000:3000" # Grafana 90 | networks: 91 | - stack 92 | volumes: 93 | - /var/run/docker.sock:/var/run/docker.sock:ro 94 | logging: *default-logging 95 | labels: 96 | traefik.enable: true 97 | traefik.http.routers.traefik.rule: Host(`localhost`) 98 | traefik.http.routers.traefik.service: api@internal 99 | traefik.http.routers.traefik.entryPoints: traefik 100 | traefik.http.routers.traefik.middlewares: "traefik-auth,traefik-retry" 101 | traefik.http.middlewares.traefik-retry.retry.attempts: 4 102 | traefik.http.middlewares.traefik-retry.retry.initialinterval: 100ms 103 | traefik.http.middlewares.traefik-auth.basicauth.removeheader: true 104 | traefik.http.middlewares.traefik-auth.basicauth.users: "${TRAEFIK_AUTH_USERS:-user:$$2y$$05$$8zbpsdxg9wDiiKdqxiB0zeAlxZtG68P1SDBOvCN4IooLFewLx70Gm}" 105 | app: 106 | <<: *base 107 | healthcheck: 108 | test: ["CMD", "curl", "--fail", "--max-time", "10", "localhost:8000/up"] 109 | <<: *default-healthcheck 110 | stop_grace_period: 35s 111 | labels: 112 | traefik.enable: true 113 | traefik.http.routers.app-secure.rule: Host(`localhost`) || Host(`app.docker.localhost`) 114 | traefik.http.routers.app-secure.entryPoints: app-secure 115 | traefik.http.routers.app-secure.priority: 10 116 | traefik.http.routers.app-secure.service: app-service 117 | traefik.http.routers.app-secure.tls: true 118 | traefik.http.routers.app-secure.middlewares: "app-retry,app-compress,security-headers" 119 | traefik.http.services.app-service.loadbalancer.server.port: 8000 # Because container exposes multiple ports 120 | traefik.http.services.app-service.loadbalancer.healthCheck.path: "/up" 121 | traefik.http.services.app-service.loadbalancer.healthCheck.hostname: "localhost" 122 | traefik.http.services.app-service.loadbalancer.healthCheck.port: 8000 123 | traefik.http.services.app-service.loadbalancer.healthCheck.interval: 2s 124 | traefik.http.services.app-service.loadbalancer.healthCheck.timeout: 5s 125 | traefik.http.middlewares.limit.buffering.maxRequestBodyBytes: 560000000 # 560mb 126 | traefik.http.middlewares.app-retry.retry.attempts: 4 127 | traefik.http.middlewares.app-retry.retry.initialinterval: 100ms 128 | traefik.http.middlewares.app-compress.compress: true 129 | traefik.http.middlewares.security-headers.headers.accesscontrolmaxage: 100 130 | traefik.http.middlewares.security-headers.headers.addvaryheader: true # Vary: Origin 131 | traefik.http.middlewares.security-headers.headers.hostsproxyheaders: X-Forwarded-Host 132 | traefik.http.middlewares.security-headers.headers.stsseconds: 63072000 # Strict-Transport-Security: max-age=63072000; includeSubDomains; preload 133 | traefik.http.middlewares.security-headers.headers.stsincludesubdomains: true 134 | traefik.http.middlewares.security-headers.headers.stspreload: true 135 | traefik.http.middlewares.security-headers.headers.forcestsheader: true 136 | traefik.http.middlewares.security-headers.headers.customFrameOptionsValue: SAMEORIGIN # X-Frame-Options: same-origin 137 | traefik.http.middlewares.security-headers.headers.contenttypenosniff: true # X-Content-Type-Options: nosniff 138 | traefik.http.middlewares.security-headers.headers.browserxssfilter: true # X-XSS-Protection: 1; mode=block 139 | traefik.http.middlewares.security-headers.headers.referrerpolicy: strict-origin-when-cross-origin 140 | traefik.http.middlewares.security-headers.headers.permissionspolicy: "camera=(), geolocation=(), microphone=(), payment=(), usb=(), interest-cohort=(), gyroscope=()" 141 | traefik.http.middlewares.security-headers.headers.customresponseheaders.X-Robots-Tag: "noindex, nofollow" 142 | horizon: 143 | <<: *base 144 | environment: 145 | CONTAINER_MODE: horizon 146 | labels: 147 | traefik.enable: false 148 | scheduler: 149 | <<: *base 150 | environment: 151 | CONTAINER_MODE: scheduler 152 | labels: 153 | traefik.enable: false 154 | reverb: 155 | <<: *base 156 | environment: 157 | CONTAINER_MODE: reverb 158 | labels: 159 | traefik.enable: true 160 | traefik.http.routers.reverb.rule: (Host(`localhost`) || Host(`app.docker.localhost`)) && PathPrefix(`/app`) 161 | traefik.http.routers.reverb.entryPoints: app-secure 162 | traefik.http.routers.reverb.priority: 30 163 | traefik.http.routers.reverb.tls: true 164 | traefik.http.routers.reverb.middlewares: "reverb-retry" 165 | traefik.http.middlewares.reverb-retry.retry.attempts: 4 166 | traefik.http.middlewares.reverb-retry.retry.initialinterval: 100ms 167 | traefik.http.routers.reverb.service: reverb-service 168 | traefik.http.services.reverb-service.loadbalancer.server.port: 8080 169 | redis: 170 | profiles: [app] 171 | image: 'redis:7-alpine' 172 | ulimits: 173 | nofile: 174 | soft: 65536 175 | hard: 65536 176 | command: [ 177 | "redis-server", 178 | "--requirepass", "${REDIS_PASSWORD}", 179 | "--maxmemory", "${REDIS_MAXMEMORY:-2gb}", 180 | "--maxmemory-policy", "allkeys-lru", 181 | "--save", "900 1", 182 | "--save", "300 10", 183 | "--save", "60 10000" 184 | ] 185 | security_opt: 186 | - no-new-privileges:true 187 | volumes: 188 | - 'stack-redis:/data' 189 | networks: 190 | - stack 191 | logging: *default-logging 192 | healthcheck: 193 | test: ["CMD", "redis-cli", "--pass", "${REDIS_PASSWORD}", "ping"] 194 | <<: *default-healthcheck 195 | restart: unless-stopped 196 | deploy: 197 | resources: 198 | limits: 199 | memory: 2.5G 200 | labels: 201 | traefik.enable: false 202 | pgsql: 203 | profiles: [app] 204 | image: 'postgres:${POSTGRES_VERSION:-18}-alpine' 205 | ulimits: 206 | nofile: 207 | soft: 65536 208 | hard: 65536 209 | # command: ["-c", "config_file=/etc/postgresql/postgresql.conf"] 210 | security_opt: 211 | - no-new-privileges:true 212 | environment: 213 | PGPASSWORD: '${DB_PASSWORD}' 214 | POSTGRES_DB: '${DB_DATABASE}' 215 | POSTGRES_USER: '${DB_USERNAME}' 216 | POSTGRES_PASSWORD: '${DB_PASSWORD}' 217 | volumes: 218 | # - './postgresql.conf:/etc/postgresql/postgresql.conf' 219 | - 'stack-pgsql:/var/lib/postgresql' 220 | - '../backup:/backup' 221 | networks: 222 | - stack 223 | healthcheck: 224 | test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE}"] 225 | <<: *default-healthcheck 226 | restart: unless-stopped 227 | logging: *default-logging 228 | labels: 229 | traefik.enable: false 230 | docker-volume-backup.stop-during-backup: true 231 | docker-volume-backup.archive-pre: /bin/sh -c 'pg_dump -U ${DB_USERNAME} -F t ${DB_DATABASE} > /backup/${DB_DATABASE}-database.tar' 232 | pgbouncer: 233 | image: bitnamilegacy/pgbouncer:latest 234 | restart: unless-stopped 235 | logging: *default-logging 236 | depends_on: 237 | pgsql: 238 | condition: service_healthy 239 | environment: 240 | POSTGRESQL_HOST: pgsql 241 | POSTGRESQL_PORT: 5432 242 | PGBOUNCER_DATABASE: '${DB_DATABASE}' 243 | POSTGRESQL_USERNAME: '${DB_USERNAME}' 244 | POSTGRESQL_PASSWORD: '${DB_PASSWORD}' 245 | PGBOUNCER_POOL_MODE: session 246 | PGBOUNCER_AUTH_TYPE: md5 247 | PGBOUNCER_MAX_CLIENT_CONN: 500 248 | PGBOUNCER_DEFAULT_POOL_SIZE: 25 249 | PGBOUNCER_USERLIST: '"${DB_USERNAME}" "${DB_PASSWORD}"' 250 | networks: 251 | - stack 252 | labels: 253 | traefik.enable: false 254 | pgadmin: 255 | profiles: [administration] 256 | image: 'dpage/pgadmin4:latest' 257 | security_opt: 258 | - no-new-privileges:true 259 | depends_on: 260 | pgsql: 261 | condition: service_healthy 262 | environment: 263 | PGADMIN_DEFAULT_EMAIL: '${PGADMIN_DEFAULT_EMAIL}' 264 | PGADMIN_DEFAULT_PASSWORD: '${PGADMIN_DEFAULT_PASSWORD}' 265 | PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: 'True' 266 | volumes: 267 | - 'stack-pgadmin:/var/lib/pgadmin' 268 | networks: 269 | - stack 270 | restart: unless-stopped 271 | logging: *default-logging 272 | labels: 273 | traefik.enable: true 274 | traefik.http.routers.pgadmin.rule: Host(`localhost`) 275 | traefik.http.routers.pgadmin.entryPoints: pgadmin 276 | traefik.http.routers.pgadmin.middlewares: "pgadmin-auth,pgadmin-retry" 277 | traefik.http.middlewares.pgadmin-retry.retry.attempts: 4 278 | traefik.http.middlewares.pgadmin-retry.retry.initialinterval: 100ms 279 | traefik.http.middlewares.pgadmin-auth.basicauth.removeheader: true 280 | traefik.http.middlewares.pgadmin-auth.basicauth.users: "user:$$2y$$05$$8zbpsdxg9wDiiKdqxiB0zeAlxZtG68P1SDBOvCN4IooLFewLx70Gm" # user:123456 281 | pghero: 282 | profiles: [administration] 283 | image: ankane/pghero 284 | security_opt: 285 | - no-new-privileges:true 286 | depends_on: 287 | pgsql: 288 | condition: service_healthy 289 | environment: 290 | PORT: 6660 291 | DATABASE_URL: postgres://${DB_USERNAME}:${DB_PASSWORD}@pgsql:5432/${DB_DATABASE} 292 | networks: 293 | - stack 294 | restart: unless-stopped 295 | logging: *default-logging 296 | labels: 297 | traefik.enable: true 298 | traefik.http.routers.pghero.rule: Host(`localhost`) 299 | traefik.http.routers.pghero.entryPoints: pghero 300 | traefik.http.routers.pghero.middlewares: "pghero-auth,pghero-retry" 301 | traefik.http.middlewares.pghero-retry.retry.attempts: 4 302 | traefik.http.middlewares.pghero-retry.retry.initialinterval: 100ms 303 | traefik.http.middlewares.pghero-auth.basicauth.removeheader: true 304 | traefik.http.middlewares.pghero-auth.basicauth.users: "user:$$2y$$05$$8zbpsdxg9wDiiKdqxiB0zeAlxZtG68P1SDBOvCN4IooLFewLx70Gm" # user:123456 305 | traefik.http.routers.pghero.service: pghero-service 306 | traefik.http.services.pghero-service.loadbalancer.server.port: 6660 307 | typesense: 308 | profiles: [app] 309 | image: 'typesense/typesense:29.0' 310 | security_opt: 311 | - no-new-privileges:true 312 | environment: 313 | TYPESENSE_DATA_DIR: '${TYPESENSE_DATA_DIR:-/typesense-data}' 314 | TYPESENSE_API_KEY: '${TYPESENSE_API_KEY}' 315 | TYPESENSE_ENABLE_CORS: '${TYPESENSE_ENABLE_CORS:-true}' 316 | volumes: 317 | - 'stack-typesense:/typesense-data' 318 | networks: 319 | - stack 320 | healthcheck: 321 | test: [CMD, bash, -c, "exec 3<>/dev/tcp/localhost/8108 && printf 'GET /health HTTP/1.1\\r\\nConnection: close\\r\\n\\r\\n' >&3 && head -n1 <&3 | grep '200' && exec 3>&-"] 322 | <<: *default-healthcheck 323 | restart: unless-stopped 324 | logging: *default-logging 325 | labels: 326 | traefik.enable: false 327 | backup: 328 | profiles: [administration] 329 | image: offen/docker-volume-backup:v2 330 | security_opt: 331 | - no-new-privileges:true 332 | environment: 333 | BACKUP_FILENAME: backup-%Y-%m-%dT%H-%M-%S.tar.gz 334 | BACKUP_PRUNING_PREFIX: backup- 335 | BACKUP_CRON_EXPRESSION: "0 2 * * *" # run every day at 2am 336 | BACKUP_RETENTION_DAYS: '7' 337 | restart: unless-stopped 338 | depends_on: 339 | pgsql: 340 | condition: service_healthy 341 | logging: *default-logging 342 | volumes: 343 | - stack-pgsql:/backup/pgsql:ro 344 | - ../backup/volumes:/archive 345 | - /var/run/docker.sock:/var/run/docker.sock:ro 346 | - /etc/timezone:/etc/timezone:ro 347 | - /etc/localtime:/etc/localtime:ro 348 | labels: 349 | traefik.enable: false 350 | minio: 351 | profiles: [app] 352 | image: 'minio/minio:latest' 353 | security_opt: 354 | - no-new-privileges:true 355 | environment: 356 | MINIO_ROOT_USER: '${MINIO_ROOT_USER}' 357 | MINIO_ROOT_PASSWORD: '${MINIO_ROOT_PASSWORD}' 358 | volumes: 359 | - 'stack-minio:/data/minio' 360 | networks: 361 | - stack 362 | command: 'minio server /data/minio --console-address ":8900"' 363 | restart: unless-stopped 364 | logging: *default-logging 365 | labels: 366 | traefik.enable: true 367 | traefik.http.routers.minio-console.rule: Host(`localhost`) 368 | traefik.http.routers.minio-console.entryPoints: minio-console 369 | traefik.http.routers.minio-console.service: minio-console-service 370 | traefik.http.routers.minio-console.middlewares: "minio-auth,minio-retry" 371 | traefik.http.services.minio-console-service.loadbalancer.server.port: 8900 372 | traefik.http.routers.minio.rule: (Host(`localhost`) || Host(`app.docker.localhost`)) && PathPrefix(`/${AWS_BUCKET}`) 373 | traefik.http.routers.minio.entryPoints: app-secure 374 | traefik.http.routers.minio.priority: 20 375 | traefik.http.routers.minio.tls: true 376 | traefik.http.routers.minio.service: minio-service 377 | traefik.http.routers.minio.middlewares: "minio-retry,minio-compress" 378 | traefik.http.services.minio-service.loadbalancer.server.port: 9000 379 | traefik.http.middlewares.minio-compress.compress: true 380 | traefik.http.middlewares.minio-retry.retry.attempts: 4 381 | traefik.http.middlewares.minio-retry.retry.initialinterval: 100ms 382 | traefik.http.middlewares.minio-auth.basicauth.removeheader: true 383 | traefik.http.middlewares.minio-auth.basicauth.users: "user:$$2y$$05$$8zbpsdxg9wDiiKdqxiB0zeAlxZtG68P1SDBOvCN4IooLFewLx70Gm" # user:123456 384 | healthcheck: 385 | test: [ "CMD", "mc", "ready", "local" ] 386 | <<: *default-healthcheck 387 | netdata: 388 | profiles: [administration] 389 | image: netdata/netdata 390 | restart: unless-stopped 391 | cap_add: 392 | - SYS_PTRACE 393 | - SYS_ADMIN 394 | security_opt: 395 | - apparmor:unconfined 396 | logging: *default-logging 397 | networks: 398 | - stack 399 | volumes: 400 | - stack-netdataconfig:/etc/netdata 401 | - stack-netdatalib:/var/lib/netdata 402 | - stack-netdatacache:/var/cache/netdata 403 | - /:/host/root:ro,rslave 404 | - /etc/passwd:/host/etc/passwd:ro 405 | - /etc/group:/host/etc/group:ro 406 | - /etc/localtime:/etc/localtime:ro 407 | - /proc:/host/proc:ro 408 | - /sys:/host/sys:ro 409 | - /etc/os-release:/host/etc/os-release:ro 410 | - /var/log:/host/var/log:ro 411 | - /var/run/docker.sock:/var/run/docker.sock:ro 412 | labels: 413 | org.label-schema.group: "monitoring" 414 | traefik.enable: true 415 | traefik.http.routers.netdata.rule: Host(`localhost`) 416 | traefik.http.routers.netdata.entryPoints: netdata 417 | traefik.http.routers.netdata.middlewares: "netdata-auth" 418 | traefik.http.middlewares.netdata-auth.basicauth.removeheader: true 419 | traefik.http.middlewares.netdata-auth.basicauth.users: "user:$$2y$$05$$8zbpsdxg9wDiiKdqxiB0zeAlxZtG68P1SDBOvCN4IooLFewLx70Gm" # user:123456 420 | prometheus: 421 | profiles: [administration] 422 | image: prom/prometheus:v3.4.2 423 | restart: unless-stopped 424 | logging: *default-logging 425 | volumes: 426 | - ./deployment/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 427 | - stack-prometheus:/prometheus 428 | command: 429 | - '--config.file=/etc/prometheus/prometheus.yml' 430 | - '--storage.tsdb.path=/prometheus' 431 | - '--web.console.libraries=/etc/prometheus/console_libraries' 432 | - '--web.console.templates=/etc/prometheus/consoles' 433 | - '--storage.tsdb.retention.time=200h' 434 | - '--storage.tsdb.retention.size=10GB' 435 | - '--web.enable-lifecycle' 436 | networks: 437 | - stack 438 | labels: 439 | org.label-schema.group: "monitoring" 440 | traefik.enable: true 441 | traefik.http.routers.prometheus.rule: Host(`localhost`) 442 | traefik.http.routers.prometheus.entryPoints: prometheus 443 | traefik.http.routers.prometheus.middlewares: "prometheus-auth" 444 | traefik.http.middlewares.prometheus-auth.basicauth.removeheader: true 445 | traefik.http.middlewares.prometheus-auth.basicauth.users: "user:$$2y$$05$$8zbpsdxg9wDiiKdqxiB0zeAlxZtG68P1SDBOvCN4IooLFewLx70Gm" 446 | grafana: 447 | profiles: [administration] 448 | image: grafana/grafana:12.0.2 449 | restart: unless-stopped 450 | logging: *default-logging 451 | volumes: 452 | - ./deployment/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards 453 | - ./deployment/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources 454 | - stack-grafana:/var/lib/grafana 455 | environment: 456 | - GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin} 457 | - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin} 458 | - GF_USERS_ALLOW_SIGN_UP=false 459 | - GF_SERVER_ENABLE_GZIP=true 460 | networks: 461 | - stack 462 | labels: 463 | org.label-schema.group: "monitoring" 464 | traefik.enable: true 465 | traefik.http.routers.grafana.rule: Host(`localhost`) 466 | traefik.http.routers.grafana.entryPoints: grafana 467 | traefik.http.routers.grafana.middlewares: "grafana-auth" 468 | traefik.http.middlewares.grafana-auth.basicauth.removeheader: true 469 | traefik.http.middlewares.grafana-auth.basicauth.users: "user:$$2y$$05$$8zbpsdxg9wDiiKdqxiB0zeAlxZtG68P1SDBOvCN4IooLFewLx70Gm" 470 | networks: 471 | stack: 472 | driver: bridge 473 | driver_opts: 474 | com.docker.network.driver.mtu: 1450 475 | volumes: 476 | stack-pgsql: 477 | driver: local 478 | stack-redis: 479 | driver: local 480 | stack-pgadmin: 481 | driver: local 482 | stack-minio: 483 | driver: local 484 | stack-typesense: 485 | driver: local 486 | stack-netdataconfig: 487 | driver: local 488 | stack-netdatalib: 489 | driver: local 490 | stack-netdatacache: 491 | driver: local 492 | stack-prometheus: 493 | driver: local 494 | stack-grafana: 495 | driver: local 496 | -------------------------------------------------------------------------------- /deployment/grafana/provisioning/dashboards/caddy.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "9.0.0" 18 | }, 19 | { 20 | "type": "datasource", 21 | "id": "prometheus", 22 | "name": "Prometheus", 23 | "version": "1.0.0" 24 | } 25 | ], 26 | "annotations": { 27 | "list": [ 28 | { 29 | "builtIn": 1, 30 | "datasource": { 31 | "type": "grafana", 32 | "uid": "-- Grafana --" 33 | }, 34 | "enable": true, 35 | "hide": true, 36 | "iconColor": "rgba(0, 211, 255, 1)", 37 | "name": "Annotations & Alerts", 38 | "type": "dashboard" 39 | } 40 | ] 41 | }, 42 | "editable": true, 43 | "fiscalYearStartMonth": 0, 44 | "graphTooltip": 0, 45 | "id": null, 46 | "links": [], 47 | "liveNow": false, 48 | "panels": [ 49 | { 50 | "gridPos": { 51 | "h": 7, 52 | "w": 4, 53 | "x": 0, 54 | "y": 0 55 | }, 56 | "id": 100, 57 | "options": { 58 | "colorMode": "value", 59 | "graphMode": "area", 60 | "justifyMode": "auto", 61 | "orientation": "auto", 62 | "reduceOptions": { 63 | "calcs": [ 64 | "lastNotNull" 65 | ], 66 | "fields": "", 67 | "values": false 68 | }, 69 | "textMode": "auto" 70 | }, 71 | "pluginVersion": "10.1.0", 72 | "targets": [ 73 | { 74 | "datasource": { 75 | "type": "prometheus", 76 | "uid": "prometheus" 77 | }, 78 | "expr": "sum(rate(caddy_http_requests_total{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__rate_interval]))", 79 | "instant": false, 80 | "legendFormat": "__auto", 81 | "range": true, 82 | "refId": "A" 83 | } 84 | ], 85 | "title": "Requests per Second (RPS)", 86 | "type": "stat", 87 | "fieldConfig": { 88 | "defaults": { 89 | "color": { 90 | "mode": "thresholds" 91 | }, 92 | "mappings": [], 93 | "thresholds": { 94 | "mode": "absolute", 95 | "steps": [ 96 | { 97 | "color": "green", 98 | "value": null 99 | } 100 | ] 101 | }, 102 | "unit": "reqps" 103 | }, 104 | "overrides": [] 105 | } 106 | }, 107 | { 108 | "gridPos": { 109 | "h": 7, 110 | "w": 4, 111 | "x": 4, 112 | "y": 0 113 | }, 114 | "id": 122, 115 | "options": { 116 | "colorMode": "value", 117 | "graphMode": "area", 118 | "justifyMode": "auto", 119 | "orientation": "auto", 120 | "reduceOptions": { 121 | "calcs": [ 122 | "lastNotNull" 123 | ], 124 | "fields": "", 125 | "values": false 126 | }, 127 | "textMode": "auto" 128 | }, 129 | "pluginVersion": "10.1.0", 130 | "targets": [ 131 | { 132 | "datasource": { 133 | "type": "prometheus", 134 | "uid": "prometheus" 135 | }, 136 | "expr": "histogram_quantile(0.50, sum(rate(caddy_http_request_duration_seconds_bucket{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__rate_interval])) by (le)) * 1000", 137 | "instant": false, 138 | "legendFormat": "__auto", 139 | "range": true, 140 | "refId": "A" 141 | } 142 | ], 143 | "title": "Median (p50) Duration", 144 | "type": "stat", 145 | "fieldConfig": { 146 | "defaults": { 147 | "color": { 148 | "mode": "thresholds" 149 | }, 150 | "mappings": [], 151 | "thresholds": { 152 | "mode": "absolute", 153 | "steps": [ 154 | { 155 | "color": "green", 156 | "value": null 157 | }, 158 | { 159 | "color": "orange", 160 | "value": 500 161 | }, 162 | { 163 | "color": "red", 164 | "value": 1000 165 | } 166 | ] 167 | }, 168 | "unit": "ms" 169 | }, 170 | "overrides": [] 171 | } 172 | }, 173 | { 174 | "gridPos": { 175 | "h": 7, 176 | "w": 4, 177 | "x": 8, 178 | "y": 0 179 | }, 180 | "id": 102, 181 | "options": { 182 | "colorMode": "value", 183 | "graphMode": "area", 184 | "justifyMode": "auto", 185 | "orientation": "auto", 186 | "reduceOptions": { 187 | "calcs": [ 188 | "lastNotNull" 189 | ], 190 | "fields": "", 191 | "values": false 192 | }, 193 | "textMode": "auto" 194 | }, 195 | "pluginVersion": "10.1.0", 196 | "targets": [ 197 | { 198 | "datasource": { 199 | "type": "prometheus", 200 | "uid": "prometheus" 201 | }, 202 | "expr": "histogram_quantile(0.95, sum(rate(caddy_http_request_duration_seconds_bucket{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__rate_interval])) by (le)) * 1000", 203 | "instant": false, 204 | "legendFormat": "__auto", 205 | "range": true, 206 | "refId": "A" 207 | } 208 | ], 209 | "title": "95th Percentile Latency", 210 | "type": "stat", 211 | "fieldConfig": { 212 | "defaults": { 213 | "color": { 214 | "mode": "thresholds" 215 | }, 216 | "mappings": [], 217 | "thresholds": { 218 | "mode": "absolute", 219 | "steps": [ 220 | { 221 | "color": "green", 222 | "value": null 223 | }, 224 | { 225 | "color": "orange", 226 | "value": 500 227 | }, 228 | { 229 | "color": "red", 230 | "value": 1000 231 | } 232 | ] 233 | }, 234 | "unit": "ms" 235 | }, 236 | "overrides": [] 237 | } 238 | }, 239 | { 240 | "gridPos": { 241 | "h": 7, 242 | "w": 4, 243 | "x": 12, 244 | "y": 0 245 | }, 246 | "id": 104, 247 | "options": { 248 | "colorMode": "value", 249 | "graphMode": "area", 250 | "justifyMode": "auto", 251 | "orientation": "auto", 252 | "reduceOptions": { 253 | "calcs": [ 254 | "lastNotNull" 255 | ], 256 | "fields": "", 257 | "values": false 258 | }, 259 | "textMode": "auto" 260 | }, 261 | "pluginVersion": "10.1.0", 262 | "targets": [ 263 | { 264 | "datasource": { 265 | "type": "prometheus", 266 | "uid": "prometheus" 267 | }, 268 | "expr": "sum(caddy_http_requests_in_flight{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"})", 269 | "instant": false, 270 | "legendFormat": "__auto", 271 | "range": true, 272 | "refId": "A" 273 | } 274 | ], 275 | "title": "In-Flight Requests", 276 | "type": "stat", 277 | "fieldConfig": { 278 | "defaults": { 279 | "color": { 280 | "mode": "thresholds" 281 | }, 282 | "mappings": [], 283 | "thresholds": { 284 | "mode": "absolute", 285 | "steps": [ 286 | { 287 | "color": "green", 288 | "value": null 289 | } 290 | ] 291 | }, 292 | "unit": "short" 293 | }, 294 | "overrides": [] 295 | } 296 | }, 297 | { 298 | "gridPos": { 299 | "h": 7, 300 | "w": 4, 301 | "x": 16, 302 | "y": 0 303 | }, 304 | "id": 120, 305 | "options": { 306 | "colorMode": "value", 307 | "graphMode": "area", 308 | "justifyMode": "auto", 309 | "orientation": "auto", 310 | "reduceOptions": { 311 | "calcs": [ 312 | "lastNotNull" 313 | ], 314 | "fields": "", 315 | "values": false 316 | }, 317 | "textMode": "auto" 318 | }, 319 | "pluginVersion": "10.1.0", 320 | "targets": [ 321 | { 322 | "datasource": { 323 | "type": "prometheus", 324 | "uid": "prometheus" 325 | }, 326 | "expr": "sum(increase(caddy_http_requests_total{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__range]))", 327 | "instant": false, 328 | "legendFormat": "__auto", 329 | "range": true, 330 | "refId": "A" 331 | } 332 | ], 333 | "title": "Total Requests Processed", 334 | "type": "stat", 335 | "fieldConfig": { 336 | "defaults": { 337 | "color": { 338 | "mode": "thresholds" 339 | }, 340 | "mappings": [], 341 | "thresholds": { 342 | "mode": "absolute", 343 | "steps": [ 344 | { 345 | "color": "green", 346 | "value": null 347 | } 348 | ] 349 | }, 350 | "unit": "short" 351 | }, 352 | "overrides": [] 353 | } 354 | }, 355 | { 356 | "gridPos": { 357 | "h": 7, 358 | "w": 4, 359 | "x": 20, 360 | "y": 0 361 | }, 362 | "id": 106, 363 | "options": { 364 | "colorMode": "background", 365 | "graphMode": "none", 366 | "justifyMode": "auto", 367 | "orientation": "auto", 368 | "reduceOptions": { 369 | "calcs": [ 370 | "lastNotNull" 371 | ], 372 | "fields": "", 373 | "values": false 374 | }, 375 | "textMode": "auto" 376 | }, 377 | "pluginVersion": "10.1.0", 378 | "targets": [ 379 | { 380 | "datasource": { 381 | "type": "prometheus", 382 | "uid": "prometheus" 383 | }, 384 | "expr": "caddy_config_last_reload_successful", 385 | "instant": true, 386 | "legendFormat": "__auto", 387 | "range": true, 388 | "refId": "A" 389 | } 390 | ], 391 | "title": "Config Reload Status", 392 | "type": "stat", 393 | "fieldConfig": { 394 | "defaults": { 395 | "color": { 396 | "mode": "thresholds" 397 | }, 398 | "mappings": [ 399 | { 400 | "options": { 401 | "0": { 402 | "color": "red", 403 | "text": "Failed" 404 | }, 405 | "1": { 406 | "color": "green", 407 | "text": "Success" 408 | } 409 | }, 410 | "type": "value" 411 | } 412 | ], 413 | "thresholds": { 414 | "mode": "absolute", 415 | "steps": [ 416 | { 417 | "color": "red", 418 | "value": null 419 | }, 420 | { 421 | "color": "green", 422 | "value": 1 423 | } 424 | ] 425 | }, 426 | "unit": "none" 427 | }, 428 | "overrides": [] 429 | } 430 | }, 431 | { 432 | "gridPos": { 433 | "h": 9, 434 | "w": 12, 435 | "x": 0, 436 | "y": 7 437 | }, 438 | "id": 110, 439 | "options": { 440 | "legend": { 441 | "calcs": [], 442 | "displayMode": "list", 443 | "placement": "bottom", 444 | "showLegend": true 445 | }, 446 | "tooltip": { 447 | "mode": "multi", 448 | "sort": "none" 449 | } 450 | }, 451 | "targets": [ 452 | { 453 | "datasource": { 454 | "type": "prometheus", 455 | "uid": "prometheus" 456 | }, 457 | "expr": "sum(rate(caddy_http_request_duration_seconds_count{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__rate_interval])) by (code)", 458 | "legendFormat": "{{code}}", 459 | "range": true, 460 | "refId": "A" 461 | } 462 | ], 463 | "title": "Request Rate by Status Code", 464 | "type": "timeseries", 465 | "fieldConfig": { 466 | "defaults": { 467 | "color": { 468 | "mode": "palette-classic" 469 | }, 470 | "custom": { 471 | "axisBorderShow": false, 472 | "axisCenteredZero": false, 473 | "axisColorMode": "text", 474 | "axisLabel": "RPS", 475 | "axisPlacement": "auto", 476 | "barAlignment": 0, 477 | "drawStyle": "line", 478 | "fillOpacity": 25, 479 | "gradientMode": "opacity", 480 | "hideFrom": { 481 | "legend": false, 482 | "tooltip": false, 483 | "viz": false 484 | }, 485 | "lineInterpolation": "linear", 486 | "lineWidth": 1, 487 | "pointSize": 5, 488 | "scaleDistribution": { 489 | "type": "linear" 490 | }, 491 | "showPoints": "auto", 492 | "spanNulls": false, 493 | "stacking": { 494 | "group": "A", 495 | "mode": "normal" 496 | }, 497 | "thresholdsStyle": { 498 | "mode": "off" 499 | } 500 | }, 501 | "mappings": [], 502 | "thresholds": { 503 | "mode": "absolute", 504 | "steps": [ 505 | { 506 | "color": "green", 507 | "value": null 508 | } 509 | ] 510 | }, 511 | "unit": "reqps" 512 | }, 513 | "overrides": [ 514 | { 515 | "matcher": { 516 | "id": "byRegexp", 517 | "options": "/^5.*/" 518 | }, 519 | "properties": [ 520 | { 521 | "id": "color", 522 | "value": { 523 | "fixedColor": "red", 524 | "mode": "fixed" 525 | } 526 | } 527 | ] 528 | }, 529 | { 530 | "matcher": { 531 | "id": "byRegexp", 532 | "options": "/^4.*/" 533 | }, 534 | "properties": [ 535 | { 536 | "id": "color", 537 | "value": { 538 | "fixedColor": "orange", 539 | "mode": "fixed" 540 | } 541 | } 542 | ] 543 | }, 544 | { 545 | "matcher": { 546 | "id": "byRegexp", 547 | "options": "/^3.*/" 548 | }, 549 | "properties": [ 550 | { 551 | "id": "color", 552 | "value": { 553 | "fixedColor": "semi-dark-yellow", 554 | "mode": "fixed" 555 | } 556 | } 557 | ] 558 | }, 559 | { 560 | "matcher": { 561 | "id": "byRegexp", 562 | "options": "/^2.*/" 563 | }, 564 | "properties": [ 565 | { 566 | "id": "color", 567 | "value": { 568 | "fixedColor": "green", 569 | "mode": "fixed" 570 | } 571 | } 572 | ] 573 | } 574 | ] 575 | } 576 | }, 577 | { 578 | "gridPos": { 579 | "h": 9, 580 | "w": 12, 581 | "x": 12, 582 | "y": 7 583 | }, 584 | "id": 124, 585 | "options": { 586 | "displayLabels": [ 587 | "name", 588 | "percent" 589 | ], 590 | "legend": { 591 | "displayMode": "list", 592 | "placement": "bottom", 593 | "showLegend": true 594 | }, 595 | "pieType": "pie", 596 | "reduceOptions": { 597 | "calcs": [ 598 | "sum" 599 | ], 600 | "fields": "", 601 | "values": false 602 | }, 603 | "tooltip": { 604 | "mode": "single", 605 | "sort": "none" 606 | } 607 | }, 608 | "pluginVersion": "10.2.0", 609 | "targets": [ 610 | { 611 | "datasource": { 612 | "type": "prometheus", 613 | "uid": "prometheus" 614 | }, 615 | "expr": "sum(increase(caddy_http_request_duration_seconds_count{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__range])) by (code)", 616 | "instant": true, 617 | "legendFormat": "{{code}}", 618 | "range": true, 619 | "refId": "A" 620 | } 621 | ], 622 | "title": "HTTP Status Code Distribution", 623 | "type": "piechart" 624 | }, 625 | { 626 | "gridPos": { 627 | "h": 9, 628 | "w": 24, 629 | "x": 0, 630 | "y": 16 631 | }, 632 | "id": 126, 633 | "options": { 634 | "legend": { 635 | "calcs": [], 636 | "displayMode": "list", 637 | "placement": "bottom", 638 | "showLegend": true 639 | }, 640 | "tooltip": { 641 | "mode": "multi", 642 | "sort": "none" 643 | } 644 | }, 645 | "targets": [ 646 | { 647 | "datasource": { 648 | "type": "prometheus", 649 | "uid": "prometheus" 650 | }, 651 | "expr": "sum(rate(caddy_http_request_duration_seconds_count{server=~\"$server\", host=~\"$host\", handler=~\"$handler\", code=~\"5..|4..\"}[$__rate_interval])) by (code)", 652 | "legendFormat": "{{code}}", 653 | "range": true, 654 | "refId": "A" 655 | } 656 | ], 657 | "title": "HTTP Status Errors Timeline (4xx/5xx)", 658 | "type": "timeseries", 659 | "fieldConfig": { 660 | "defaults": { 661 | "color": { 662 | "mode": "palette-classic" 663 | }, 664 | "custom": { 665 | "axisBorderShow": false, 666 | "axisCenteredZero": false, 667 | "axisColorMode": "text", 668 | "axisLabel": "RPS", 669 | "axisPlacement": "auto", 670 | "barAlignment": 0, 671 | "drawStyle": "line", 672 | "fillOpacity": 25, 673 | "gradientMode": "opacity", 674 | "hideFrom": { 675 | "legend": false, 676 | "tooltip": false, 677 | "viz": false 678 | }, 679 | "lineInterpolation": "linear", 680 | "lineWidth": 1, 681 | "pointSize": 5, 682 | "scaleDistribution": { 683 | "type": "linear" 684 | }, 685 | "showPoints": "auto", 686 | "spanNulls": false, 687 | "stacking": { 688 | "group": "A", 689 | "mode": "normal" 690 | }, 691 | "thresholdsStyle": { 692 | "mode": "off" 693 | } 694 | }, 695 | "mappings": [], 696 | "thresholds": { 697 | "mode": "absolute", 698 | "steps": [ 699 | { 700 | "color": "green", 701 | "value": null 702 | } 703 | ] 704 | }, 705 | "unit": "reqps" 706 | }, 707 | "overrides": [ 708 | { 709 | "matcher": { 710 | "id": "byRegexp", 711 | "options": "/^5.*/" 712 | }, 713 | "properties": [ 714 | { 715 | "id": "color", 716 | "value": { 717 | "fixedColor": "red", 718 | "mode": "fixed" 719 | } 720 | } 721 | ] 722 | }, 723 | { 724 | "matcher": { 725 | "id": "byRegexp", 726 | "options": "/^4.*/" 727 | }, 728 | "properties": [ 729 | { 730 | "id": "color", 731 | "value": { 732 | "fixedColor": "orange", 733 | "mode": "fixed" 734 | } 735 | } 736 | ] 737 | } 738 | ] 739 | } 740 | }, 741 | { 742 | "gridPos": { 743 | "h": 10, 744 | "w": 12, 745 | "x": 0, 746 | "y": 25 747 | }, 748 | "id": 128, 749 | "options": { 750 | "legend": { 751 | "calcs": [], 752 | "displayMode": "list", 753 | "placement": "bottom", 754 | "showLegend": true 755 | }, 756 | "tooltip": { 757 | "mode": "multi", 758 | "sort": "none" 759 | } 760 | }, 761 | "targets": [ 762 | { 763 | "datasource": { 764 | "type": "prometheus", 765 | "uid": "prometheus" 766 | }, 767 | "expr": "histogram_quantile(0.50, sum(rate(caddy_http_request_duration_seconds_bucket{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__rate_interval])) by (le, host))", 768 | "legendFormat": "p50 {{host}}", 769 | "range": true, 770 | "refId": "A" 771 | } 772 | ], 773 | "title": "Median Response Duration Timeline", 774 | "type": "timeseries", 775 | "fieldConfig": { 776 | "defaults": { 777 | "color": { 778 | "mode": "palette-classic" 779 | }, 780 | "custom": { 781 | "axisBorderShow": false, 782 | "axisCenteredZero": false, 783 | "axisColorMode": "text", 784 | "axisLabel": "", 785 | "axisPlacement": "auto", 786 | "barAlignment": 0, 787 | "drawStyle": "line", 788 | "fillOpacity": 10, 789 | "gradientMode": "none", 790 | "hideFrom": { 791 | "legend": false, 792 | "tooltip": false, 793 | "viz": false 794 | }, 795 | "lineInterpolation": "linear", 796 | "lineWidth": 2, 797 | "pointSize": 5, 798 | "scaleDistribution": { 799 | "type": "linear" 800 | }, 801 | "showPoints": "auto", 802 | "spanNulls": false, 803 | "stacking": { 804 | "group": "A", 805 | "mode": "none" 806 | }, 807 | "thresholdsStyle": { 808 | "mode": "off" 809 | } 810 | }, 811 | "mappings": [], 812 | "thresholds": { 813 | "mode": "absolute", 814 | "steps": [ 815 | { 816 | "color": "green", 817 | "value": null 818 | } 819 | ] 820 | }, 821 | "unit": "s" 822 | }, 823 | "overrides": [] 824 | } 825 | }, 826 | { 827 | "gridPos": { 828 | "h": 10, 829 | "w": 12, 830 | "x": 12, 831 | "y": 25 832 | }, 833 | "id": 114, 834 | "options": { 835 | "legend": { 836 | "calcs": [], 837 | "displayMode": "list", 838 | "placement": "bottom", 839 | "showLegend": true 840 | }, 841 | "tooltip": { 842 | "mode": "multi", 843 | "sort": "none" 844 | } 845 | }, 846 | "targets": [ 847 | { 848 | "datasource": { 849 | "type": "prometheus", 850 | "uid": "prometheus" 851 | }, 852 | "expr": "histogram_quantile(0.90, sum(rate(caddy_http_request_duration_seconds_bucket{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__rate_interval])) by (le, host))", 853 | "hide": false, 854 | "legendFormat": "p90 {{host}}", 855 | "range": true, 856 | "refId": "B" 857 | }, 858 | { 859 | "datasource": { 860 | "type": "prometheus", 861 | "uid": "prometheus" 862 | }, 863 | "expr": "histogram_quantile(0.95, sum(rate(caddy_http_request_duration_seconds_bucket{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__rate_interval])) by (le, host))", 864 | "hide": false, 865 | "legendFormat": "p95 {{host}}", 866 | "range": true, 867 | "refId": "C" 868 | }, 869 | { 870 | "datasource": { 871 | "type": "prometheus", 872 | "uid": "prometheus" 873 | }, 874 | "expr": "histogram_quantile(0.99, sum(rate(caddy_http_request_duration_seconds_bucket{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__rate_interval])) by (le, host))", 875 | "hide": false, 876 | "legendFormat": "p99 {{host}}", 877 | "range": true, 878 | "refId": "D" 879 | } 880 | ], 881 | "title": "Request Latency Percentiles (p90, p95, p99)", 882 | "type": "timeseries", 883 | "fieldConfig": { 884 | "defaults": { 885 | "color": { 886 | "mode": "palette-classic" 887 | }, 888 | "custom": { 889 | "axisBorderShow": false, 890 | "axisCenteredZero": false, 891 | "axisColorMode": "text", 892 | "axisLabel": "", 893 | "axisPlacement": "auto", 894 | "barAlignment": 0, 895 | "drawStyle": "line", 896 | "fillOpacity": 10, 897 | "gradientMode": "none", 898 | "hideFrom": { 899 | "legend": false, 900 | "tooltip": false, 901 | "viz": false 902 | }, 903 | "lineInterpolation": "linear", 904 | "lineWidth": 2, 905 | "pointSize": 5, 906 | "scaleDistribution": { 907 | "type": "linear" 908 | }, 909 | "showPoints": "auto", 910 | "spanNulls": false, 911 | "stacking": { 912 | "group": "A", 913 | "mode": "none" 914 | }, 915 | "thresholdsStyle": { 916 | "mode": "off" 917 | } 918 | }, 919 | "mappings": [], 920 | "thresholds": { 921 | "mode": "absolute", 922 | "steps": [ 923 | { 924 | "color": "green", 925 | "value": null 926 | } 927 | ] 928 | }, 929 | "unit": "s" 930 | }, 931 | "overrides": [] 932 | } 933 | }, 934 | { 935 | "gridPos": { 936 | "h": 10, 937 | "w": 24, 938 | "x": 0, 939 | "y": 35 940 | }, 941 | "id": 116, 942 | "options": { 943 | "bucket": {}, 944 | "color": { 945 | "mode": "scheme", 946 | "scheme": "Oranges" 947 | }, 948 | "legend": { 949 | "show": true 950 | }, 951 | "tooltip": { 952 | "mode": "single", 953 | "show": true 954 | }, 955 | "yAxis": { 956 | "axisPlacement": "left" 957 | } 958 | }, 959 | "targets": [ 960 | { 961 | "datasource": { 962 | "type": "prometheus", 963 | "uid": "prometheus" 964 | }, 965 | "expr": "sum(rate(caddy_http_request_duration_seconds_bucket{server=~\"$server\", host=~\"$host\", handler=~\"$handler\"}[$__rate_interval])) by (le)", 966 | "format": "heatmap", 967 | "instant": false, 968 | "legendFormat": "{{le}}", 969 | "range": true, 970 | "refId": "A" 971 | } 972 | ], 973 | "title": "Latency Heatmap", 974 | "type": "heatmap", 975 | "fieldConfig": { 976 | "defaults": { 977 | "custom": { 978 | "hideFrom": { 979 | "legend": false, 980 | "tooltip": false, 981 | "viz": false 982 | } 983 | }, 984 | "unit": "s" 985 | }, 986 | "overrides": [] 987 | } 988 | } 989 | ], 990 | "refresh": "10s", 991 | "schemaVersion": 38, 992 | "style": "dark", 993 | "tags": [ 994 | "caddy", 995 | "webserver" 996 | ], 997 | "templating": { 998 | "list": [ 999 | { 1000 | "current": {}, 1001 | "datasource": { 1002 | "type": "prometheus", 1003 | "uid": "prometheus" 1004 | }, 1005 | "definition": "label_values(caddy_http_requests_in_flight, server)", 1006 | "hide": 0, 1007 | "includeAll": true, 1008 | "multi": true, 1009 | "name": "server", 1010 | "options": [], 1011 | "query": { 1012 | "query": "label_values(caddy_http_requests_in_flight, server)", 1013 | "refId": "Prometheus-server-Variable-Query" 1014 | }, 1015 | "refresh": 2, 1016 | "regex": "", 1017 | "skipUrlSync": false, 1018 | "sort": 0, 1019 | "type": "query" 1020 | }, 1021 | { 1022 | "current": {}, 1023 | "datasource": { 1024 | "type": "prometheus", 1025 | "uid": "prometheus" 1026 | }, 1027 | "definition": "label_values(caddy_http_requests_in_flight{server=~\"$server\"}, host)", 1028 | "hide": 0, 1029 | "includeAll": true, 1030 | "multi": true, 1031 | "name": "host", 1032 | "options": [], 1033 | "query": { 1034 | "query": "label_values(caddy_http_requests_in_flight{server=~\"$server\"}, host)", 1035 | "refId": "Prometheus-host-Variable-Query" 1036 | }, 1037 | "refresh": 2, 1038 | "regex": "", 1039 | "skipUrlSync": false, 1040 | "sort": 0, 1041 | "type": "query" 1042 | }, 1043 | { 1044 | "current": {}, 1045 | "datasource": { 1046 | "type": "prometheus", 1047 | "uid": "prometheus" 1048 | }, 1049 | "definition": "label_values(caddy_http_requests_in_flight{server=~\"$server\", host=~\"$host\"}, handler)", 1050 | "hide": 0, 1051 | "includeAll": true, 1052 | "multi": true, 1053 | "name": "handler", 1054 | "options": [], 1055 | "query": { 1056 | "query": "label_values(caddy_http_requests_in_flight{server=~\"$server\", host=~\"$host\"}, handler)", 1057 | "refId": "Prometheus-handler-Variable-Query" 1058 | }, 1059 | "refresh": 2, 1060 | "regex": "", 1061 | "skipUrlSync": false, 1062 | "sort": 0, 1063 | "type": "query" 1064 | } 1065 | ] 1066 | }, 1067 | "time": { 1068 | "from": "now-1h", 1069 | "to": "now" 1070 | }, 1071 | "timepicker": {}, 1072 | "timezone": "", 1073 | "title": "Caddy Server Advanced Overview", 1074 | "uid": "caddy-overview-advanced", 1075 | "version": 5, 1076 | "weekStart": "" 1077 | } -------------------------------------------------------------------------------- /deployment/grafana/provisioning/dashboards/frankenphp.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "9.0.0" 18 | }, 19 | { 20 | "type": "datasource", 21 | "id": "prometheus", 22 | "name": "Prometheus", 23 | "version": "1.0.0" 24 | } 25 | ], 26 | "annotations": { 27 | "list": [ 28 | { 29 | "builtIn": 1, 30 | "datasource": { 31 | "type": "grafana", 32 | "uid": "-- Grafana --" 33 | }, 34 | "enable": true, 35 | "hide": true, 36 | "iconColor": "rgba(0, 211, 255, 1)", 37 | "name": "Annotations & Alerts", 38 | "type": "dashboard" 39 | } 40 | ] 41 | }, 42 | "editable": true, 43 | "fiscalYearStartMonth": 0, 44 | "graphTooltip": 0, 45 | "id": null, 46 | "links": [], 47 | "liveNow": false, 48 | "panels": [ 49 | { 50 | "collapsed": false, 51 | "gridPos": { 52 | "h": 1, 53 | "w": 24, 54 | "x": 0, 55 | "y": 0 56 | }, 57 | "id": 100, 58 | "panels": [], 59 | "title": "Request Performance", 60 | "type": "row" 61 | }, 62 | { 63 | "gridPos": { 64 | "h": 8, 65 | "w": 6, 66 | "x": 0, 67 | "y": 1 68 | }, 69 | "id": 20, 70 | "options": { 71 | "colorMode": "value", 72 | "graphMode": "area", 73 | "justifyMode": "auto", 74 | "orientation": "auto", 75 | "reduceOptions": { 76 | "calcs": [ 77 | "lastNotNull" 78 | ], 79 | "fields": "", 80 | "values": false 81 | }, 82 | "textMode": "auto" 83 | }, 84 | "targets": [ 85 | { 86 | "datasource": { 87 | "type": "prometheus", 88 | "uid": "prometheus" 89 | }, 90 | "expr": "sum(rate(frankenphp_worker_request_count{worker=~\"$worker\"}[$__rate_interval]))", 91 | "instant": false, 92 | "legendFormat": "__auto", 93 | "range": true, 94 | "refId": "A" 95 | } 96 | ], 97 | "title": "Request Rate (Numeric)", 98 | "type": "stat", 99 | "fieldConfig": { 100 | "defaults": { 101 | "color": { 102 | "mode": "thresholds" 103 | }, 104 | "mappings": [], 105 | "thresholds": { 106 | "mode": "absolute", 107 | "steps": [ 108 | { 109 | "color": "green", 110 | "value": null 111 | } 112 | ] 113 | }, 114 | "unit": "reqps" 115 | }, 116 | "overrides": [] 117 | } 118 | }, 119 | { 120 | "gridPos": { 121 | "h": 8, 122 | "w": 18, 123 | "x": 6, 124 | "y": 1 125 | }, 126 | "id": 6, 127 | "options": { 128 | "legend": { 129 | "calcs": [], 130 | "displayMode": "list", 131 | "placement": "bottom", 132 | "showLegend": true 133 | }, 134 | "tooltip": { 135 | "mode": "single", 136 | "sort": "none" 137 | } 138 | }, 139 | "targets": [ 140 | { 141 | "datasource": { 142 | "type": "prometheus", 143 | "uid": "prometheus" 144 | }, 145 | "expr": "sum by (worker) (rate(frankenphp_worker_request_count{worker=~\"$worker\"}[$__rate_interval]))", 146 | "legendFormat": "{{worker}}", 147 | "range": true, 148 | "refId": "A" 149 | } 150 | ], 151 | "title": "Request Rate (Timeline)", 152 | "type": "timeseries", 153 | "fieldConfig": { 154 | "defaults": { 155 | "color": { 156 | "mode": "palette-classic" 157 | }, 158 | "custom": { 159 | "axisBorderShow": false, 160 | "axisCenteredZero": false, 161 | "axisColorMode": "text", 162 | "axisLabel": "", 163 | "axisPlacement": "auto", 164 | "barAlignment": 0, 165 | "drawStyle": "line", 166 | "fillOpacity": 10, 167 | "gradientMode": "opacity", 168 | "hideFrom": { 169 | "legend": false, 170 | "tooltip": false, 171 | "viz": false 172 | }, 173 | "lineInterpolation": "linear", 174 | "lineWidth": 1, 175 | "pointSize": 5, 176 | "scaleDistribution": { 177 | "type": "linear" 178 | }, 179 | "showPoints": "auto", 180 | "spanNulls": false, 181 | "stacking": { 182 | "group": "A", 183 | "mode": "none" 184 | }, 185 | "thresholdsStyle": { 186 | "mode": "off" 187 | } 188 | }, 189 | "mappings": [], 190 | "thresholds": { 191 | "mode": "absolute", 192 | "steps": [ 193 | { 194 | "color": "green", 195 | "value": null 196 | } 197 | ] 198 | }, 199 | "unit": "reqps" 200 | }, 201 | "overrides": [] 202 | } 203 | }, 204 | { 205 | "gridPos": { 206 | "h": 8, 207 | "w": 6, 208 | "x": 0, 209 | "y": 9 210 | }, 211 | "id": 18, 212 | "options": { 213 | "colorMode": "value", 214 | "graphMode": "area", 215 | "justifyMode": "auto", 216 | "orientation": "auto", 217 | "reduceOptions": { 218 | "calcs": [ 219 | "lastNotNull" 220 | ], 221 | "fields": "", 222 | "values": false 223 | }, 224 | "textMode": "auto" 225 | }, 226 | "targets": [ 227 | { 228 | "datasource": { 229 | "type": "prometheus", 230 | "uid": "prometheus" 231 | }, 232 | "expr": "sum(rate(frankenphp_worker_request_time{worker=~\"$worker\"}[$__rate_interval])) / sum(rate(frankenphp_worker_request_count{worker=~\"$worker\"}[$__rate_interval])) * 1000", 233 | "instant": false, 234 | "legendFormat": "__auto", 235 | "range": true, 236 | "refId": "A" 237 | } 238 | ], 239 | "title": "Average Latency (Numeric)", 240 | "type": "stat", 241 | "fieldConfig": { 242 | "defaults": { 243 | "color": { 244 | "mode": "thresholds" 245 | }, 246 | "mappings": [], 247 | "thresholds": { 248 | "mode": "absolute", 249 | "steps": [ 250 | { 251 | "color": "green", 252 | "value": null 253 | }, 254 | { 255 | "color": "orange", 256 | "value": 200 257 | }, 258 | { 259 | "color": "red", 260 | "value": 500 261 | } 262 | ] 263 | }, 264 | "unit": "ms" 265 | }, 266 | "overrides": [] 267 | } 268 | }, 269 | { 270 | "gridPos": { 271 | "h": 8, 272 | "w": 18, 273 | "x": 6, 274 | "y": 9 275 | }, 276 | "id": 10, 277 | "options": { 278 | "legend": { 279 | "calcs": [], 280 | "displayMode": "list", 281 | "placement": "bottom", 282 | "showLegend": true 283 | }, 284 | "tooltip": { 285 | "mode": "single", 286 | "sort": "none" 287 | } 288 | }, 289 | "targets": [ 290 | { 291 | "datasource": { 292 | "type": "prometheus", 293 | "uid": "prometheus" 294 | }, 295 | "expr": "sum by (worker) (rate(frankenphp_worker_request_time{worker=~\"$worker\"}[$__rate_interval])) / sum by (worker) (rate(frankenphp_worker_request_count{worker=~\"$worker\"}[$__rate_interval]))", 296 | "legendFormat": "{{worker}}", 297 | "range": true, 298 | "refId": "A" 299 | } 300 | ], 301 | "title": "Average Latency (Timeline)", 302 | "type": "timeseries", 303 | "fieldConfig": { 304 | "defaults": { 305 | "color": { 306 | "mode": "palette-classic" 307 | }, 308 | "custom": { 309 | "axisBorderShow": false, 310 | "axisCenteredZero": false, 311 | "axisColorMode": "text", 312 | "axisLabel": "", 313 | "axisPlacement": "auto", 314 | "barAlignment": 0, 315 | "drawStyle": "line", 316 | "fillOpacity": 10, 317 | "gradientMode": "opacity", 318 | "hideFrom": { 319 | "legend": false, 320 | "tooltip": false, 321 | "viz": false 322 | }, 323 | "lineInterpolation": "linear", 324 | "lineWidth": 1, 325 | "pointSize": 5, 326 | "scaleDistribution": { 327 | "type": "linear" 328 | }, 329 | "showPoints": "auto", 330 | "spanNulls": false, 331 | "stacking": { 332 | "group": "A", 333 | "mode": "none" 334 | }, 335 | "thresholdsStyle": { 336 | "mode": "off" 337 | } 338 | }, 339 | "mappings": [], 340 | "thresholds": { 341 | "mode": "absolute", 342 | "steps": [ 343 | { 344 | "color": "green", 345 | "value": null 346 | } 347 | ] 348 | }, 349 | "unit": "s" 350 | }, 351 | "overrides": [] 352 | } 353 | }, 354 | { 355 | "collapsed": false, 356 | "gridPos": { 357 | "h": 1, 358 | "w": 24, 359 | "x": 0, 360 | "y": 17 361 | }, 362 | "id": 102, 363 | "panels": [], 364 | "title": "Queue Depth", 365 | "type": "row" 366 | }, 367 | { 368 | "gridPos": { 369 | "h": 8, 370 | "w": 6, 371 | "x": 0, 372 | "y": 18 373 | }, 374 | "id": 16, 375 | "options": { 376 | "colorMode": "value", 377 | "graphMode": "area", 378 | "justifyMode": "auto", 379 | "orientation": "auto", 380 | "reduceOptions": { 381 | "calcs": [ 382 | "lastNotNull" 383 | ], 384 | "fields": "", 385 | "values": false 386 | }, 387 | "textMode": "auto" 388 | }, 389 | "targets": [ 390 | { 391 | "datasource": { 392 | "type": "prometheus", 393 | "uid": "prometheus" 394 | }, 395 | "expr": "sum(frankenphp_queue_depth)", 396 | "instant": false, 397 | "legendFormat": "__auto", 398 | "range": true, 399 | "refId": "A" 400 | } 401 | ], 402 | "title": "Queue Depth (Numeric)", 403 | "type": "stat", 404 | "fieldConfig": { 405 | "defaults": { 406 | "color": { 407 | "mode": "thresholds" 408 | }, 409 | "mappings": [], 410 | "thresholds": { 411 | "mode": "absolute", 412 | "steps": [ 413 | { 414 | "color": "green", 415 | "value": null 416 | }, 417 | { 418 | "color": "orange", 419 | "value": 10 420 | }, 421 | { 422 | "color": "red", 423 | "value": 50 424 | } 425 | ] 426 | }, 427 | "unit": "short" 428 | }, 429 | "overrides": [] 430 | } 431 | }, 432 | { 433 | "gridPos": { 434 | "h": 8, 435 | "w": 18, 436 | "x": 6, 437 | "y": 18 438 | }, 439 | "id": 104, 440 | "options": { 441 | "legend": { 442 | "calcs": [], 443 | "displayMode": "list", 444 | "placement": "bottom", 445 | "showLegend": true 446 | }, 447 | "tooltip": { 448 | "mode": "single", 449 | "sort": "none" 450 | } 451 | }, 452 | "targets": [ 453 | { 454 | "datasource": { 455 | "type": "prometheus", 456 | "uid": "prometheus" 457 | }, 458 | "expr": "frankenphp_queue_depth", 459 | "legendFormat": "Queue Depth", 460 | "range": true, 461 | "refId": "A" 462 | } 463 | ], 464 | "title": "Queue Depth (Timeline)", 465 | "type": "timeseries", 466 | "fieldConfig": { 467 | "defaults": { 468 | "color": { 469 | "mode": "palette-classic" 470 | }, 471 | "custom": { 472 | "axisBorderShow": false, 473 | "axisCenteredZero": false, 474 | "axisColorMode": "text", 475 | "axisLabel": "", 476 | "axisPlacement": "auto", 477 | "barAlignment": 0, 478 | "drawStyle": "line", 479 | "fillOpacity": 10, 480 | "gradientMode": "opacity", 481 | "hideFrom": { 482 | "legend": false, 483 | "tooltip": false, 484 | "viz": false 485 | }, 486 | "lineInterpolation": "linear", 487 | "lineWidth": 1, 488 | "pointSize": 5, 489 | "scaleDistribution": { 490 | "type": "linear" 491 | }, 492 | "showPoints": "auto", 493 | "spanNulls": false, 494 | "stacking": { 495 | "group": "A", 496 | "mode": "none" 497 | }, 498 | "thresholdsStyle": { 499 | "mode": "off" 500 | } 501 | }, 502 | "mappings": [], 503 | "thresholds": { 504 | "mode": "absolute", 505 | "steps": [ 506 | { 507 | "color": "green", 508 | "value": null 509 | } 510 | ] 511 | }, 512 | "unit": "short" 513 | }, 514 | "overrides": [] 515 | } 516 | }, 517 | { 518 | "collapsed": false, 519 | "gridPos": { 520 | "h": 1, 521 | "w": 24, 522 | "x": 0, 523 | "y": 26 524 | }, 525 | "id": 106, 526 | "panels": [], 527 | "title": "Thread Status", 528 | "type": "row" 529 | }, 530 | { 531 | "gridPos": { 532 | "h": 8, 533 | "w": 4, 534 | "x": 0, 535 | "y": 27 536 | }, 537 | "id": 4, 538 | "options": { 539 | "colorMode": "value", 540 | "graphMode": "area", 541 | "justifyMode": "auto", 542 | "orientation": "auto", 543 | "reduceOptions": { 544 | "calcs": [ 545 | "lastNotNull" 546 | ], 547 | "fields": "", 548 | "values": false 549 | }, 550 | "textMode": "auto" 551 | }, 552 | "targets": [ 553 | { 554 | "datasource": { 555 | "type": "prometheus", 556 | "uid": "prometheus" 557 | }, 558 | "expr": "sum(frankenphp_busy_threads)", 559 | "instant": false, 560 | "legendFormat": "__auto", 561 | "range": true, 562 | "refId": "A" 563 | } 564 | ], 565 | "title": "Busy Threads", 566 | "type": "stat", 567 | "fieldConfig": { 568 | "defaults": { 569 | "color": { 570 | "mode": "thresholds" 571 | }, 572 | "mappings": [], 573 | "thresholds": { 574 | "mode": "absolute", 575 | "steps": [ 576 | { 577 | "color": "green", 578 | "value": null 579 | } 580 | ] 581 | }, 582 | "unit": "short" 583 | }, 584 | "overrides": [] 585 | } 586 | }, 587 | { 588 | "gridPos": { 589 | "h": 8, 590 | "w": 4, 591 | "x": 4, 592 | "y": 27 593 | }, 594 | "id": 108, 595 | "options": { 596 | "colorMode": "value", 597 | "graphMode": "area", 598 | "justifyMode": "auto", 599 | "orientation": "auto", 600 | "reduceOptions": { 601 | "calcs": [ 602 | "lastNotNull" 603 | ], 604 | "fields": "", 605 | "values": false 606 | }, 607 | "textMode": "auto" 608 | }, 609 | "targets": [ 610 | { 611 | "datasource": { 612 | "type": "prometheus", 613 | "uid": "prometheus" 614 | }, 615 | "expr": "sum(frankenphp_total_threads)", 616 | "instant": false, 617 | "legendFormat": "__auto", 618 | "range": true, 619 | "refId": "A" 620 | } 621 | ], 622 | "title": "Total Threads", 623 | "type": "stat", 624 | "fieldConfig": { 625 | "defaults": { 626 | "color": { 627 | "mode": "thresholds" 628 | }, 629 | "mappings": [], 630 | "thresholds": { 631 | "mode": "absolute", 632 | "steps": [ 633 | { 634 | "color": "green", 635 | "value": null 636 | } 637 | ] 638 | }, 639 | "unit": "short" 640 | }, 641 | "overrides": [] 642 | } 643 | }, 644 | { 645 | "gridPos": { 646 | "h": 8, 647 | "w": 4, 648 | "x": 8, 649 | "y": 27 650 | }, 651 | "id": 12, 652 | "options": { 653 | "orientation": "auto", 654 | "reduceOptions": { 655 | "calcs": [ 656 | "lastNotNull" 657 | ], 658 | "fields": "", 659 | "values": false 660 | }, 661 | "showThresholdLabels": false, 662 | "showThresholdMarkers": true 663 | }, 664 | "targets": [ 665 | { 666 | "datasource": { 667 | "type": "prometheus", 668 | "uid": "prometheus" 669 | }, 670 | "expr": "sum(frankenphp_busy_threads) / sum(frankenphp_total_threads) * 100", 671 | "instant": false, 672 | "legendFormat": "__auto", 673 | "range": true, 674 | "refId": "A" 675 | } 676 | ], 677 | "title": "Thread Utilization", 678 | "type": "gauge", 679 | "fieldConfig": { 680 | "defaults": { 681 | "color": { 682 | "mode": "thresholds" 683 | }, 684 | "mappings": [], 685 | "thresholds": { 686 | "mode": "absolute", 687 | "steps": [ 688 | { 689 | "color": "green", 690 | "value": null 691 | }, 692 | { 693 | "color": "orange", 694 | "value": 70 695 | }, 696 | { 697 | "color": "red", 698 | "value": 90 699 | } 700 | ] 701 | }, 702 | "unit": "percent" 703 | }, 704 | "overrides": [] 705 | } 706 | }, 707 | { 708 | "gridPos": { 709 | "h": 8, 710 | "w": 12, 711 | "x": 12, 712 | "y": 27 713 | }, 714 | "id": 110, 715 | "options": { 716 | "legend": { 717 | "calcs": [], 718 | "displayMode": "list", 719 | "placement": "bottom", 720 | "showLegend": true 721 | }, 722 | "tooltip": { 723 | "mode": "multi", 724 | "sort": "none" 725 | } 726 | }, 727 | "targets": [ 728 | { 729 | "datasource": { 730 | "type": "prometheus", 731 | "uid": "prometheus" 732 | }, 733 | "expr": "sum(frankenphp_busy_threads)", 734 | "legendFormat": "Busy", 735 | "range": true, 736 | "refId": "A" 737 | }, 738 | { 739 | "datasource": { 740 | "type": "prometheus", 741 | "uid": "prometheus" 742 | }, 743 | "expr": "sum(frankenphp_total_threads)", 744 | "hide": false, 745 | "legendFormat": "Total", 746 | "range": true, 747 | "refId": "B" 748 | } 749 | ], 750 | "title": "Threads (Timeline)", 751 | "type": "timeseries", 752 | "fieldConfig": { 753 | "defaults": { 754 | "color": { 755 | "mode": "palette-classic" 756 | }, 757 | "custom": { 758 | "axisBorderShow": false, 759 | "axisCenteredZero": false, 760 | "axisColorMode": "text", 761 | "axisLabel": "", 762 | "axisPlacement": "auto", 763 | "barAlignment": 0, 764 | "drawStyle": "line", 765 | "fillOpacity": 25, 766 | "gradientMode": "opacity", 767 | "hideFrom": { 768 | "legend": false, 769 | "tooltip": false, 770 | "viz": false 771 | }, 772 | "lineInterpolation": "linear", 773 | "lineWidth": 1, 774 | "pointSize": 5, 775 | "scaleDistribution": { 776 | "type": "linear" 777 | }, 778 | "showPoints": "auto", 779 | "spanNulls": false, 780 | "stacking": { 781 | "group": "A", 782 | "mode": "none" 783 | }, 784 | "thresholdsStyle": { 785 | "mode": "off" 786 | } 787 | }, 788 | "mappings": [], 789 | "thresholds": { 790 | "mode": "absolute", 791 | "steps": [ 792 | { 793 | "color": "green", 794 | "value": null 795 | } 796 | ] 797 | }, 798 | "unit": "short" 799 | }, 800 | "overrides": [ 801 | { 802 | "matcher": { 803 | "id": "byName", 804 | "options": "Busy" 805 | }, 806 | "properties": [ 807 | { 808 | "id": "custom.stacking", 809 | "value": { 810 | "group": "A", 811 | "mode": "normal" 812 | } 813 | } 814 | ] 815 | } 816 | ] 817 | } 818 | }, 819 | { 820 | "collapsed": false, 821 | "gridPos": { 822 | "h": 1, 823 | "w": 24, 824 | "x": 0, 825 | "y": 35 826 | }, 827 | "id": 112, 828 | "panels": [], 829 | "title": "Worker Status", 830 | "type": "row" 831 | }, 832 | { 833 | "gridPos": { 834 | "h": 8, 835 | "w": 4, 836 | "x": 0, 837 | "y": 36 838 | }, 839 | "id": 114, 840 | "options": { 841 | "colorMode": "value", 842 | "graphMode": "area", 843 | "justifyMode": "auto", 844 | "orientation": "auto", 845 | "reduceOptions": { 846 | "calcs": [ 847 | "lastNotNull" 848 | ], 849 | "fields": "", 850 | "values": false 851 | }, 852 | "textMode": "auto" 853 | }, 854 | "targets": [ 855 | { 856 | "datasource": { 857 | "type": "prometheus", 858 | "uid": "prometheus" 859 | }, 860 | "expr": "sum(frankenphp_busy_workers{worker=~\"$worker\"})", 861 | "instant": false, 862 | "legendFormat": "__auto", 863 | "range": true, 864 | "refId": "A" 865 | } 866 | ], 867 | "title": "Busy Workers", 868 | "type": "stat", 869 | "fieldConfig": { 870 | "defaults": { 871 | "color": { 872 | "mode": "thresholds" 873 | }, 874 | "mappings": [], 875 | "thresholds": { 876 | "mode": "absolute", 877 | "steps": [ 878 | { 879 | "color": "green", 880 | "value": null 881 | } 882 | ] 883 | }, 884 | "unit": "short" 885 | }, 886 | "overrides": [] 887 | } 888 | }, 889 | { 890 | "gridPos": { 891 | "h": 8, 892 | "w": 4, 893 | "x": 4, 894 | "y": 36 895 | }, 896 | "id": 116, 897 | "options": { 898 | "colorMode": "value", 899 | "graphMode": "area", 900 | "justifyMode": "auto", 901 | "orientation": "auto", 902 | "reduceOptions": { 903 | "calcs": [ 904 | "lastNotNull" 905 | ], 906 | "fields": "", 907 | "values": false 908 | }, 909 | "textMode": "auto" 910 | }, 911 | "targets": [ 912 | { 913 | "datasource": { 914 | "type": "prometheus", 915 | "uid": "prometheus" 916 | }, 917 | "expr": "sum(frankenphp_ready_workers{worker=~\"$worker\"})", 918 | "instant": false, 919 | "legendFormat": "__auto", 920 | "range": true, 921 | "refId": "A" 922 | } 923 | ], 924 | "title": "Ready Workers", 925 | "type": "stat", 926 | "fieldConfig": { 927 | "defaults": { 928 | "color": { 929 | "mode": "thresholds" 930 | }, 931 | "mappings": [], 932 | "thresholds": { 933 | "mode": "absolute", 934 | "steps": [ 935 | { 936 | "color": "green", 937 | "value": null 938 | } 939 | ] 940 | }, 941 | "unit": "short" 942 | }, 943 | "overrides": [] 944 | } 945 | }, 946 | { 947 | "gridPos": { 948 | "h": 8, 949 | "w": 4, 950 | "x": 8, 951 | "y": 36 952 | }, 953 | "id": 118, 954 | "options": { 955 | "colorMode": "value", 956 | "graphMode": "area", 957 | "justifyMode": "auto", 958 | "orientation": "auto", 959 | "reduceOptions": { 960 | "calcs": [ 961 | "lastNotNull" 962 | ], 963 | "fields": "", 964 | "values": false 965 | }, 966 | "textMode": "auto" 967 | }, 968 | "targets": [ 969 | { 970 | "datasource": { 971 | "type": "prometheus", 972 | "uid": "prometheus" 973 | }, 974 | "expr": "sum(frankenphp_total_workers{worker=~\"$worker\"})", 975 | "instant": false, 976 | "legendFormat": "__auto", 977 | "range": true, 978 | "refId": "A" 979 | } 980 | ], 981 | "title": "Total Workers", 982 | "type": "stat", 983 | "fieldConfig": { 984 | "defaults": { 985 | "color": { 986 | "mode": "thresholds" 987 | }, 988 | "mappings": [], 989 | "thresholds": { 990 | "mode": "absolute", 991 | "steps": [ 992 | { 993 | "color": "green", 994 | "value": null 995 | } 996 | ] 997 | }, 998 | "unit": "short" 999 | }, 1000 | "overrides": [] 1001 | } 1002 | }, 1003 | { 1004 | "gridPos": { 1005 | "h": 8, 1006 | "w": 12, 1007 | "x": 12, 1008 | "y": 36 1009 | }, 1010 | "id": 2, 1011 | "options": { 1012 | "legend": { 1013 | "calcs": [], 1014 | "displayMode": "list", 1015 | "placement": "bottom", 1016 | "showLegend": true 1017 | }, 1018 | "tooltip": { 1019 | "mode": "multi", 1020 | "sort": "none" 1021 | } 1022 | }, 1023 | "targets": [ 1024 | { 1025 | "datasource": { 1026 | "type": "prometheus", 1027 | "uid": "prometheus" 1028 | }, 1029 | "expr": "sum by (worker) (frankenphp_busy_workers{worker=~\"$worker\"})", 1030 | "legendFormat": "Busy: {{worker}}", 1031 | "range": true, 1032 | "refId": "A" 1033 | }, 1034 | { 1035 | "datasource": { 1036 | "type": "prometheus", 1037 | "uid": "prometheus" 1038 | }, 1039 | "expr": "sum by (worker) (frankenphp_ready_workers{worker=~\"$worker\"})", 1040 | "hide": false, 1041 | "legendFormat": "Ready: {{worker}}", 1042 | "range": true, 1043 | "refId": "B" 1044 | }, 1045 | { 1046 | "datasource": { 1047 | "type": "prometheus", 1048 | "uid": "prometheus" 1049 | }, 1050 | "expr": "sum by (worker) (frankenphp_total_workers{worker=~\"$worker\"})", 1051 | "hide": false, 1052 | "legendFormat": "Total: {{worker}}", 1053 | "range": true, 1054 | "refId": "C" 1055 | } 1056 | ], 1057 | "title": "Worker Status (Timeline)", 1058 | "type": "timeseries", 1059 | "fieldConfig": { 1060 | "defaults": { 1061 | "color": { 1062 | "mode": "palette-classic" 1063 | }, 1064 | "custom": { 1065 | "axisBorderShow": false, 1066 | "axisCenteredZero": false, 1067 | "axisColorMode": "text", 1068 | "axisLabel": "", 1069 | "axisPlacement": "auto", 1070 | "barAlignment": 0, 1071 | "drawStyle": "line", 1072 | "fillOpacity": 25, 1073 | "gradientMode": "scheme", 1074 | "hideFrom": { 1075 | "legend": false, 1076 | "tooltip": false, 1077 | "viz": false 1078 | }, 1079 | "lineInterpolation": "smooth", 1080 | "lineWidth": 2, 1081 | "pointSize": 5, 1082 | "scaleDistribution": { 1083 | "type": "linear" 1084 | }, 1085 | "showPoints": "auto", 1086 | "spanNulls": false, 1087 | "stacking": { 1088 | "group": "A", 1089 | "mode": "normal" 1090 | }, 1091 | "thresholdsStyle": { 1092 | "mode": "off" 1093 | } 1094 | }, 1095 | "mappings": [], 1096 | "thresholds": { 1097 | "mode": "absolute", 1098 | "steps": [ 1099 | { 1100 | "color": "green", 1101 | "value": null 1102 | } 1103 | ] 1104 | } 1105 | }, 1106 | "overrides": [ 1107 | { 1108 | "matcher": { 1109 | "id": "byFrameRefID", 1110 | "options": "C" 1111 | }, 1112 | "properties": [ 1113 | { 1114 | "id": "custom.stacking", 1115 | "value": { 1116 | "group": "A", 1117 | "mode": "none" 1118 | } 1119 | }, 1120 | { 1121 | "id": "custom.fillOpacity", 1122 | "value": 0 1123 | }, 1124 | { 1125 | "id": "custom.lineStyle", 1126 | "value": { 1127 | "dash": [ 1128 | 10, 1129 | 10 1130 | ], 1131 | "fill": "dash" 1132 | } 1133 | } 1134 | ] 1135 | } 1136 | ] 1137 | } 1138 | } 1139 | ], 1140 | "refresh": "10s", 1141 | "schemaVersion": 38, 1142 | "style": "dark", 1143 | "tags": [ 1144 | "frankenphp", 1145 | "php" 1146 | ], 1147 | "templating": { 1148 | "list": [ 1149 | { 1150 | "current": {}, 1151 | "datasource": { 1152 | "type": "prometheus", 1153 | "uid": "prometheus" 1154 | }, 1155 | "definition": "label_values(frankenphp_total_workers, worker)", 1156 | "hide": 0, 1157 | "includeAll": true, 1158 | "multi": true, 1159 | "name": "worker", 1160 | "options": [], 1161 | "query": { 1162 | "query": "label_values(frankenphp_total_workers, worker)", 1163 | "refId": "Prometheus-worker-Variable-Query" 1164 | }, 1165 | "refresh": 2, 1166 | "regex": "", 1167 | "skipUrlSync": false, 1168 | "sort": 0, 1169 | "type": "query" 1170 | } 1171 | ] 1172 | }, 1173 | "time": { 1174 | "from": "now-1h", 1175 | "to": "now" 1176 | }, 1177 | "timepicker": {}, 1178 | "timezone": "", 1179 | "title": "FrankenPHP Detailed Overview", 1180 | "uid": "frankenphp-detailed-dashboard", 1181 | "version": 2, 1182 | "weekStart": "" 1183 | } -------------------------------------------------------------------------------- /deployment/grafana/provisioning/dashboards/traefik.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__elements": {}, 13 | "__requires": [ 14 | { 15 | "type": "grafana", 16 | "id": "grafana", 17 | "name": "Grafana", 18 | "version": "9.3.1" 19 | }, 20 | { 21 | "type": "panel", 22 | "id": "piechart", 23 | "name": "Pie chart", 24 | "version": "" 25 | }, 26 | { 27 | "type": "datasource", 28 | "id": "prometheus", 29 | "name": "Prometheus", 30 | "version": "1.0.0" 31 | }, 32 | { 33 | "type": "panel", 34 | "id": "stat", 35 | "name": "Stat", 36 | "version": "" 37 | }, 38 | { 39 | "type": "panel", 40 | "id": "timeseries", 41 | "name": "Time series", 42 | "version": "" 43 | } 44 | ], 45 | "annotations": { 46 | "list": [ 47 | { 48 | "builtIn": 1, 49 | "datasource": { 50 | "type": "grafana", 51 | "uid": "-- Grafana --" 52 | }, 53 | "enable": true, 54 | "hide": true, 55 | "iconColor": "rgba(0, 211, 255, 1)", 56 | "name": "Annotations & Alerts", 57 | "target": { 58 | "limit": 100, 59 | "matchAny": false, 60 | "tags": [], 61 | "type": "dashboard" 62 | }, 63 | "type": "dashboard" 64 | } 65 | ] 66 | }, 67 | "description": "Official dashboard for Standalone Traefik", 68 | "editable": false, 69 | "fiscalYearStartMonth": 0, 70 | "gnetId": 17346, 71 | "graphTooltip": 0, 72 | "id": null, 73 | "links": [], 74 | "liveNow": false, 75 | "panels": [ 76 | { 77 | "collapsed": false, 78 | "gridPos": { 79 | "h": 1, 80 | "w": 24, 81 | "x": 0, 82 | "y": 0 83 | }, 84 | "id": 9, 85 | "panels": [], 86 | "title": "General", 87 | "type": "row" 88 | }, 89 | { 90 | "datasource": { 91 | "type": "prometheus", 92 | "uid": "prometheus" 93 | }, 94 | "description": "", 95 | "fieldConfig": { 96 | "defaults": { 97 | "color": { 98 | "mode": "thresholds" 99 | }, 100 | "mappings": [], 101 | "thresholds": { 102 | "mode": "absolute", 103 | "steps": [ 104 | { 105 | "color": "green", 106 | "value": null 107 | }, 108 | { 109 | "color": "red", 110 | "value": 80 111 | } 112 | ] 113 | } 114 | }, 115 | "overrides": [] 116 | }, 117 | "gridPos": { 118 | "h": 2, 119 | "w": 5, 120 | "x": 0, 121 | "y": 1 122 | }, 123 | "id": 13, 124 | "options": { 125 | "colorMode": "value", 126 | "graphMode": "area", 127 | "justifyMode": "auto", 128 | "orientation": "auto", 129 | "reduceOptions": { 130 | "calcs": [ 131 | "lastNotNull" 132 | ], 133 | "fields": "", 134 | "values": false 135 | }, 136 | "textMode": "auto" 137 | }, 138 | "pluginVersion": "9.3.1", 139 | "targets": [ 140 | { 141 | "datasource": { 142 | "type": "prometheus", 143 | "uid": "prometheus" 144 | }, 145 | "editorMode": "code", 146 | "expr": "count(traefik_config_reloads_total)", 147 | "legendFormat": "__auto", 148 | "range": true, 149 | "refId": "A" 150 | } 151 | ], 152 | "title": "Traefik Instances", 153 | "type": "stat" 154 | }, 155 | { 156 | "datasource": { 157 | "type": "prometheus", 158 | "uid": "prometheus" 159 | }, 160 | "description": "", 161 | "fieldConfig": { 162 | "defaults": { 163 | "color": { 164 | "mode": "palette-classic" 165 | }, 166 | "custom": { 167 | "axisCenteredZero": false, 168 | "axisColorMode": "text", 169 | "axisLabel": "", 170 | "axisPlacement": "auto", 171 | "barAlignment": 0, 172 | "drawStyle": "line", 173 | "fillOpacity": 0, 174 | "gradientMode": "none", 175 | "hideFrom": { 176 | "legend": false, 177 | "tooltip": false, 178 | "viz": false 179 | }, 180 | "lineInterpolation": "linear", 181 | "lineWidth": 1, 182 | "pointSize": 5, 183 | "scaleDistribution": { 184 | "type": "linear" 185 | }, 186 | "showPoints": "auto", 187 | "spanNulls": false, 188 | "stacking": { 189 | "group": "A", 190 | "mode": "none" 191 | }, 192 | "thresholdsStyle": { 193 | "mode": "off" 194 | } 195 | }, 196 | "mappings": [], 197 | "thresholds": { 198 | "mode": "absolute", 199 | "steps": [ 200 | { 201 | "color": "green", 202 | "value": null 203 | }, 204 | { 205 | "color": "red", 206 | "value": 80 207 | } 208 | ] 209 | }, 210 | "unit": "reqps" 211 | }, 212 | "overrides": [] 213 | }, 214 | "gridPos": { 215 | "h": 8, 216 | "w": 7, 217 | "x": 5, 218 | "y": 1 219 | }, 220 | "id": 7, 221 | "options": { 222 | "legend": { 223 | "calcs": [ 224 | "mean", 225 | "max" 226 | ], 227 | "displayMode": "table", 228 | "placement": "bottom", 229 | "showLegend": true, 230 | "sortBy": "Max", 231 | "sortDesc": true 232 | }, 233 | "tooltip": { 234 | "mode": "multi", 235 | "sort": "desc" 236 | } 237 | }, 238 | "targets": [ 239 | { 240 | "datasource": { 241 | "type": "prometheus", 242 | "uid": "prometheus" 243 | }, 244 | "editorMode": "code", 245 | "expr": "sum(rate(traefik_entrypoint_requests_total{entrypoint=~\"$entrypoint\"}[$interval])) by (entrypoint)", 246 | "legendFormat": "{{entrypoint}}", 247 | "range": true, 248 | "refId": "A" 249 | } 250 | ], 251 | "title": "Requests per Entrypoint", 252 | "type": "timeseries" 253 | }, 254 | { 255 | "datasource": { 256 | "type": "prometheus", 257 | "uid": "prometheus" 258 | }, 259 | "description": "https://medium.com/@tristan_96324/prometheus-apdex-alerting-d17a065e39d0", 260 | "fieldConfig": { 261 | "defaults": { 262 | "color": { 263 | "mode": "palette-classic" 264 | }, 265 | "custom": { 266 | "axisCenteredZero": false, 267 | "axisColorMode": "text", 268 | "axisLabel": "", 269 | "axisPlacement": "auto", 270 | "barAlignment": 0, 271 | "drawStyle": "line", 272 | "fillOpacity": 0, 273 | "gradientMode": "none", 274 | "hideFrom": { 275 | "legend": false, 276 | "tooltip": false, 277 | "viz": false 278 | }, 279 | "lineInterpolation": "linear", 280 | "lineWidth": 1, 281 | "pointSize": 5, 282 | "scaleDistribution": { 283 | "type": "linear" 284 | }, 285 | "showPoints": "auto", 286 | "spanNulls": false, 287 | "stacking": { 288 | "group": "A", 289 | "mode": "none" 290 | }, 291 | "thresholdsStyle": { 292 | "mode": "off" 293 | } 294 | }, 295 | "mappings": [], 296 | "thresholds": { 297 | "mode": "absolute", 298 | "steps": [ 299 | { 300 | "color": "green", 301 | "value": null 302 | }, 303 | { 304 | "color": "red", 305 | "value": 80 306 | } 307 | ] 308 | } 309 | }, 310 | "overrides": [] 311 | }, 312 | "gridPos": { 313 | "h": 8, 314 | "w": 12, 315 | "x": 12, 316 | "y": 1 317 | }, 318 | "id": 6, 319 | "options": { 320 | "legend": { 321 | "calcs": [ 322 | "mean", 323 | "max" 324 | ], 325 | "displayMode": "table", 326 | "placement": "bottom", 327 | "showLegend": true, 328 | "sortBy": "Max", 329 | "sortDesc": true 330 | }, 331 | "tooltip": { 332 | "mode": "multi", 333 | "sort": "desc" 334 | } 335 | }, 336 | "targets": [ 337 | { 338 | "datasource": { 339 | "type": "prometheus", 340 | "uid": "prometheus" 341 | }, 342 | "editorMode": "code", 343 | "expr": "(sum(rate(traefik_entrypoint_request_duration_seconds_bucket{le=\"0.3\",code=\"200\",entrypoint=~\"$entrypoint\"}[$interval])) by (method) + \n sum(rate(traefik_entrypoint_request_duration_seconds_bucket{le=\"1.2\",code=\"200\",entrypoint=~\"$entrypoint\"}[$interval])) by (method)) / 2 / \n sum(rate(traefik_entrypoint_request_duration_seconds_count{code=\"200\",entrypoint=~\"$entrypoint\"}[$interval])) by (method)\n", 344 | "legendFormat": "{{method}}", 345 | "range": true, 346 | "refId": "A" 347 | } 348 | ], 349 | "title": "Apdex score", 350 | "type": "timeseries" 351 | }, 352 | { 353 | "datasource": { 354 | "type": "prometheus", 355 | "uid": "prometheus" 356 | }, 357 | "description": "Mean Distribution", 358 | "fieldConfig": { 359 | "defaults": { 360 | "color": { 361 | "mode": "palette-classic" 362 | }, 363 | "custom": { 364 | "hideFrom": { 365 | "legend": false, 366 | "tooltip": false, 367 | "viz": false 368 | } 369 | }, 370 | "mappings": [], 371 | "unit": "reqps" 372 | }, 373 | "overrides": [] 374 | }, 375 | "gridPos": { 376 | "h": 6, 377 | "w": 5, 378 | "x": 0, 379 | "y": 3 380 | }, 381 | "id": 14, 382 | "options": { 383 | "legend": { 384 | "displayMode": "list", 385 | "placement": "right", 386 | "showLegend": true, 387 | "values": [ 388 | "percent" 389 | ] 390 | }, 391 | "pieType": "pie", 392 | "reduceOptions": { 393 | "calcs": [ 394 | "mean" 395 | ], 396 | "fields": "", 397 | "values": false 398 | }, 399 | "tooltip": { 400 | "mode": "multi", 401 | "sort": "asc" 402 | } 403 | }, 404 | "targets": [ 405 | { 406 | "datasource": { 407 | "type": "prometheus", 408 | "uid": "prometheus" 409 | }, 410 | "editorMode": "code", 411 | "expr": "sum(rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[$interval])) by (method, code)", 412 | "legendFormat": "{{method}}[{{code}}]", 413 | "range": true, 414 | "refId": "A" 415 | } 416 | ], 417 | "title": "Http Code ", 418 | "type": "piechart" 419 | }, 420 | { 421 | "datasource": { 422 | "type": "prometheus", 423 | "uid": "prometheus" 424 | }, 425 | "description": "", 426 | "fieldConfig": { 427 | "defaults": { 428 | "color": { 429 | "mode": "palette-classic" 430 | }, 431 | "custom": { 432 | "axisCenteredZero": false, 433 | "axisColorMode": "text", 434 | "axisLabel": "", 435 | "axisPlacement": "auto", 436 | "barAlignment": 0, 437 | "drawStyle": "line", 438 | "fillOpacity": 0, 439 | "gradientMode": "none", 440 | "hideFrom": { 441 | "legend": false, 442 | "tooltip": false, 443 | "viz": false 444 | }, 445 | "lineInterpolation": "linear", 446 | "lineWidth": 1, 447 | "pointSize": 5, 448 | "scaleDistribution": { 449 | "type": "linear" 450 | }, 451 | "showPoints": "auto", 452 | "spanNulls": false, 453 | "stacking": { 454 | "group": "A", 455 | "mode": "none" 456 | }, 457 | "thresholdsStyle": { 458 | "mode": "off" 459 | } 460 | }, 461 | "mappings": [], 462 | "thresholds": { 463 | "mode": "absolute", 464 | "steps": [ 465 | { 466 | "color": "green", 467 | "value": null 468 | }, 469 | { 470 | "color": "red", 471 | "value": 80 472 | } 473 | ] 474 | }, 475 | "unit": "s" 476 | }, 477 | "overrides": [] 478 | }, 479 | "gridPos": { 480 | "h": 8, 481 | "w": 12, 482 | "x": 0, 483 | "y": 9 484 | }, 485 | "id": 23, 486 | "options": { 487 | "legend": { 488 | "calcs": [ 489 | "mean", 490 | "max" 491 | ], 492 | "displayMode": "table", 493 | "placement": "right", 494 | "showLegend": true, 495 | "sortBy": "Mean", 496 | "sortDesc": true 497 | }, 498 | "tooltip": { 499 | "mode": "multi", 500 | "sort": "desc" 501 | } 502 | }, 503 | "targets": [ 504 | { 505 | "datasource": { 506 | "type": "prometheus", 507 | "uid": "prometheus" 508 | }, 509 | "editorMode": "code", 510 | "expr": "topk(15,\n label_replace(\n traefik_service_request_duration_seconds_sum{service=~\"$service.*\",protocol=\"http\"} / \n traefik_service_request_duration_seconds_count{service=~\"$service.*\",protocol=\"http\"},\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)\n\n", 511 | "legendFormat": "{{method}}[{{code}}] on {{service}}", 512 | "range": true, 513 | "refId": "A" 514 | } 515 | ], 516 | "title": "Top slow services", 517 | "type": "timeseries" 518 | }, 519 | { 520 | "datasource": { 521 | "type": "prometheus", 522 | "uid": "prometheus" 523 | }, 524 | "description": "", 525 | "fieldConfig": { 526 | "defaults": { 527 | "color": { 528 | "mode": "palette-classic" 529 | }, 530 | "custom": { 531 | "axisCenteredZero": false, 532 | "axisColorMode": "text", 533 | "axisLabel": "", 534 | "axisPlacement": "auto", 535 | "barAlignment": 0, 536 | "drawStyle": "line", 537 | "fillOpacity": 0, 538 | "gradientMode": "none", 539 | "hideFrom": { 540 | "legend": false, 541 | "tooltip": false, 542 | "viz": false 543 | }, 544 | "lineInterpolation": "linear", 545 | "lineWidth": 1, 546 | "pointSize": 5, 547 | "scaleDistribution": { 548 | "type": "linear" 549 | }, 550 | "showPoints": "auto", 551 | "spanNulls": false, 552 | "stacking": { 553 | "group": "A", 554 | "mode": "none" 555 | }, 556 | "thresholdsStyle": { 557 | "mode": "off" 558 | } 559 | }, 560 | "mappings": [], 561 | "thresholds": { 562 | "mode": "absolute", 563 | "steps": [ 564 | { 565 | "color": "green", 566 | "value": null 567 | }, 568 | { 569 | "color": "red", 570 | "value": 80 571 | } 572 | ] 573 | }, 574 | "unit": "reqps" 575 | }, 576 | "overrides": [] 577 | }, 578 | "gridPos": { 579 | "h": 8, 580 | "w": 12, 581 | "x": 12, 582 | "y": 9 583 | }, 584 | "id": 5, 585 | "options": { 586 | "legend": { 587 | "calcs": [ 588 | "mean", 589 | "max" 590 | ], 591 | "displayMode": "table", 592 | "placement": "right", 593 | "showLegend": true, 594 | "sortBy": "Mean", 595 | "sortDesc": true 596 | }, 597 | "tooltip": { 598 | "mode": "multi", 599 | "sort": "desc" 600 | } 601 | }, 602 | "targets": [ 603 | { 604 | "datasource": { 605 | "type": "prometheus", 606 | "uid": "prometheus" 607 | }, 608 | "editorMode": "code", 609 | "expr": "topk(15,\n label_replace(\n sum by (service,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", 610 | "legendFormat": "[{{code}}] on {{service}}", 611 | "range": true, 612 | "refId": "A" 613 | } 614 | ], 615 | "title": "Most requested services", 616 | "type": "timeseries" 617 | }, 618 | { 619 | "collapsed": true, 620 | "gridPos": { 621 | "h": 1, 622 | "w": 24, 623 | "x": 0, 624 | "y": 17 625 | }, 626 | "id": 11, 627 | "panels": [ 628 | { 629 | "datasource": { 630 | "type": "prometheus", 631 | "uid": "prometheus" 632 | }, 633 | "description": "", 634 | "fieldConfig": { 635 | "defaults": { 636 | "color": { 637 | "mode": "palette-classic" 638 | }, 639 | "custom": { 640 | "axisCenteredZero": false, 641 | "axisColorMode": "text", 642 | "axisLabel": "", 643 | "axisPlacement": "auto", 644 | "barAlignment": 0, 645 | "drawStyle": "line", 646 | "fillOpacity": 0, 647 | "gradientMode": "none", 648 | "hideFrom": { 649 | "legend": false, 650 | "tooltip": false, 651 | "viz": false 652 | }, 653 | "lineInterpolation": "linear", 654 | "lineWidth": 1, 655 | "pointSize": 5, 656 | "scaleDistribution": { 657 | "type": "linear" 658 | }, 659 | "showPoints": "auto", 660 | "spanNulls": false, 661 | "stacking": { 662 | "group": "A", 663 | "mode": "none" 664 | }, 665 | "thresholdsStyle": { 666 | "mode": "off" 667 | } 668 | }, 669 | "mappings": [], 670 | "thresholds": { 671 | "mode": "absolute", 672 | "steps": [ 673 | { 674 | "color": "green" 675 | } 676 | ] 677 | }, 678 | "unit": "percentunit" 679 | }, 680 | "overrides": [] 681 | }, 682 | "gridPos": { 683 | "h": 8, 684 | "w": 12, 685 | "x": 0, 686 | "y": 18 687 | }, 688 | "id": 3, 689 | "options": { 690 | "legend": { 691 | "calcs": [ 692 | "mean", 693 | "max" 694 | ], 695 | "displayMode": "table", 696 | "placement": "right", 697 | "showLegend": true, 698 | "sortBy": "Max", 699 | "sortDesc": true 700 | }, 701 | "tooltip": { 702 | "mode": "multi", 703 | "sort": "desc" 704 | } 705 | }, 706 | "targets": [ 707 | { 708 | "datasource": { 709 | "type": "prometheus", 710 | "uid": "prometheus" 711 | }, 712 | "editorMode": "code", 713 | "expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"1.2\",service=~\"$service.*\"}[$interval])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[$interval]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)", 714 | "legendFormat": "{{service}}", 715 | "range": true, 716 | "refId": "A" 717 | } 718 | ], 719 | "title": "Services failing SLO of 1200ms", 720 | "type": "timeseries" 721 | }, 722 | { 723 | "datasource": { 724 | "type": "prometheus", 725 | "uid": "prometheus" 726 | }, 727 | "description": "", 728 | "fieldConfig": { 729 | "defaults": { 730 | "color": { 731 | "mode": "palette-classic" 732 | }, 733 | "custom": { 734 | "axisCenteredZero": false, 735 | "axisColorMode": "text", 736 | "axisLabel": "", 737 | "axisPlacement": "auto", 738 | "barAlignment": 0, 739 | "drawStyle": "line", 740 | "fillOpacity": 0, 741 | "gradientMode": "none", 742 | "hideFrom": { 743 | "legend": false, 744 | "tooltip": false, 745 | "viz": false 746 | }, 747 | "lineInterpolation": "linear", 748 | "lineWidth": 1, 749 | "pointSize": 5, 750 | "scaleDistribution": { 751 | "type": "linear" 752 | }, 753 | "showPoints": "auto", 754 | "spanNulls": false, 755 | "stacking": { 756 | "group": "A", 757 | "mode": "none" 758 | }, 759 | "thresholdsStyle": { 760 | "mode": "off" 761 | } 762 | }, 763 | "mappings": [], 764 | "thresholds": { 765 | "mode": "absolute", 766 | "steps": [ 767 | { 768 | "color": "green" 769 | } 770 | ] 771 | }, 772 | "unit": "percentunit" 773 | }, 774 | "overrides": [] 775 | }, 776 | "gridPos": { 777 | "h": 8, 778 | "w": 12, 779 | "x": 12, 780 | "y": 18 781 | }, 782 | "id": 4, 783 | "options": { 784 | "legend": { 785 | "calcs": [ 786 | "mean", 787 | "max" 788 | ], 789 | "displayMode": "table", 790 | "placement": "right", 791 | "showLegend": true, 792 | "sortBy": "Max", 793 | "sortDesc": true 794 | }, 795 | "tooltip": { 796 | "mode": "multi", 797 | "sort": "desc" 798 | } 799 | }, 800 | "targets": [ 801 | { 802 | "datasource": { 803 | "type": "prometheus", 804 | "uid": "prometheus" 805 | }, 806 | "editorMode": "code", 807 | "expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"0.3\",service=~\"$service.*\"}[$interval])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[$interval]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)", 808 | "legendFormat": "{{service}}", 809 | "range": true, 810 | "refId": "A" 811 | } 812 | ], 813 | "title": "Services failing SLO of 300ms", 814 | "type": "timeseries" 815 | } 816 | ], 817 | "title": "SLO", 818 | "type": "row" 819 | }, 820 | { 821 | "collapsed": true, 822 | "gridPos": { 823 | "h": 1, 824 | "w": 24, 825 | "x": 0, 826 | "y": 18 827 | }, 828 | "id": 16, 829 | "panels": [ 830 | { 831 | "datasource": { 832 | "type": "prometheus", 833 | "uid": "prometheus" 834 | }, 835 | "description": "", 836 | "fieldConfig": { 837 | "defaults": { 838 | "color": { 839 | "mode": "palette-classic" 840 | }, 841 | "custom": { 842 | "axisCenteredZero": false, 843 | "axisColorMode": "text", 844 | "axisLabel": "", 845 | "axisPlacement": "auto", 846 | "barAlignment": 0, 847 | "drawStyle": "line", 848 | "fillOpacity": 0, 849 | "gradientMode": "none", 850 | "hideFrom": { 851 | "legend": false, 852 | "tooltip": false, 853 | "viz": false 854 | }, 855 | "lineInterpolation": "linear", 856 | "lineWidth": 1, 857 | "pointSize": 5, 858 | "scaleDistribution": { 859 | "type": "linear" 860 | }, 861 | "showPoints": "auto", 862 | "spanNulls": false, 863 | "stacking": { 864 | "group": "A", 865 | "mode": "none" 866 | }, 867 | "thresholdsStyle": { 868 | "mode": "off" 869 | } 870 | }, 871 | "mappings": [], 872 | "thresholds": { 873 | "mode": "absolute", 874 | "steps": [ 875 | { 876 | "color": "green" 877 | }, 878 | { 879 | "color": "red", 880 | "value": 80 881 | } 882 | ] 883 | }, 884 | "unit": "reqps" 885 | }, 886 | "overrides": [] 887 | }, 888 | "gridPos": { 889 | "h": 12, 890 | "w": 8, 891 | "x": 0, 892 | "y": 19 893 | }, 894 | "id": 17, 895 | "options": { 896 | "legend": { 897 | "calcs": [ 898 | "mean", 899 | "max" 900 | ], 901 | "displayMode": "table", 902 | "placement": "bottom", 903 | "showLegend": true, 904 | "sortBy": "Mean", 905 | "sortDesc": true 906 | }, 907 | "tooltip": { 908 | "mode": "multi", 909 | "sort": "desc" 910 | } 911 | }, 912 | "targets": [ 913 | { 914 | "datasource": { 915 | "type": "prometheus", 916 | "uid": "prometheus" 917 | }, 918 | "editorMode": "code", 919 | "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"2..\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", 920 | "legendFormat": "{{method}}[{{code}}] on {{service}}", 921 | "range": true, 922 | "refId": "A" 923 | } 924 | ], 925 | "title": "2xx over $interval", 926 | "type": "timeseries" 927 | }, 928 | { 929 | "datasource": { 930 | "type": "prometheus", 931 | "uid": "prometheus" 932 | }, 933 | "description": "", 934 | "fieldConfig": { 935 | "defaults": { 936 | "color": { 937 | "mode": "palette-classic" 938 | }, 939 | "custom": { 940 | "axisCenteredZero": false, 941 | "axisColorMode": "text", 942 | "axisGridShow": true, 943 | "axisLabel": "", 944 | "axisPlacement": "auto", 945 | "barAlignment": 0, 946 | "drawStyle": "line", 947 | "fillOpacity": 0, 948 | "gradientMode": "none", 949 | "hideFrom": { 950 | "legend": false, 951 | "tooltip": false, 952 | "viz": false 953 | }, 954 | "lineInterpolation": "linear", 955 | "lineWidth": 1, 956 | "pointSize": 5, 957 | "scaleDistribution": { 958 | "type": "linear" 959 | }, 960 | "showPoints": "auto", 961 | "spanNulls": false, 962 | "stacking": { 963 | "group": "A", 964 | "mode": "none" 965 | }, 966 | "thresholdsStyle": { 967 | "mode": "off" 968 | } 969 | }, 970 | "mappings": [], 971 | "thresholds": { 972 | "mode": "absolute", 973 | "steps": [ 974 | { 975 | "color": "green" 976 | }, 977 | { 978 | "color": "red", 979 | "value": 80 980 | } 981 | ] 982 | }, 983 | "unit": "reqps" 984 | }, 985 | "overrides": [] 986 | }, 987 | "gridPos": { 988 | "h": 12, 989 | "w": 8, 990 | "x": 8, 991 | "y": 19 992 | }, 993 | "id": 18, 994 | "options": { 995 | "legend": { 996 | "calcs": [ 997 | "mean", 998 | "max" 999 | ], 1000 | "displayMode": "table", 1001 | "placement": "bottom", 1002 | "showLegend": true, 1003 | "sortBy": "Mean", 1004 | "sortDesc": true 1005 | }, 1006 | "tooltip": { 1007 | "mode": "multi", 1008 | "sort": "desc" 1009 | } 1010 | }, 1011 | "targets": [ 1012 | { 1013 | "datasource": { 1014 | "type": "prometheus", 1015 | "uid": "prometheus" 1016 | }, 1017 | "editorMode": "code", 1018 | "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"5..\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", 1019 | "legendFormat": "{{method}}[{{code}}] on {{service}}", 1020 | "range": true, 1021 | "refId": "A" 1022 | } 1023 | ], 1024 | "title": "5xx over $interval", 1025 | "type": "timeseries" 1026 | }, 1027 | { 1028 | "datasource": { 1029 | "type": "prometheus", 1030 | "uid": "prometheus" 1031 | }, 1032 | "description": "", 1033 | "fieldConfig": { 1034 | "defaults": { 1035 | "color": { 1036 | "mode": "palette-classic" 1037 | }, 1038 | "custom": { 1039 | "axisCenteredZero": false, 1040 | "axisColorMode": "text", 1041 | "axisGridShow": true, 1042 | "axisLabel": "", 1043 | "axisPlacement": "auto", 1044 | "barAlignment": 0, 1045 | "drawStyle": "line", 1046 | "fillOpacity": 0, 1047 | "gradientMode": "none", 1048 | "hideFrom": { 1049 | "legend": false, 1050 | "tooltip": false, 1051 | "viz": false 1052 | }, 1053 | "lineInterpolation": "linear", 1054 | "lineWidth": 1, 1055 | "pointSize": 5, 1056 | "scaleDistribution": { 1057 | "type": "linear" 1058 | }, 1059 | "showPoints": "auto", 1060 | "spanNulls": false, 1061 | "stacking": { 1062 | "group": "A", 1063 | "mode": "none" 1064 | }, 1065 | "thresholdsStyle": { 1066 | "mode": "off" 1067 | } 1068 | }, 1069 | "mappings": [], 1070 | "thresholds": { 1071 | "mode": "absolute", 1072 | "steps": [ 1073 | { 1074 | "color": "green" 1075 | }, 1076 | { 1077 | "color": "red", 1078 | "value": 80 1079 | } 1080 | ] 1081 | }, 1082 | "unit": "reqps" 1083 | }, 1084 | "overrides": [] 1085 | }, 1086 | "gridPos": { 1087 | "h": 12, 1088 | "w": 8, 1089 | "x": 16, 1090 | "y": 19 1091 | }, 1092 | "id": 19, 1093 | "options": { 1094 | "legend": { 1095 | "calcs": [ 1096 | "mean", 1097 | "max" 1098 | ], 1099 | "displayMode": "table", 1100 | "placement": "bottom", 1101 | "showLegend": true, 1102 | "sortBy": "Mean", 1103 | "sortDesc": true 1104 | }, 1105 | "tooltip": { 1106 | "mode": "multi", 1107 | "sort": "desc" 1108 | } 1109 | }, 1110 | "targets": [ 1111 | { 1112 | "datasource": { 1113 | "type": "prometheus", 1114 | "uid": "prometheus" 1115 | }, 1116 | "editorMode": "code", 1117 | "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code!~\"2..|5..\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", 1118 | "legendFormat": "{{method}}[{{code}}] on {{service}}", 1119 | "range": true, 1120 | "refId": "A" 1121 | } 1122 | ], 1123 | "title": "Other codes over $interval", 1124 | "type": "timeseries" 1125 | }, 1126 | { 1127 | "datasource": { 1128 | "type": "prometheus", 1129 | "uid": "prometheus" 1130 | }, 1131 | "description": "", 1132 | "fieldConfig": { 1133 | "defaults": { 1134 | "color": { 1135 | "mode": "palette-classic" 1136 | }, 1137 | "custom": { 1138 | "axisCenteredZero": false, 1139 | "axisColorMode": "text", 1140 | "axisGridShow": true, 1141 | "axisLabel": "", 1142 | "axisPlacement": "auto", 1143 | "barAlignment": 0, 1144 | "drawStyle": "line", 1145 | "fillOpacity": 0, 1146 | "gradientMode": "none", 1147 | "hideFrom": { 1148 | "legend": false, 1149 | "tooltip": false, 1150 | "viz": false 1151 | }, 1152 | "lineInterpolation": "linear", 1153 | "lineWidth": 1, 1154 | "pointSize": 5, 1155 | "scaleDistribution": { 1156 | "type": "linear" 1157 | }, 1158 | "showPoints": "auto", 1159 | "spanNulls": false, 1160 | "stacking": { 1161 | "group": "A", 1162 | "mode": "none" 1163 | }, 1164 | "thresholdsStyle": { 1165 | "mode": "off" 1166 | } 1167 | }, 1168 | "mappings": [], 1169 | "thresholds": { 1170 | "mode": "absolute", 1171 | "steps": [ 1172 | { 1173 | "color": "green" 1174 | }, 1175 | { 1176 | "color": "red", 1177 | "value": 80 1178 | } 1179 | ] 1180 | }, 1181 | "unit": "binBps" 1182 | }, 1183 | "overrides": [] 1184 | }, 1185 | "gridPos": { 1186 | "h": 8, 1187 | "w": 12, 1188 | "x": 0, 1189 | "y": 31 1190 | }, 1191 | "id": 20, 1192 | "options": { 1193 | "legend": { 1194 | "calcs": [ 1195 | "mean", 1196 | "max" 1197 | ], 1198 | "displayMode": "table", 1199 | "placement": "right", 1200 | "showLegend": true, 1201 | "sortBy": "Mean", 1202 | "sortDesc": true 1203 | }, 1204 | "tooltip": { 1205 | "mode": "multi", 1206 | "sort": "desc" 1207 | } 1208 | }, 1209 | "targets": [ 1210 | { 1211 | "datasource": { 1212 | "type": "prometheus", 1213 | "uid": "prometheus" 1214 | }, 1215 | "editorMode": "code", 1216 | "expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_requests_bytes_total{service=~\"$service.*\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", 1217 | "legendFormat": "{{method}} on {{service}}", 1218 | "range": true, 1219 | "refId": "A" 1220 | } 1221 | ], 1222 | "title": "Requests Size", 1223 | "type": "timeseries" 1224 | }, 1225 | { 1226 | "datasource": { 1227 | "type": "prometheus", 1228 | "uid": "prometheus" 1229 | }, 1230 | "description": "", 1231 | "fieldConfig": { 1232 | "defaults": { 1233 | "color": { 1234 | "mode": "palette-classic" 1235 | }, 1236 | "custom": { 1237 | "axisCenteredZero": false, 1238 | "axisColorMode": "text", 1239 | "axisGridShow": true, 1240 | "axisLabel": "", 1241 | "axisPlacement": "auto", 1242 | "barAlignment": 0, 1243 | "drawStyle": "line", 1244 | "fillOpacity": 0, 1245 | "gradientMode": "none", 1246 | "hideFrom": { 1247 | "legend": false, 1248 | "tooltip": false, 1249 | "viz": false 1250 | }, 1251 | "lineInterpolation": "linear", 1252 | "lineWidth": 1, 1253 | "pointSize": 5, 1254 | "scaleDistribution": { 1255 | "type": "linear" 1256 | }, 1257 | "showPoints": "auto", 1258 | "spanNulls": false, 1259 | "stacking": { 1260 | "group": "A", 1261 | "mode": "none" 1262 | }, 1263 | "thresholdsStyle": { 1264 | "mode": "off" 1265 | } 1266 | }, 1267 | "mappings": [], 1268 | "thresholds": { 1269 | "mode": "absolute", 1270 | "steps": [ 1271 | { 1272 | "color": "green" 1273 | }, 1274 | { 1275 | "color": "red", 1276 | "value": 80 1277 | } 1278 | ] 1279 | }, 1280 | "unit": "binBps" 1281 | }, 1282 | "overrides": [] 1283 | }, 1284 | "gridPos": { 1285 | "h": 8, 1286 | "w": 12, 1287 | "x": 12, 1288 | "y": 31 1289 | }, 1290 | "id": 24, 1291 | "options": { 1292 | "legend": { 1293 | "calcs": [ 1294 | "mean", 1295 | "max" 1296 | ], 1297 | "displayMode": "table", 1298 | "placement": "right", 1299 | "showLegend": true, 1300 | "sortBy": "Mean", 1301 | "sortDesc": true 1302 | }, 1303 | "tooltip": { 1304 | "mode": "multi", 1305 | "sort": "desc" 1306 | } 1307 | }, 1308 | "targets": [ 1309 | { 1310 | "datasource": { 1311 | "type": "prometheus", 1312 | "uid": "prometheus" 1313 | }, 1314 | "editorMode": "code", 1315 | "expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_responses_bytes_total{service=~\"$service.*\",protocol=\"http\"}[$interval])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", 1316 | "legendFormat": "{{method}} on {{service}}", 1317 | "range": true, 1318 | "refId": "A" 1319 | } 1320 | ], 1321 | "title": "Responses Size", 1322 | "type": "timeseries" 1323 | }, 1324 | { 1325 | "datasource": { 1326 | "type": "prometheus", 1327 | "uid": "prometheus" 1328 | }, 1329 | "description": "", 1330 | "fieldConfig": { 1331 | "defaults": { 1332 | "color": { 1333 | "mode": "palette-classic" 1334 | }, 1335 | "custom": { 1336 | "axisCenteredZero": false, 1337 | "axisColorMode": "text", 1338 | "axisLabel": "", 1339 | "axisPlacement": "auto", 1340 | "barAlignment": 0, 1341 | "drawStyle": "line", 1342 | "fillOpacity": 0, 1343 | "gradientMode": "none", 1344 | "hideFrom": { 1345 | "legend": false, 1346 | "tooltip": false, 1347 | "viz": false 1348 | }, 1349 | "lineInterpolation": "linear", 1350 | "lineWidth": 1, 1351 | "pointSize": 5, 1352 | "scaleDistribution": { 1353 | "type": "linear" 1354 | }, 1355 | "showPoints": "auto", 1356 | "spanNulls": false, 1357 | "stacking": { 1358 | "group": "A", 1359 | "mode": "none" 1360 | }, 1361 | "thresholdsStyle": { 1362 | "mode": "off" 1363 | } 1364 | }, 1365 | "mappings": [], 1366 | "thresholds": { 1367 | "mode": "absolute", 1368 | "steps": [ 1369 | { 1370 | "color": "green" 1371 | }, 1372 | { 1373 | "color": "red", 1374 | "value": 80 1375 | } 1376 | ] 1377 | }, 1378 | "unit": "short" 1379 | }, 1380 | "overrides": [] 1381 | }, 1382 | "gridPos": { 1383 | "h": 8, 1384 | "w": 12, 1385 | "x": 12, 1386 | "y": 39 1387 | }, 1388 | "id": 21, 1389 | "options": { 1390 | "legend": { 1391 | "calcs": [ 1392 | "mean", 1393 | "max" 1394 | ], 1395 | "displayMode": "table", 1396 | "placement": "right", 1397 | "showLegend": true, 1398 | "sortBy": "Max", 1399 | "sortDesc": true 1400 | }, 1401 | "tooltip": { 1402 | "mode": "multi", 1403 | "sort": "desc" 1404 | } 1405 | }, 1406 | "targets": [ 1407 | { 1408 | "datasource": { 1409 | "type": "prometheus", 1410 | "uid": "prometheus" 1411 | }, 1412 | "editorMode": "code", 1413 | "expr": "sum(traefik_open_connections{entrypoint=~\"$entrypoint\"}) by (entrypoint)\n", 1414 | "legendFormat": "{{entrypoint}}", 1415 | "range": true, 1416 | "refId": "A" 1417 | } 1418 | ], 1419 | "title": "Connections per Entrypoint", 1420 | "type": "timeseries" 1421 | } 1422 | ], 1423 | "title": "HTTP Details", 1424 | "type": "row" 1425 | } 1426 | ], 1427 | "refresh": false, 1428 | "schemaVersion": 37, 1429 | "style": "dark", 1430 | "tags": [], 1431 | "templating": { 1432 | "list": [ 1433 | { 1434 | "current": { 1435 | "selected": false, 1436 | "text": "Prometheus", 1437 | "value": "Prometheus" 1438 | }, 1439 | "hide": 0, 1440 | "includeAll": false, 1441 | "multi": false, 1442 | "name": "DS_PROMETHEUS", 1443 | "label": "datasource", 1444 | "options": [], 1445 | "query": "prometheus", 1446 | "refresh": 1, 1447 | "regex": "", 1448 | "skipUrlSync": false, 1449 | "type": "datasource" 1450 | }, 1451 | { 1452 | "auto": true, 1453 | "auto_count": 30, 1454 | "auto_min": "1m", 1455 | "current": { 1456 | "selected": false, 1457 | "text": "auto", 1458 | "value": "$__auto_interval_interval" 1459 | }, 1460 | "hide": 0, 1461 | "name": "interval", 1462 | "options": [ 1463 | { 1464 | "selected": true, 1465 | "text": "auto", 1466 | "value": "$__auto_interval_interval" 1467 | }, 1468 | { 1469 | "selected": false, 1470 | "text": "1m", 1471 | "value": "1m" 1472 | }, 1473 | { 1474 | "selected": false, 1475 | "text": "5m", 1476 | "value": "5m" 1477 | }, 1478 | { 1479 | "selected": false, 1480 | "text": "10m", 1481 | "value": "10m" 1482 | }, 1483 | { 1484 | "selected": false, 1485 | "text": "30m", 1486 | "value": "30m" 1487 | }, 1488 | { 1489 | "selected": false, 1490 | "text": "1h", 1491 | "value": "1h" 1492 | }, 1493 | { 1494 | "selected": false, 1495 | "text": "2h", 1496 | "value": "2h" 1497 | }, 1498 | { 1499 | "selected": false, 1500 | "text": "4h", 1501 | "value": "4h" 1502 | }, 1503 | { 1504 | "selected": false, 1505 | "text": "8h", 1506 | "value": "8h" 1507 | } 1508 | ], 1509 | "query": "1m,5m,10m,30m,1h,2h,4h,8h", 1510 | "refresh": 2, 1511 | "skipUrlSync": false, 1512 | "type": "interval" 1513 | }, 1514 | { 1515 | "current": {}, 1516 | "datasource": { 1517 | "type": "prometheus", 1518 | "uid": "prometheus" 1519 | }, 1520 | "definition": "label_values(traefik_open_connections, entrypoint)", 1521 | "hide": 0, 1522 | "includeAll": true, 1523 | "multi": false, 1524 | "name": "entrypoint", 1525 | "options": [], 1526 | "query": { 1527 | "query": "label_values(traefik_open_connections, entrypoint)", 1528 | "refId": "StandardVariableQuery" 1529 | }, 1530 | "refresh": 1, 1531 | "regex": "", 1532 | "skipUrlSync": false, 1533 | "sort": 0, 1534 | "type": "query" 1535 | }, 1536 | { 1537 | "current": {}, 1538 | "datasource": { 1539 | "type": "prometheus", 1540 | "uid": "prometheus" 1541 | }, 1542 | "definition": "label_values(traefik_service_requests_total, service)", 1543 | "hide": 0, 1544 | "includeAll": true, 1545 | "multi": false, 1546 | "name": "service", 1547 | "options": [], 1548 | "query": { 1549 | "query": "label_values(traefik_service_requests_total, service)", 1550 | "refId": "StandardVariableQuery" 1551 | }, 1552 | "refresh": 2, 1553 | "regex": "", 1554 | "skipUrlSync": false, 1555 | "sort": 1, 1556 | "type": "query" 1557 | } 1558 | ] 1559 | }, 1560 | "time": { 1561 | "from": "now-6h", 1562 | "to": "now" 1563 | }, 1564 | "timepicker": {}, 1565 | "timezone": "", 1566 | "title": "Traefik Official Standalone Dashboard", 1567 | "uid": "n5bu_kv45", 1568 | "version": 7, 1569 | "weekStart": "" 1570 | } --------------------------------------------------------------------------------