Deploy your PHP Codebase with Ansible and GitHub Actions

Published on by

Deploy your PHP Codebase with Ansible and GitHub Actions image

This tutorial will show you how you can automate the deploy of your PHP codebase with the help of GitHub Actions and Ansible.

There are a lot of different approaches when it comes to deploying your PHP application to your production server(s). Nowadays, most projects use git and live on a platform like GitHub, GitLab or Bitbucket. But how do you roll out your changes to your server? You could manually SSH into your server after each change and pull the latest code from GitHub, you could use GitHub webhooks to do this automatically for you, or you could use an external software to manage and do all deploys for you. However, today I'd like to show you how you can automate your deploys with a tool called Ansible and GitHub Actions. Ansible is a powerful open-source automation software written in python which simplifies the process of setting up and managing remote machines in an automated way.

Generally, Ansible works like this: You need to create an Ansible Inventory which contains the information of your server(s) and you need to set up a way for Ansible to log into these servers. Ideally, you create a new user on your server just for Ansible which logs in with an SSH key for added security. Everything else gets configured with YAML files. You can do a lot of things with Ansible: Set up your (web-)servers, keep them up-to-date, install software, automatically scale up or down and so much more - however, this guide will only focus on how to automatically deploy the latest version of your code with Ansible. We'll prepare our servers and set up a GitHub Action which will automatically run Ansible and deploy our code to our server on each push to our production branch.

Preparations

For Ansible to be able to connect to and work on your server, you need to do two things: install some required software (Python and Git) and create a separate admin user on your server for Ansible. To do this, connect to your server via SSH and run the following commands:

# 1. Install Python and Git
sudo apt install python3 git
 
# 2. Create a superuser named "ansible"
sudo useradd -m -G sudo ansible
sudo passwd ansible

Now that you have an ansible user on your server, you should also create an SSH key-pair for it to use instead of a password, so it can connect more securely. Make sure to enable this SSH key for the newly created ansible user, not for your default root user on the server:

If you have multiple servers, repeat this step for each one of them (ideally, use the same SSH public key for the Ansible users on all servers).

Set up the Inventory

The so-called Ansible Inventory holds all information about your server(s) and any possible additional variables. You can either put the content in a file, or you can put the content in a GitHub "Repository Secret" and get the content later in your GitHub Action. I'd recommend going with the second option, because some of the content of the inventory (for example the IP addresses of the servers) may be considered confidential and shouldn't appear in your git version control (especially when your server is behind a Cloudflare firewall or something similar).

The structure of the inventory is divided into groups of servers and is pretty straight-forward:

---
all:
vars:
# here you can put any global variables you might need in Ansible playbooks
 
webserver: # the name of our first server group, you can rename this group or add more groups as you like
hosts:
254.254.254.254: # replace this with the IP address of your server. If you have multiple servers, just add them in the following lines

Create the Playbook

To tell Ansible what to do when deploying our code, we need to create a so-called Ansible Playbook. It's a YAML file with instructions for Ansible separated into different tasks (steps). I'd recommend you to create a new directory for your Ansible Playbook in your PHP code repository. You could also create a new repository just for your Ansible Playbooks and set it up so pushes to your main branch from your PHP code repository triggers the execution of the GitHub Action in your Ansible repository - this is great if you want to keep these things more separated, but it's a bit more complicated to set up. That's why I'd recommend you to go the easy route for now and just create a directory in your main repository.

Make sure to configure your webserver to forbid accessing your Ansible folder publicly.

In this directory, create a new YAML file - we'll call it deploy-playbook.yml. In this playbook, we tell Ansible to do the following things:

  1. Clone our PHP code repository into a temporary folder on the server
  2. Optional: Do any additional steps we might need to do on deploy (e.g. install composer packages)
  3. Copy the code to the right destination (in most cases the DocumentRoot of your webserver)
  4. Update the file permissions (so they are accessible by the webserver)

We'll add for each of these steps a task in the playbook:

---
- hosts: webserver
become: yes
become_user: root
vars:
temp_repo_path: "/tmp_repo" # path to the temporary downloadfolder of the repo
tasks:
 
# Get the latest code from your GitHub code repository
- name: Get latest Application Codebase
git:
repo: https://github.com/AnTheMaker/Pesto.git # place the HTTPS git URL of your codebase here
version: main # replace this with the name of your "production" branch (in most cases "main" or "master")
dest: "{{ temp_repo_path }}"
single_branch: yes
accept_hostkey: true
depth: 1
 
