GitHub Actions CI/CD for PHP 8.3 with Zero‑Downtime Deploys

You don’t need a full platform like Envoyer or Forge to get reliable PHP deployments. A small GitHub Actions workflow plus SSH and rsync is enough for most projects.

Prerequisites

  • PHP 8.1+ app with composer.json
  • Linux server with SSH access
  • Nginx + PHP-FPM installed and working
  • A non-root deploy user with access to the web root

On the server, create a deploy user and base directories:

Bash
sudo adduser deploy
sudo mkdir -p /var/www/myapp/{releases,shared}
sudo chown -R deploy:www-data /var/www/myapp
sudo chmod -R g+w /var/www/myapp

Typical layout:

  • /var/www/myapp/current: symlink to the latest release
  • /var/www/myapp/releases/20251105123000: code for a specific deployment
  • /var/www/myapp/shared: persistent storage (.env, storage etc)

Nginx configuration for the release symlink

Point Nginx at the current/public directory so deployment are just symlink switches:

Nginx
server {
    server_name example.com;

    root /var/www/myapp/current/public;

    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
    }

    access_log /var/log/nginx/myapp.access.log;
    error_log  /var/log/nginx/myapp.error.log;
}

Reload Nginx:

Bash
sudo systemctl reload nginx

Generating an SSH key for GitHub Actions:

Bash
ssh-keygen -t ed25519 -C "github-actions@myapp" -f ./myapp_deploy_key

Copy the public key to the server:

Bash
ssh-copy-id -i ./myapp_deploy_key.pub deploy@your-server

Add the private key (myapp_deploy_key) to your GitHub repo secrets as SSH_PRIVATE_KEY.

You’ll also want:

  • DEPLOY_HOST: your server
  • DEPLOY_USER: deploy
  • DEPLOY_PATH: /var/www/myapp

GitHub Actions workflow file

Create a file at .github/workflows/deploy.yml:

YAML
name: Deploy PHP app

on:
  push:
    branches: [ "main" ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, intl, pdo_mysql
          coverage: none

      - name: Install Composer dependencies
        run: composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader

      - name: Run tests
        run: |
          php vendor/bin/phpunit --testsuite=Unit

      - name: Prepare artifact
        run: |
          mkdir -p artifact
          rsync -a \
            --exclude=.git \
            --exclude=node_modules \
            --exclude=tests \
            ./ artifact/

      - name: Add SSH key
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Deploy via rsync
        env:
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
        run: |
          RELEASE=$(date +"%Y%m%d%H%M%S")
          RSYNC_TARGET="$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/releases/$RELEASE"

          rsync -az --delete \
            artifact/ \
            "$RSYNC_TARGET"

          ssh $DEPLOY_USER@$DEPLOY_HOST bash << 'EOF'
            set -e
            RELEASE_DIR="$DEPLOY_PATH/releases/$RELEASE"
            CURRENT_LINK="$DEPLOY_PATH/current"
            SHARED_DIR="$DEPLOY_PATH/shared"

            mkdir -p "$SHARED_DIR/storage" "$SHARED_DIR/bootstrap/cache"

            # Link shared directories and files
            rm -rf "$RELEASE_DIR/storage"
            ln -s "$SHARED_DIR/storage" "$RELEASE_DIR/storage"

            if [ -f "$SHARED_DIR/.env" ]; then
              ln -sf "$SHARED_DIR/.env" "$RELEASE_DIR/.env"
            fi

            cd "$RELEASE_DIR"

            php artisan config:cache || true
            php artisan route:cache || true
            php artisan view:cache || true

            ln -sfn "$RELEASE_DIR" "$CURRENT_LINK"
          EOF

This uses the community setup-php action for a clean PHP 8.x environment.

To add front-end builds, insert npm ci / rpm run build before the artefact step.

Common mistakes and fixes

MistakeFix
Typing to run Composer on the server during every deploymentRun composer in CI, deploy the vendor directory, and keep your server lean
Pointing Nginx at /var/www/myapp/releases/...Always point at /var/www/myapp/current/public and switch the symlink atomically
Using root for deploymentsUse a dedicated deploy user with minimal privileges and strict SSH keys only

Once it’s wired, every push to main gives you a tested, repeatable, near-zero-downtime deployment with one YAML file.


Posted

in

, , ,

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *