├── .gitignore ├── README.md ├── composer.json ├── config ├── backup.php └── seq.php ├── docker-compose-prod.env ├── docker-compose-prod.yml ├── docker-compose-worker.yml └── src └── ServiceProvider.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Installation 2 | ``` 3 | composer require "downtoworld/laravel-devops:*" 4 | ``` 5 | 6 | Publish the **required** files: 7 | ``` 8 | php artisan vendor:publish --tag=laravel-devops 9 | ``` 10 | 11 | ### Example with Portainer and Cloudflare Tunnels 12 | - Create the Docker network `cloudflared` (bridge) 13 | - Deploy the Docker container as specified @ Cloudflare -> Zero Trust -> Access -> Tunnels -> Create a tunnel (**Additionally**: attach the container to the previously created network by specifying `--network cloudflared`) 14 | - Create a Git-repo based Stack @ Portainer webUI 15 | - At **Compose path** specify: `docker-compose-prod.yml` 16 | - Enable **GitOps updates** 17 | - Fill the required environment variables: 18 | - `APP_NAME`: *several Laravel configurations use it for prefixing* 19 | - `APP_KEY`: *can be generated on your local environment* 20 | - `APP_DOMAIN`: domain of the app in production Example: *yourdomain.com* 21 | - `APP_DOCKER_STACK`: the name of the stack you are configuring @ Portainer. Example: *mystack* 22 | - [*You can also configure here any Laravel env variables like `APP_NAME` or `APP_DEBUG`*] 23 | - *Deploy the stack* 24 | - Add public hostnames to the tunnel @ Cloudflare: 25 | - **Webpage (Nginx)**: *yourdomain.com* HTTP *mystack*-nginx-1:80 26 | - **S3 Storage (Minio)**: *cdn.yourdomain.com* HTTP *mystack*-minio-1:9000 27 | - **Websocket server (Soketi)**: *ws.yourdomain.com* HTTPS (tls-check-disabled and ws-enabled options) *mystack*-soketi-1:6001 28 | 29 | ### Accessing private services (MySQL, Redis, etc) locally 30 | - Run `docker run -d --name cloudflare-docker-dns --restart always --network cloudflared -e DNS_FORWARDER=127.0.0.11 cytopia/bind` and copy it's assigned IP (*your-assigned-ip*) from Portainer UI. 31 | - Go to Portainer networks and copy `cloudflared` assigned IPV4 IPAM Subnet (*your-network-ip-range*) 32 | - Go to Cloudflare -> Zero Trust -> Access -> Tunnels and configure a new `Private network` at your tunnel with `CIDR`: *your-network-ip-range* 33 | - Go to Cloudflare Zero Trust Settings -> WARP Client -> Configure "Default" Device Settings: 34 | - Add a Local Domain Fallback: `domain`: cloudflared `DNS Servers`: *your-assigned-ip* 35 | - Set Split Tunnels to `Include IPs and domains` and add `Selector`: IP Address `Value`: *your-network-ip-range* 36 | - Give your email access at Cloudflare Zero Trust Settings -> WARP Client -> Device enrollment permissions. 37 | - Install [Cloudflare WARP](https://1.1.1.1/) on your computer, connect it to your Zero Trust org and enable it. 38 | - Now you can access all your cloudflared-network-connected docker containers locally as `mystack-service-1.cloudflared:port` 39 | 40 | The list of services you can access: 41 | - *mystack*-mysql-1.cloudflared:3306 `User`: root `Password`: secret 42 | - *mystack*-redis-1.cloudflared:6379 no-password 43 | - http://*mystack*-seq-1.cloudflared 44 | - http://*mystack*-minio-1.cloudflared:8900 45 | - http://*mystack*-mailpit-1.cloudflared:8025 46 | - http://*mystack*-meilisearch-1.cloudflared:7700 47 | 48 | ### Environment variables 49 | Application environment variables can be managed at `docker-compose-prod.env` file. 50 | 51 | ### Queue and Scheduler 52 | **Scheduler** and **Horizon** supervisors are running separately on their containers. 53 | 54 | ### PHP Versions, Extensions and INI files 55 | 8.2 version is running by default but can be switched to `8.1` or `7.4` using the `APP_PHP_VERSION` environment variable. 56 | 57 | ### Thank yous 58 | - Big one for Cloudflare Team for making this possible for free. 59 | - Portainer project made it possible with their GitOps updates. 60 | - ServerSideUp team made it possible with their PHP Docker images. 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "downtoworld/laravel-devops", 3 | "description": "Laravel Cloudflare-Tunnels Ready Production Docker-Compose", 4 | "keywords": ["laravel", "docker compose", "docker", "devops", "cloudflare tunnels", "portainer", "ci cd", "production"], 5 | "license": "MIT", 6 | 7 | "authors": [ 8 | { 9 | "name": "Sergio Ródenas", 10 | "homepage": "https://sergiorodenas.com", 11 | "role": "developer" 12 | } 13 | ], 14 | 15 | "require": { 16 | "php": ">=7.4", 17 | "hedii/laravel-gelf-logger": "*", 18 | "laravel/framework": "^8.0|^9.0|^10.0", 19 | "spatie/laravel-backup": "^8.5" 20 | }, 21 | 22 | "autoload": { 23 | "psr-4": { 24 | "DownToWorld\\LaravelDevops\\": "src" 25 | } 26 | }, 27 | 28 | "config": { 29 | "sort-packages": true 30 | }, 31 | 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "DownToWorld\\LaravelDevops\\ServiceProvider" 36 | ] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /config/backup.php: -------------------------------------------------------------------------------- 1 | [ 6 | 7 | 'name' => env('APP_NAME', 'laravel-backup'), 8 | 9 | 'source' => [ 10 | 11 | 'files' => [ 12 | 'include' => [ 13 | // We store files on S3 14 | ], 15 | ], 16 | 17 | 'databases' => [ 18 | 'mysql', 19 | ], 20 | ], 21 | 22 | 'database_dump_compressor' => Spatie\DbDumper\Compressors\GzipCompressor::class, 23 | 24 | 'destination' => [ 25 | 26 | 'disks' => [ 27 | 's3' => [ 28 | 'driver' => 's3', 29 | ], 30 | ], 31 | ], 32 | 33 | 'temporary_directory' => storage_path('app/backup-temp'), 34 | 35 | 'password' => env('BACKUP_ARCHIVE_PASSWORD'), 36 | ], 37 | 38 | 'notifications' => [ 39 | 40 | 'notifications' => [ 41 | \Spatie\Backup\Notifications\Notifications\BackupHasFailedNotification::class => ['mail'], 42 | \Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFoundNotification::class => ['mail'], 43 | \Spatie\Backup\Notifications\Notifications\CleanupHasFailedNotification::class => ['mail'], 44 | \Spatie\Backup\Notifications\Notifications\BackupWasSuccessfulNotification::class => ['mail'], 45 | \Spatie\Backup\Notifications\Notifications\HealthyBackupWasFoundNotification::class => ['mail'], 46 | \Spatie\Backup\Notifications\Notifications\CleanupWasSuccessfulNotification::class => ['mail'], 47 | ], 48 | 49 | 'notifiable' => \Spatie\Backup\Notifications\Notifiable::class, 50 | 51 | 'discord' => [ 52 | 'webhook_url' => env('DISCORD_NOTIFICATIONS_WEBHOOK'), 53 | ], 54 | ], 55 | 56 | 57 | 'monitor_backups' => [ 58 | [ 59 | 'name' => env('APP_NAME', 'laravel-backup'), 60 | 'disks' => ['s3'], 61 | 'health_checks' => [ 62 | \Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays::class => 1, 63 | \Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes::class => 5000, 64 | ], 65 | ], 66 | 67 | ], 68 | 69 | 'cleanup' => [ 70 | 'strategy' => \Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy::class, 71 | 72 | 'default_strategy' => [ 73 | 74 | /* 75 | * The number of days for which backups must be kept. 76 | */ 77 | 'keep_all_backups_for_days' => 7, 78 | 79 | /* 80 | * The number of days for which daily backups must be kept. 81 | */ 82 | 'keep_daily_backups_for_days' => 16, 83 | 84 | /* 85 | * The number of weeks for which one weekly backup must be kept. 86 | */ 87 | 'keep_weekly_backups_for_weeks' => 8, 88 | 89 | /* 90 | * The number of months for which one monthly backup must be kept. 91 | */ 92 | 'keep_monthly_backups_for_months' => 4, 93 | 94 | /* 95 | * The number of years for which one yearly backup must be kept. 96 | */ 97 | 'keep_yearly_backups_for_years' => 2, 98 | 99 | /* 100 | * After cleaning up the backups remove the oldest backup until 101 | * this amount of megabytes has been reached. 102 | */ 103 | 'delete_oldest_backups_when_using_more_megabytes_than' => 5000, 104 | ], 105 | ], 106 | 107 | ]; 108 | -------------------------------------------------------------------------------- /config/seq.php: -------------------------------------------------------------------------------- 1 | env('SEQ_HOST', null), 5 | 'port' => env('SEQ_PORT', null), 6 | ]; -------------------------------------------------------------------------------- /docker-compose-prod.env: -------------------------------------------------------------------------------- 1 | APP_NAME="${APP_NAME:-Laravel}" 2 | APP_KEY="${APP_KEY}" 3 | APP_ENV="${APP_ENV:-production}" 4 | 5 | APP_DEBUG="${APP_DEBUG:-false}" 6 | APP_URL="${APP_URL:-https://${APP_DOMAIN:-laravel.test}}" 7 | 8 | # Our app is being served internally as HTTP but Cloudflare serves it as HTTPS so we need to force HTTPS 9 | FORCE_HTTPS="${FORCE_HTTPS:-true}" 10 | 11 | LOG_CHANNEL="${LOG_CHANNEL:-gelf}" 12 | LOG_DEPRECATIONS_CHANNEL="${LOG_DEPRECATIONS_CHANNEL:-null}" 13 | LOG_LEVEL="${LOG_LEVEL:-debug}" 14 | 15 | DB_CONNECTION="${DB_CONNECTION:-mysql}" 16 | DB_HOST="${DB_HOST:-${APP_DOCKER_STACK:-mystack}-mysql-1}" 17 | DB_PORT="${DB_PORT:-3306}" 18 | DB_DATABASE="${DB_DATABASE:-laravel}" 19 | DB_USERNAME="${DB_USERNAME:-root}" 20 | DB_PASSWORD="${DB_PASSWORD:-secret}" 21 | 22 | BROADCAST_DRIVER="${BROADCAST_DRIVER:-pusher}" 23 | CACHE_DRIVER="${CACHE_DRIVER:-redis}" 24 | FILESYSTEM_DISK="${FILESYSTEM_DISK:-s3}" 25 | QUEUE_CONNECTION="${QUEUE_CONNECTION:-redis}" 26 | SESSION_DRIVER="${SESSION_DRIVER:-redis}" 27 | SESSION_LIFETIME="${SESSION_LIFETIME:-120}" 28 | 29 | MEMCACHED_HOST="${MEMCACHED_HOST:-${APP_DOCKER_STACK:-mystack}-memcached-1}" 30 | 31 | REDIS_HOST="${REDIS_HOST:-${APP_DOCKER_STACK:-mystack}-redis-1}" 32 | REDIS_PASSWORD="${REDIS_PASSWORD:-null}" 33 | REDIS_PORT="${REDIS_PORT:-6379}" 34 | 35 | MAIL_MAILER="${MAIL_MAILER:-smtp}" 36 | MAIL_HOST="${MAIL_HOST:-${APP_DOCKER_STACK:-mystack}-mailpit-1}" 37 | MAIL_PORT="${MAIL_PORT:-1025}" 38 | MAIL_USERNAME="${MAIL_USERNAME:-null}" 39 | MAIL_PASSWORD="${MAIL_PASSWORD:-null}" 40 | MAIL_ENCRYPTION="${MAIL_ENCRYPTION:-null}" 41 | MAIL_FROM_ADDRESS="${MAIL_FROM_ADDRESS:-no-reply@${APP_DOMAIN:-laravel.test}}" 42 | MAIL_FROM_NAME="${MAIL_FROM_NAME:-Laravel}" 43 | 44 | AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-laravel}" 45 | AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-secret}" 46 | AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-us-east-1}" 47 | AWS_BUCKET="${AWS_BUCKET:-app}" 48 | AWS_ENDPOINT="${AWS_ENDPOINT:-http://${APP_DOCKER_STACK:-mystack}-minio-1:9000}" 49 | AWS_USE_PATH_STYLE_ENDPOINT="${AWS_USE_PATH_STYLE_ENDPOINT:-true}" 50 | AWS_URL="${AWS_URL:-https://cdn.${APP_DOMAIN:-laravel.test}:9000}" 51 | 52 | PUSHER_APP_ID="${PUSHER_APP_ID:-null}" 53 | PUSHER_APP_KEY="${PUSHER_APP_KEY:-null}" 54 | PUSHER_APP_SECRET="${PUSHER_APP_SECRET:-null}" 55 | PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER:-mt1}" 56 | PUSHER_HOST="${PUSHER_HOST:-null}" 57 | PUSHER_PORT="${PUSHER_PORT:-443}" 58 | PUSHER_SCHEME="${PUSHER_SCHEME:-https}" 59 | 60 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 61 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 62 | 63 | DUSK_DRIVER_URL="${DUSK_DRIVER_URL:-http://${APP_DOCKER_STACK:-mystack}-selenium-1:4444}" 64 | 65 | SCOUT_DRIVER="${SCOUT_DRIVER:-meilisearch}" 66 | MEILISEARCH_HOST="${MEILISEARCH_HOST:-http://${APP_DOCKER_STACK:-mystack}-meilisearch-1:7700}" 67 | MEILISEARCH_KEY="${MEILISEARCH_KEY:-masterKey}" 68 | 69 | SOKETI_DEFAULT_APP_ID="${PUSHER_APP_ID}" 70 | SOKETI_DEFAULT_APP_KEY="${PUSHER_APP_KEY}" 71 | SOKETI_DEFAULT_APP_SECRET="${PUSHER_APP_SECRET}" 72 | 73 | SEQ_HOST="${SEQ_HOST:-${APP_DOCKER_STACK:-mystack}-seq-input-gelf-1}" 74 | SEQ_PORT="${SEQ_PORT:-12201}" 75 | 76 | DISCORD_NOTIFICATIONS_WEBHOOK="${DISCORD_NOTIFICATIONS_WEBHOOK}" -------------------------------------------------------------------------------- /docker-compose-prod.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | nginx: 5 | image: "${APP_DOCKER_STACK}-loadbalancer/php:beta-${APP_PHP_VERSION:-8.2}-unit" 6 | restart: always 7 | pull_policy: build 8 | 9 | environment: 10 | AUTORUN_LARAVEL_STORAGE_LINK: false 11 | AUTORUN_LARAVEL_MIGRATION: true 12 | SSL_MODE: "off" 13 | 14 | build: 15 | context: . 16 | dockerfile_inline: | 17 | FROM serversideup/php:beta-${APP_PHP_VERSION:-8.2}-unit 18 | 19 | USER root 20 | 21 | RUN install-php-extensions gmp 22 | RUN install-php-extensions intl 23 | 24 | COPY . /var/www/html 25 | 26 | RUN chown -R www-data:www-data /var/www/html 27 | 28 | USER www-data 29 | 30 | RUN composer install --no-interaction --prefer-dist --no-scripts -o 31 | 32 | env_file: 33 | - docker-compose-prod.env 34 | depends_on: 35 | mysql: 36 | condition: service_healthy 37 | networks: 38 | - laravel 39 | - cloudflared #80 40 | 41 | horizon: 42 | image: "${APP_DOCKER_STACK}-cli/app:beta-${APP_PHP_VERSION:-8.2}-cli" 43 | restart: always 44 | pull_policy: build 45 | 46 | env_file: 47 | - docker-compose-prod.env 48 | 49 | build: 50 | context: . 51 | dockerfile_inline: | 52 | FROM serversideup/php:beta-${APP_PHP_VERSION:-8.2}-cli 53 | 54 | USER root 55 | 56 | RUN install-php-extensions gmp 57 | RUN install-php-extensions intl 58 | 59 | COPY . /var/www/html 60 | 61 | RUN chown -R www-data:www-data /var/www/html 62 | 63 | USER www-data 64 | 65 | RUN composer install --no-interaction --prefer-dist --no-scripts -o 66 | 67 | command: ["gosu", "www-data", "/usr/local/bin/php", "artisan", "horizon"] 68 | depends_on: 69 | mysql: 70 | condition: service_healthy 71 | networks: 72 | - laravel 73 | 74 | scheduler: 75 | image: "${APP_DOCKER_STACK}-cli/app:beta-${APP_PHP_VERSION:-8.2}-cli" 76 | restart: always 77 | pull_policy: build 78 | 79 | env_file: 80 | - docker-compose-prod.env 81 | 82 | build: 83 | context: . 84 | dockerfile_inline: | 85 | FROM serversideup/php:beta-${APP_PHP_VERSION:-8.2}-cli 86 | 87 | USER root 88 | 89 | RUN install-php-extensions gmp 90 | RUN install-php-extensions intl 91 | 92 | COPY . /var/www/html 93 | 94 | RUN chown -R www-data:www-data /var/www/html 95 | 96 | USER www-data 97 | 98 | RUN composer install --no-interaction --prefer-dist --no-scripts -o 99 | 100 | command: ["gosu", "www-data", "/usr/local/bin/php", "artisan", "schedule:work"] 101 | 102 | depends_on: 103 | mysql: 104 | condition: service_healthy 105 | networks: 106 | - laravel 107 | 108 | mysql: 109 | image: 'mariadb:10' 110 | restart: always 111 | environment: 112 | MYSQL_ROOT_PASSWORD: '${DB_PASSWORD:-secret}' 113 | MYSQL_ROOT_HOST: "%" 114 | MYSQL_DATABASE: '${DB_DATABASE:-laravel}' 115 | MYSQL_USER: '${DB_USERNAME:-laravel}' 116 | MYSQL_PASSWORD: '${DB_PASSWORD:-secret}' 117 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 118 | volumes: 119 | - 'mysql:/var/lib/mysql' 120 | networks: 121 | - laravel 122 | - cloudflared #3306 TCP !!Use Cloudflare Zero Trust Access Applications for security!! 123 | healthcheck: 124 | test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"] 125 | retries: 3 126 | timeout: 5s 127 | 128 | redis: 129 | image: 'redis:alpine' 130 | restart: always 131 | volumes: 132 | - 'redis:/data' 133 | networks: 134 | - laravel 135 | - cloudflared 136 | healthcheck: 137 | test: ["CMD", "redis-cli", "ping"] 138 | retries: 3 139 | timeout: 5s 140 | 141 | minio: 142 | image: 'minio/minio:RELEASE.2020-10-28T08-16-50Z.hotfix.23bfe9811' 143 | restart: always 144 | environment: 145 | MINIO_ROOT_USER: '${MINIO_ROOT_USER:-laravel}' 146 | MINIO_ROOT_PASSWORD: '${MINIO_ROOT_PASSWORD:-secret}' 147 | volumes: 148 | - 'minio:/data/minio' 149 | networks: 150 | - laravel 151 | - cloudflared #9000 && #8900 !!Use Cloudflare Zero Trust Access Applications for security!! 152 | command: minio server /data/minio --console-address ":8900" 153 | healthcheck: 154 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 155 | retries: 3 156 | timeout: 5s 157 | 158 | selenium: 159 | image: 'selenium/standalone-chrome' 160 | restart: always 161 | extra_hosts: 162 | - 'host.docker.internal:host-gateway' 163 | volumes: 164 | - '/dev/shm:/dev/shm' 165 | networks: 166 | - laravel 167 | 168 | mailpit: 169 | image: 'axllent/mailpit:latest' 170 | restart: always 171 | networks: 172 | - laravel 173 | - cloudflared #8025 174 | 175 | soketi: 176 | image: 'quay.io/soketi/soketi:latest-16-alpine' 177 | restart: always 178 | environment: 179 | SOKETI_DEBUG: '${SOKETI_DEBUG:-0}' 180 | SOKETI_METRICS_ENABLED: 1 181 | SOKETI_METRICS_SERVER_PORT: '9601' 182 | env_file: 183 | - docker-compose-prod.env 184 | networks: 185 | - laravel 186 | - cloudflared #6001 websocket-option && #9601 !!Use Cloudflare Zero Trust Access Applications for security!! 187 | 188 | meilisearch: 189 | restart: always 190 | image: 'getmeili/meilisearch:v1.6.1' 191 | environment: 192 | MEILI_MASTER_KEY: '${MEILISEARCH_MASTER_KEY:-masterKey}' 193 | MEILI_NO_ANALYTICS: '${MEILISEARCH_NO_ANALYTICS:-false}' 194 | volumes: 195 | - 'meilisearch:/meili_data' 196 | networks: 197 | - laravel 198 | - cloudflared #7700 !!Use Cloudflare Zero Trust Access Applications for security!! 199 | healthcheck: 200 | test: ["CMD", "wget", "--no-verbose", "--spider", "http://localhost:7700/health"] 201 | retries: 3 202 | timeout: 5s 203 | 204 | # [...] https://github.com/laravel/sail/tree/1.x/stubs 205 | 206 | # MOVING FROM UDP TO HTTP ENDPOINT FOR SIMPLICITY AND REACHABILITY THROUGH CLOUDFLARE TUNNELS 207 | seq: 208 | image: datalust/seq:latest 209 | environment: 210 | ACCEPT_EULA: Y 211 | restart: always 212 | volumes: 213 | - seq-data:/data 214 | networks: 215 | - laravel 216 | - cloudflared #80 !!Use Cloudflare Zero Trust Access Applications for security!! 217 | 218 | ## THIS IS FOR BACKING UP MINIO. DATABASE SHOULD BE BACKED UP BY SPATIE/BACKUP 219 | backup: 220 | # Restore backup: https://offen.github.io/docker-volume-backup/how-tos/restore-volumes-from-backup.html 221 | image: offen/docker-volume-backup:v2 222 | restart: always 223 | environment: 224 | NOTIFICATION_URLS: "${BACKUP_NOTIFICATION_URLS}" # Ex: telegram://token@telegram?chats=@channel-1 // ERROR BACKUP 225 | BACKUP_RETENTION_DAYS: "${BACKUP_RETENTION_DAYS:-7}" 226 | BACKUP_COMPRESSION: "gz" 227 | BACKUP_FROM_SNAPSHOT: "true" 228 | NOTIFICATION_LEVEL: "${BACKUP_NOTIFICATION_LEVEL:-error}" 229 | 230 | AWS_S3_PATH: "${BACKUP_AWS_S3_PATH}" 231 | AWS_S3_BUCKET_NAME: "${BACKUP_AWS_S3_BUCKET_NAME}" 232 | AWS_ACCESS_KEY_ID: "${BACKUP_AWS_ACCESS_KEY_ID}" 233 | AWS_SECRET_ACCESS_KEY: "${BACKUP_AWS_SECRET_ACCESS_KEY}" 234 | AWS_STORAGE_CLASS: "${BACKUP_AWS_STORAGE_CLASS}" 235 | 236 | WEBDAV_URL: "${BACKUP_WEBDAV_URL}" 237 | WEBDAV_PATH: "${BACKUP_WEBDAV_PATH}" 238 | WEBDAV_USERNAME: "${BACKUP_WEBDAV_USERNAME}" 239 | WEBDAV_PASSWORD: "${BACKUP_WEBDAV_PASSWORD}" 240 | 241 | SSH_HOST_NAME: "${BACKUP_SSH_HOST_NAME}" 242 | SSH_PORT: "${BACKUP_SSH_PORT}" 243 | SSH_REMOTE_PATH: "${BACKUP_SSH_REMOTE_PATH}" 244 | SSH_USER: "${BACKUP_SSH_USER}" 245 | SSH_PASSWORD: "${BACKUP_SSH_PASSWORD}" 246 | volumes: 247 | - 'minio:/backup/minio:ro' 248 | 249 | networks: 250 | laravel: 251 | cloudflared: 252 | external: true 253 | 254 | volumes: 255 | mysql: 256 | redis: 257 | minio: 258 | meilisearch: 259 | seq-data: 260 | -------------------------------------------------------------------------------- /docker-compose-worker.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | horizon: 5 | image: "${APP_DOCKER_STACK}-cli/app:beta-${APP_PHP_VERSION:-8.2}-cli" 6 | pull_policy: build 7 | 8 | env_file: 9 | - docker-compose-prod.env 10 | 11 | build: 12 | context: . 13 | dockerfile_inline: | 14 | FROM serversideup/php:beta-${APP_PHP_VERSION:-8.2}-cli 15 | 16 | RUN install-php-extensions gmp 17 | RUN install-php-extensions intl 18 | 19 | RUN docker-php-serversideup-dep-install-debian gosu 20 | 21 | COPY . /var/www/html 22 | 23 | RUN composer install --no-interaction --prefer-dist --no-scripts \ 24 | && composer dump-autoload --optimize \ 25 | && chown -R www-data:www-data /var/www/html 26 | 27 | command: ["gosu", "www-data", "/usr/local/bin/php", "artisan", "horizon"] 28 | depends_on: 29 | mysql: 30 | condition: service_healthy 31 | networks: 32 | - laravel 33 | 34 | networks: 35 | laravel: 36 | cloudflared: 37 | external: true 38 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 28 | __DIR__ . '/../docker-compose-prod.yml' => base_path('docker-compose-prod.yml'), 29 | __DIR__ . '/../docker-compose-worker.yml' => base_path('docker-compose-worker.yml'), 30 | __DIR__ . '/../docker-compose-prod.env' => base_path('docker-compose-prod.env'), 31 | 32 | __DIR__ . '/../config/seq.php' => config_path('seq.php'), 33 | __DIR__ . '/../config/backup.php' => config_path('backup.php'), 34 | ], 'laravel-devops'); 35 | 36 | Config::set('logging.channels.gelf.system_name', Cache::rememberForever('hostname_ip', function () { 37 | return Str::betweenFirst(Http::get('https://cloudflare.com/cdn-cgi/trace')->body(), 'ip=', "\n"); 38 | })); 39 | 40 | $this->app->booted(function () { 41 | $schedule = $this->app->make(Schedule::class); 42 | $schedule->command('backup:run --only-db')->daily(); 43 | }); 44 | } 45 | 46 | public function register() 47 | { 48 | if (config('seq.host')) { 49 | $this->setGelfDriverAtLoggingConfig(); 50 | } 51 | } 52 | 53 | protected function setGelfDriverAtLoggingConfig() 54 | { 55 | Config::set('logging.channels.gelf', [ 56 | 'driver' => 'custom', 57 | 58 | 'via' => \Hedii\LaravelGelfLogger\GelfLoggerFactory::class, 59 | 60 | // This optional option determines the processors that should be 61 | // pushed to the handler. This option is useful to modify a field 62 | // in the log context (see NullStringProcessor), or to add extra 63 | // data. Each processor must be a callable or an object with an 64 | // __invoke method: see monolog documentation about processors. 65 | // Default is an empty array. 66 | 'processors' => [ 67 | \Hedii\LaravelGelfLogger\Processors\NullStringProcessor::class, 68 | \Hedii\LaravelGelfLogger\Processors\RenameIdFieldProcessor::class, 69 | // another processor... 70 | ], 71 | 72 | // This optional option determines the minimum "level" a message 73 | // must be in order to be logged by the channel. Default is 'debug' 74 | 'level' => 'debug', 75 | 76 | // This optional option determines the channel name sent with the 77 | // message in the 'facility' field. Default is equal to app.env 78 | // configuration value 79 | 'name' => config('app.name'), 80 | 81 | // This optional option determines the system name sent with the 82 | // message in the 'source' field. When forgotten or set to null, 83 | // the current hostname is used. 84 | 'system_name' => null, 85 | 86 | // This optional option determines if you want the UDP, TCP or HTTP 87 | // transport for the gelf log messages. Default is UDP 88 | 'transport' => 'udp', 89 | 90 | // This optional option determines the host that will receive the 91 | // gelf log messages. Default is 127.0.0.1 92 | 'host' => config('seq.host'), 93 | 94 | // This optional option determines the port on which the gelf 95 | // receiver host is listening. Default is 12201 96 | 'port' => config('seq.port'), 97 | 98 | // This optional option determines the chunk size used when 99 | // transferring message via UDP transport. Default is 1420. 100 | 'chunk_size' => 1420, 101 | 102 | // This optional option determines the path used for the HTTP 103 | // transport. When forgotten or set to null, default path '/gelf' 104 | // is used. 105 | 'path' => null, 106 | 107 | // This optional option enable or disable ssl on TCP or HTTP 108 | // transports. Default is false. 109 | 'ssl' => false, 110 | 111 | // If ssl is enabled, the following configuration is used. 112 | 'ssl_options' => [ 113 | // Enable or disable the peer certificate check. Default is 114 | // true. 115 | 'verify_peer' => true, 116 | 117 | // Path to a custom CA file (eg: "/path/to/ca.pem"). Default 118 | // is null. 119 | 'ca_file' => null, 120 | 121 | // List of ciphers the SSL layer may use, formatted as 122 | // specified in ciphers(1). Default is null. 123 | 'ciphers' => null, 124 | 125 | // Whether self-signed certificates are allowed. Default is 126 | // false. 127 | 'allow_self_signed' => false, 128 | ], 129 | 130 | // This optional option determines the maximum length per message 131 | // field. When forgotten or set to null, the default value of 132 | // \Monolog\Formatter\GelfMessageFormatter::DEFAULT_MAX_LENGTH is 133 | // used (currently this value is 32766) 134 | 'max_length' => null, 135 | 136 | // This optional option determines the prefix for 'context' fields 137 | // from the Monolog record. Default is null (no context prefix) 138 | 'context_prefix' => null, 139 | 140 | // This optional option determines the prefix for 'extra' fields 141 | // from the Monolog record. Default is null (no extra prefix) 142 | 'extra_prefix' => null, 143 | 144 | // This optional option determines whether errors thrown during 145 | // logging should be ignored or not. Default is true. 146 | 'ignore_error' => true, 147 | ]); 148 | } 149 | } 150 | --------------------------------------------------------------------------------