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:

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)

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

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:

sudo systemctl reload nginx

Generating an SSH key for GitHub Actions

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

Copy the public key to the server:

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 need to set the following additional secrets for your GitHub repository:

  • DEPLOY_HOST : your server

  • DEPLOY_USER : deploy

  • DEPLOY_PATH : /var/www/myapp

GitHub Actions workflow file

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

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

Issue

Fix

Trying to run Composer on the server during a deployment

Run 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 deployments

Use 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.