An app deployed on Laravel Vapor loses user uploads and file-cache entries between requests; why does that happen, and what should it use instead?
answer
- Lambda: only /tmp is writable
- storage path becomes /tmp/storage
- containers are ephemeral and unshared
- S3 disk plus signed-storage-url route
- DynamoDB, Redis or database cache
basics
~20 sOn Vapor the app runs on AWS Lambda, where only /tmp is writable; vapor-core moves the storage path to /tmp/storage, which belongs to one short-lived container. Keep files on S3 and cache and sessions in DynamoDB, Redis or a database.
solid answer
~40 sVapor runs each request inside an AWS Lambda execution environment whose filesystem is read-only except `/tmp`. The vapor-core runtime therefore calls `$app->useStoragePath()` with `/tmp/storage` at boot and creates `app`, `logs`, `bootstrap/cache`, `framework/cache` and `framework/views` under it. That directory belongs to one container: concurrent containers each have their own, and a container is retired after a run of invocations (`VAPOR_MAX_REQUESTS`, 250 by default) or whenever Lambda recycles it. So `Storage::disk('local')`, the `file` cache store and the `file` session driver seem to work and then lose data. Put files on the `s3` disk (for browser uploads, Vapor's `POST /vapor/signed-storage-url` route returns a pre-signed S3 PUT URL) and point cache and sessions at DynamoDB, Redis or the database.
code
ini · 3 linesFILESYSTEM_DISK=s3
CACHE_STORE=dynamodb
SESSION_DRIVER=databasego deeper
Recall that Vapor only lets the app write to /tmp and that user files belong on the s3 disk.
Explain why /tmp/storage is per-container and short-lived, and name the drivers to switch: filesystem disk, cache store and session driver.
Walk through the signed-storage-url upload flow, the uploadFiles gate, and how you would audit an app for hidden local-disk writes before migrating it.
Weigh the refactoring cost of removing local-disk assumptions against the scaling benefit, and whether a managed or server host avoids that cost.
## Why only /tmp **Laravel Vapor** deploys a Laravel app to **AWS Lambda**. A Lambda function runs in an **execution environment**: a short-lived container that AWS starts, reuses for a while, and throws away. The deployed code is mounted read-only; the one writable location is `/tmp`. Two properties of `/tmp` matter: - It is **private to one execution environment**. When traffic rises, Lambda runs many environments side by side, and each has its own `/tmp`. - It is **ephemeral**. When the environment is retired, everything in it is gone. On a Forge-managed server, by contrast, every request shares one persistent disk, so writing to `storage/app` or using the `file` cache store works indefinitely. ## What vapor-core does at boot The runtime stubs shipped in `laravel/vapor-core` (2.47) prepare Laravel for that environment each time a container starts: 1. `StorageDirectories::create()` makes `/tmp/storage/app`, `/tmp/storage/logs`, `/tmp/storage/bootstrap/cache`, `/tmp/storage/framework/cache` and `/tmp/storage/framework/views`. 2. `$app->useStoragePath(StorageDirectories::PATH)` points `storage_path()` at `/tmp/storage`. 3. Secrets from AWS SSM are injected into the environment, and `config:cache` runs so later invocations read one cached config file. 4. The runtime loops over invocations and terminates the container after `VAPOR_MAX_REQUESTS` invocations (250 by default), so even a warm container's `/tmp` has a bounded life. The result is that Laravel keeps working (views compile, logs can be written) but nothing written under storage survives beyond that container. ## Which Laravel features quietly break | Feature | Works on a server because | On Vapor | Use instead | |---|---|---|---| | `Storage::disk('local')` uploads | one persistent disk | lands in `/tmp/storage/app`, lost with the container | the `s3` disk | | `file` cache store | shared directory | each container sees only its own entries | `dynamodb`, `redis` or `database` store | | `file` session driver | shared directory | users appear logged out when routed to another container | `cookie`, `database`, `redis` or `dynamodb` | | Files a queued job reads later | same disk as the web request | the job may run in a different container | pass an S3 path, not a local path | vapor-core helps with one of these: on Vapor it fills `cache.stores.dynamodb` from the environment, defaulting the table name to `cache`, so `CACHE_STORE=dynamodb` works once the table exists. ## Uploads: the signed storage URL flow Sending large files through a Lambda-backed HTTP request is fragile, so vapor-core registers `POST /vapor/signed-storage-url` (in the `web` middleware group by default, and only while the feature is enabled). The flow is: 1. The browser asks that route for an upload URL. The controller calls `Gate::authorize('uploadFiles', [$user, $bucket])`, so the app must define an `uploadFiles` gate. 2. It answers `201` with a pre-signed S3 **PUT** URL, a `uuid`, the bucket and a key under `tmp/`, valid for 5 minutes by default and private by default. 3. The browser PUTs the file straight to S3. 4. The browser tells your controller the key, and the controller copies the object from `tmp/` to its permanent path with the `Storage` facade. The file never touches the function's disk. ## What /tmp is still good for `/tmp` is fine as **scratch space inside one invocation**: unzip an archive, render a PDF, resize an image, then upload the result to S3 before the invocation ends. It is wrong for anything a later request, another container or a queued job must read. ## Container images Vapor can also package an environment as a **container image** instead of an uploaded bundle, usually to add system libraries or PHP extensions or to fit a larger application. The image still runs on Lambda, so the `/tmp`-only rule and everything above apply unchanged; only the packaging differs. ## Auditing an app before moving it Before a first Vapor deploy, search the codebase for anything that assumes a durable local disk: - calls to `Storage::disk('local')`, `storage_path()` or `file_put_contents()` whose output a later request reads; - `CACHE_STORE=file` or `SESSION_DRIVER=file` in the production environment; - queued jobs that receive a local file path instead of an S3 key; - packages that write their own caches or export files under `storage/`. Each one either moves to a shared service or is rewritten to finish its work inside a single invocation. Logs are a special case: anything written to `storage/logs` lands in `/tmp/storage/logs` and disappears with the container, so production logging should use a channel that leaves the function, such as `stderr`.
- Why does Vapor's signed-storage-url route answer 403 in a fresh app?Its controller calls `Gate::authorize('uploadFiles', [$user, $bucket])` before signing anything, and a fresh app defines no `uploadFiles` gate, so authorization is denied. Define that gate to decide who may upload to which bucket. The route also needs `AWS_BUCKET` (or a bucket in the request), the region and credentials in the environment, or it throws before signing.
- Is writing to /tmp ever acceptable on Vapor?Yes, as scratch space within one invocation: extract an archive, generate a PDF, then upload the result to S3 before returning. It is not acceptable for anything a later request, another container or a queued job must read, because nothing guarantees the same container handles that next invocation, and containers are retired regularly.
- Does deploying a Vapor environment as a container image lift the /tmp restriction?No. An image changes how code, system libraries and PHP extensions are packaged, but the image still runs as a Lambda function, so the same read-only filesystem and per-container `/tmp` apply. Storage, cache and session drivers must still be durable, shared services.
saying these in an interview costs you the question
- Vapor syncs /tmp/storage to S3 automatically after each request.
- The file cache store is fine on Vapor because all requests share one disk.
- Lambda lets the app write anywhere inside its deployed project folder.
- Browser uploads should be posted to a controller and saved on the local disk.
- A /tmp file written by one request is guaranteed visible to the next one.