decorative image for blog showing how to use ansible to deploy php
April 4, 2024

How to Install (and Configure) Ansible to Deploy a PHP Application

PHP Development

Scaling your deployed PHP applications to meet demand can be complex, and requires careful configuration management to do it effectively. Luckily there are a number of configuration management tools and platforms up for the job. 

In this blog, we’ll provide some concrete reasons why an organization should utilize configuration management technologies, and give an applied example of how to deploy a PHP application using Ansible — including some Ansible playbook examples.

Back to top

Benefits of Configuration Management for PHP Apps

Many DevOps strategies in place at enterprises deploying PHP applications include management of virtual machines. It is the LAMP stack, after all, and Linux, MySQL or PostgreSQL, and Apache or nginx all contribute to the complete picture of any PHP application. When considering the benefits of adding or enhancing existing investments into a platform or tool like Puppet or Ansible, the benefits of managing and deploying updates to the entire host must be considered. After all, why would one want to completely re-write, re-train, and re-test all of an existing deployment strategies bash scripts to a completely new technology? We contend that the benefits outweigh the work and risk.

Configuration management tools or platforms like Puppet or Ansible can provision a virtual machine from scratch, performing configuration management tasks. It can configure virtual hosts in Apache, change values in php.ini, and perform application deployments, automating git pulls and changing branches for PHP applications deployed in an enterprise environment.

While there are a number of benefits of configuration management tools we could cover as they apply to managing PHP apps, the biggest ones are:

  1. No Unintended Side Effects
  2. Availability of Open Source, Agentless, and Automatable Options
  3. Ability to Create Identical Dev, UAT, and Production Environments

Since we're using Ansible for this specific article, we'll approach these benefits from an Ansible-centric viewpoint. Other platforms, like Puppet, do offer these same general benefits.

Benefit #1: No Unintended Side Effects

An Ansible playbook does not consider the state of an existing host. This means that if I have an Ansible playbook that, for example, updates PHP, I can count on it to work on a fresh install of a VM, or a VM that was a year old with many active operators working on it. That playbook might be responsible for doing tasks such as downloading package manager via yum or dnf, installing the latest version of PHP, and restarting PHP-FPM. Ansible playbooks make it easy and in some cases automatic to build in logic that doesn’t rely on state.  

Let’s say that PHP wasn’t installed at all, on a fresh machine. Ansible would install the latest version of PHP, not error out because PHP was missing. If there wasn’t a PHP update, the playbook could be written to skip the restart of PHP-FPM, which is excellent for automation: it’s safer. This kind of logic only takes a few lines of configuration in the playbook, reducing boilerplate bash code that your organization is responsible for maintaining.

Benefit #2: Open Source, Agentless, And Automatable

Ansible has a healthy contributor of contributors, and is a free and open source product released under the Apache 2.0 license, the same license that powers the Apache webserver in the LAMP stack. The thousands of collections of in Ansible Galaxy present a ready to use solution for configuring all kinds of third party tools, from nginx to Apache Cassandra.

All of the above combined with the idea that “using” Ansible boils down to Python connecting to your inventory of hosts from a single process over SSH and reporting its work to STDOUT means that you can start using Ansible in place of bash scripts right now, without installing any special software across all of your environments. Install it once on your development laptop and start creating; and once your done? Have your Jenkins CI/CD server run the playbook for you automatically.

Benefit #3: Creates Identical Dev, UAT, and Production Environments

That part about Jenkins running the playbook? It becomes one of the best reasons to really embrace Ansible. If a developer creates a new feature, and it needs to be tested in a lower Dev environment, your CI/CD system can use Ansible to automatically take the artifacts produced by the build process and publish them to Dev automatically.

That same Ansible playbook can take an argument and an inventory to push those same successful Dev artifacts to UAT. This means your Dev, UAT, and Prod environments can be identical, which is the DevOps ideal!

Back to top

Comparing Configuration Management Technologies for PHP Apps

The problem of configuration management has been solved with bubblegum-and-baling-wire scripts since the early internet. This of course means there have been many ways that the open source community has worked to solve the problem of Configuration Management, the problem Ansible works to solve. Let’s take a look at other options that may already be in place in your organization, or up for your consideration.


Puppet is an open source configuration management platform that involves building manifests with the Puppet language.The main way Puppet manifests differ from Ansible is that Puppet’s manifest internals are written in the Ruby-based PuppetDSL (Domain Specific Language), whereas Ansible is based in Python. Puppet is also a pull-based model, with agents installed on each host in the inventory polling a central controller for updates. This has benefits, like increased resilience during network outages, but at the expense of having one’s Puppet controller become critical infrastructure that must be kept online and maintained.


Chef is another open source, pull-based, agent-driven configuration management technology. Unlike Puppet, though, Chef’s “cookbooks” don’t just utilize Ruby internally, but are a full-blown Ruby Domain Specific Language (DSL). If your team is already writing code in Ruby, Chef might be a great choice for DevOps.

Back to top

Ansible Installation Guide for PHP Apps

Let’s take a look at how we could deploy a Laminas / Mezzio PHP application on a Linux VM with Ansible. 

Required Materials

To follow along with this example, you should have an IDE that supports Ansible syntax highlighting (Visual Studio Code, IntelliJ IDEA, etc.), the ability to run Python, and SSH access to the host you want to configure.

Back to top

Configuration Steps

With installation out of the way, it's time to start configuration -- which applies the configuration you’re going to create in your playbook across an “inventory” of individual hosts. Creating one is really simple, but scales with your organization to include features like dynamic inventory (for example, an API that asks AWS which machines have the “webservers” tag). 

Step #1: Create a host inventory

For this configuration, i started with just one host in my inventory, a “free-tier” Ubuntu 22 server I spun up on AWS.

Here’s what the inventory file looks like:

There’s a ton of documentation on the Ansible inventory process here, but the important bits are on line 2. I’ll break out each directive I used: 

  1. – the IP address or DNS hostname of our host 
  1. ansible_ssh_private_key_file the SSH credential supplied by AWS, which is how ansible will log into the host without a password 
  1. ansible_user this is the SSH username that goes with the key 
  1. ansible_become this directive tells ansible to use “sudo” immediately and switch to another user 
  1. ansible_become_user the user we’re sudoing to, root

Step #2: Create the playbook directory structure

The directory structure of an Ansible playbook looks like this:

Notice out “inventory.ini” file sitting at the root of the project directory. Next to it is where we’re going to write our playbook, “deploy-website.yaml.” For this example, we’re going to create a role called “website” in the “roles” folder with three directories that comprise the role:

  1. defaults – contains values that ansible will default to variables aren’t overridden at runtime 
  1. tasks – the ansible tasks that get called by the role 
  1. templates –jinja templates that will get rendered by the tasks 

Step #3: Write the playbook

The Ansible playbook is a yaml file that informs ansible of the roles that should be applied to specific hosts. Let’s take a look at ours:

Line 2 is related to line 1 in the inventory file: all hosts that are “webservers” in the inventory.ini file will execute these tasks.

Line 3 is a list of two tasks: “install NGINX” and “Run Webserver Tasks.” “Install NGINX” is a community-supported role that is available on Ansible Galaxy. To use this role which isn’t loaded with Ansible by default, we’ll run Ansible Galaxy to load it:

ansible-galaxy role install nginxinc.nginx 

Long story short with this role is that we’re installing NGINX with some customizations!

Starting on line 27 is where our new role, “Run Webserver Tasks” will live. Line 28 instructs ansible to run the role “website” on line 29

Step #4: Write your first role!

I’ve included a complete role for you to start with as an example! Let’s check it out, section by section (you can visit our companion GitHub Gist to reference it with line numbers and proper YAML formatting).

            - name: configure zendphp repo credentials 
                src: templates/zendphp.conf.j2 
                dest: /etc/apt/auth.conf.d/zendphp.conf 
                owner: root 
                group: root 
                mode: '0644' 
            - name: Configure zend repo 
                - name: zend |no apt key 
                    dest: /etc/apt/keyrings/zend.asc 
                - name: zend |apt source 
                    repo: "deb [signed-by=/etc/apt/keyrings/zend.asc]{{ ansible_distribution | lower }}{{ ansible_distribution_version | regex_replace('\\.', '') }}/ zendphp non-free" 
                    state: present 
                    update_cache: true 
            - name: Install  zendphp 
                  - "php{{ php_version }}-zend" 
                  - "php{{ php_version }}-zend-fpm" 
                  - "php{{ php_version }}-zend-dev" 
                  - "php{{ php_version }}-zend-xml" 
              register: zend_install 
            - name: Install extensions for project 
                  - "php{{ php_version }}-zend-{{ item }}" 
                state: present 
              with_items: "{{ extensions }}" 
            - name: configure nginx for php-fpm 
                src: templates/nginx.conf.j2 
                dest: /etc/nginx/sites-available/default 
                owner: root 
                group: root 
                mode: '0644' 
            - name: upload website 
                src: zend-se-zendhq-demo/worker/ 
                dest: /var/www/worker/ 
                owner: www-data 
                group: www-data 
            - name: start php fpm if not started 
                name: "php{{ php_version }}-zend-fpm" 
                state: started 
            - name: start nginx  if not started 
                name: nginx 
                state: started 
            - name: restart php fpm if zend install has changed 
                name: "php{{ php_version }}-zend-fpm" 
                state: restarted 
              when: zend_install is changed 
              register: fpm_restart 
            - name: restart nginx if fpm has been restarted 
                name: nginx 
                state: restarted 
              when: fpm_restart is changed 

