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

Nishank KoulNishank Koul
9 min read

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

ComponentTool Used
ApplicationYii2 PHP
ContainerizationDocker
OrchestrationDocker Swarm
Web ServerNginx
CI/CDGitHub Actions
AutomationAnsible
MonitoringPrometheus + Node Exporter
HostingAWS 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

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

0
Subscribe to my newsletter

Read articles from Nishank Koul directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nishank Koul
Nishank Koul