---
title: Deploy Symfony on Vercel with Docker
description: Build a Symfony application with FrankenPHP and Docker, then deploy it to Vercel with production configuration, external storage, and preview deployments.
url: /kb/guide/symfony-php-with-docker
canonical_url: "https://vercel.com/kb/guide/symfony-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/laravel-php-with-docker
  - /kb/guide/deploy-php-on-vercel-with-docker
  - /kb/guide/docker
  - /docs/functions/container-images
  - /docs/project-configuration/vercel-json
  - /docs/services
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Symfony 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 Symfony application, warms its production cache during the image build, and keeps runtime configuration outside the image.

## 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 parts:

- `src/Controller/StatusController.php` returns JSON for `/` and `/health`.
  
- `Caddyfile` serves static files only from `public/` and routes application requests to `public/index.php`.
  
- `Dockerfile.vercel` installs production dependencies, clears the production cache, and copies the complete Symfony application into FrankenPHP.
  
- `vercel.json` declares the container service and routes requests to it.
  

## Steps

### 1\. Create the Symfony application

Create a Symfony 7.4 LTS project:

```bash
composer create-project symfony/skeleton:"7.4.*" vercel-docker-symfony
cd vercel-docker-symfony
```

Create `src/Controller/StatusController.php`:

```php
<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

final class StatusController
{
    #[Route('/', name: 'app_home', methods: ['GET'])]
    public function home(): JsonResponse
    {
        return new JsonResponse([
            'framework' => 'Symfony',
            'message' => 'Hello from Symfony and FrankenPHP on Vercel',
        ]);
    }

    #[Route('/health', name: 'app_health', methods: ['GET'])]
    public function health(): JsonResponse
    {
        return new JsonResponse(['status' => 'ok']);
    }
}
```

The route attributes are discovered through the controller resource in `config/routes.yaml`.

### 2\. Configure FrankenPHP

Create `Caddyfile` in the project root:

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

    php_server {
        try_files {path} index.php
    }

    @phpFile path *.php*
    error @phpFile "Not found" 404
}
```

The server follows Vercel's `PORT`, serves static files from `public`, and returns 404 for other PHP paths instead of executing them.

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

Create `Dockerfile.vercel` at 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

ENV APP_ENV=prod \
    APP_DEBUG=0

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

COPY . .
RUN composer dump-autoload --no-dev --classmap-authoritative \
    && php bin/console cache:clear

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 /app/var

ENV PORT=80 \
    APP_ENV=prod \
    APP_DEBUG=0

USER www-data

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

The dependency layer installs packages with Composer scripts disabled. After copying the source, the build regenerates an authoritative autoloader and explicitly clears and warms Symfony’s production cache.

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

Create a `.dockerignore` file at the project root to exclude files from the Docker image:

```gitignore
.git
.env.local
.env.*.local
node_modules
tests
var
vendor
```

Symfony's committed `.env` contains non-secret defaults. Keep production secrets in Vercel environment variables or uncommitted `.env.local` files, which this configuration excludes from the image.

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

Create a `vercel.json` file at the project root to configure the Vercel 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 Symfony service.

### 6\. Run locally

Start the application through Vercel's local container runtime. The `-L` flag uses the local project configuration without linking to a Vercel project:

```bash
vercel dev -L --listen 8080
```

Test both routes:

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

### 7\. Deploy to Vercel

After both local requests succeed, deploy the project to production:

```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

### Environment variables

Add `APP_SECRET`, `DATABASE_URL`, and other application values in the Vercel project settings or with `vercel env add NAME`. Real environment variables override values from Symfony's configuration `.env` file.

When relying exclusively on runtime environment variables, you can generate an optimized empty environment map during the build with `composer dump-env prod --empty`. Do not compile production secret values into the image.

### Database and migrations

Install the PDO extension required by your database, such as `pdo_pgsql` or `pdo_mysql`, with `install-php-extensions`. A Vercel deployment can run multiple container instances, and each concurrent PHP request (or each FrankenPHP worker when worker mode is enabled) can open a connection. Use an external pooler or proxy when needed, and set its limits to match expected concurrency.

Use a serverless-compatible database, external connection pooler, or database proxy. Open connections lazily, keep pool sizes conservative, use short idle timeouts, and deploy the Function in a region close to the database. Run Doctrine migrations as a separate release or CI step rather than during container startup.

### Cache, files, and logs

The image contains Symfony’s warmed production cache, which reduces startup work for new instances. Although `/app/var` is writable at runtime, its contents are ephemeral, local to one container instance, and discarded when that instance is replaced or scaled down. Do not use the local file system for uploads, sessions, queues, or other durable state. Store durable data in an external database, cache, or object store such as [Vercel Blob](https://vercel.com/docs/vercel-blob). Write production logs to `stderr` so Vercel can collect them.

### Assets

If the project uses AssetMapper, run `php bin/console asset-map:compile` during the image build. If it uses Webpack Encore, build assets in a Node.js stage and copy the generated files into `public/build`.

### Messenger and scheduled commands

Long-running Symfony Messenger workers and in-process schedulers assume the container stays active. Do not run `messenger:consume` or an in-process scheduler as an always-on process because the Function may shut down midway. Use [Vercel Cron](https://vercel.com/docs/cron-jobs) for authenticated scheduled endpoints and [Queues](https://vercel.com/docs/queues) for durable multi-step work.

## Optionally enable FrankenPHP worker mode

Symfony 7.4 supports FrankenPHP worker mode natively. To enable it, add `worker /app/public/index.php` inside the `php_server` block. This boots one Symfony kernel per FrankenPHP worker and reuses it while the container instance remains active. Treat all in-memory state as ephemeral and local to that worker. Audit stateful services and implement `ResetInterface` where necessary. Symfony’s runtime recycles workers after 500 requests by default; configure this with `FRANKENPHP_LOOP_MAX`.

## Troubleshooting

### Requests return 502

**Cause:** FrankenPHP is listening on a fixed port or only on localhost.

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

### Routes return 404

**Cause:** The document root or front-controller fallback is incorrect.

**Fix:** Keep `root * /app/public` and `try_files {path} index.php`, then confirm the route with `php bin/console debug:router`.

### The application uses development configuration

**Cause:** `APP_ENV` or `APP_DEBUG` overrides the production defaults.

**Fix:** Set `APP_ENV=prod` and `APP_DEBUG=0`, clear the cache, and redeploy.

## Related resources

- [Deploy Laravel with Docker](https://vercel.com/kb/guide/laravel-php-with-docker)
  

- [Deploy PHP on Vercel with Docker](https://vercel.com/kb/guide/deploy-php-on-vercel-with-docker)
  
- Learn how to run [Docker on Vercel](https://vercel.com/kb/guide/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