Deploying a Yii2 PHP Application on AWS with Docker Swarm, Ansible, GitHub Actions & Prometheus


Modern DevOps practices demand automation, scalability, and observability. In this blog, we walk through the complete lifecycle of deploying a Yii2 PHP application on AWS using a full-fledged DevOps toolchain, including Docker Swarm for orchestration, NGINX as a reverse proxy, Ansible for infrastructure automation, GitHub Actions for CI/CD, and Prometheus for monitoring.
Letβs break it down, step by step.
π¦ Project Stack
Component | Tool Used |
Application | Yii2 PHP |
Containerization | Docker |
Orchestration | Docker Swarm |
Web Server | Nginx |
CI/CD | GitHub Actions |
Automation | Ansible |
Monitoring | Prometheus + Node Exporter |
Hosting | AWS EC2 |
1οΈβ£ Application Deployment
a. π§ Using a Sample Yii2 PHP Application
We start with provisioning an Ubuntu EC2 Instance and manually running a Yii2 PHP application.
Step 1: Provision EC2
AMI: Ubuntu Server 22.04 LTS
Instance Type: t2.medium or equivalent
Security Group Inbound Rules:
TCP 22 (SSH)
TCP 80 (HTTP)
TCP 8080 (Custom App Port)
Step 2: SSH into Instance
ssh -i /path/to/key.pem ubuntu@<EC2-Public-IP>
Step 3: Update System
sudo apt update && sudo apt upgrade -y
Step 4: Install PHP CLI, Composer, Yii2
sudo apt install php-cli unzip curl php-xml -y
curl -sS https://getcomposer.org/installer -o composer-setup.php
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
composer --version
composer install --ignore-platform-req=ext-curl
Step 5: Create Yii2 App
composer create-project --prefer-dist yiisoft/yii2-app-basic hello-world-yii2
cd hello-world-yii2
php -S 0.0.0.0:8080 -t web
Step 6: Open Port 8080 in EC2
Go to EC2 > Security Group > Edit inbound rules
Add:
Type: Custom TCP
Port: 8080
Source: Anywhere
Access the app at:
http://<EC2-Public-IP>:8080
b. βοΈ Set up Docker Swarm Mode
Step 1: Install Docker
sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt install docker-ce -y
sudo systemctl start docker
sudo systemctl enable docker
docker --version
Step 2: Initialize Swarm
sudo docker swarm init --advertise-addr <EC2-Public-IP>
docker info | grep Swarm
c. π³ Containerize Yii2 App with Docker
Step 1: Dockerfile
FROM yiisoftware/yii2-php:8.2-fpm
WORKDIR /app
COPY . /app
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN composer install --no-dev --optimize-autoloader
EXPOSE 8080
CMD php -S 0.0.0.0:8080 -t web
Step 2: Build Docker Image
docker build -t <dockerhub-username>/hello-world-yii2-app:v1.0 .
Step 3: Push to DockerHub
docker login
docker push <dockerhub-username>/hello-world-yii2-app:v1.0
Step 4: Docker Compose File (Swarm-ready)
version: '3.8'
services:
php:
image: <dockerhub-username>/hello-world-yii2-app:2.7
volumes:
- ./:/app:delegated
ports:
- '8080:8080'
restart: always
deploy:
replicas: 2
resources:
limits:
cpus: '0.50'
memory: 512M
update_config:
parallelism: 1
delay: 10s
healthcheck:
test: ["CMD", "curl", "--silent", "--fail", "http://localhost:8080/"]
interval: 30s
retries: 3
timeout: 10s
start_period: 10s
Step 5: Deploy with Docker Swarm
docker stack deploy -c docker-compose.yml myapp
docker service ls
Access app at:
http://<EC2-IP>:8080
d. π Configure NGINX as Reverse Proxy
Step 1: Install NGINX
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
Step 2: Create NGINX Config
# /etc/nginx/sites-available/yii2-app
server {
listen 80;
server_name _;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ /\.ht {
deny all;
}
}
sudo ln -s /etc/nginx/sites-available/yii2-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo rm /etc/nginx/sites-enabled/default
Access app at:
http://<EC2-IP>
2οΈβ£ CI/CD Pipeline with GitHub Actions
a. π Workflow File
Path: .github/workflows/deploy.yml
This pipeline:
Builds and pushes Docker image
Increments version
Deploys via SSH to EC2
Performs automatic rollback on failure
name: CI/CD Pipeline for Docker Swarm Deployment with Rollback
on:
push:
branches:
- main
jobs:
deploy:
name: Build, Push Docker Image, Deploy to Swarm, and Rollback on Failure
runs-on: ubuntu-latest
env:
IMAGE_NAME: nishankkoul/hello-world-yii2-app
COMPOSE_FILE: docker-compose.yml
SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }}
SSH_USER: ${{ secrets.EC2_USER }}
SSH_HOST: ${{ secrets.EC2_HOST }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get Latest Image Tag
id: latest_tag
run: |
tags=$(curl -s https://hub.docker.com/v2/repositories/${{ env.IMAGE_NAME }}/tags/?page_size=100 | jq -r '.results[].name')
latest=$(echo "$tags" | grep -E '^[0-9]+\.[0-9]+$' | sort -V | tail -n1)
echo "Latest tag: $latest"
echo "latest_tag=$latest" >> $GITHUB_ENV
- name: Calculate New Tag
id: new_tag
run: |
if [ -z "${{ env.latest_tag }}" ]; then
new_tag="1.0"
else
new_tag=$(awk "BEGIN {printf \"%.1f\", ${{ env.latest_tag }} + 0.1}")
fi
echo "New tag: $new_tag"
echo "new_tag=$new_tag" >> $GITHUB_ENV
- name: Build Docker Image
run: |
docker build -t ${{ env.IMAGE_NAME }}:${{ env.new_tag }} .
- name: Push Docker Image
run: |
docker push ${{ env.IMAGE_NAME }}:${{ env.new_tag }}
- name: Update docker-compose.yml with New Image Tag
run: |
sed -i "s|${{ env.IMAGE_NAME }}:[0-9.]*|${{ env.IMAGE_NAME }}:${{ env.new_tag }}|" ${{ env.COMPOSE_FILE }}
cat ${{ env.COMPOSE_FILE }}
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add ${{ env.COMPOSE_FILE }}
git commit -m "Update image tag to ${{ env.new_tag }} [skip ci]" || echo "No changes to commit"
git remote set-url origin https://${{ secrets.GITHUB_TOKEN }}@github.com/nishankkoul/yii2-app.git
git push origin main
- name: SSH into EC2 and Deploy
id: deploy_app
uses: appleboy/ssh-action@v0.1.7
continue-on-error: true
with:
host: ${{ env.SSH_HOST }}
username: ${{ env.SSH_USER }}
key: ${{ env.SSH_PRIVATE_KEY }}
script: |
set -e
cd hello-world-yii2
git pull origin main
docker pull ${{ env.IMAGE_NAME }}:${{ env.new_tag }}
docker stack deploy -c docker-compose.yml myapp
- name: Rollback if Deployment Failed
if: steps.deploy_app.outcome == 'failure'
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ env.SSH_HOST }}
username: ${{ env.SSH_USER }}
key: ${{ env.SSH_PRIVATE_KEY }}
script: |
echo "Deployment failed! Rolling back to previous version..."
cd hello-world-yii2
sed -i "s|${{ env.IMAGE_NAME }}:[0-9.]*|${{ env.IMAGE_NAME }}:${{ env.latest_tag }}|" ${{ env.COMPOSE_FILE }}
git checkout -- docker-compose.yml
docker pull ${{ env.IMAGE_NAME }}:${{ env.latest_tag }}
docker stack deploy -c docker-compose.yml myapp
echo "Rollback to version ${{ env.latest_tag }} completed!"
This GitHub Actions pipeline automates the complete CI/CD process for deploying a Dockerized Yii2 application on a Docker Swarm cluster. When code is pushed to the main branch, the pipeline builds a new Docker image, tags it with an incremented version, pushes it to DockerHub, and updates the docker-compose.yml file with the new image tag. It then commits and pushes the updated file back to the repository. Next, the pipeline connects to an EC2 instance via SSH, pulls the new image, and deploys the application using Docker Swarm. If the deployment fails, it automatically rolls back to the previously working image version to ensure service continuity.
b. π GitHub Secrets to Add
DOCKERHUB_USERNAME β your DockerHub username
DOCKERHUB_TOKEN β your DockerHub personal access token
EC2_SSH_KEY - private SSH Key to authenticate with the EC2 Instance
EC2_HOST - public IPv4 Address of the EC2 Instance
EC2_USER - what user should be used to perform actions inside the EC2 Instance
c. β± Trigger Pipeline
git add .
git commit -m "Trigger deployment pipeline"
git push origin main
3οΈβ£ Infrastructure Automation with Ansible
a. Install Ansible
sudo apt update && sudo apt install ansible -y
b. Ansible Playbook: playbook.yml
Create a file named playbook.yml with your desired Ansible tasks. This installs Docker, NGINX, PHP, Composer, clones repo, deploys via Docker Stack, and configures NGINX.
---
- hosts: localhost
become: yes
vars:
ansible_connection: local
project_dir: "/home/ubuntu/yii2-app" # Set the path to your Yii2 project
tasks:
- name: Install Docker, Docker Compose, Git, PHP, and NGINX
apt:
update_cache: yes
name:
- docker.io
- docker-compose
- git
- nginx
- php
- php-fpm
- php-xml
- php-mbstring
state: present
- name: Start and enable Docker
systemd:
name: docker
state: started
enabled: yes
- name: Add user to docker group
user:
name: ubuntu
groups: docker
append: yes
- name: Clone the Yii2 repository
git:
repo: "https://github.com/nishankkoul/yii2-app.git"
dest: "{{ project_dir }}"
version: main
- name: Install Composer
shell: |
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
args:
creates: /usr/local/bin/composer
- name: Install PHP dependencies using Composer
command: composer install
args:
chdir: "{{ project_dir }}"
- name: Install yii2-bootstrap5 via Composer
composer:
command: require
arguments: yiisoft/yii2-bootstrap5
working_dir: "{{ project_dir }}"
environment:
COMPOSER_ALLOW_SUPERUSER: 1
- name: Set alias for yii2-bootstrap5 in web.php
lineinfile:
path: "{{ project_dir }}/config/web.php"
regexp: '/Yii::setAlias/'
line: "Yii::setAlias('@yii2-bootstrap5', dirname(__DIR__) . '/vendor/yiisoft/yii2-bootstrap5');"
insertafter: "components:"
- name: Initialize Docker Swarm
shell: |
docker swarm init || true
- name: Check Docker Swarm status
shell: docker info | grep "Swarm"
register: swarm_status
changed_when: false
- name: Debug Swarm status
debug:
var: swarm_status.stdout
- name: Pull the latest Docker image
shell: |
docker pull nishankkoul/hello-world-yii2-app:2.4
args:
chdir: "{{ project_dir }}"
- name: Deploy Stack (if you have a docker-compose.yml in your repo)
shell: |
docker stack deploy -c docker-compose.yml myapp
args:
chdir: "{{ project_dir }}"
- name: Configure NGINX for Yii2 App
template:
src: "/home/ubuntu/nginx.conf.j2"
dest: "/etc/nginx/sites-available/default"
- name: Restart NGINX
service:
name: nginx
state: restarted
Run it with:
ansible-playbook playbook.yml
Make sure you are in the same directory with nginx.conf.j2
available.
4οΈβ£ Monitoring with Prometheus & Node Exporter
a. π Install Prometheus
Follow these commands:
# Create user and directories
sudo useradd --no-create-home --shell /bin/false prometheus
sudo mkdir /etc/prometheus /var/lib/prometheus
# Download and install Prometheus
wget https://github.com/prometheus/prometheus/releases/download/v2.43.0/prometheus-2.43.0.linux-amd64.tar.gz
tar -xvzf prometheus*.tar.gz
cd prometheus-2.43.0.linux-amd64
sudo mv prometheus /usr/local/bin/
sudo mv promtool /usr/local/bin/
# Configuration
sudo mv prometheus.yml consoles console_libraries /etc/prometheus/
sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus
# Systemd unit file
sudo vim /etc/systemd/system/prometheus.service
Paste:
[Unit]
Description=Prometheus
After=network.target
[Service]
User=prometheus
ExecStart=/usr/local/bin/prometheus \
--config.file /etc/prometheus/prometheus.yml \
--storage.tsdb.path /var/lib/prometheus
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start prometheus
sudo systemctl enable prometheus
Access:
http://<EC2-IP>:9090
b. π Install Node Exporter
wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz
tar -xvzf node_exporter*.tar.gz
sudo mv node_exporter-*/node_exporter /usr/local/bin/
sudo useradd -rs /bin/false node_exporter
Create service file:
sudo vim /etc/systemd/system/node_exporter.service
Paste:
[Unit]
Description=Node Exporter
After=network.target
[Service]
User=node_exporter
ExecStart=/usr/local/bin/node_exporter
[Install]
WantedBy=default.target
Start Node Exporter:
sudo systemctl daemon-reload
sudo systemctl start node_exporter
sudo systemctl enable node_exporter
c. π Link Node Exporter to Prometheus
Edit Prometheus config:
sudo vim /etc/prometheus/prometheus.yml
Add under scrape_configs
:
- job_name: 'node_exporter'
static_configs:
- targets: ['localhost:9100']
Restart Prometheus:
sudo systemctl restart prometheus
Access:
http://<EC2-IP>:9090/targets
β Conclusion
This end-to-end DevOps project demonstrates the powerful integration of:
A containerized Yii2 PHP application
Scalable deployment via Docker Swarm
Host-based reverse proxying using NGINX
CI/CD automation with GitHub Actions and rollback safety
Infrastructure provisioning and configuration via Ansible
Monitoring stack with Prometheus and Node Exporter
Together, these tools deliver a scalable, automated, and production-grade deployment pipeline suitable for real-world cloud-native applications. It reinforces key DevOps principles: automation, observability, repeatability, and scalability.
GitHub Repository for Reference: https://github.com/nishankkoul/yii2-app
Subscribe to my newsletter
Read articles from Nishank Koul directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
