How to Properly Deploy Web Apps via SFTP with Git
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.
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:
- RSA key
- Password Protected RSA key
- 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:
- Fetch added, modified, and deleted files between the two commits
- Transfer new and modified files to SFTP server
- 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
- 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.
- 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.
- 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!