PHP
Article
By Viraj Khatavkar

How to Properly Deploy Web Apps via SFTP with Git

By Viraj Khatavkar

This article was peer reviewed by Haydar KÜLEKCİ and Wern Ancheta. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


Uploading files is an integral aspect of any deployment process, and the underlying implementation can vary depending on the type of your server.

You can easily upload your files to an SFTP server using an open source desktop client like Filezilla. Those who have used this are aware that this process is cumbersome and irritating as it doesn’t let us automate our deployment process, and we always need to upload the whole project, even if we have modified only a part of the files of our codebase.

Abstract image symbolizing upload - a cloud shape made from glowing teal pixels, and in front of it, or inside it, an arrow pointing upwards

The PHPSECLIB (PHP Secure Communications Library) package has an awesome API for routine SFTP tasks: it uses some optional PHP extensions if they’re available, and falls back on an internal PHP implementation otherwise. You don’t need any additional PHP extension to use this package, the default extensions that are packaged with PHP will do. In this article, we will first cover various features of PHPSECLIB – SFTP, including but not limited to uploading or deleting files. Then, we will take a look at how we can use Git in combination with this library to automate our SFTP deployment process.

PhpSecLib Installation

composer require phpseclib/phpseclib

This will install the most recent stable version of the library via Composer.

Authentication

By default, password authentication is used to connect to your SFTP server. A cryptographic key-pair is more secure because a private key takes the place of a password, which is generally much more difficult to brute-force. Using phpseclib, you can connect to your SFTP server with any of the following authentication methods:

  1. RSA key
  2. Password Protected RSA key
  3. Username and Password (Not recommended)

RSA Key

We will assume that you have a secure RSA key already generated. If you are not familiar with generating a secure RSA key pair, you can go through this article. For a video explanation, you can refer to Creating and Using SSH Keys from Servers For Hackers.

To log in to a remote server using RSA key authentication:

namespace App;

use phpseclib\Crypt\RSA;
use phpseclib\Net\SFTP;

$key = new RSA();
$key->loadKey(file_get_contents('privatekey'));

//Remote server's ip address or hostname
$sftp = new SFTP('192.168.0.1');

if (!$sftp->login('username', $key)) {
    exit('Login Failed');
}

Password Protected RSA Key

If your RSA keys are password protected, do not worry. PHPSECLIB takes care of this particular use case:

namespace App;

use phpseclib\Crypt\RSA;
use phpseclib\Net\SFTP;

$key = new RSA();
$key->setPassword('your-secure-password');
$key->loadKey(file_get_contents('privatekey'));

//Remote server's ip address or hostname
$sftp = new SFTP('192.168.0.1');

if (!$sftp->login('username', $key)) {
    exit('Login Failed');
}

Username and Password

Alternatively, to log in to your remote server using a username and password (we don’t recommend this practice):

namespace App;

use phpseclib\Net\SFTP;

//Remote server's ip address or hostname
$sftp = new SFTP('192.168.0.1');

if (!$sftp->login('username', 'password')) {
    exit('Login Failed');
}

For other options such as No Authentication or Multi-Factor authentication please refer to the documentation.

Uploading and Deleting Files

A large part of the deployment process includes uploading files to a server. Uploading files essentially means transferring the contents of a local file to a remote file. The example below creates an index.php file on the server with the contents This is a dummy file:

namespace App;

use phpseclib\Crypt\RSA;
use phpseclib\Net\SFTP;

$key = new RSA();
$key->loadKey(file_get_contents('privatekey'));

//Remote server's ip address or hostname
$sftp = new SFTP('192.168.0.1');

if (!$sftp->login('username', $key)) {
    exit('Login Failed');
}

$sftp->put('index.php', 'This is a dummy file');

By default, put does not read from the local filesystem. The contents are dumped straight into the remote file. In a real world scenario, you need to fetch the contents of local files and dump them into the remote file:

$contents = file_get_content('path/to/local/file');
$sftp->put('index.php', $contents);

During our deployment process, we need to add one more step of deleting files and directories which are no longer required by the application. You can easily delete a single file or recursively delete all the files and directories from a specific directory.

//Deleting a single file. This does not delete directories.
$sftp->delete('index.php');

