How To Deploy a Basic PHP Application Using Ansible on Ubuntu 14.04
Table of Contents
Introduction #
This tutorial covers the process of provisioning a basic PHP application using Ansible. The goal at the end of this tutorial is to have your new web server serving a basic PHP application without a single SSH connection or manual command run on the target Droplet.
We will be using the Laravel framework as an example PHP application, but these instructions can be easily modified to support other frameworks and applications if you already have your own.
Prerequisites #
For this tutorial, we will be using Ansible to install and configure Nginx, PHP, and other services on a Ubuntu 14.04 Droplet. This tutorial builds on basic Ansible knowledge, so if you are new to Ansible, you can read through this basic Ansible tutorial first.
To follow this tutorial, you will need:
One Ubuntu 14.04 Droplet of any size that we will be using to configure and deploy our PHP applicaton onto. The IP address of this machine will be referred to as your_server_ip
throughout the tutorial.
One Ubuntu 14.04 Droplet which will be used for Ansible. This is the Droplet you will be logged into for the entirety of this tutorial.
Sudo non-root users configured for both Droplets.
SSH keys for the Ansible Droplet to authorize login on the PHP deployment Droplet, which you can set up by following this tutorial on your Ansible Droplet.
Step 1 — Installing Ansible #
The first step is to install Ansible. This is easily accomplished by installing the PPA (Personal Package Archive), and installing the Ansible package with apt
.
First, add the PPA using the apt-add-repository
command.
sudo apt-add-repository ppa:ansible/ansible
Once that has finished, update the apt
cache.
sudo apt-get update
Finally, install Ansible.
sudo apt-get install ansible
Once Ansible is installed, we’ll create a new directory to work in and set up a basic configuration. By default, Ansible uses a hosts file located at /etc/ansible/hosts
, which contains all of the servers it is managing. While that file is fine for some use cases, it’s global, which isn’t what we want here.
For this tutorial, we will create a local hosts file and use that instead. We can do this by creating a new Ansible configuration file within our working directory, which we can use to tell Ansible to look for a hosts file within the same directory.
Create a new directory (which we will use for the rest of this tutorial).
mkdir ~/ansible-php
Move into the new directory.
cd ~/ansible-php/
Create a new file called ansible.cfg
and open it for editing using nano
or your favorite text editor.
nano ansible.cfg
Add in the hostfile
configuration option with the value of hosts
in the [defaults]
group by copying the following into the ansible.cfg
file.
ansible.cfg
[defaults]
hostfile = hosts
Save and close the ansible.cfg
file. Next, we’ll create the hosts
file, which will contain the IP address of the PHP Droplet where we will deploy our application.
nano hosts
Copy the below to add in a section for php
, replacing your_server_ip
with your server IP address and sammy
with the sudo non-root user you created in the prerequisites on your PHP Droplet.
hosts
[php]
your_server_ip ansible_ssh_user=sammy
Save and close the hosts
file. Let’s run a simple check to make sure Ansible is able to connect to the host as expected by calling the ping
module on the new php
group.
ansible php -m ping
You may get an SSH host authentication check, depending on if you’ve ever logged into that host before. The ping should come back with a successful response, which looks something like this:
111.111.111.111 | success >> {
"changed": false,
"ping": "pong"
}
Ansible is now be installed and configured; we can move on to setting up our web server.
Step 2 — Installing Required Packages #
In this step we will install some required system packages using Ansible and apt
. In particular, we will install git
, nginx
, sqlite3
, mcrypt
, and a couple of php5-*
packages.
Before we add in the apt
module to install the packages we want, we need to create a basic playbook. We’ll build on this playbook as we go through the tutorial. Create a new playbook called php.yml
.
nano php.yml
Paste in the following configuration. The first two lines specifies the hosts group we wish to use (php
) and makes sure it runs commands with sudo
by default. The rest adds in a module with the packages that we need. You can customize this for your own application, or use the configuration below if you’re following along with the example Laravel application.
---
- hosts: php
sudo: yes
tasks:
- name: install packages
apt: name={{ item }} update_cache=yes state=latest
with_items:
- git
- mcrypt
- nginx
- php5-cli
- php5-curl
- php5-fpm
- php5-intl
- php5-json
- php5-mcrypt
- php5-sqlite
- sqlite3
Save the php.yml
file. Finally, run ansible-playbook
to install the packages on the Droplet. Don’t forget to use the --ask-sudo-pass
option if your sudo user on your PHP Droplet requires a password.
ansible-playbook php.yml --ask-sudo-pass
Step 3 — Modifying System Configuration Files #
In this section we will modify some of the system configuration files on the PHP Droplet. The most important configuration option to change (aside from Nginx’s files, which will be covered in a later step) is the cgi.fix_pathinfo
option in php5-fpm
, because the default value is a security risk.
We’ll first explain all the sections we’re going to add to this file, then include the entire php.yml
file for you to copy and paste in.
The lineinfile module can be used to ensure the configuration value within the file is exactly as we expect it. This can be done using a generic regular expression so Ansible can understand most forms the parameter is likely to be in. We’ll also need to restart php5-fpm
and nginx
to ensure the change takes effect, so we need to add in two handlers as well, in a new handlers
section. Handlers are perfect for this, as they are only fired when the task changes. They also run at the end of the playbook, so multiple tasks can call the same handler and it will only run once.
The section to do the above will look like this:
- name: ensure php5-fpm cgi.fix_pathinfo=0
lineinfile: dest=/etc/php5/fpm/php.ini regexp='^(.*)cgi.fix_pathinfo=' line=cgi.fix_pathinfo=0
notify:
- restart php5-fpm
- restart nginx
handlers:
- name: restart php5-fpm
service: name=php5-fpm state=restarted
- name: restart nginx
service: name=nginx state=restarted
Note: Ansible version 1.9.1 bug
There is a bug with Ansible version 1.9.1 that prevents php5-fpm
from being restarted with the service
module, as we have used in our handlers.
Until a fix is released, you can work around this issue by changing the restart php5-fpm
handler from using the service
command to using the shell
command, like this:
- name: restart php5-fpm
shell: service php5-fpm restart
This will bypass the issue and correctly restart php5-fpm
.
Next, we also need to ensure the php5-mcrypt
module is enabled. This is done by running the php5enmod
script with the shell task, and checking the 20-mcrypt.ini
file is in the right place when it’s enabled. Note that we are telling Ansible that the task creates a specific file. If that file exists, the task won’t be run.
- name: enable php5 mcrypt module
shell: php5enmod mcrypt
args:
creates: /etc/php5/cli/conf.d/20-mcrypt.ini
Now, open php.yml
for editing again.
nano php.yml
Add the above tasks and handlers, so the file matches the below:
---
- hosts: php
sudo: yes
tasks:
- name: install packages
apt: name={{ item }} update_cache=yes state=latest
with_items:
- git
- mcrypt
- nginx
- php5-cli
- php5-curl
- php5-fpm
- php5-intl
- php5-json
- php5-mcrypt
- php5-sqlite
- sqlite3
- name: ensure php5-fpm cgi.fix_pathinfo=0
lineinfile: dest=/etc/php5/fpm/php.ini regexp='^(.*)cgi.fix_pathinfo=' line=cgi.fix_pathinfo=0
notify:
- restart php5-fpm
- restart nginx
- name: enable php5 mcrypt module
shell: php5enmod mcrypt
args:
creates: /etc/php5/cli/conf.d/20-mcrypt.ini
handlers:
- name: restart php5-fpm
service: name=php5-fpm state=restarted
- name: restart nginx
service: name=nginx state=restarted
Finally, run the playbook.
ansible-playbook php.yml --ask-sudo-pass
The Droplet now has all the required packages installed and the basic configuration set up and ready to go.
Step 4 — Cloning the Git Repository #
In this section we will clone the Laravel framework repository onto our Droplet using Git. Like in Step 3, we’ll explain all the sections we’re going to add to the playbook, then include the entire php.yml
file for you to copy and paste in.
Before we clone our Git repository, we need to make sure /var/www
exists. We can do this by creating a task with the file module.
- name: create /var/www/ directory
file: dest=/var/www/ state=directory owner=www-data group=www-data mode=0700
As mentioned above, we need to use the Git module to clone the repository onto our Droplet. The process is simple because all we normally require for a git clone
command is the source repository. In this case, we will also define the destination, and tell Ansible to not update the repository if it already exists by setting update=no
. Because we are using Laravel, the git repository URL we will use is https://github.com/laravel/laravel.git
.
However, we need to run the task as the www-data
user to ensure that the permissions are correct. To do this, we can tell Ansible to run the command as a specific user using sudo
. The final task will look like this:
- name: Clone git repository
git: >
dest=/var/www/laravel
repo=https://github.com/laravel/laravel.git
update=no
sudo: yes
sudo_user: www-data
Note: For SSH-based repositories you can add accept_hostkey=yes
to prevent SSH host verification from hanging the task.
As before, open the php.yml
file for editing.
nano php.yml
Add the above tasks to the the playbook; the end of the file should match the following:
...
- name: enable php5 mcrypt module
shell: php5enmod mcrypt
args:
creates: /etc/php5/cli/conf.d/20-mcrypt.ini
- name: create /var/www/ directory
file: dest=/var/www/ state=directory owner=www-data group=www-data mode=0700
- name: Clone git repository
git: >
dest=/var/www/laravel
repo=https://github.com/laravel/laravel.git
update=no
sudo: yes
sudo_user: www-data
handlers:
- name: restart php5-fpm
service: name=php5-fpm state=restarted
- name: restart nginx
service: name=nginx state=restarted
Save and close the playbook, then run it.
ansible-playbook php.yml --ask-sudo-pass
Step 5 — Creating an Application with Composer #
In this step, we will use Composer to install the PHP application and its dependencies.
Composer has a create-project
command that installs all of the required dependencies and then runs the project creation steps defined in the post-create-project-cmd
section of the composer.json
file. This is the best way to ensure the application is set up correctly for its first use.
We can use the following Ansible task to download and install Composer globally as /usr/local/bin/composer
. It will then be accessible by anyone using the Droplet, including Ansible.
- name: install composer
shell: curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
args:
creates: /usr/local/bin/composer
With Composer installed, there is a Composer module that we can use. In our case, we want to tell Composer where our project is (using the working_dir
paramter), and to run the create-project
command. We also need to add optimize_autoloader=no
parameter, as this flag isn’t supported by the create-project
command. Like the git
command, we also want to run this as the www-data
user to ensure permissions are valid. Putting it all together, we get this task:
- name: composer create-project
composer: command=create-project working_dir=/var/www/laravel optimize_autoloader=no
sudo: yes
sudo_user: www-data
Note: create-project
task may take a significant amount of time on a fresh Droplet, as Composer will have an empty cache and will need download everything fresh.
Now, open the php.yml
file for editing.
nano php.yml
Add the tasks above at the end of the tasks
section, above handlers
, so that the end of the playbook matches the following:
...
- name: Clone git repository
git: >
dest=/var/www/laravel
repo=https://github.com/laravel/laravel.git
update=no
sudo: yes
sudo_user: www-data
- name: install composer
shell: curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
args:
creates: /usr/local/bin/composer
- name: composer create-project
composer: command=create-project working_dir=/var/www/laravel optimize_autoloader=no
sudo: yes
sudo_user: www-data
handlers:
- name: restart php5-fpm
service: name=php5-fpm state=restarted
- name: restart nginx
service: name=nginx state=restarted
Finally, run the playbook.
ansible-playbook php.yml --ask-sudo-pass
What would happen if we ran Ansible again now? The composer create-project
would run again, and in the case of Laravel, this means a new APP_KEY
. So what we want instead is to set that task to only run after a fresh clone. We can ensure that it is only run once by registering a variable with the results of the git clone
task, and then checking those results within the composer create-project
task. If the git clone
task was Changed, then we run composer create-project
, if not, it is skipped.
Note: There appears to be a bug in some versions of the Ansible composer
module, and it may output OK instead of Changed, as it ignores that scripts were executed even though no dependencies were installed.
Open the php.yml
file for editing.
nano php.yml
Find the git clone
task. Add the register
option to save the results of the task into the the cloned
variable, like this:
- name: Clone git repository
git: >
dest=/var/www/laravel
repo=https://github.com/laravel/laravel.git
update=no
sudo: yes
sudo_user: www-data
register: cloned
Next, find the composer create-project
task. Add the when
option to check the cloned
variable to see if it has changed or not.
- name: composer create-project
composer: command=create-project working_dir=/var/www/laravel optimize_autoloader=no
sudo: yes
sudo_user: www-data
when: cloned|changed
Save the playbook, and run it:
ansible-playbook php.yml --ask-sudo-pass
Now Composer will stop changing the APP_KEY
each time it is run.
Step 6 — Updating Environment Variables #
In this step, we will update the environment variables for our application.
Laravel comes with a default .env
file which sets the APP_ENV
to local
and APP_DEBUG
to true
. We want to swap them for production
and false
, respectively. This can be done simply using the lineinfile
module with the following tasks.
- name: set APP_DEBUG=false
lineinfile: dest=/var/www/laravel/.env regexp='^APP_DEBUG=' line=APP_DEBUG=false
- name: set APP_ENV=production
lineinfile: dest=/var/www/laravel/.env regexp='^APP_ENV=' line=APP_ENV=production
Open the php.yml
file for editing.
nano php.yml
Add this task to the the playbook; the end of the file should match the following:
...
- name: composer create-project
composer: command=create-project working_dir=/var/www/laravel optimize_autoloader=no
sudo: yes
sudo_user: www-data
when: cloned|changed
- name: set APP_DEBUG=false
lineinfile: dest=/var/www/laravel/.env regexp='^APP_DEBUG=' line=APP_DEBUG=false
- name: set APP_ENV=production
lineinfile: dest=/var/www/laravel/.env regexp='^APP_ENV=' line=APP_ENV=production
handlers:
- name: restart php5-fpm
service: name=php5-fpm state=restarted
- name: restart nginx
service: name=nginx state=restarted
Save and run the playbook:
ansible-playbook php.yml --ask-sudo-pass
The lineinfile
module is very useful for quick tweaks of any text file, and it’s great for ensuring environment variables like this are set correctly.
Step 7 — Configuring Nginx #
In this section we will configure a Nginx to serve the PHP application.
If you visit your Droplet in your web browser now (i.e. http://your_server_ip/
), you will see the Nginx default page instead of the Laravel new project page. This is because we still need to configure our Nginx web server to serve the application from the /var/www/laravel/public
directory. To do this we need to update our Nginx default configuration with that directory, and add in support for php-fpm
, so it can handle PHP scripts.
Create a new file called nginx.conf
:
nano nginx.conf
Save this server block within that file. You can check out Step 4 of this tutorial for more details about this Nginx configuration; the modifications below are specifying where the Laravel public directory is and making sure Nginx uses the hostname we’ve defined in the hosts
file as the server_name
with the inventory_hostname
variable.
nginx.conf
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
root /var/www/laravel/public;
index index.php index.html index.htm;
server_name {{ inventory_hostname }};
location / {
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/www/laravel/public;
}
location ~ .php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+.php)(/.+)$;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Save and close the nginx.conf
file.
Now, we can use the template module to push our new configuration file across. The template
module may look and sound very similar to the copy
module, but there is a big difference. copy
will copy one or more files across without making any changes, while template
copies a single files and will resolve all variables within the the file. Because we have used {{ inventory_hostname }}
within our config file, we use the template
module so it is resolved into the IP address that we used in the hosts
file. This way, we don’t need to hard code the configuration files that Ansible uses.
However, as is usual when writing tasks, we need to consider the what will happen on the Droplet. Because we are changing the Nginx configuration, we need to restart Nginx and php-fpm
. This is done using the notify
options.
- name: Configure nginx
template: src=nginx.conf dest=/etc/nginx/sites-available/default
notify:
- restart php5-fpm
- restart nginx
Open your php.yml
file:
nano php.yml
Add in this nginx task at the end of the tasks section. The entire php.yml
file should now look like this:
php.yml
---
- hosts: php
sudo: yes
tasks:
- name: install packages
apt: name={{ item }} update_cache=yes state=latest
with_items:
- git
- mcrypt
- nginx
- php5-cli
- php5-curl
- php5-fpm
- php5-intl
- php5-json
- php5-mcrypt
- php5-sqlite
- sqlite3
- name: ensure php5-fpm cgi.fix_pathinfo=0
lineinfile: dest=/etc/php5/fpm/php.ini regexp='^(.*)cgi.fix_pathinfo=' line=cgi.fix_pathinfo=0
notify:
- restart php5-fpm
- restart nginx
- name: enable php5 mcrypt module
shell: php5enmod mcrypt
args:
creates: /etc/php5/cli/conf.d/20-mcrypt.ini
- name: create /var/www/ directory
file: dest=/var/www/ state=directory owner=www-data group=www-data mode=0700
- name: Clone git repository
git: >
dest=/var/www/laravel
repo=https://github.com/laravel/laravel.git
update=no
sudo: yes
sudo_user: www-data
register: cloned
- name: install composer
shell: curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
args:
creates: /usr/local/bin/composer
- name: composer create-project
composer: command=create-project working_dir=/var/www/laravel optimize_autoloader=no
sudo: yes
sudo_user: www-data
when: cloned|changed
- name: set APP_DEBUG=false
lineinfile: dest=/var/www/laravel/.env regexp='^APP_DEBUG=' line=APP_DEBUG=false
- name: set APP_ENV=production
lineinfile: dest=/var/www/laravel/.env regexp='^APP_ENV=' line=APP_ENV=production
- name: Configure nginx
template: src=nginx.conf dest=/etc/nginx/sites-available/default
notify:
- restart php5-fpm
- restart nginx
handlers:
- name: restart php5-fpm
service: name=php5-fpm state=restarted
- name: restart nginx
service: name=nginx state=restarted
Save and run the playbook again:
ansible-playbook php.yml --ask-sudo-pass
Once it completes, go back to your browser and refresh. You should now see the Laravel new project page!
Conclusion #
This tutorial covers deploying a PHP application with a public repository. While it is perfect for learning how Ansible works, you won’t always be working on fully open source projects with open repositories. This means that you will need to authenticate the git clone
in Step 3 with your private repository. This can be very easily done using SSH keys.
For example, once you have your SSH deploy keys created and set on your repository, you can use Ansible to copy and configure them on your server before the git clone
task:
- name: create /var/www/.ssh/ directory
file: dest=/var/www/.ssh/ state=directory owner=www-data group=www-data mode=0700
- name: copy private ssh key
copy: src=deploykey_rsa dest=/var/www/.ssh/id_rsa owner=www-data group=www-data mode=0600
That should allow the server to correctly authenticate and deploy your application.
You have just deployed a basic PHP application on a Ubuntu-based Nginx web server using Composer to manage dependencies! All of it has been completed without a needing to log directly into your PHP Droplet and run a single manual command.