Can Symfony Apps Be Fast on Vagrant? Let’s Check with SuluCMS!
- CMS & FrameworksDebugging & DeploymentDevelopment EnvironmentFrameworksInstallationPerformancePerformance & ScalingSymfony
In this short tutorial, we’ll set up Sulu, a new Symfony based CMS, and optimize it on a Vagrant environment. Why a dedicated tutorial handling this? Besides the fact that Sulu has a rather complex initialization procedure, it is based on Symfony which is infamously slow on virtual machines with shared filesystems, and thus needs additional optimizations post-install. The performance hacks in this post, while Sulu-specific, can be applied to any Symfony application to make it faster on Vagrant.
Would you like to learn more about Symfony and/or SuluCMS? Come join us at WebSummerCamp – the only conference filled to the brim with long, hands-on workshops. The program is out and it’s awesome! Super early bird tickets available until the end of June!
As usual, we’ll be using our Homestead Improved box as the base, but the steps below are explained in enough detail for you to follow them on any environment.
New Box and Folder Sharing
We start by downloading a fresh box and setting up folder sharing.
git clone https://github.com/swader/homestead_improved hi_sulu
cd hi_sulu; bin/folderfix.sh
Once this is done, it’s recommended to set the file sharing type to nfs
, due to this known issue, also described here.
App Type and Vagrant Boot
Homestead Improved comes with a symfony-sulu
application type, which configures Nginx for Sulu specifically. We add a new site into Homestead.yaml
:
- map: test.app
to: /home/vagrant/Code/sulu/web
type: symfony-sulu
The generated Nginx is below. If you’re using something other than Homestead Improved, you’re encouraged to copy it from here:
server {
listen 80;
listen 443 ssl;
server_name test.app;
root "/home/vagrant/Code/sulu/web";
charset utf-8;
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log /var/log/nginx/test.app-ssl-error.log error;
sendfile off;
client_max_body_size 100m;
# PROD
location ~ ^/(website|admin|app)\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
# Prevents URIs that include the front controller. This will 404:
# https://domain.tld/app.php/some-path
# Remove the internal directive to allow URIs like this
internal;
}
# strip app.php/ prefix if it is present
rewrite ^/app\.php/?(.*)$ /test.app permanent;
location /admin {
index admin.php;
try_files $uri @rewriteadmin;
}
location @rewriteadmin {
rewrite ^(.*)$ /admin.php/test.app last;
}
location / {
index website.php;
try_files $uri @rewritewebsite;
}
# expire
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
try_files $uri /website.php/test.app;
access_log off;
expires 30d;
add_header Pragma public;
add_header Cache-Control public;
}
location @rewritewebsite {
rewrite ^(.*)$ /website.php/test.app last;
}
location ~ /\.ht {
deny all;
}
ssl_certificate /etc/nginx/ssl/test.app.crt;
ssl_certificate_key /etc/nginx/ssl/test.app.key;
}
Remove the SSL lines at the bottom if you’re not using HTTPS.
Then, let’s boot up the VM and SSH into it.
vagrant up; vagrant ssh
Add test.app
(or another URL if you chose it instead) to your host OS’ etc/hosts
file. If this sentence makes no sense, please read the Homestead Improved Quick Start.
Installing Sulu
All steps below happen inside the VM.
cd Code
git clone https://github.com/sulu-io/sulu-standard sulu; cd sulu
git checkout master
composer install
This will take a while, and might ask for a Github authentication token.
Note that depending on the version of PHP you have installed, this command may fail. For example, zend-code at the time of this writing requires PHP of any version that isn’t 7.0.5., and incidentally, that’s the exact version installed on Homestead Improved 0.4.4. This is easily fixed by running sudo apt-get upgrade php7.0-fpm
.
Post install scripts will ask us for some parameters – the only ones we need to fill out are the database name (homestead
, unless you make one specifically for Sulu) and credentials (homestead
/ secret
). Other can be left at their default values for now, and configured later if needed.
Configuring Sulu
cp app/Resources/webspaces/sulu.io.xml.dist app/Resources/webspaces/sulu.io.xml
cp app/Resources/pages/default.xml.dist app/Resources/pages/default.xml
cp app/Resources/pages/overview.xml.dist app/Resources/pages/overview.xml
cp app/Resources/snippets/default.xml.dist app/Resources/snippets/default.xml
rm -rf app/cache/*
rm -rf app/logs/*
The above installs a default webspace.
In the file app/Resources/webspaces/sulu.io.xml
we replace the name
and key
with the values we prefer.
Once done, we run:
app/console sulu:build dev
The above command will generate a user admin
with the password admin
and do some other development-friendly initializations. It’s recommended to run this in development environments. Note that this command should never be run in production. Instead, in production, you run app/console sulu:build prod
.
As an aside: at this point, we have an admin user with the credentials admin/admin
working. To create new roles in the future, we execute:
app/console sulu:security:role:create
and to create more users, we execute:
app/console sulu:security:user:create
The back end at test.app/admin
should work now, albeit very slowly.
To make the front end work, we use:
app/console assetic:dump
This will generate the required JS and CSS files and have the default theme include them.
As a final preparatory step, let’s turn on dev mode by changing the line:
defined('SYMFONY_ENV') || define('SYMFONY_ENV', getenv('SYMFONY_ENV') ?: 'prod');
to
defined('SYMFONY_ENV') || define('SYMFONY_ENV', getenv('SYMFONY_ENV') ?: 'dev');
in both web/admin.php
and web/website.php
. These will provide us with the famous Symfony debugbar at the bottom of the screen.
Speed Improvement Hacks
Symfony apps are notoriously slow on Vagrant. The following steps will reduce page load times from 25 seconds per route (this includes ajax calls), to around 400 ms per route, regardless of host OS platform or file sharing type. Other guides will often focus on NFS or other issues known to work only on some operating systems, but this procedure is guaranteed to work wonders on any setup.
Log and Cache
In app/AbstractKernel.php
, replace the getLogDir
and getCacheDir
methods with these:
/**
* {@inheritDoc}
*/
public function getCacheDir()
{
if (in_array($this->environment, array('dev', 'test'))) {
return '/dev/shm/appname/cache/' . $this->getContext() . '/' . $this->environment;
}
return $this->rootDir . '/cache/' . $this->getContext() . '/' . $this->environment;
}
/**
* {@inheritDoc}
*/
public function getLogDir()
{
if (in_array($this->environment, array('dev', 'test'))) {
return '/dev/shm/appname/logs' . $this->getContext() . '/' . $this->environment;
}
return $this->rootDir . '/logs/' . $this->getContext() . '/' . $this->environment;
}
This forces Sulu to write logs and cache into a folder not shared with the host OS, thus avoiding rapid-writing of unimportant data to a shared hard drive.
Moving Vendor
Symfony apps are notorious for installing hundreds of packages – even the default framework overflows with them, let alone apps built on it. When so many classes have to be looked up on the hard drive, and the hard drive is shared from the VM to the host OS as it is with Vagrant, load times are understandably slow.
Seeing as there’s little to no chance we’ll ever be editing something inside the vendor
folder, we can move it out of the synchronized folders and into a location inside the VM which is local to the machine’s “disk” only. That means we lose syncability and have to run composer
commands exclusively from within the VM (or proxied from the host OS, but this is outside the scope of this post), but all for the sake of epic speed gains.
To automatically do this, inside the sulu
folder execute:
~/Code/bin/sulu/vendorfix.sh
(alternatively, see below for full procedure of what you need to do)
This will change some paths in composer.json
, in app/autoload.php
, and in app/config/sulu.yml
, delete the current vendor
folder, and run Composer install. Inspect the shell script to see what it does, if you’re curious, or see the next section.
To make sure you keep autocompletion in your IDE, make sure you add home/vagrant/vendors/sulu-test.app/
to your IDE’s include path. You’ll probably have to copy the vendor folder into the host OS somewhere – a command like this should help:
cp -R ~/vendors/sulu-test.app ~/Code/
Then, just do the following in an IDE, e.g. PhpStorm:
Moving Vendor: The Long Way Around
As the most important part of the optimization (i.e. the one yielding the most results), the vendor-moving approach deserves more clarification, in order to be easy to replicate in non-HI environments which don’t have the vendorfix.sh
script. What follows is the step by step procedure (skip it if you used the step above and it worked fine):
-
Pick a VM-only location for your vendor folder. It’s a good idea to put them all in one folder, and then each vendor location into a subfolder per project. For example:
/home/vagrant/vendors/sulu-myapp
. Let’s assume the project will be calledmyapp
. Create that folder:mkdir -p /home/vagrant/vendors/sulu-myapp
-
Change the configuration in
composer.json
to reflect this newvendor
location by modifying or adding theconfig
value:"config": { "vendor-dir": "/home/vagrant/vendors/sulu-sulu.io/", "bin-dir": "vendor/bin" },
-
Open
app/autoload.php
and replace the line declaring$loader
with the path leading to our new, customvendor
location:// $loader = require __DIR__ . '/../vendor/autoload.php'; $loader = require "/home/vagrant/vendors/sulu-myapp/autoload.php";
-
Open
app/config/sulu.yml
and replace all instances of%kernel.root_dir%/../vendor/
with the full path to our new vendor folder. This should happen in two locations: the Doctrine configuration near the top, and thesulu_core.content.structure.sulu
entry in the middle of the file. -
Remove the old folder with
rm -rf vendor
from inside the app’s folder (myapp
). -
Run
composer install
andcomposer update
to make sure everything went well. -
Clear cache with
app/console cache:clear
.
That’s it. The custom vendor location should now be configured. Please post below if you run into any difficulties.
Note that while this process is Sulu-specific, it applies to any Symfony app (or any app whatsoever for that matter). What it comes down to, is defining a custom vendor folder in composer.json
, and replacing all references to the old vendor folder location with paths to the new one.
APC Autoload
Allow APC caching for the autoloader by uncommenting the appropriate lines in both web/website.php
and web/admin.php
. We need APC installed to use this, but Homestead Improved is already equipped for that.
Miscellaneous
-
we can update the value of
realpath_cache_size
in/etc/php/7.0/fpm/php.ini
to a bigger value like 4096k, andrealpath_cache_ttl
to something like 7200. -
we can get some marginal gains by installing the C extension for Twig.
-
we can disable XDebug with:
sudo phpdismod xdebug; sudo service php7.0-fpm restart
Troubleshooting
Optimize Symfony Debugging
Consisting of an abnormal number of classes and files, Symfony apps usually compile their core files into a single app/bootstrap.php.cache
file. This makes it almost impossible to debug Symfony apps properly without further alterations. To allow for easier debugging, refer to these docs.
__php_incomplete_class has no unserializer
This is usually due to stale cache. First, optimize environment for debugging (see above). Then:
app/console cache:clear
rm -rf app/cache/*
composer update
Additionally, if you’re using APC, bust the cache (see below).
Changes have no effect / classes being looked for in wrong location
This usually happens when the APC cache needs to be busted. One way to do this is to put:
apc_clear_cache();
at the top of website.php
or admin.php
, whichever you’re running, and then remove it after the first request. Or, just make a separate file with just this command, so you can call it whenever you want.
Conclusion
Both https://test.app/admin
and https://test.app
(if that’s the vhost domain you used) should now work and be lightning fast. You should be ready to start developing your CMS-powered application on Sulu.
As we mentioned above, these hacks are admittedly Sulu-specific, but can be applied to any Symfony application. The most important ones being the vendor fix and the log/cache fix, they will significantly improve the app’s load times during development. The other hacks are production-friendly, too, and can be used to speed up the application when deployed.
If you have any other tips and tricks you’d like to share for improving performance of Symfony apps, Vagrant-powered development environments, or both – Symfony on Vagrant – please let us know in the comments section below and we’ll do our best to update the post.
Would you like to learn more about Symfony and/or SuluCMS? Come join us at WebSummerCamp – the only conference filled to the brim with long, hands-on workshops. The program is out and it’s awesome! Super early bird tickets available until the end of June!