Deploying Symfony 4/5/6 Apps on Heroku
Last updated May 30, 2024
Table of Contents
This guide will walk you through the steps of deploying a Symfony version 4 or later application on Heroku. For an introduction to using PHP on Heroku, please refer to Getting Started with PHP on Heroku.
This article is for Symfony versions 4 and later. If you’re instead looking to deploy a Symfony 3 application, please refer to Deploying Symfony 3 apps on Heroku.
Prerequisites
- Knowledge of the PHP language;
- A Heroku user account, signup is free and instant;
- Familiarity with the getting Started with PHP on Heroku guide, with PHP, Composer and the Heroku CLI installed on your computer;
- An existing Symfony 4 or later application you’d like to deploy (or an empty skeleton app, see below).
Creating a Symfony application
If you’d like to simply follow the along this guide, you may create an empty Symfony application using the following steps.
The application in this tutorial is based on the Symfony Quick Tour guide. It’s worth a read before (or while) following the instructions in this article.
Installing a Symfony Website Skeleton project
Use the composer create-project symfony/website-skeleton symfony-heroku
command to bootstrap a new project based on the Symfony website application skeleton, which provides you with the basic structure for a website application. The command below sets it up in a directory named symfony-heroku
using the latest version of Symfony.
After downloading the skeleton’s dependencies, the installer will prompt you to enter a few details regarding a database connection and mailer transports. For now, you can simply hit enter at each of these prompts to accept the default values; you can always change them later.
After it’s done, Composer has set up a fully functional Symfony website project in the directory you specified, so you can cd
to it.
$ composer create-project symfony/website-skeleton symfony-heroku/
Installing symfony/website-skeleton (v4.3.1.4)
- Installing symfony/website-skeleton (v4.3.1.4): Downloading (100%)
Created project in symfony-heroku/
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
…
$ cd symfony-heroku
Initializing a Git repository
It’s now time to initialize a Git repository and commit the current state:
$ git init -b main
Initialized empty Git repository in ~/symfony-heroku/.git/
$ git add .
$ git commit -m "initial import"
[main (root-commit) 241cbc8] initial import
42 files changed, 4397 insertions(+)
…
Initial Setup
You’re now going to create an application on Heroku, configure the Symfony environment to use, and then you can simply git push
to deploy for the first time!
Creating a new application on Heroku
To create a new Heroku application that you can push to, use the CLI’s create
command:
$ heroku create
Creating app... done
https://floating-badlands-41656.herokuapp.com/ | https://git.heroku.com/floating-badlands-41656.git
You are now almost ready to deploy the application. Follow the next section to ensure your Symfony app runs with the settings for the right environment.
Creating a Procfile
To deploy your application to Heroku, you must often first create a Procfile
, which tells Heroku what command to use to launch the web server with the correct settings. By default, Heroku will launch an Apache web server together with PHP to serve applications.
However, a special circumstance applies to your Symfony application: the document root is in the public/
directory, and not in the root directory, of the application.
Some older versions of Symfony sometimes created this file automatically, but you should explicitly create this Procfile
as part of your application, something that will become necessary anyway once you begin using additional process types.
Simply create a file with the correct command for the web
process type and commit it:
$ echo 'web: heroku-php-apache2 public/' > Procfile
$ git add Procfile
$ git commit -m "Heroku Procfile"
Configuring Symfony to run in the prod
environment
If you don’t explicitly configure the environment (dev
, prod
, etc.) to use, Symfony will, by default, use the dev
environment in console commands and at runtime.
For Symfony to know it needs to use the prod
environment at all times, it reads from the APP_ENV
environment variable. You can set environment variables using the heroku config
feature. Running following command will set a configuration variable that lets Symfony to know to run in production mode:
$ heroku config:set APP_ENV=prod
Setting config vars and restarting floating-badlands-41656... done
APP_ENV: prod
Deploying to Heroku
Next up, it’s finally time to deploy to Heroku for the first time:
$ git push heroku main
…
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> PHP app detected
remote: -----> Bootstrapping...
remote: -----> Installing system packages...
…
remote: -----> Installing dependencies...
remote: Composer version 1.8.6
remote: Loading composer repositories with package information
remote: Installing dependencies from lock file
…
remote: -----> Preparing runtime environment...
remote: -----> Checking for additional extensions to install...
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing... done, 87.3MB
remote: -----> Launching...
remote: Released v4
remote: https://floating-badlands-41656.herokuapp.com/ deployed to Herok
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/floating-badlands-41656.git
* [new branch] main -> main
And that’s it! If you now open your browser, either by manually pointing it to the URL heroku create
gave you, or by using the Heroku CLI to launch a new browser window, the application will respond:
$ heroku open
Opening floating-badlands-41656... done
If you created an empty application skeleton earlier, you will see a 404 error page that reads “Oops! An Error Occurred” in red letters. This is expected behavior - your application is completely empty, and Symfony is correctly serving a 404 page as a result.
It is likely that you have to set additional environment variables, enable logging, and so forth, for your application to be functional, so follow the next steps to finalize your deployment.
Logging
By default, Symfony will use a built-in minimalist PSR-3 compliant logger that writes its messages to STDERR, meaning they will show up in Heroku’s logs correctly.
If you’re instead using Monolog for logging, then the default configuration will result in logs getting written into your application’s var/log/
directory, which isn’t ideal as Heroku uses an ephemeral filesystem and treats logs as streams of events across all running dynos.
Changing the log destination for production
All that’s required to have Monolog write its output to STDERR
is changing config/packages/prod/monolog.yaml
. Locate each section in this file that uses a stream
logger, and change the value of path
to "php://stderr"
, so it looks roughly like this:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
nested:
type: stream
path: "php://stderr"
level: debug
You can now git add
, git commit
, and git push heroku main
that change as usual.
Viewing application logs
Next, run heroku logs --tail
to keep the stream of logs from Heroku open in your terminal. Switch back to your browser and navigate to your Heroku application. As you refresh the page, you’ll see the web server’s access logs on your terminal, as well as anything your application may be logging.
Press Ctrl+C
on your keyboard again to leave the heroku logs --tail
command.
Environment variables
Your application will likely want to connect to a database, communicate with an email gateway, or just dynamically react to configuration. For this purpose, Heroku will expose any config var, whether it was set by yourself or by an add-on, as an environment variable that you may read in PHP code from the $_ENV
superglobal or using getenv()
.
Using config vars to define environment specific info such as database credentials, log levels, or e-mail gateway information is one of the fundamental principles of the Twelve-Factor App.
Some environment variables, such as APP_SECRET
or DATABASE_URL
, are used in the default configs for a new application. For example, your config/packages/doctrine.yaml
probably looks like this:
doctrine:
dbal:
# configure these for your database server
driver: 'pdo_mysql'
server_version: '5.7'
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_ci
url: '%env(resolve:DATABASE_URL)%'
And in config/packages/framework.yaml
, the APP_SECRET
environment variable is used:
framework:
secret: '%env(APP_SECRET)%'
This is how you configure Symfony using environment variables, and some variables have pre-defined defaults in your app’s .env
file. Make sure you override any values in that file that need custom values at runtime by running heroku config:set
.
For example, to change APP_SECRET
in production (this will set it to a random 32 character string):
$ heroku config:set APP_SECRET=$(php -r 'echo bin2hex(random_bytes(16));')
A DATABASE_URL
environment variable, on the other hand, will automatically be set when you add a Heroku Postgres instance to your app.
It is recommended that you do not use the symfony/dotenv
package in production, but instead set all required environment variables explicitly using heroku config
. Moving symfony/dotenv
from require
to require-dev
in your composer.json
will ensure that .env
files from your application root aren’t picked up in the first place. This corresponds to option #2 in post-deploy step B of the Symfony documentation.
URL Rewrites
Symfony 4 and newer applications no longer contain a .htaccess
or other configuration to allow rewriting of URLs in such a way that they do not require the index.php
script in the path.
In order to quickly enable rewriting for the Apache web server, you can install the symfony/apache-pack
recipe, which places a suitable .htaccess
file into your public/
directory:
$ composer require symfony/apache-pack
$ git add composer.json composer.lock symfony.lock public/.htaccess
$ git commit -m "apache-pack"
After deploying this change using git push heroku main
, you can access any URL on your application without having to include /index.php
in the path.
Trusting the Heroku Router
Heroku’s HTTP Routing routes each request through a layer of reverse proxies which are, among other things, responsible for load balancing and terminating SSL connections. This means that requests received by a dyno will have the last router’s IP address in the REMOTE_ADDR
environment variable, and the internal request will always be made using the HTTP protocol, even if the original request was made over HTTPS.
Like most common load balancers or reverse proxies, Heroku provides the original request information in X-Forwarded-…
headers (as documented here). Symfony can easily be configured to trust such headers.
Since Heroku sends all requests to an application through a load balancer first, and that load balancer always sets the headers (making it impossible for a client to forge their values), you can configure Symfony to treat the current remote addresses (which is the Heroku router) as a trusted proxy in index.php
.
It is important to also prevent Symfony from trusting the Forwarded
and X-Forwarded-Host
headers, because Heroku’s router does not set those, but Symfony trusts them out of the box once a trusted proxy is set.
This can be achieved by setting the proxy trust method to HEADER_X_FORWARDED_AWS_ELB
, which is a shorthand for the alternative notation, Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST
.
By default, your public/index.php
already contains a section that uses the TRUSTED_PROXIES
environment variable to quickly configure this aspect of your application, and it uses the correct Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST
combination of allowed headers:
…
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) {
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
}
…
However, the internal remote address of the Heroku routing node that forwards the request to a dyno will be different every time, so it must be added to the list of trusted proxies manually if the application is running in the prod
environment.
In order to preserve the ability to set additional trusted proxy IP ranges (for instance when using a CDN), while at the same time trusting the Heroku router, you could combine the existing logic that reads the TRUSTED_PROXIES
environment variable with a conditional addition of REMOTE_ADDR
to the list depending on the application env, like so:
...
$trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false;
$trustedProxies = $trustedProxies ? explode(',', $trustedProxies) : [];
if($_SERVER['APP_ENV'] == 'prod') $trustedProxies[] = $_SERVER['REMOTE_ADDR'];
if($trustedProxies) {
Request::setTrustedProxies($trustedProxies, Request::HEADER_X_FORWARDED_AWS_ELB);
}
...
This approach is safe on Heroku, because all traffic to your application must go through Heroku’s router, so you can rely on the remote IP address being a trustworthy proxy. In other environments, this may not be the case.
Using the Nginx Web Server
In the initial deployment step further above, the Procfile
you created uses the heroku-php-apache2
command to launch the application. The .htaccess
file installed by the symfony/apache-pack
Flex recipe ensures that URLs are correctly rewritten to the index.php
“front controller” script. You may instead use Nginx to run your application.
Creating an Nginx configuration include
Nginx does not support a mechanism similar to Apache’s .htaccess
configs, so you have to create a dedicated configuration include and instruct Nginx to use it.
For a Symfony application, the rewrites needed are very simple; you can place the following directives into a file named nginx_app.conf
in your application’s root directory:
location / {
# try to serve file directly, fallback to rewrite
try_files $uri @rewriteapp;
}
location @rewriteapp {
# rewrite all to index.php
rewrite ^(.*)$ /index.php/$1 last;
}
location ~ ^/index\.php(/|$) {
try_files @heroku-fcgi @heroku-fcgi;
# ensure that /index.php isn't accessible directly, but only through a rewrite
internal;
}
Creating a Procfile
for Nginx
This time around, you will use the heroku-php-nginx
script, and pass it your custom configuration snippet using the -C
option:
$ echo 'web: heroku-php-nginx -C nginx_app.conf public/' > Procfile
$ git add Procfile nginx_app.conf
$ git commit -m "Nginx Procfile and config"
After the next git push heroku main
, your application will run using the Nginx web server.
Further reading
- Check out the PHP on Heroku Reference to learn about available versions, extensions, features and behaviors.
- Review the instructions on customizing web server and runtime settings for PHP to learn more about configuring Apache, Nginx and PHP.
- Browse the PHP category on Dev Center for more resources.