//Deleting directories and files recursively
$sftp->delete('dir_name', true);

Automating Deployment with Git

Git is widely accepted as the industry standard of versioning tools. Using the power of Git, we can save some time and bandwidth by uploading only those files that changed since the last upload. Using the SFTP API of PHPSECLIB, we will try to deploy our files to our SFTP server.

I have divided the whole deployment process into a few steps below to better explain each. Once we go through all the steps, we will bring it all together.

Some Git basics

First, we need to understand some specific Git commands which will assist us in our deployment process. We will create a class which will help us execute Git related tasks. Git commands are executed from the command line – we will use the Process component for that. The component provides an abstraction layer for executing shell commands and takes care of the subtle differences between the different platforms. You can refer to its documentation for more information. For now, we will write code specific to fetching the files added, modified or deleted between two given commits.

<?php

namespace App;

use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

class Git 
{
    public function diffFiles($start_commit, $end_commit)
        {
        //Get new and modified files
        $process = new Process("git diff --name-only --diff-filter=AM $start_commit $end_commit");

        $process->setTimeout(3600);
        $process->setWorkingDirectory('/path/to/local/repository');
        $process->run();

        if (!$process->isSuccessful()) {
            throw new ProcessFailedException($process);
        }

    //Extract all file paths from the process output
        $files["added"] = array_unique(
        array_merge($files["added"], 
        array_filter(explode("\n", $process->getOutput()), 'strlen'))
    );

    //Get deleted files
        $process = new Process("git diff-tree --diff-filter=D --name-only -t $start_commit $end_commit");

        $process->setTimeout(3600);
        $process->setWorkingDirectory('/path/to/local/repository');
        $process->run();

        if (!$process->isSuccessful()) {
            throw new ProcessFailedException($process);
        }

        $files["deleted"] = array_filter(explode("\n", $process->getOutput()), 'strlen');

        return $files;
    }
}

Get contents of a file at a specific commit

Git stores the historical data regarding a given repository in the hidden .git folder. To move forward with our deployment process, we need to fetch the contents of files recorded by Git in the specified commit. We will add another method to the Git class:

public function getContent($commit, $file)
{
    //Get contents of a file at a specific commit
    $process = new Process("git show \"$commit:$file\"");

    $process->setTimeout(3600);
    $process->setWorkingDirectory('path/to/repository');
    $process->run();

    if (!$process->isSuccessful()) {
        throw new ProcessFailedException($process);
    }

    return $process->getOutput();
}

Deploying files

Next, we need to write a piece of code which will take care of executing the deployment process. Let us first list the important steps we need to take care of:

  1. Fetch added, modified, and deleted files between the two commits
  2. Transfer new and modified files to SFTP server
  3. Delete the files which were removed between the two commits

Now that we are clear on what we need to achieve, let’s put that into code:

$files = $git->diffFiles($start_commit, $end_commit);

if(count($files['added']) > 0) {
    foreach($files['added'] as $file) {
        $content = $git->getContent($end_commit, $file);

        //Ensure a directory exists - if it doesn't create it recursively.
        if (!$sftp->file_exists(dirname($file))) {
            $sftp->mkdir(dirname($path), -1, true);
        }

        $sftp->put('/path/to/remote/file', $content);
    }
}

if(count($files['deleted']) > 0) {
    foreach($files['deleted'] as $file) {

        if ($sftp->file_exists($file)) {
            $sftp->delete($file, true);
        }

    }
}

The above code will transfer all the updated files and delete the files removed between the two specified commits. It also takes care of automatic directory creation. If the directories are not present, the above code will ensure that those directories are created in the process.

Executing remote commands

You can execute any shell commands on the remote server before or after your deployment is completed with the exec method.

$sftp->exec("your-remote-command-here");

Just like with SSH, a call to the exec method does not carry state forward to the next exec call. To execute multiple commands without losing state:

$sftp->exec(
    "php artisan up" . PHP_EOL
    "composer dump-autoload -o" . PHP_EOL
    "php artisan optimize"
);

The phpseclib\Net\SFTP class extends the phpseclib\Net\SSH class. You can utilize the full API of the SSH class to execute remote commands or gather output. For an in-depth implementation of SSH, see our previous tutorial.

