Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Cache Permission Secrets

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $9.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Why are the cache files writable by everyone? The answer is inside our code.

Open up bin/console. In my project, I uncommented a umask(0000) line:

27 lines bin/console
... lines 1 - 7
// if you don't want to setup permissions the proper way, just uncomment the following PHP line
// read http://symfony.com/doc/current/book/installation.html#configuration-and-setup for more information
umask(0000);
... lines 11 - 27

I also added this in web/app.php:

27 lines web/app.php
... lines 1 - 4
// If you don't want to setup permissions the proper way, just uncomment the following PHP line
// read http://symfony.com/doc/current/book/installation.html#checking-symfony-application-configuration-and-setup
// for more information
umask(0000);
... lines 9 - 27

Thanks to this, whenever Symfony creates a file - like cache files - the permissions default to be writable by everyone.

No umask: Making Cache not Writable

I added these precisely to avoid permissions problems. But it's time to fix them properly. In app.php, comment that out:

27 lines web/app.php
... lines 1 - 4
// If you don't want to setup permissions the proper way, just uncomment the following PHP line
// read http://symfony.com/doc/current/book/installation.html#checking-symfony-application-configuration-and-setup
// for more information
//umask(0000);
... lines 9 - 27

In console, comment it out... but also copy it and move it inside the debug if statement:

28 lines bin/console
... lines 1 - 7
// if you don't want to setup permissions the proper way, just uncomment the following PHP line
// read http://symfony.com/doc/current/book/installation.html#configuration-and-setup for more information
//umask(0000);
... lines 11 - 19
if ($debug) {
umask(0000);
... line 22
}
... lines 24 - 28

During development, umask() makes our life really easy... cache files can be created and re-created by everyone. So I want to keep it. In fact, in web/app_dev.php, we also have a umask() call:

32 lines web/app_dev.php
... lines 1 - 5
// If you don't want to setup permissions the proper way, just uncomment the following PHP line
// read http://symfony.com/doc/current/book/installation.html#checking-symfony-application-configuration-and-setup
// for more information
umask(0000);
... lines 10 - 32

Again, this matches how Symfony 4 will work, out of the box.

Find your local terminal, commit those changes and push them:

git add -u
git commit -m "no writable in prod mode"
git push origin master

Deploy!

ansible-playbook ansible/deploy.yml -i ansible/hosts.ini --ask-vault-pass

Ok! Let's see what happens without umask on production. When it finishes, find your server terminal, move out of current/ and then back in. Check the permissions:

ls -la var/cache/prod

There it is! The files are writable by the user and group, but not by everyone. Our web server user - www-data - is not in the same group as our terminal user. Translation: the cache files are not writable by the web server.

So... will it blend? I mean, will the site work? Moment of Truth. Refresh! It does work! Woh! This is huge!

The Magical cache:warmup

How is this possible? How can the site work if our Symfony app can't write to the cache/ directory? The key is the cache:warmup task:

... lines 1 - 14
- name: Warm up the cache
command: '{{ release_console_path }} cache:warmup --env=prod'
... lines 17 - 48

I'm going to tell you a small lie first. The cache:warmup command creates every single cache file that your application will ever need. Thanks to this, the cache directory can totally be read-only after running this command.

Some Cache Cannot be Warmed Up

Great, right? Now, here is the true story. The cache:warmup task creates almost all of the cache files that you will ever need. But, there are a few types of things that simply can't be cached during warm up: they must be cached at the moment they're needed. These include the serializer and validation cache, for example.

Knowing this, our site works now, but it should break as soon as we try to use the serializer or validation system... because Symfony won't be able to cache their config. Well, let's try it!

I created an API endpoint: /api/videos that uses the serializer. Try it! Woh! It works! But... the serializer cache shouldn't be able to save. What the heck is going on?

The Dynamic cache.system Service

Here is the secret: whenever Symfony needs to cache something after cache:warmup, it uses a service called cache.system to do this:

./bin/console debug:container cache.system

This is not a service you should use directly, but it's critically important.

Tip

Actually, you can use this service, but only to cache things that are needed to make your app work (e.g. config). It's cleared on each deploy

This service is special because it automatically tries several ways of caching. First, if APCu is available, it uses that. On the server, check for it:

php -i | grep apcu

Right now, we don't have that. No problem, the service then checks to see if OpCache is installed:

php -i | grep opcache

We do have this installed, and you should to. Thanks to it, instead of trying to write to the var/cache directory, Symfony uses temporary file storage and a super fast caching mechanism.

If neither APCu nor OpCache are installed, then it finally falls back to trying to write to the cache/ directory. So basically, in order for the cache directory to be read only... we don't need to do anything! Just, install OpCache - which you should always have - or APCu.

Great! But, I do have one more question: if we use APCu or OpCache, do we need to clear these caches when we deploy? For example, if some validation config was cached to APCu and that config is changed... don't we need to clear the old cache when we deploy? Actually, no! Each time you deploy, well, each time you run cache:warmup, Symfony chooses a new, random cache key to use for system.cache. This effectively clears the system cache on each deploy automatically!

This is a long way of saying that... well... the cache directory simply does not need to be writable. But, we can do a few things to improve performance!

Leave a comment!

4
Login or Register to join the conversation
Cesar Avatar

Hi guys:
I returned to check Chapter 16 because I have deployed a Symfony App that uses Easy Admin Bundle and I got an error saying ../var/cache/prod/easy_admin was not writable. I fixed it giving 0777 to that folder but I wonder if there is a better solution. Do you have any tip about this? I will appreciate it.
Cesar

Reply

Hey Cesar

You can add a step for fixing file permissions, something like this:


- name: Fix directories permissions
  become: true
  file:
    path: 'var/cache/prod/easy_admin' # adjust this value to your needs
    state: directory
    mode: 0777
    recurse: yes

Cheers!

Reply

Can't try '/api/videos': 404 Not Found
grep also couldn't find anything.

Reply

Hey Ivan,

I just double checked it: downloaded the course code and ⁨look at src⁩/⁨AppBundle⁩/⁨Controller⁩/DefaultController.php in finish/ directory and I can see the endpoint there, here's its code:


    /**
     * @Route("/api/videos")
     */
    public function videosApiAction()
    {
        $videos = $this->getVideoRepository()
            ->findAll();

        return $this->json(['videos' => $videos]);
    }

Maybe you have to clear the cache? :)

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the fundamentals of Ansistrano haven't changed, this tutorial is built using Symfony 3, which has significant differences versus Symfony 4 and later.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "doctrine/doctrine-bundle": "^1.6", // 1.6.8
        "doctrine/orm": "^2.5", // v2.7.2
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "sensio/distribution-bundle": "^5.0.19", // v5.0.20
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
        "symfony/monolog-bundle": "^3.1.0", // v3.1.0
        "symfony/polyfill-apcu": "^1.0", // v1.4.0
        "symfony/swiftmailer-bundle": "^2.3.10", // v2.6.3
        "symfony/symfony": "3.3.*", // v3.3.5
        "twig/twig": "^1.0||^2.0", // v1.34.4
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.2.1
        "predis/predis": "^1.1", // v1.1.1
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.6
        "symfony/phpunit-bridge": "^3.0", // v3.3.5
        "doctrine/data-fixtures": "^1.1", // 1.3.3
        "hautelook/alice-bundle": "^1.3" // v1.4.1
    }
}

What Ansible libraries does this tutorial use?

# ansible/requirements.yml
-
    src: DavidWittman.redis
    version: 1.2.4
-
    src: ansistrano.deploy
    version: 2.7.0
-
    src: ansistrano.rollback
    version: 2.0.1
userVoice