diff --git a/docs/content/docs/3.framework-guides/1.laravel/1.automations.md b/docs/content/docs/3.framework-guides/1.laravel/1.automations.md index c320ba38..bb61bbb4 100644 --- a/docs/content/docs/3.framework-guides/1.laravel/1.automations.md +++ b/docs/content/docs/3.framework-guides/1.laravel/1.automations.md @@ -32,9 +32,61 @@ In order for this script to run,`AUTORUN_ENABLED` must be set to `true`. Once th | `AUTORUN_LARAVEL_OPTIMIZE` | `true` | `php artisan optimize`: Optimizes the application. | | `AUTORUN_LARAVEL_ROUTE_CACHE` | `true` | `php artisan route:cache`: Caches the routes. | | `AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND` | `false` | When `true`, the script will exit gracefully (without error) if Laravel is not detected in `APP_BASE_DIR`, instead of failing the container. Useful when `AUTORUN_ENABLED=true` is set on a shared image where Laravel may not always be present (e.g. before the first `composer install`). | +| `AUTORUN_LARAVEL_STORAGE_INIT` | `false` | Creates the `storage` and `bootstrap/cache` directory structure if missing and ensures they are writable by the owner and group. | | `AUTORUN_LARAVEL_STORAGE_LINK` | `true` | `php artisan storage:link`: Creates a symbolic link from `public/storage` to `storage/app/public`. | | `AUTORUN_LARAVEL_VIEW_CACHE` | `true` | `php artisan view:cache`: Caches the views. | +## Storage Initialization +Laravel expects a specific set of directories inside `storage/` and `bootstrap/cache` to exist and be writable. If they're missing, your application will fail to boot. Depending on which directory is missing, you'll see errors like these: + +- `The /var/www/html/bootstrap/cache directory must be present and writable.` +- `View path not found.` (when running `php artisan optimize` or `php artisan view:cache`) + +This commonly happens in containers when: +- Your `.dockerignore` excludes `storage/` or `bootstrap/cache` from the image build +- You mount an empty volume over `storage/` + +Setting `AUTORUN_LARAVEL_STORAGE_INIT=true` runs this step first, before every other automation, so the rest of the Laravel automations can rely on these directories being in place. + +::steps{level="3"} + +### Create missing directories +The script creates any of these directories that don't already exist: + +- `bootstrap/cache` +- `storage/app/private` +- `storage/app/public` +- `storage/framework/cache` +- `storage/framework/sessions` +- `storage/framework/testing` +- `storage/framework/views` +- `storage/logs` +- `storage/pail` + +Directories that already exist are left alone, so this step is safe to run on every container start. + +::warning +Failing to keep the [storage directory structure](https://laravel.com/framework/docs/master/structure#the-storage-directory) causes containerized Laravel processes to crash when trying to write logs or cache files. +:: + +### Make the directories writable +Each directory in the list above is made readable and writable by its owner and group. Only the directories themselves are changed. Files inside them, such as user uploads in `storage/app`, are never modified. + +::note +Only the owner of a directory can change its permissions. If a directory is owned by another user (for example, `root`), the script logs a message, skips that directory, and continues starting the container. +:: + +### Using a volume for storage +The script runs as the unprivileged `www-data` user, so the directory you mount a volume on must be writable by `www-data`. When you create a new named volume, Docker copies the ownership of the matching directory in your image. If that directory doesn't exist in the image, Docker creates the volume owned by `root`, and the script won't be able to create anything inside it. + +To avoid this, make sure `storage/` exists in your image. + +::warning +If the script can't create a directory, the container will fail to start. Make sure the directory you mount on exists in your image and is owned by `www-data`. +:: + +:: + ## Database Connection Checks Before running migrations, the automation script performs connection checks to ensure your database is ready. Understanding this process helps you configure timeouts and troubleshoot connection issues. diff --git a/docs/content/docs/8.reference/1.environment-variable-specification.md b/docs/content/docs/8.reference/1.environment-variable-specification.md index bc157974..838f8f8a 100644 --- a/docs/content/docs/8.reference/1.environment-variable-specification.md +++ b/docs/content/docs/8.reference/1.environment-variable-specification.md @@ -43,6 +43,7 @@ Setting environment variables all depends on what method you're using to run you `AUTORUN_LARAVEL_MIGRATION_TIMEOUT`
*Default: "30"*|The number of seconds to wait for the database to come online before attempting `php artisan migrate`..
ℹ️ Requires `AUTORUN_ENABLED = true` to run.| all `AUTORUN_LARAVEL_ROUTE_CACHE`
*Default: "true"*|Automatically run "php artisan route:cache" on container start.
ℹ️ Requires `AUTORUN_ENABLED = true` to run.| all `AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND`
*Default: "false"*|When set to `true`, the Laravel Automations script will exit gracefully (without error) if Laravel is not detected in `APP_BASE_DIR`, instead of failing the container. Useful when `AUTORUN_ENABLED=true` is set on a shared image where Laravel may not always be present (e.g. before the first `composer install`).
ℹ️ Requires `AUTORUN_ENABLED = true` to have any effect.| all +`AUTORUN_LARAVEL_STORAGE_INIT`
*Default: "false"*|Automatically create the Laravel `storage` and `bootstrap/cache` directory structure and make them writable on container start.
ℹ️ Requires `AUTORUN_ENABLED = true` to run.| all `AUTORUN_LARAVEL_STORAGE_LINK`
*Default: "true"*|Automatically run "php artisan storage:link" on container start.
ℹ️ Requires `AUTORUN_ENABLED = true` to run.| all `AUTORUN_LARAVEL_VIEW_CACHE`
*Default: "true"*|Automatically run "php artisan view:cache" on container start.
ℹ️ Requires `AUTORUN_ENABLED = true` to run.| all `CADDY_ACME_PROFILE`
*Default: "off"*|Select a Let's Encrypt ACME certificate profile. Valid options: `off` (default, no profile — keeps the stock Let's Encrypt + ZeroSSL issuers), `shortlived` (~6-day certs; also required for IP-address certificates), `tlsserver`, or `classic`. Setting any profile pins issuance to Let's Encrypt only and renews more frequently, so the container needs reliable egress to the ACME CA. (Official docs)|frankenphp diff --git a/src/common/etc/entrypoint.d/50-laravel-automations.sh b/src/common/etc/entrypoint.d/50-laravel-automations.sh index 6f4a811a..fee030dc 100644 --- a/src/common/etc/entrypoint.d/50-laravel-automations.sh +++ b/src/common/etc/entrypoint.d/50-laravel-automations.sh @@ -11,7 +11,8 @@ script_name="laravel-automations" : "${AUTORUN_DEBUG:=false}" : "${AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND:=false}" -# Set default values for storage link +# Set default values for storage +: "${AUTORUN_LARAVEL_STORAGE_INIT:=false}" : "${AUTORUN_LARAVEL_STORAGE_LINK:=true}" # Set default values for optimizations @@ -165,6 +166,33 @@ artisan_storage_link() { fi } +laravel_storage_init() { + echo "🚀 Initializing Laravel storage structure and permissions in $APP_BASE_DIR" + for dir in \ + bootstrap/cache \ + storage/app/private \ + storage/app/public \ + storage/framework/cache \ + storage/framework/sessions \ + storage/framework/testing \ + storage/framework/views \ + storage/logs \ + storage/pail; do + if [ ! -d "$APP_BASE_DIR/$dir" ] && ! mkdir_error=$(mkdir -p "$APP_BASE_DIR/$dir" 2>&1); then + echo "❌ $script_name: Unable to create directory: $APP_BASE_DIR/$dir" + debug_log "$mkdir_error" + return 1 + fi + + # Only the owner can change permissions, so directories owned by another user are skipped rather than failing startup + if ! chmod ug+rwx "$APP_BASE_DIR/$dir" 2>/dev/null; then + echo "ℹ️ Unable to update permissions on $APP_BASE_DIR/$dir, skipping" + fi + done + + return 0 +} + artisan_optimize() { debug_log "Starting Laravel optimizations..." @@ -443,6 +471,11 @@ wait_for_database_connection() { ############################################################################ if laravel_is_installed; then + # Runs first because Artisan fails to boot without bootstrap/cache + if [ "$AUTORUN_LARAVEL_STORAGE_INIT" = "true" ]; then + laravel_storage_init + fi + if [ "$LOG_OUTPUT_LEVEL" = "debug" ] || [ "$AUTORUN_DEBUG" = "true" ]; then echo "Laravel detected: v$(get_laravel_version)" echo "Automation settings:"