In Ansible, each item in a role is named. For example, on line 2, after the “---“ on line 1, we see the - character, and the yaml key “name.” Each named task is referred to by this name, so for example, the sixth task in this list is “upload website.” I’ll list the tasks I’ve shown below, and explain what each of them do, though each named task does explain its objective!  

  1. configure zendphp repo credentials 
    1. Here I’m rendering a template “zendphp.conf” to /etc/apt/auth.conf.d and updating it with a username and password to use Zend’s customer repository 
    2. The template looks like this: 
    3. The variables come from the “defaults” folder I mentioned before. main.yaml in defaults looks like this: 
  2. Configure zend repo
    1. Here we’re using ansible’s built in apt repository support. First we download Zend’s public key (ansible.builtin.get_url) and then we create the Zend apt repository (ansible.builtin.apt_repository) 
    2. Notice that we’ve automated the retrieval of the right versions with a template on line 19: 
      1. deb_{{ ansible_distribution | lower }}{{ ansible_distribution_version | regex_replace('\\.', '') }}/zendphp non-free 
      2. will render as 
      3. deb_ubuntu2204/ zendphp non-free
  3. Install zendphp
    1. Here we’re using ansbile’s built in apt package management features with “ansible.builtin.apt” 
    2. We’re asking it to install a list of packages, and replacing the template variable “{{ php_version }}” with “8.2” which is defined on line 2 of main.yaml in the “defaults” directory. 
  4. Install extensions for project
    1. We’re doing the same here, but for a list of extensions. I’ve separated this out to show both ways of doing this. In this case, we’re looping over the list on line 5 of the default values from above with the “with_item” directive. We also template the value on line 35 of the playbook, which will render something like “php8.3-zend-bcmath”. 
  5. configure nginx for php-fpm
    1. Here I’m outputting an nginx configuration that works with my setup! The only variable in this template is on line 11, which gives nginx the correct php fastcgi socket to connect to.
  6. Upload Website
    1. here we upload the php files from our app into a location on the web server 
  7. start php fpm if not already started
    1. here we ensure that the php-fpm service is started. If it’s already started, nothing happens; this will be skipped unless it’s stopped.
  8. start nginx  if not started 
    1. same here with nginx 
  9. restart php fpm if zend install has changed 
    1. however, if php-fpm is started, and as it says on line 69, “zend_install is changed,” php-fpm will be restarted regardless of its current status 
    2. Line 69’s value refers to the “register” directive on line 30. In otherwords, if the task “Install zendphp” is changed (yellow text), then and only then will this task run!  
    3. This is useful for when we do software updates automatically! 

Step #5: Run It!

Running the playbook is super easy: 

ansible-playbook -i inventory.ini deploy-website.yaml 

Watch the output: Blue text means a task has been skipped (perfectly normal), green text means the state has already been reached (for example, php is already installed), and yellow text means the task has changed something on the system.

After running this playbook, my process tree looks like this:

And I can access the app I deployed! 

Back to top

Wrapping Up

Ansible is an excellent tool for managing persistent hosts like bare metal and virtual machines. When learning it for the first time, don’t worry about running a playbook over and over while you are debugging your first roles. Practice makes perfect. Just don’t practice in production!

As noted in our newsletter, we're also working on an ansible playbook for teams using Zend Server that will help teams to migrate faster from Zend Server to ZendPHP. Keep an eye on our blog for more info on that release!

Ready to Deploy Secure PHP at Scale?

ZendPHP makes it easy to deploy and scale fully-patched and supported PHP -- even if it has reached community support end of life. Try for free, to visit our ZendPHP product page to learn more.

Try ZendPHP Free   Explore ZendPHP

Additional Resources

Back to top