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:"