Tutorial

Deploy PHP Project

Have you event wanted to build your own solution to deploy any PHP project just like Deployer or Envoyer do? Let's make it with Storm!
If you have not a node up and running yet, follow this guide first.

How are you deploying simultaneously on multiple servers?

There is not a single way, but for the purpose of this tutorial, we try to remain as general as possible following these steps:

  1. GIT archive a specific version of the repository
  2. TAR extract the archive into a new directory
  3. Symlink some existing files or directories
  4. composer install
  5. Wait all servers
  6. Activate the new deployment
  7. Optionally, a composer command post-activation
  8. Optionally, cleanup older deployments
Analyse these steps in more detail.

GIT archive and TAR extract

We want to download an archive of our remote repository, of a specific commit, tag, branch or, in general, a tree-ish.
Through the file .gitattributes we can exclude unnecessary files and directory from the export, reducing download time and improving safety.
For example to exclude a tests directory:

  # .gitattributes
tests/ export-ignore

Is it also useful to exclude some file or directory from the extraction, if we have it into the repository but is not meant to be replaced on production servers. Like a storage directory, for example.

Take a look at complete shell commands with the same result:

  [email protected]:org/repo.git
GIT_ARCHIVE_TREEISH=production
mkdir new_deploy && cd new_deploy
git archive --remote "$REPOSITORY_URL" --worktree-attributes "$GIT_ARCHIVE_TREEISH" | tar -x --exclude=storage

After this commands you should have a fresh copy of the production branch into the new_deploy directory.
Just check if you have the necessary permissions to download the archive from your provider, usually you have to add the SSH key of the host for a read access.
Make sure there are not files or directory excluded from your .gitattributes file, if any, nor those excluded via the TAR command.

Symlink project files

Often we have an .env file, or a storage directory, that are stored on the server and is not meant to be replaced by the deploy but directly used instead.
To allow this kind of behaviour we need to specify a base project path, where those files are stored, and a list of paths to symlink, making them available to our new fresh deploy.

For example:

  PROJECT_PATH=/opt/phpproject
ln -s "$PROJECT_PATH"/.env new_deploy/.env
ln -s "$PROJECT_PATH"/storage new_deploy/storage

Composer install and wait all servers

Now that we have all files in place, we have to make the directory able to serve future requests. All dependencies should be installed and any other task required.

  COMPOSER_PATH=composer
COMPOSER_COMMAND=install
$COMPOSER_PATH "$COMPOSER_COMMAND" --no-ansi --no-interaction --no-plugins --no-dev --no-progress --no-suggest

You can personalize the composer options or the executed command through the composer.json, for example:

  # composer.json
{
...
  "scripts": {
    "deploy": [
      "@composer  install --no-dev",
      "phpunit"
    ]
  },
  "scripts-descriptions": {
    "deploy": "Prepare deployment directory and run all tests"
  },
  "config": {
    "optimize-autoloader": true,
    "preferred-install": "dist",
    "sort-packages": true
  }
}

and then set COMPOSER_COMMAND=deploy .

At this stage, the new_deploy directory contains a workin copy of our application, but is not serving real requests yet, because is not where the web/fpm server is serving those.

Also, you can have multiple servers at this stage, and all need to synchronize for the real activation. We will see how to ensure that with a Storm Workflow.

Activate the new deployment

A common configuration between WebServer and a PHP project, is to point the WebServer virtual host root directory to something like /opt/phpproject_prod/public.
/opt/phpproject_prod can be a symlink to a real directory containing a working copy of the application.
To activate a fresh deployment we will move the symlink to the new directory, for clarification:

  CURRENT_SYMLINK_PATH=/opt/phpproject_prod
ln -s new_deploy "$CURRENT_SYMLINK_PATH".tmp
mv "$CURRENT_SYMLINK_PATH".tmp "$CURRENT_SYMLINK_PATH"

Now the new code is running!

Composer command post-activation

Usually some post-deployment tasks are required to really make the new code working well. I refer for example at an artisan queue:restart or a sudo service php7.4-fpm reload.

To make these steps documented and all in one place, we can create a custom command into composer.json as well:

  # composer.json
{
...
  "scripts": {
    ...
    "postdeploy": [
      "sudo service php7.4-fpm reload",
      "artisan queue:restart"
    ]
  }
}

Using it as follow:

  COMPOSER_PATH=composer
COMPOSER_POST_ACTIVATION_COMMAND=postdeploy
$COMPOSER_PATH "$COMPOSER_POST_ACTIVATION_COMMAND" --no-ansi --no-interaction --no-plugins

Make sure that each command is executable by the current user. To restart fpm-server without root you need to grant the user via sudo.

Use Storm nodes to deploy

Obviously there is a plugin to execute all this flow already, you can find it on Github.
Let's see how to configure it on Storm and how to create a complete Workflow.

Plugin and commands

To proceed, install the cway-storm-php-deployer plugin and create a new plugin command for each server that needs to receive the deployments. All nodes should be in the same workspace. Follow the instructions here to set arguments to fit your needs, for example:

PROJECT_PATH
/opt/phpproject
REPOSITORY_URL
[email protected]:org/repo.git
GIT_ARCHIVE_TREEISH
production
CURRENT_SYMLINK_PATH
/opt/phpproject_prod
DEPLOYMENTS_DIRECTORY
/opt/prod_deployments
TAR_EXTRACT_EXCLUDE_PATHS
storage
PROJECT_MANAGED_SYMLINKS
.env,storage

Workflow

Create a new Workflow on the same workspace as nodes, with the following elements:

  1. A Button to start the workflow
  2. Connect the Button Action, to Commands Action's. Each is the previously created plugin command, one per node.
  3. Connect each Command Output to a Decision element, to check if $.DEPLOY_PHASE is equal to the string "ACTIVATE"
  4. Connect each Decision outputs to a Logical Operator in AND
  5. If false, stop, some error occurred while installing
  6. If true, continue the workflow. The "true" output must be connected with a new series of plugin commands, like in step 2. Connect the output of the first series to these one's arguments input.
  7. Through new Decision elements, check if $.DEPLOY_PHASE is equal to the string "ACTIVATED"
  8. Connect each Decision outputs to a Logical Operator in AND
  9. If false, stop, some error occurred while activating
  10. If true, your deploy is completed!

A full picture of the previous workflow:

Conclusion

We saw how to deploy a PHP project in the most general way.

With Storm and the power of fully customizable plugin commands and workflows, you can build almost any automation, able to perform any task you want, without giving external services access to your servers.

Thank You for reading!