Modern PHP, soft skills, productivity and time management.

[Edit] How to deploy Symfony app with Capistrano 3 on cheap OVH VPS


How to deploy Symfony app with Capistrano 3 on cheap OVH VPS

tl;dr

If you want to deploy Symfony app using Capistrano 3 to cheap OVH VPS then you’ll need to write a simple task to set proper permissions.


I have a very cheap VPS in OVH, it costs only about $2 per month. It is perfect for my needs, I have my blog there, I have my friend’s blog as well. And now I want to move my wife’s portfolio from (even cheaper :P) shared hosting to this VPS.

Until now deploy to shared hosting looked like this:

  1. open FTP
  2. go to portfolio directory
  3. move changed files
  4. app/console cache:clear

Nightmare.

Fortunately, I had access to SSH on my hosting (not popular thing) so I could try to automate this process a little bit. But because I already have VPS then the better idea was to move the portfolio there and use some tool for deployment.

The portfolio is written using Symfony 2 framework so the best idea is to use Capistrano. I had some experience with Capifony which is pretty old, unmaintained customization for Capistrano 2 (which is also quite old). I don’t want to use old stuff. So the goal is easy: deploy Symfony 2 app using Capistrano 3 to OVH, cheap VPS.

Configuration

First of all, you’ll need to install Capistrano on your local machine. I used official instruction, a good start will be Quick Start. Then you’ll need to add Symfony tasks to Capistrano (you can write tasks yourself, but it’ll be reinventing the wheel): https://github.com/capistrano/symfony

And at last, you have to configure it. I used a pretty good description from this post. In case it’ll disappear here are my configs:

// production.rb
#######################
# Setup Server
########################
server "YOUR SERVER HOST", user: "YOUR USER", roles: %w{web}
set :deploy_to, "PATH TO DEPLOY ON YOUR SERVER"

#########################
# Capistrano Symfony
#########################
set :file_permissions_users, ['www-data']
set :webserver_user, "www-data"

#########################
# Setup Git
#########################
set :branch, "master"
// deploy.rb
# config valid only for current version of Capistrano
lock "3.9.1"

set :application, "APP NAME"
set :repo_url, "REPO URL"

# Symfony console commands will use this environment for execution
set :symfony_env, "prod"

# Set this to 2 for the old directory structure
set :symfony_directory_structure, 2
# Set this to 4 if using the older SensioDistributionBundle
set :sensio_distribution_version, 4

# symfony-standard edition directories
set :app_path, "app"
set :web_path, "web"

# The next 3 settings are lazily evaluated from the above values, so take care
# when modifying them
set :app_config_path, "app/config"
set :log_path, "app/logs"
set :cache_path, "app/cache"

set :symfony_console_path, "app/console"
set :symfony_console_flags, "--no-debug"

# Remove app_dev.php during deployment, other files in web/ can be specified here
set :controllers_to_clear, ["app_*.php"]

# asset management
set :assets_install_path, "web"
set :assets_install_flags, '--symlink'

# Share files/directories between releases
set :linked_files, ["app/config/parameters.yml"]
set :linked_dirs, ["app/logs", "web/uploads"]

# Composer
set :composer_install_flags, '--no-interaction --optimize-autoloader'

# Default value for keep_releases is 5
set :keep_releases, 3

Having this I tried to deploy my application using `cap production deploy` command. The deploy went successfully, but I get HTTP ERROR 500 when I went to project URL…

Main oponent

One of the problems, maybe the biggest, is changing permissions to `/logs` and `/cache` directories. Deploy is performed using one user, but the HTTP server works using a different one, often named `www-data`. So after deploying all permissions are granted for deploying user and, because `/cache` directory is not writable for `www-data`, the website is simply not working (you can see the error in `/logs/prod.log` file, in the web browser you’ll see only `HTTP ERROR 500`).

There are 3 most used methods to change permissions:

  1. `ACL`
  2. `CHOWN`
  3. `CHMOD`

ACL simply allows you to specify different permissions for many users, so changing them using `chown` is not needed. Unfortunately, it’s not available on the cheapest OVH VPS 🙁 (I think it is available on the cheapest VPS in DigitalOcean, but it’s $5 ?)

So, I tried `chown` – that means changing the owner of `/cache` directory to `www-data`. It worked, the website was online, but during next deploy, when old releases are deleted there was permission denied error while deleting `/cache` directory. Which is obviously correct, deploying user is not an owner of it.

Custom task to the rescue!

So, at last, I tried `chmod` – setting permissions to `777` allows anyone to write to `/cache` and `/logs` directory and changing ownership is not needed. Perfect, isn’t it?

No ?

`chmod` needs root permissions to be executed. I tried built-in methods in Capistrano to use `chmod` after deploy, but I failed to make it works. Fortunately, you can write your own tasks, so did I.

The concept is easy: after deploying change permissions to `777` using `sudo` and a previously typed password. There is a way to make it works without a password, but granting permissions to call `chmod` without password looks a little dangerous. And typing password during deploy seemed to me like a good compromise.

My task looks like this:

