Deploying a Symfony application with Deployer
Tags: symfony, devops, php
Published: 2021-04-30 11:43:40

# Introduction
Deploying the latest updates to a live project can be daunting. But with Deployer it's easy! You specify a **recipe** for your project and let Deployer handle the rest. An added bonus is that it becomes easy to return to an old project and make important changes and know that you won't have to revisit how this particular project needs to be deployed. Your recipe already specifies that!

In this article we will take a look at how we can use Deployer to launch a Symfony application to a shared hosting service. If you are new to SSH and Deployer, I will try to make it as easy as possible to follow along. Feel free to ask questions, or suggestions for improvement!

*This is a synthesized version of two of my blog posts from my website:*
* [*Logging on to shared hosting with SSH*](https://andersbjorkland.online/blog/2021/04/21/Logging%2520on%2520to%2520shared%2520hosting%2520with%2520SSH)
* [*Deploy a Symfony application with Deployer*](https://andersbjorkland.online/blog/2021/04/28/Deploy%2520a%2520Symfony%2520application%2520with%2520Deployer)

## Assumptions and Recommendations
This article assumes a **Windows 10** environment. You may follow most of the instructions detailed here if you're on Mac or Linux but you may have different commands or experiences when using a terminal or the Deployer tool.

I recommend **Git Bash** for following along with this tutorial. It comes with some basic tools that will be useful for the rest of this article: ssh, ssh-keygen, git, nano (or vim if you prefer that).

The shared hosting I will be connecting to is **one.com**. It has its limitations, as many shared hosting services do, but most important is that it supports SSH.

# Preparations
## SSH
Secure Shell (SSH) is a way to securely access a container or a server. The connection can be secured with a password or using a private key and a public key. In the latter case you will hold the private key on your local computer while having the public key on the remote server. This way, no one but someone with the private key can access the remote server. And as long as you keep your private key for yourself, no one but you is granted access.

*Note that we will be using either* example *or* satius *for inputs. You can switch these out for your domain name.*

### Get SSH credentials
*This is specifically for* **one.com** *, but the process of getting an SSH inlog may be similar with your hosting service.*

**Acquire SSH username and password in 5 steps**
1. Log in to your account and select the domain you want to add an SSH to.

2. Select the item 'SSH & FTP' under 'Advanced Settings'.
![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2p3teo0hel96505zuzkv.png)
 
3. Activate 'Allow SSH & SFTP access' and click 'SEND'. Take note of host (ssh.example.com) and username (example.com)
![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6lc15b281vema8qcs77.png)

4. You should have received an email from one.com to the address you registered with them. Look for an email titled 'Change SSH/SFTP password' or similar. The email will contain a link ro reset password, click it.
![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lf1iv5xvspnx84au8t6u.png)

5. Make a secure password and retype it. After this step you will have the password and username you need to log onto your webspace.
![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rk31j943vx604mg7heej.png)

**Logging in with username and password** 
You can now log on with SSH using Git Bash. This is the way to do it: `ssh example.com@ssh.example.com`
This will prompt for your password. No output will show as you are entering your password so don't be too thrown off by it. When you've entered the correct password you will be granted access!
![ssh-p-u](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wjoha4sfrw9ragogef85.gif)
 
### Logging in with SSH key
Sometimes it can be tedious to rely on entering host address, username and password. And when you are using deploy tools it can be entirely cumbersome. So let's set up a public and private key to help us along. We will continue to be using Git Bash for Windows in this section.

**Generating public and private keys**
We have access to the tool **ssh-keygen** in Git Bash. We will be using it, but before we do. Let's navigate to *~/.ssh*. This is the *.ssh*-directory in your home catalog. If you don't have this directory you can create it with `mkdir ~/.ssh`.

In the directory *~/.ssh*, run ssh-keygen. This will start the key-generation. It will prompt you for a name for the keys. Either enter one or accept the default (*id_rsa*). Then it will prompt you for a passphrase if you would like to add one to the keys. If not, it's just as simple as pressing enter twice.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m8vy0h9gtrshf27la0z2.png)

If you check what's in the directory now, you will see that two files were added. One private key (default *id_rsa*) and one public key (default *id_rsa.pub*). 

Next we are going to add the public key (*id_rsa.pub*) to the remote server. First we will copy the contents of the key. Run `cat id_rsa.pub`. Copy the text string. In Git Bash for Windows you can do this by pressing *ctrl*+*ins*.
![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/la7qlg8e67b4r1jg1g7i.png)

Let's login to the remote server: `ssh example.com@ssh.example.com`

You will be prompted for your password, so enter it. Remember that no output will be generated as you type. Upon correct password you will be logged on.

We will now create a way to access this webspace with the key we generated. First we create a directory: `mkdir ~/.ssh`

This is a hidden directory, so it will only show up when you run `ls -a`

Move into this newly created directory: `cd .ssh`

Now we will create a file to store the public key we generated: `touch authorized_keys`

Open the file with a text editor. Nano is quite simple: `nano authorized_keys`

Paste the contents of the public key into the file by pressing *shift*+*ins*, provided you did copy the public key into your clipboard. Press *ctrl*+*x* to exit Nano. It will prompt you for saving a buffer, press *y* and then press enter again to save it with the same filename (*authorized_keys*). 

You can now exit the connection to the remote server by typing exit.

Next up you are going to configure a SSH connection to the remote server so we don't have to type host, username, and password each time. Create a config file by running the following command: `touch ~/.ssh/config`

Then let's add some info into the config file. Open it with `nano ~/.ssh/config` and type in the following:
```
Host example
    HostName ssh.example.com
    User example.com
    IdentityFile /c/Users/example/.ssh/id_rsa
```

Exit the editor by pressing *ctrl*+*x*. Save the buffer when prompted to the same filename as before (*config*). 

You can now logon by typing: `ssh example`. This will check the config file and look for the host 'example'. It will then use the private key to connect to the host.

## Installing Deployer
You can install Deployer with **Composer**, either globally or specifically for each project. When using globally I have encountered problem running some recipes requiring access to remote server’s environment variables, so locally is what I’ve come to prefer. Navigate to your Symfony application and run:

`composer require deployer/deployer --dev`

Installing Deployer locally for your project means that you will run it from your terminal or console with this syntax `php vendor/bin/dep init`.

If you are on Windows, Deployer may complain about how paths are translated. If that’s the case, you can use this syntax instead:

`php vendor/deployer/deployer/bin/dep init`

**Initialize a Git repository**
Deployer keeps track of changes with the help of Git. For this reason, make sure you have initialized a git repository and tracking it via GitHub or similar service. Read more about it on Github Docs: [Adding an existing project to GitHub using the command line - GitHub Docs](https://docs.github.com/en/github/importing-your-projects-to-github/adding-an-existing-project-to-github-using-the-command-line).

## Set up a recipe
A recipe is a set of instructions for Deployer on how to connect to a remote server and deploy your project. Your specific recipe will depend on:
* Your project’s specifications.
* Your development environment’s specifications.
* The remote server’s specifications.

While many instructions will be similar between the same type of project (Symfony applications for instance), you will have to tweak your recipe depending on how you installed Deployer and the configuration of the remote server. Keeping this in mind, this is what I've been working with:
**Local setup**:
* PHP 7.4
* Symfony 5 application
* Windows (but Mac has similar commands)
* Git Bash (useful for setting up SSH agent)
* Deployer as a local dependency
**The remote server**:
* PHP 7.4
* Ubuntu 20
* The public directory is at example.com/httpd.www
* The private directory is at example.com/httpd.private [or ~, user home directory] 

**Initialize recipe**
Now on to starting a new recipe. We let Deployer generate the defaults for us:
`php vendor/deployer/deployerbin/dep init`

During the initialization process you will be prompted to select what type of application you are deploying. Select **Symfony**. Then you will be prompted to type in the URL for your git repository. Deployer will detect the URL for you if your project is already connected to a remote repository. Upon completion you will have a new file in the root of your project titled *deploy.php*. 

**Configure recipe**
You will now have a recipe with the most common configurations for deploying a Symfony application set up for you. As of Deployer v.6 it will assume a recipe suitable for a Symfony 3 application. It comes bundled with a recipe for Symfony 4 so we will require that recipe instead. What this actually means is that Deployer will look for Symfony CLI tools in bin/console instead of app/console. **Open** *deploy.php* and make it require symfony4.php instead of symfony.php:
`require 'recipe/symfony4.php';`

Next up you will set your deployment project name, then set tty and multiplexing to false. Also, the Symfony 4 recipe assumes some soone to be deprecated options for Composer, so we will override it with some updated ones:
 ```
set('application', 'homepage');
set('git_tty', false);
set('ssh_multiplexing', false);
set('composer_options', '{{composer_action}} --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader');
```

Having set these Deployer variables, we will move on to the most important option: what host to deploy to. If you have configured a SSH connection in a .ssh/config file, you can reference that connection by assigning that name to the host option:

``` 
// Hosts
host(example)
    ->set('deploy_path', '~/{{application}}')
    ->set('http_user', example.com);
```

You can see that we also set *deploy_path* and *http_user*. I’ll be using a shared hosting service like one.com where the http_user will be the same as your domain name.

Having a shared hosting puts some constraints on what you will be able to do. But something you will have to contend with is that you don’t directly dictate what directory should be the public one, as the hosting service will have it specified for you. On one.com the public directory is */www*, which also can be referred to as **example.com/httpd.www**. But your project will have its public directory at ~/example/current/public, which also can be referred to as **example.com/httpd.private/example/current/public**. 

There are two strategies to make your public content accessible. The first strategy is to use symbolic links between the two directories. Alas, I have not been able to make this work on one.com. The second strategy is to copy the content of the project’s public directory to the server’s public directory. The second strategy will also mean that you will have to make some configurations to your *index.php* file, and eventually some other configuration if you are going to be uploading files for public consumption. Add either of these tasks depending on your strategy:

```
task('symlink:public', function() {    
    run('ln -s {{release_path}}/public/*  /www);
});

task('copy:public', function() {    
    run('cp -R {{release_path}}/public/*  /www && cp -R {{release_path}}/public/.[^.]* /www');
});
```

Now we can set up some strategies. I will be setting up two different strategies: one for setting up the basic deployment structure for the remote server, which I will call initialize, and another for the full deployment that I call mydeploy. The second strategy is similar to the deploy strategy in the symfony4.php recipe except that it doesn’t include the *deploy:writable task*, as it conflicts with the remote server configuration that I’ve been working with.

**initialize**
```
task('initialize', [        
    'deploy:info',        
    'deploy:prepare',        
    'deploy:lock',        
    'deploy:release',        
    'deploy:update_code',        
    'deploy:shared',        
    'deploy:unlock',        
    'cleanup',
]);
```

**mydeploy**
```
task('mydeploy', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'deploy:update_code',
    'deploy:shared',
    'deploy:vendors',
    'deploy:cache:clear',
    'deploy:cache:warmup',
    'deploy:symlink',
    'copy:public',
    'deploy:unlock',
    'cleanup',
]);
```

The full recipe would look like this:
```php
<?php
namespace Deployer;

require 'recipe/symfony4.php';

/*
 * Run either 'deploy' (Symfony 4 apps) or 'mydeploy' (adjusted for shared host one.com).
 * If running deployer as a project dependency on Windows you may need to run this:
 * php vendor/deployer/deployer/bin/dep deploy
 * instead of php vendor/bin/dep deploy
 */

// Project name
set('application', 'homepage');

// Project repository
set('repository', 'https://github.com/andersbjorkland/project-online');

// [Optional] Allocate tty for git clone. Default value is false.
set('git_tty', false);
set('ssh_multiplexing', false);

set('composer_options', '{{composer_action}} --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader');


// Shared files/dirs between deploys 
add('shared_files', []);
add('shared_dirs', []);

// Writable dirs by web server 
add('writable_dirs', []);
set('allow_anonymous_stats', false);

// Hosts
host('anders')
    ->set('deploy_path', '~/{{application}}')
    ->set('http_user', 'andersbjorkland.online')
;
    
// Tasks
task('symlink:public', function() {
    run('ln -s {{release_path}}/public/*  /www &&  ln -s {{release_path}}/public/.[^.]* /www');
});

task('cache:clear', function () {
    run('php {{release_path}}/bin/console cache:clear');
});

/* Is used when symlink from public folder doesn't behave as expected.
 * The downside of using it this way is that it doesn't remove files no longer present in git repo.
 * Assumed public directory is /www
 */
task('copy:public', function() {
    run('cp -R {{release_path}}/public/*  /www && cp -R {{release_path}}/public/.[^.]* /www');
});

/* Uploads built assets from local to remote. Requires rsync.
 * Useful when you use Symfony encore/webpack and remote machine doesn't support npm/yarn.
 */
task('upload:build', function() {
    upload("public/build/", '{{release_path}}/public/build/');
});

task('upload:build', function() {
    upload("public/build/", '{{release_path}}/public/build/');
});

task('init:database', function() {
    run('{{bin/php}} {{bin/console}} doctrine:schema:create');
});

task('echo:options', function() {
    writeln('OPTIONS: {{composer_options}}');
});

task('build', function () {
    run('cd {{release_path}} && build');
});

task('initialize', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'deploy:update_code',
    'deploy:shared',
    'deploy:unlock',
    'cleanup',
]);

task('mydeploy', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'deploy:update_code',
    'deploy:shared',
    'deploy:vendors',
    'deploy:cache:clear',
    'deploy:cache:warmup',
    'deploy:symlink',
    'copy:public',
    'deploy:unlock',
    'cleanup',
]);

// [Optional] if deploy fails automatically unlock.
after('deploy:failed', 'deploy:unlock');
//after('deploy:unlock', 'copy:public');


// Migrate database before symlink new release.
before('deploy:symlink', 'database:migrate');
```

# Deployment
Having configured your SSH connection and your recipe, you can now get ready to deploy. Let’s first configure the deployment structure on the remote server:
`php vendor/deployer/deployer/bin/dep initialize`

When the tasks are done, SSH into the remote server and configure your *.env* files under *~/example/shared* with correct credentials for database and SMTP. Once that is done, you are ready for a full deployment strategy which you can reuse whenever you want to update your application. So exit the connection to the remote server and run the following command:
`php vendor/deployer/deployer/bin/dep mydeploy`

*Running this on one.com can take about 2 minutes. For larger Symfony applications I’ve encountered errors when Deployer is either clearing or warming up the cache. This is probably because the memory is running low. One.com specifies PHP to use 128MB RAM and I've not been able to adjust that. You can resort to manually running the cache:clear command, as I’ve been able to do when it first might have failed.*

Your application should now be up and running. If you have encountered any problems, feel free to contact me at Twitter: [@abjorkland](https://twitter.com/abjorkland) or comment below.