Managing Permissions

Permissions are the basic defense system of a file. We need to assign some specific rights to users or groups of users for files on the server to prevent potential attacks.

Setting permissions on a single file returns the new file permissions on success or false on error:

$sftp->chmod(0777, 'path/to/remote/file');

Alternatively, you can set the permissions for all files in a directory recursively:

$sftp->chmod(0777, 'path/to/remote/file', true);

Below is the summary of the most important methods regarding file permissions in the SFTP class:

Method Use case
chgrp($filename, $gid, $recursive = false) Changes file or directory group.
chown($filename, $uid, $recursive = false) Changes file or directory owner.
truncate($filename, $new_size) Truncates a file to a given length.
touch($filename, $time = null, $atime = null) Sets access and modification time of file. If the file does not exist, it will be created.

Downloading files

Using PHPSECLIB, we can also download things like backup files or user uploaded media from a server:

//Gets the remote file's contents
$content = $sftp->get('path/to/remote/file');

//Downloads and saves to the local file
$sftp->get('path/to/remote/file', 'path/to/local/file');

If the file is not present locally, it will be created automatically for you.

Alternatives

  1. git-deploy-php – a simple PHP based tool which deploys your changed files to your SFTP servers. It will transfer files automatically for you for the specified commits.
  2. PHPloy – another PHP based deployment tool to transfer files to SFTP servers. It has more features like rollbacks, support for multiple servers and submodules, and more. The downside is too much involvement in manual configuration for triggering a deployment.
  3. Deploy-Tantra – automatically deploys your repositories to SFTP servers once you push to the specified branch. The upside is a more managed workflow. The downside is it being commercial.

Conclusion

In this article, we introduced a particular way to help automate the deployment process for SFTP servers, and we have covered the configuration options necessary to get started.

How do you deploy to your SFTP servers? Can you think of some advanced ways of deploying? What are they? Let us know in the comments!

  • Aqeel Ahmad

    Nice article. However, I would like to understand how are you going to manage conflicts between two commits.

    • You don’t manage conflicts during deployment. You cannot fully push a branch until conflicts are resolved, and that’s something you do way before deployment. When you push, everything is resolved and production-ready.

      • Aqeel Ahmad

        But the problem is SFTP is not maintaining git state and technically you are not pulling your files here so if any files were changed on the server, your deployment will overwrite.

        • Viraj Khatavkar

          This process is not for the use-cases where doing a simple git pull over over your server will deploy your code. When you typically don’t have an SSH access and need to deploy something over SFTP servers, the above mentioned process can surely help you in automating your process :)

  • Viraj Khatavkar

    @disqus_oESvXYwTrQ:disqus As far as I see, Git-ftp deploys to your ftp servers and not to SFTP servers :) But, I do agree that there maybe other such tools. I tried to lay down a process here which shows you how you may achieve the same process by writing custom code.

    • Johannes Müller

      Under the hood ftp upload is processed by curl, which has SFTP support right out of the box.
      I’d just prefer to use a proven tool over any custom solution as long as it does the required job. No need to reinvent the damn wheel. But still, of course it’s always good to know some alternative approach.

  • David Llanes

    I just clone the repository from github on hosting. New updates are just pulls, and I can rollback to any other commit on the past. Of course a good .gitignore file is required.

    • Viraj Khatavkar

      Agreed, but many hosts don’t have SSH access to execute those commands. Many WordPress users use WP-Engine, Godaddy or many other SFTP servers which don’t provide a proper SSH access. Hence, this custom workflow can be used/developed :)

  • Viraj Khatavkar

    Agreed :) This article just goes down the rabbit hole over how we can actually do it at a lower level 😉

  • Taylor Ren

    IMHO, rsync would also do.

    • Johannes Müller

      Rsync needs a shell to run the remote rsync process. If you just have SFTP access it just won’t do.

  • Viraj Khatavkar

    Yes, but IMHO that is not the objective of the article :)

  • Not bad

  • Oskar Calvo

    I think the same, I will be better to speak about deployer, robo or rocketer.

    If you want to go down, it will be better start with Symfony console as backbone of the project.

Recommended
Sponsors
Get the latest in PHP, once a week, for free.