# Chmod on app/cache and app/logs dirs
namespace :deploy do
    desc "Chmod on app/cache and app/logs dirs"
    task :cache_logs_chmod do
        on roles(:web) do
            ask(:password, nil, echo: false)

            execute "echo #{fetch(:password)} | sudo -S chmod 777 -R #{current_path}/app/logs"
            execute "echo #{fetch(:password)} | sudo -S chmod 777 -R #{current_path}/app/cache"
        end
    end
end

The main part is just a 3 lines:

  1. asking for a password with `ask` command
  2. executing `chmod` on `/cache` directory
  3. executing `chmod` on `/logs` directory

The interesting part is setting up permissions. I used `sudo` with `-S` option which allowed me to use previously typed password without user interaction. Using just `sudo` will not work, deploy will simply stick on this. So, with `-S` option it uses a password from the standard input, in this example from echoing it. And to use password typed in `ask` command I used `#{fetch(:password)}`:

  • `:password` is just a variable name in which the password is stored
  • `fetch()` fetches a value from the variable
  • `#{}` displays the value

To use this task you should tell Capistrano to call it after deploy: `after :deploy, “deploy:cache_logs_chmod”`.

The whole `deploy.rb` file looks like this:

# config valid only for current version of Capistrano
lock "3.9.1"

set :application, "aswitalska.art.pl"
set :repo_url, "[email protected]:zelazowy/smartfolio.git"

# Symfony console commands will use this environment for execution
set :symfony_env,  "prod"

# Set this to 2 for the old directory structure
set :symfony_directory_structure, 2
# Set this to 4 if using the older SensioDistributionBundle
set :sensio_distribution_version, 4

# symfony-standard edition directories
set :app_path, "app"
set :web_path, "web"

# The next 3 settings are lazily evaluated from the above values, so take care
# when modifying them
set :app_config_path, "app/config"
set :log_path, "app/logs"
set :cache_path, "app/cache"

set :symfony_console_path, "app/console"
set :symfony_console_flags, "--no-debug"

# Remove app_dev.php during deployment, other files in web/ can be specified here
set :controllers_to_clear, ["app_*.php"]

# asset management
set :assets_install_path, "web"
set :assets_install_flags,  '--symlink'

# Share files/directories between releases
set :linked_files, ["app/config/parameters.yml"]
set :linked_dirs, ["app/logs", "web/uploads"]

# Composer
set :composer_install_flags, '--no-interaction --optimize-autoloader'

after :deploy, "deploy:cache_logs_chmod"

# Default value for keep_releases is 5
set :keep_releases, 3

# Chmod on app/cache and app/logs dirs
namespace :deploy do
    desc "Chmod on app/cache and app/logs dirs"
    task :cache_logs_chmod do
        on roles(:web) do
            ask(:password, nil, echo: false)
            # -S allows to pass password from standard input
            execute "echo #{fetch(:password)} | sudo -S chmod 777 -R #{current_path}/app/logs"
            execute "echo #{fetch(:password)} | sudo -S chmod 777 -R #{current_path}/app/cache"
        end
    end
end

Summary

And that’s it. Deploy works, the website works and the only drawback is typing password during deployment. Not a high price for using a very good tool on the very cheap VPS 😉

I couldn’t find a way to do it in a different way so I made it myself and I hope it’ll be useful for you. If I’m wrong and my way to deal with permissions is wrong – let me know, I’ll be very happy to make it works better 🙂

After some deploys…

Unfortunately, this isn’t working properly ? Using different users for deployment and web server without ACL is hard. One user creates directories, warmups cache and so on, and the other makes additional files, caches and so on.

Setting `chmod 777` to `/cache` and `/logs` directories allows `www-data` user to write and read from them, but any file created by it has new permissions which prevent deleting directory during rotating releases in deploy.

To better visualization let’s say, that I have user `deploy` for deployment and `www` for a web server.

  1. `deploy` creates `/cache` directory and set permissions to 777, but he is still its owner
  2. `www` after some requests creates new files in `/cache` which have different permissions AND their owner is `www` user
  3. during deployment `deploy` user tries to remove old releases and it couldn’t, because some files are not his and deploy stops.

Temporary solution

For now, I make deploy semi-automatically. Before deploying I simply delete old release from server (it needs `sudo`) and because there are fewer releases than the limit in `deploy.rb` deployments work fine.

Final solution

I want to try it on DigitalOcean which hasn’t problems with ACL management. I’ll do it in a couple of months when my OVH VPS expire ?


Want more tech meat?

3 Comments

  1. olidev

    You could have used Envoyer deploy the symfony app much quicker. Envoyer is a Laravel product, but it can be used with any PHP app. Apart from quick deployment, there is also benefit of zero downtime deployment. This means if you use Envoyer with push a branch from git, the new changes will be updated on your website without any downtime. Source: https://www.cloudways.com/blog/automate-symfony-deployment-through-envoyer/

    • krzych

      Thanks, I didn’t considered any other option than Capistrano, maybe I should 😉 I’ll take a look.

  2. gunny

    What about symfony 4 😉 ?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2020 Krzych Jończyk

Theme by Anders NorenUp ↑