---
title: Deploy Laravel on Vercel with Docker
description: Build a Laravel application with FrankenPHP and Docker, then deploy it to Vercel with production configuration, external storage, and preview deployments.
url: /kb/guide/laravel-php-with-docker
canonical_url: "https://vercel.com/kb/guide/laravel-php-with-docker"
published: 2026-09-03
last_updated: 2026-09-03
authors: Anshuman Bhardwaj
related:
  - /docs/container-registry
  - /docs/cli
  - /docs/vercel-blob
  - /docs/cron-jobs
  - /docs/queues
  - /kb/guide/symfony-php-with-docker
  - /kb/guide/deploy-php-on-vercel-with-docker
  - /docs/functions/container-images
  - /docs/project-configuration/vercel-json
  - /docs/services
  - /kb/guide/docker
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Laravel can run in a container-backed Vercel Function. FrankenPHP combines the PHP runtime with a production web server built on Caddy. Vercel builds the application into an image, stores it in [Vercel Container Registry](https://vercel.com/docs/container-registry), and scales Function instances with traffic.

This guide deploys a stateless Laravel application, routes requests through `public/index.php`, and explains how to supply configuration and store durable state.

## Prerequisites

- A [Vercel account](https://vercel.com)
  
- Docker Desktop or a running Docker daemon
  
- [Vercel CLI](https://vercel.com/docs/cli) (`npm install -g vercel`)
  
- Composer 2, when creating the project outside Docker
  

## How it works

The project has these deployment-specific files:

- `routes/web.php` returns a JSON response from Laravel and exposes `/health` for deployment smoke tests.
  
- `Caddyfile` exposes only `public` and sends application routes to `public/index.php`.
  
- `Dockerfile.vercel` installs production dependencies and copies the complete Laravel application into FrankenPHP.
  
- `vercel.json` declares the container service and routes requests to it.
  

## Steps

### 1\. Create the Laravel application

Create a Laravel project:

```bash
composer create-project laravel/laravel vercel-docker-laravel
cd vercel-docker-laravel
```

Replace the default route in `routes/web.php` with a response that identifies the running framework:

```php
<?php

use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Route;

Route::get('/', static function (): JsonResponse {
    return response()->json([
        'framework' => 'Laravel',
        'message' => 'Hello from Laravel and FrankenPHP on Vercel',
    ]);
});
```

Add a JSON health endpoint after the `/` route:

```php
Route::get('/health', static function (): JsonResponse {
    return response()->json(['status' => 'ok']);
});
```

Laravel also registers `/up` as its default framework health route in `bootstrap/app.php`.

### 2\. Configure FrankenPHP

Create `Caddyfile` in the project root:

```plaintext
:{$PORT:80} {
    root * /app/public
    encode zstd gzip

    php_server {
        try_files {path} index.php
    }
}
```

The site address reads the `PORT` environment variable and defaults to port 80. The document root prevents requests from accessing Laravel configuration or source files outside `public`.

### 3\. Add `Dockerfile.vercel`

Create the `Dockerfile.vercel` file in the project root:

```dockerfile
FROM dunglas/frankenphp:1-php8.4-bookworm AS base

RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
    && install-php-extensions intl zip

WORKDIR /app

FROM base AS dependencies

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --no-interaction \
    --no-progress \
    --no-scripts \
    --optimize-autoloader \
    --prefer-dist

COPY . .
RUN mkdir -p \
        storage/framework/cache \
        storage/framework/sessions \
        storage/framework/views \
        bootstrap/cache \
    && composer dump-autoload --no-dev --classmap-authoritative

FROM base AS runtime

COPY --from=dependencies --chown=www-data:www-data /app /app
COPY --chown=www-data:www-data Caddyfile /etc/frankenphp/Caddyfile

RUN setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp \
    && chown -R www-data:www-data /config/caddy /data/caddy

ENV PORT=80 \
    APP_ENV=production \
    APP_DEBUG=false \
    LOG_CHANNEL=stderr \
    CACHE_STORE=array \
    SESSION_DRIVER=array \
    QUEUE_CONNECTION=sync

USER www-data

CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile"]
```

The initial Composer installation skips scripts to preserve dependency-layer caching. After the application is copied, `composer dump-autoload` runs Laravel’s `post-autoload-dump` hooks, including package discovery.

The array cache and session drivers keep the example independent of a database. Configure shared external stores before using sessions, caches, or queues in a production application.

`QUEUE_CONNECTION=sync` executes jobs during the request and is suitable only for this minimal example. It does not provide asynchronous background processing.

### 4\. Exclude local files from the image

Create a `.dockerignore` file to specify excluded files:

```plaintext
.git
.env
.env.*
!.env.example
database/database.sqlite
node_modules
tests
vendor
```

The image must not contain `.env` files. Vercel injects the deployment’s environment variables when the container starts.

### 5\. Add `vercel.json`

Create a `vercel.json` file to configure your project:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "services": {
    "app": {
      "root": ".",
      "entrypoint": "Dockerfile.vercel",
      "runtime": "container"
    }
  },
  "rewrites": [
    { "source": "/(.*)", "destination": { "service": "app" } }
  ]
}
```

The catch-all rewrite sends every public path to the Laravel service.

### 6\. Run locally

Laravel uses the `APP_KEY` environment variable to encrypt and authenticate cookies and session data.

Generate a local application key:

```bash
php artisan key:generate --show
```

Store the displayed value in an uncommitted `.env.local` file:

```dotenv
APP_KEY=base64:your_generated_app_key
```

The `-L` flag does not automatically inject `.env.local` into a container service. Export the file into the current shell, then start the application through Vercel's local container runtime:

```bash
set -a
source .env.local
set +a
vercel dev -L --listen 8080
```

Test both routes:

```bash
curl http://localhost:8080/
curl --fail http://localhost:8080/health
```

### 7\. Configure the application key

Add the generated key as an environment variable in your Vercel project:

```bash
vercel env add APP_KEY
```

Use a stable `APP_KEY` within each environment, so redeployments can decrypt existing cookies and encrypted data. Do not reuse the production key in Preview or Development unless those environments intentionally share encrypted data. Keep `APP_DEBUG=false` in production.

### 8\. Deploy to Vercel

Once ready, deploy your project to Vercel with a single command:

```bash
vercel deploy --prod
```

Run `vercel deploy` without `--prod` to create a Preview Deployment.

For Git deployments, import the repository in Vercel and keep `Dockerfile.vercel`, `Caddyfile`, and `vercel.json` at the configured project root.

## Configure production services

### Database, sessions, and cache

Add database credentials as Vercel environment variables and install the matching PHP extension in `Dockerfile.vercel`, such as `pdo_pgsql` or `pdo_mysql`. Use an external database or Redis-compatible service for the state that must be shared across instances. Because Fluid compute can process concurrent requests in one instance and scale across multiple instances, use database connection pooling where available and size connection limits for aggregate concurrency.

Run database migrations as a separate release or continuous integration step:

```bash
php artisan migrate --force
```

Do not run migrations at container startup because multiple instances may start concurrently.

### Files and logs

The container file system is not durable. Store uploads in an external object store such as [Vercel Blob](https://vercel.com/docs/vercel-blob). `LOG_CHANNEL=stderr` sends application logs to Vercel runtime logs.

### Deployment optimization

Laravel’s `php artisan optimize` command caches configuration, events, routes, and views. However, configuration caching resolves environment variables when the command runs. Because Vercel provides values such as `APP_KEY` and database credentials at runtime, do not run the full command while building the container image.

Caches generated during the Docker build become immutable image contents, so every instance created from that deployment receives them. Files written while an instance is running may remain temporarily available to that instance, but they are not shared with other instances and disappear when the instance is recycled or the deployment changes.

You can cache components that do not depend on runtime environment variables:

```dockerfile
RUN php artisan event:cache \
    && php artisan view:cache
```

You can also run `php artisan route:cache`. Laravel 13 can cache the closure routes in this example, although closures that capture unserializable values may still fail. Regenerate the route cache whenever routes change.

Avoid generating these caches from the container entrypoint. That would repeat the work whenever a new instance starts and add to its startup time. Keep the configuration uncached so each instance reads its Vercel runtime environment variables correctly.

### Queues and scheduled tasks

Long-running Laravel queue workers and the in-process scheduler assume the container stays active. Use [Vercel Cron](https://vercel.com/docs/cron-jobs) to call an authenticated HTTP endpoint on a schedule and [Queues](https://vercel.com/docs/queues) for durable multi-step work.

## Troubleshooting

### Requests return 502

**Cause:** FrankenPHP failed to start, listens only on localhost, or listens on a port other than the configured `PORT`.

**Fix:** Keep `:{$PORT:80}` in `Caddyfile` so the server follows Vercel's port contract.

### Laravel reports that no application encryption key is set

**Cause:** `APP_KEY` is missing from the deployment environment.

**Fix:** Generate the key once and add it with `vercel env add APP_KEY`. Redeploy after changing environment variables.

### Requests fail while writing cache or session files

**Cause:** `storage` or `bootstrap/cache` is not writable, or the application uses a local driver across multiple instances.

**Fix:** Preserve the ownership commands in `Dockerfile.vercel` and configure an external session or cache store when data must be shared.

## Related resources

- [Deploy Symfony with Docker](https://vercel.com/kb/guide/symfony-php-with-docker)
  
- [Deploy PHP on Vercel with Docker](https://vercel.com/kb/guide/deploy-php-on-vercel-with-docker)
  
- Read the [Vercel container images documentation](https://vercel.com/docs/functions/container-images)
  
- Review the [`vercel.json`](https://vercel.com/docs/project-configuration/vercel-json) [reference](https://vercel.com/docs/project-configuration/vercel-json)
  
- Learn how [Vercel Services](https://vercel.com/docs/services) routes traffic
  
- Learn how to run [Docker on Vercel](https://vercel.com/kb/guide/docker)