# if your git repository is private, you'll need to create a GitHub deploy key (https://docs.github.com/en/developers/overview/managing-deploy-keys#deploy-keys), upload the private key to your server and uncomment the following line:
# key_file: ~/github_deploy_private_key.key
 
register: code_upload # set a variable in Ansible if this task succeeds
 
# do any additional tasks you may need to do. E.g. install composer packages: (uncomment/change as you like)
# - name: Install Composer Packages
# composer:
# command: "install"
# working_dir: "{{ temp_repo_path }}"
# environment:
# COMPOSER_NO_INTERACTION: "1"
# when: code_upload.changed # only run this task when the code has changed
 
- name: Replace live Codebase
delegate_to: "{{ inventory_hostname }}"
synchronize:
src: "{{ temp_repo_path }}"
dest: /var/www/
recursive: yes
delete: yes
when: code_upload.changed # only run this task when the code has changed
 
- name: Update Permissions
file:
path: /var/www/html
state: directory
recurse: yes
 
# change the following two lines to the user of your webserver
owner: www-data
group: www-data

Set up GitHub Actions

Now that we have an Ansible Playbook with instructions on how to deploy our code and Inventory containing our server IPs, we just need to set up GitHub actions to automatically run Ansible whenever we push to our production branch (or whatever you may have called it in your repository).

Luckily, there already exists a great pre-made GitHub action to do just that, called Run Ansible Playbook - we just need to set it up to work with our setup.

To do that, we need to create a GitHub Action YAML file in your repository under the following path: .github/workflows/main.yml

name: Deploy
on:
# Triggers this Action on push or pull request events on the "main" branch and when manually requested from the "Actions" tab
push:
branches: [ main ]
workflow_dispatch:
 
jobs:
deploy_code:
runs-on: ubuntu-latest
steps:
- name: Run Ansible playbook
uses: dawidd6/action-ansible-playbook@v2.5.0
with:
playbook: ansible/deploy-playbook.yml # path to your Ansible playbook
directory: ./
key: ${{ secrets.ansible_ssh_private_key }} # the ssh private key for ansible to use to connect to the servers, stored as "ansible_ssh_private_key" in the GitHub secrets
inventory: ${{ secrets.ansible_inventory }} # the ansible inventory to use, stored as "ansible_inventory" in the GitHub secrets

Saving this workflow file and pushing it to GitHub should automatically enable this Action. From now on, whenever you push to production (or manually request a deploy from your Repositories "Actions" tab), this Action should be executed which will run Ansible, connect to your server, and deploy your new code. When the action is running, you can click on the "Actions" tab and view the currently running action (together with a full live log of the Ansible run with any possible errors or warnings).

Further Resources

Anton Röhm photo

19-year-old self-taught full stack web developer from Germany. Creator of SpaceHey. Photo: Walzberg/Tincon

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Laravel Idea for PhpStorm logo

Laravel Idea for PhpStorm

Ultimate PhpStorm plugin for Laravel developers, delivering lightning-fast code completion, intelligent navigation, and powerful generation tools to supercharge productivity.

Laravel Idea for PhpStorm
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
JetShip - Laravel Starter Kit logo

JetShip - Laravel Starter Kit

A Laravel SaaS Boilerplate and a starter kit built on the TALL stack. It includes authentication, payments, admin panels, and more. Launch scalable apps fast with clean code, seamless deployment, and custom branding.

JetShip - Laravel Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Streamlining Route Parameters in Laravel Using URL Defaults image

Streamlining Route Parameters in Laravel Using URL Defaults

Read article
Flexible Docker Images with PHP INI Environment Variables image

Flexible Docker Images with PHP INI Environment Variables

Read article
Dynamic Form Validation in Laravel with prohibited_if image

Dynamic Form Validation in Laravel with prohibited_if

Read article
Add Approvals to Your Laravel Application image

Add Approvals to Your Laravel Application

Read article
Enhancing Data Processing with Laravel's transform() Method image

Enhancing Data Processing with Laravel's transform() Method

Read article
Get Xdebug Working With Docker and PHP 8.4 in One Minute image

Get Xdebug Working With Docker and PHP 8.4 in One Minute

Read article