# Deploy Symfony to a VPS with FrankenPHP and Doctrine Deploy Symfony to your own VPS: ox runs composer install for prod, Doctrine migrations and FrankenPHP under systemd, and asks for APP_SECRET first. To deploy Symfony to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `bin/console` and `composer.json`, runs `composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader` and `php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration` for the migrations, then starts `frankenphp php-server --root public --listen 127.0.0.1:$PORT` under systemd behind Caddy with HTTPS. Run `ox check` in your repo to see the same plan before the first deploy. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Symfony repo in ox's tests. A deploy on a real server is not recorded yet. ox reads `bin/console` and `composer.json`, builds the plan on the card, and starts `frankenphp php-server --root public --listen 127.0.0.1:$PORT` on your server. ## What ox detects in a Symfony repo This is the plan `ox check` prints for a minimal a Symfony app, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `APP_ENV=prod APP_DEBUG=0 XDG_CONFIG_HOME=/tmp XDG_DATA_HOME=/tmp exec frankenphp php-server --root public --listen 127.0.0.1:$PORT` | `detected:bin/console` | | `build.install` | `APP_ENV=prod composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader` | `detected:composer.json` | | `build.migrate` | `APP_ENV=prod php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration` | `detected:composer.json` | | `tools.github:php/frankenphp` | `1.13.0` | `default` | | `packages` | `composer` | `detected:composer.json` | | `packages` | `php-cli` | `detected:composer.json` | | `packages` | `php-xml` | `detected:composer.json` | | `packages` | `unzip` | `detected:composer.json` | `ox check` also prints these hints for it: - `Symfony runs with var/ read-only: the build warms its cache, so log to php://stderr (Monolog's prod default) and keep sessions out of var/` ## The ox.toml for Symfony None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. ox.toml, optional: ```toml domains = ["www.example.com"] [app] ``` ## Check and deploy Symfony Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Symfony Set `APP_SECRET` before the first deploy: `ox check` asks for it, and the deploy stops until it has a value. Use the dashboard's Variables tab or `ox vars set KEY`, which asks for the value. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL` and `PUBLIC_HOST`. ## If the Symfony deploy fails - `set these before deploying: APP_SECRET`: A variable the app needs has no value. Set it on the dashboard's Variables tab, or with `ox vars set KEY`. [More](https://deploywithox.com/docs/operate/variables#set). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://deploywithox.com/docs/troubleshooting#health-check-failed). - `no index.php at the root or in public/; set [app] start = "frankenphp php-server --root --listen 127.0.0.1:$PORT"`: Put `index.php` at the root or in `public/`, or set `[app] start` with your document root. [More](https://deploywithox.com/docs/config#app). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Laravel guide](https://deploywithox.com/docs/guides/laravel): Composer, a queue worker and the scheduler. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. - [Set variables and secrets](https://deploywithox.com/docs/operate/variables) on the dashboard or with `ox vars`. ## Symfony FAQ ### Do I need a Dockerfile or an ox.toml to deploy Symfony to a VPS? No. ox does not use Docker, and `ox check` on a Symfony repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Symfony? Only what the plan names: `github:php/frankenphp 1.13.0` (ox's default) and the apt packages `composer`, `php-cli`, `php-xml` and `unzip`. Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Where do Symfony logs go? To `php://stderr`, Monolog's prod default, which ox keeps in the app's log. `var/` is read-only in a release, so keep logs and sessions out of it. ## Related guides - [**Plain PHP**Deploy a plain PHP site to your own VPS: ox finds index.php, installs FrankenPHP, and serves the site under systemd behind Caddy with HTTPS, no ox.toml.](https://deploywithox.com/docs/guides/php) - [**Phoenix**Deploy an Elixir Phoenix app to your own VPS: ox builds a mix release with its assets, runs Release.migrate, starts it under systemd, and adds PostgreSQL.](https://deploywithox.com/docs/guides/phoenix) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/symfony