Building and deploying PHP applications - PART ONE



Let's reinvent the wheel for educational purposes. It's useful to know how e.g. Jenkins etc. works under the hood.

Our goal is to build and deploy a PHP application using simple tools.


  • Create a build
  • Deploy build
  • Two environments: test and prod
  • Automatic build and deploy once a day

The server

Let's start from a fresh ubuntu 18.04 digital ocean droplet. Its hostname is ubuntu and its ip address is

Add an entry to ~/.ssh/config for convenience:

Host ubuntu

Let's not bother with DNS so put this into into /etc/hosts: test.example.com prod.example.com

Log in as root: ssh -lroot ubuntu

Create a regular account ubuntu:

# useradd -d /home/ubuntu -m -s/bin/bash ubuntu
# mkdir /home/ubuntu/.ssh
# cp .ssh/authorized_keys /home/ubuntu/.ssh/
# chown ubuntu:ubuntu /home/ubuntu/.ssh /home/ubuntu/.ssh/authorized_keys

Install nginx and php-fpm (PHP 7.2):

# apt install nginx php-fpm

Configure /etc/nginx/sites-available/default:

server {
    server_name test.example.com;
    listen 80;
    root /var/www/test;
    error_log /var/log/nginx/test.example.com.error.log;
    access_log /var/log/nginx/test.example.com.access.log;

    index ua.php;

    location / {
        try_files $uri $uri/ =404;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;

server {
    server_name prod.example.com;
    listen 80;
    root /var/www/prod;
    error_log /var/log/nginx/prod.example.com.error.log;
    access_log /var/log/nginx/prod.example.com.access.log;

    index ua.php;

    location / {
        try_files $uri $uri/ =404;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;

Reload: systemctl reload nginx.

Create document root for test and production:

# mkdir /var/www/{test,prod}
# chown ubuntu:ubuntu /var/www/{test,prod}
  • /var/www/test/ the test environment
  • /var/www/prod/ the production environment

The test and prod environments are now accessible at:



Exit and login as ubuntu:

$ ssh -lubuntu ubuntu

Create folders:

$ mkdir ~/{app,bin}

The ~/app contains the application source code and ~/bin contains our bash scripts.

Update PATH and source ~/.bashrc:

$ echo "PATH=~/bin/:$PATH" >> .bashrc
$ . .bashrc

The application has a single file ua.php which prints the browser's ua:


print $_SERVER['HTTP_USER_AGENT'] ?? 'not set';

Set permission: chmod 700 ~/app/ua.php

The build script

We are going to do our work in ~/jobs/test so create it and some other useful folders:

mkdir -p ~/jobs/test/{builds,workspace}

Create the script ~/bin/fetch:


set -euf -o pipefail

cp -vr "$2/." "$1/workspace"

Create the script ~/bin/build:


set -euf -o pipefail

NAME=$1/builds/$(date '+%Y-%m-%d_%H_%M_%S').tgz

tar -czf "$NAME" -C "$1/workspace" .

echo "$NAME"

Make them executable: chmod +x ~/bin/{fetch,build}, and run them:

$ fetch ~/jobs/test ~/app
'/home/ubuntu/app/./ua.php' -> '/home/ubuntu/jobs/test/workspace/./ua.php'

$ build ~/jobs/test

The deploy script

Create the script ~/bin/deploy which deploys a build to the specified environment:


set -euf -o pipefail

tar -xvf "$1" -C "$2"

Make it executable: chmod +x ~/bin/deploy, deploy our build to the test environment:

$ deploy /home/ubuntu/jobs/test/builds/2020-05-30_18_23_47.tgz /var/www/test

Oh no. The swap file created by Vim was accidentally deployed. This is not good.

$ curl -s http://test.example.com/.ua.php.swp | file -
/dev/stdin: Vim swap file, version 8.1, pid 22552, user ubuntu, host ubuntu ...

Unwanted files

This highlights a problem. We probably shouldn't create a build directly from a dirty application source folder. Because we run the risk of creating a build with unwanted files.

Let's put the application into a git repository and create a build based off of a fresh clone.

Create a git repository in ~/app

$ cd ~/app
$ git init
Initialized empty Git repository in /home/ubuntu/app/.git/
$ git add ua.php 
$ git commit -m'init'
[master (root-commit) 015fd8f] init
 1 file changed, 4 insertions(+)
 create mode 100700 ua.php

Create the script ~/bin/fetch-git which clones a git repository into the workspace:


set -euf -o pipefail

rm -rf "$1/workspace"

git clone "$2" "$1/workspace"

Clean out /var/www/test/:

$ rm /var/www/test/.ua.php.swp
$ rm -rf /var/www/test/.git

Try again:

$ fetch-git ~/jobs/test ~/app
Cloning into '/home/ubuntu/jobs/test/workspace'...

$ build ~/jobs/test

$ deploy /home/ubuntu/jobs/test/builds/2020-05-30_18_34_13.tgz /var/www/test

Oh no. The entire .git folder was deployed. This is not good.

Let's delete the .git folder in ~/bin/fetch-git:


set -euf -o pipefail

rm -rf "$1/workspace"

git clone "$2" "$1/workspace"

rm -rf "$1/workspace/.git"

Try again:

$ rm -rf /var/www/test/.git

$ fetch-git ~/jobs/test ~/app
Cloning into '/home/ubuntu/jobs/test/workspace'...

$ build ~/jobs/test

$ deploy /home/ubuntu/jobs/test/builds/2020-05-30_18_38_07.tgz /var/www/test

All is well. But if someone were to commit a sensitive file it would get exposed by nginx. Don't commit sensitive files.

Let's move all three steps into ~/jobs/test/run:


set -euf -o pipefail

fetch-git "$1" ~/app

BUILD=$(build "$1")

deploy "$BUILD" /var/www/test

Make it executable chmod +x ~/jobs/test/run and run it:

$ ~/jobs/test/run ~/jobs/test
Cloning into 'PREFIX/workspace'...

Periodic build and deploy

Let's create a cronjob that builds and deploys to the test environment. It runs each minute.

Write a crontab:

$ crontab -e

* * * * * $HOME/jobs/test/run $HOME/jobs/test >> $HOME/jobs/test/cronjob.log 2>&1


Let's add a build step that lints the application.

Create ~/bin/lint-php:


set -euf -o pipefail

php -l "$1/workspace/ua.php"

Make it executable: chmod +x ~/bin/lint-php.

And run it from ~/jobs/test/run:

lint-php "$1"

A pattern emerges

The job abstraction emerges. And it does the following:

  • Fetches the source code (source code managment)
  • Prepares correct file permissions
  • Inspect the source code for errors (lint)
  • Creates a build (packaging)
  • Deploys the build to an environment
  • Writes to log
  • Periodically repeat itself (cronjob)


  • configuration managment
  • running build number
  • opcache
  • restart nginx
  • tls certs
  • process/job managment
  • health checks
  • notification from failed jobs
  • use bash trap
  • consider deb packaging
  • github web hooks
  • storage-full check
  • atomic deployment

PART TWO (todo)