Skip to content
Docs

Deploy Symfony on Edgenivo with Docker

Build a Symfony application with FrankenPHP and Docker, then deploy it to Edgenivo with production configuration, external storage, and preview deployments.

Anshuman BhardwajContent Engineer

Symfony can run in a container-backed Edgenivo Function. FrankenPHP combines the PHP runtime with a production web server built on Caddy. Edgenivo builds the application into an image, stores it in Edgenivo 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.

Copy link to headingPrerequisites

  • A Edgenivo account
  • Docker Desktop or a running Docker daemon
  • Edgenivo CLI (npm install -g vercel)
  • Composer 2 when creating the project outside Docker

Copy link to headingHow 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 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.

Copy link to headingSteps

Copy link to heading1. Create the Symfony application

Create a Symfony 7.4 LTS project:

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

Create src/Controller/StatusController.php:

src/Controller/StatusController.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 Edgenivo',
]);
}
#[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.

Copy link to heading2. Configure FrankenPHP

Create Caddyfile in the project root:

Caddyfile
:{$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 Edgenivo's PORT, serves static files from public, and returns 404 for other PHP paths instead of executing them.

Copy link to heading3. Add Dockerfile

Create Dockerfile 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.

A Dockerfile.vercel or Containerfile.vercel also works and takes precedence over a detected framework.

Copy link to heading4. Exclude local files from the image

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

.dockerignore
.git
.env.local
.env.*.local
node_modules
tests
var
vendor

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

Copy link to heading5. Add vercel.json

Create a vercel.json file at the project root to configure the Edgenivo project:

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

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

Copy link to heading6. Run locally

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

Terminal
vercel dev -L --listen 8080

Test both routes:

Terminal
curl http://localhost:8080/
curl --fail http://localhost:8080/health

Copy link to heading7. Deploy to Edgenivo

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

Terminal
vercel deploy --prod

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

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

Copy link to headingConfigure production services

Copy link to headingEnvironment variables

Add APP_SECRET, DATABASE_URL, and other application values in the Edgenivo 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.

Copy link to headingDatabase and migrations

Install the PDO extension required by your database, such as pdo_pgsql or pdo_mysql, with install-php-extensions. A Edgenivo 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.

Copy link to headingCache, 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 Edgenivo Blob. Write production logs to stderr so Edgenivo can collect them.

Copy link to headingAssets

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.

Copy link to headingMessenger 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 Edgenivo Cron for authenticated scheduled endpoints and Queues for durable multi-step work.

Copy link to headingOptionally 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.

Copy link to headingTroubleshooting

Copy link to headingRequests return 502

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

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

Copy link to headingRoutes 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.

Copy link to headingThe 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 documentation

More Edgenivo Services guides