Part 2/2 - Deploying Symfony 4 with rsync


In the previous video we covered how to deploy Symfony 4 using rsync. We saw that the rsync command usage itself was really quite straightforward, but that our naive process could - and likely would - lead to problems.

To make this process a little more robust we are going to introduce the concept of a build directory.

Instead of simply uploading whatever code we have on our local machine directly over the top of the code on our live, production web server, we will create timestamped, point-in-time copies of our code, and direct nginx or Apache to serve the specific build we want.

It sounds a little more complicated than it actually is, so let's dive right in, and see for ourselves how easy this can be.

Build A Bear Workshop

The first change we need to make is on our production web server.

We're no longer going to use the site root directory (/var/www/crvfakeexample.com) as the direct location of our code.

Instead, we will create a new directory under this root directory, called builds:

# from the web server
cd /var/www/crvfakeexample.com

mkdir builds

chown www-data:www-data builds

We make the new builds directory, and we change the owning user and group to www-data so our web server user can access the contents appropriately.

I am assuming that you have nothing inside the /var/www/crvfakeexample.com directory at this point, as though you have installed a brand new, fresh server. If you do have existing directory contents, here's my quick hack solution:

cd /var/www/crvfakeexample.com
sudo mv var ../
sudo rm -rf /var/www/crvfakeexample.com/*
sudo rm -rf /var/www/crvfakeexample.com/.*
sudo mv ../var .

What this does is clear off any existing directory contents, including hidden files and folders, but preserve the var directory so you don't have to re-setup all the permissions again.

Of course, don't do this on a real production server. This is purely for demo purposes.

Now that we have a builds directory, the next thing we need to do is tell Apache or nginx to serve from this new location:

Revised Apache Vhost Configuration

<VirtualHost *:80>
    ServerName crvfakeexample.com
    ServerAlias www.crvfakeexample.com

    DocumentRoot /var/www/crvfakeexample.com/current/public
    <Directory /var/www/crvfakeexample.com/current/public>
        AllowOverride None
        Require all granted
        Allow from All

        <IfModule mod_rewrite.c>
            Options -MultiViews
            RewriteEngine On
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteRule ^(.*)$ index.php [QSA,L]
        </IfModule>
    </Directory>

    # uncomment the following lines if you install assets as symlinks
    # or run into problems when compiling LESS/Sass/CoffeeScript assets
    # <Directory /var/www/crvfakeexample.com/current>
    #     Options FollowSymlinks
    # </Directory>

    # optionally disable the RewriteEngine for the asset directories
    # which will allow apache to simply reply with a 404 when files are
    # not found instead of passing the request into the full symfony stack
    <Directory /var/www/crvfakeexample.com/current/public/bundles>
        <IfModule mod_rewrite.c>
            RewriteEngine Off
        </IfModule>
    </Directory>
    ErrorLog /var/log/apache2/crvfakeexample.com_error.log
    CustomLog /var/log/apache2/crvfakeexample.com_access.log combined

    # optionally set the value of the environment variables used in the application
    SetEnv APP_ENV ${APP_ENV}
    SetEnv APP_DEBUG ${APP_DEBUG}
    SetEnv APP_SECRET ${APP_SECRET}
    SetEnv DATABASE_URL ${DATABASE_URL}
</VirtualHost>

Be sure to run an Apache reload after changing this config:

service apache2 reload

Revised nginx Server Configuration

server {
    listen 80 default;
    server_name crvfakeexample.com www.crvfakeexample.com;
    root /var/www/crvfakeexample.com/current/public;

    location / {
        # try to serve file directly, fallback to index.php
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;

        # optionally set the value of the environment variables used in the application
        fastcgi_param APP_ENV prod;
        fastcgi_param APP_SECRET some_new_secret_123;
        fastcgi_param APP_DEBUG 0;
        fastcgi_param DATABASE_URL "mysql://not_root:different_pass@127.0.0.1:3306/symfony_demo";
        fastcgi_param MAILER_URL null://localhost;

        # When you are using symlinks to link the document root to the
        # current version of your application, you should pass the real
        # application path instead of the path to the symlink to PHP
        # FPM.
        # Otherwise, PHP's OPcache may not properly detect changes to
        # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126
        # for more information).
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        # Prevents URIs that include the front controller. This will 404:
        # http://domain.tld/index.php/some-path
        # Remove the internal directive to allow URIs like this
        internal;
    }

    # return 404 for all other php files not matching the front controller
    # this prevents access to other php files you don't want to be accessible.
    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/crvfakeexample.com_error.log;
    access_log /var/log/nginx/crvfakeexample.com_access.log;
}

Be sure to run an nginx reload after changing this config:

service nginx configtest
service nginx reload

The only change in both files is in updating any paths from /var/www/crvfakeexample.com/ to /var/www/crvfakeexample.com/current.

We don't yet have anything setup at /var/www/crvfakeexample.com/current, but we will get to this shortly.

A New -Hope- Build

At this point we don't have an automated process for coming up with our build names. They can be anything we like.

For my purposes I am going to use a rough 'timestamp'.

At the time of writing, it's 10:17 on 16th January 2018.

I'm going to create a new build using this info as: 20180116-1017.

I won't have to create a new directory on my own PC to accomplish this. Instead, I will simply update the rsync command to transfer / copy my files and folders to this new subdirectory:

# from my local pc

rsync --exclude '.git' --exclude=.env --exclude=var -avzh \
  . \
  root@104.236.215.199:/var/www/crvfakeexample.com/builds/20180116-1017

What I need to do next, and what is super easy to forget, is to change the permissions on this new build sub directory:

# from the server

sudo chown -R www-data:www-data /var/www/crvfakeexample.com/builds/20180116-1017

Even though this step is easy enough to forget, there's another gotcha just waiting to bite you:

The var directory.

Our rsync command explicitly excludes the var directory.

But we will need it.

If we don't copy up the directory then - hopefully - the permissions will be sufficient to allow the www-data user to create the var directory, and use it as expected.

But if we recreate the var directory on each new deploy then every time we deploy, any existing logged in users will be logged out. And all our log files will be separated. And overall things will be quite confusing.

It's at this point that this deployment process really starts to show signs of strain. Yes, it will work, but it's far too manual to be reliable. Humans make mistakes, and we need to put processes in place to reduce or eliminate potential problem points.

All the same, let's continue doing this process by hand, and cover an automated approach in a few videos time.

Full Power To The Symlinks

I'm going to use two symlinks.

Symlinks are shortcuts from one folder to another.

My symlinks will be as follows:

/var/www/crvfakeexample.com/builds/20180116-1017/var will really point to /var/www/crvfakeexample.com/var

and

/var/www/crvfakeexample.com/current will really point to /var/www/crvfakeexample.com/builds/20180116-1017

Let's set these up, then cover what and why.

ln -sfn /var/www/crvfakeexample.com/var /var/www/crvfakeexample.com/builds/20180116-1017/var

and

ln -sfn /var/www/crvfakeexample.com/builds/20180116-1017 /var/www/crvfakeexample.com/current

You can read about the specific switches used in the ln command here.

Ok, so let's cover the why:

ln -sfn /var/www/crvfakeexample.com/var /var/www/crvfakeexample.com/builds/20180116-1017/var

This command creates a shortcut in our build/20180116-1017 directory, as a directory called var, which really points to our var directory in our project root.

Why do this?

To share the var directory. This means people won't be logged out between builds. All our log files will be centralised. And so on.

It also means we only need to sort out our var directory permissions just one time.

The downside is we need to run this command every time we create a new build. And that means we can, and likely will, forget to do so, even just one time.

Next:

ln -sfn /var/www/crvfakeexample.com/builds/20180116-1017 /var/www/crvfakeexample.com/current

This one is straightforward enough.

We are going to create a link / shortcut where the current directory can be switched out to point to any build we like. This means we can easily switch builds if we need to roll back, or do a new deploy, or whatever.

Wrapping Up

This is the gist of our rsync deploy process.

During the Symfony 2 / Symfony 3 implementation we split this over three videos. Here's its just two. If unsure on any of this, those videos do dive a little deeper.

I cannot recommend this approach as ideal for your real world. There's just too much to go wrong.

Why do I cover it then?

Because this is how automated deployment tools generally handle the process. Albeit, they do it with far more automation / scripting.

If you understand this approach, building your own variant, or using the automated tools is easier.

Next up we will cover a similar approach using Git. In doing so, we will cover adding in automation of our own.

Code For This Video

Get the code for this video.

Episodes