Deploying PHP applications with Deployer and GitLab CI
A simple guide to easy deployment of PHP/Symfony applications from GitLab as well as from your local machine.
In this article
Deploying a new version of an application is usually a routine sequence of many steps, and forgetting any of them is not a good idea. Things can get a bit more complicated when, for example, something changes about data that is not versioned in the repository – such as write permissions or the structure of application data.
The ideal solution is therefore to automate the whole process as much as possible, build it into your development workflow and make it quick and easy to run from there. If you run your application in a container engine, you are probably already using one of the container orchestration tools to deploy it. But if you run your application the traditional way, you can easily achieve automated deployment with, for example, a simple Bash script and a way to run it remotely. For deploying new versions of Symfony applications this way, however, the tool that has worked best for us is Deployer.
The following guide adds the option to trigger a deployment manually from GitLab using CI, as well as from a local machine with a cloned repository.
Deployer
As a first step, we add the Deployer and Composer phar files to the repository. Although deployer.org shortens the file name to just dep, I recommend keeping the .phar extension. PhpStorm will then let you expand its contents and easily browse and explore all the different 'recipes' for various frameworks and situations.
$ curl -LO https://deployer.org/deployer.phar
After that, all you need is a simple deploy.php script describing the individual steps for bringing the application up and the order in which they should run. For the record, the bundled recipes do not include a specific recipe for the current Symfony5. However, deploying applications is no different from Symfony4, so you can simply use the recipe for that version. The recipe below has to be adjusted in its specifics (repo URL, shared files and directories, writable directories, etc.) to your particular needs.
<?php
namespace Deployer;
require 'recipe/symfony4.php';
set('application', 'iD-SIGN Website');
set('repository', 'ssh://git@gitlab.url/your/repo');
set('shared_dirs', ['var/log', 'var/sessions', 'public/media', 'data']);
set('shared_files', ['.env.local', 'public/robots.txt']);
set('writable_dirs', ['public/media', 'data', 'var/cache']);
set('bin/composer', '{{release_path}}/composer.phar');
set('keep_releases', 3);
// Hosts
host('production')
->hostname('yourserver.com')
->user('user')
->set('deploy_path', '/var/www/id-sign.com/www')
->set('http_user', 'www-data')
->become('id-sign');
// Tasks
task('deploy:build_assets', static function () {
run('cd {{release_path}} && yarn install && yarn encore production');
});
// Migrate database before symlink new release.
before('deploy:symlink', 'database:migrate');
before('database:migrate', 'deploy:build_assets');
// [Optional] if deploy fails automatically unlock.
after('deploy:failed', 'deploy:unlock');
The script itself is pretty much self-explanatory. To make it quicker to understand without having to study the code, here is also the list of tasks that the Symfony4 recipe defines and runs as part of the 'deploy' task.
desc('Deploy project');
task('deploy', [
'deploy:info',
'deploy:prepare',
'deploy:lock',
'deploy:release',
'deploy:update_code',
'deploy:shared',
'deploy:vendors',
'deploy:writable',
'deploy:cache:clear',
'deploy:cache:warmup',
'deploy:symlink',
'deploy:unlock',
'cleanup',
]);
How does it all work? Deployer connects over SSH to the hostname() server as user(). If you then need to switch to a different user, you set it with the become() method, and sudo is used for the switch by default. So this user() -> become() switch needs to be allowed.
Setting up sudo is easy – just add a configuration file to /etc/sudoers.d, e.g. /etc/sudoers.d/projectname, put a simple rule in it and reload sudo. Here is an example configuration that allows the user newman to switch to the user id-sign without a password, and that user in turn to switch to the user www-data.
newman ALL=(id-sign) NOPASSWD:ALL
id-sign ALL=(www-data) NOPASSWD:ALL
You can verify that everything works very easily, for example with:
newman@server$ sudo -u id-sign bash
Before the first test run, note that Deployer creates its own directory structure on the server for individual releases. So if you already have the application installed on the server, you will most likely need to change its location as well as your web server configuration.
I recommend making your first attempts outside of the production environment :-)
We can do a test run from our local machine, from the directory with the cloned project. For full comfort, it is a good idea to have SSH key login to the server set up. If you don't, it is very easy to fix.
Generating an SSH key:
$ ssh-keygen
Installing the key on the server (adjust the server user and hostname to your situation):
$ ssh-copy-id user@your-server.com
Now we can try your first deploy, which may look like this:
$ php deployer.phar deploy production
✈︎ Deploying master on production
✔ Executing task deploy:prepare
✔ Executing task deploy:lock
✔ Executing task deploy:release
✔ Executing task deploy:update_code
✔ Executing task deploy:shared
✔ Executing task deploy:vendors
✔ Executing task deploy:writable
✔ Executing task deploy:cache:clear
✔ Executing task deploy:cache:warmup
✔ Executing task deploy:build_assets
✔ Executing task database:migrate
✔ Executing task deploy:symlink
✔ Executing task deploy:unlock
✔ Executing task cleanup
Successfully deployed!
If an error occurs during deployment, Deployer will tell you about it fairly clearly. If everything went fine, you should have a new directory structure in the application directory on the server:
$ ls -l
celkem 8
lrwxrwxrwx 1 id-sign id-sign 10 čec 9 13:15 current -> releases/1
drwxr-xr-x 8 id-sign id-sign 4096 čec 9 13:17 releases
drwxr-xr-x 5 id-sign id-sign 4096 lis 18 2020 shared
Adjust your web server configuration so that the document root points to the public directory inside the current directory. If anything above is unclear, or if you are interested in all the configuration options, I recommend going through the Deployer documentation
GitLab CI
We can also use GitLab for deployment, so that new versions can be deployed directly from its web interface. This comes in handy if part of your job is, say, overseeing the merging of various development branches into the main branch and releasing it afterwards. Of course, all of this can be set up to run automatically after tests, linting, etc. pass successfully. For simplicity, though, this example configures a manual release, which you can then easily adapt to your needs.
As a first step, we add a .gitlab-ci.yml file with the following content to the repository (adjust the server hostname and PHP version as needed):
stages:
- deploy
deploy:
image: chialab/php:8.0
stage: deploy
script:
- eval "$(ssh-agent -s)"
- ssh-add <(echo "$DEPLOY_KEY" | openssl base64 -A -d)
- mkdir ~/.ssh && ssh-keyscan -H your.server.com >> ~/.ssh/known_hosts
- php ./deployer.phar deploy production --branch $CI_COMMIT_REF_NAME -vv
when: manual
Next, in GitLab under the repository settings Settings -> CI/CD, we create a masked variable DEPLOY_KEY in which we store the base64 string of the private SSH key we will use to log in to the server. For security reasons, we do not want the private key for logging in to the server to be part of the repository. You can easily get the base64 string either from an online service or with the base64 command on Linux. To be able to mask the variable in GitLab, its value must be on a single line – the -w 0 parameter turns off line wrapping of the generated text:
$ base64 -w 0 private.key > private64
Copy the contents of the resulting file and save it in the DEPLOY_KEY variable mentioned above.
And that is basically it. You should now see an option to run the CI deploy job in GitLab, and if you have CI pipelines available for the repository, everything should run smoothly. This gives you the ability to deploy, for example, right after merging changes from various branches into the main branch, without having to open the command line and log in to the server.