What GitOps Means for Your Deployment Process
GitOps is an operational model that treats your Git repository as the single source of truth for both application code and infrastructure configuration. When a change lands on the main branch, your deployment pipeline picks it up and pushes it to the target environment automatically. Infrastructure updates follow the same pattern. This removes manual steps from the deployment process, makes every change traceable through Git history, and turns rollbacks into a simple commit revert rather than a scramble to fix things manually.
If you have been managing deployments by hand or using scattered scripts across different servers, GitOps brings consistency and predictability to the way your application reaches production. This guide walks through setting up a complete GitOps pipeline using GitHub Actions for a PHP application deployed to a Linux server, with SSH and rsync handling the file transfer step. For teams already working with PHP, a solid CI/CD setup for PHP projects forms the foundation of this workflow.
Repository Structure That Supports GitOps
A GitOps repository keeps application code and deployment configuration together. This means anyone with repository access can see exactly how the application is built and deployed. The exact structure depends on your application, but a typical PHP web application might look like this:
/
├── .github/
│ └── workflows/
│ ├── deploy.yml
│ └── rollback.yml
├── app/
│ └── (application source code)
├── config/
│ └── (environment-specific configuration)
├── web/
│ └── (public webroot files)
├── tests/
│ └── (PHPUnit or other test suites)
├── deploy.sh
├── Dockerfile
└── .env.example
The workflow files in .github/workflows/ define how code moves from your repository to the server. The deployment script lives alongside your application code, which means it gets reviewed with every pull request and updates automatically when the application architecture changes.
This structure also works well for teams using different deployment strategies. If you are working with a Git branching strategy for business web development, the same workflow pattern applies whether you deploy from main, a dedicated release branch, or a production branch.
Building the Deployment Workflow
The deployment workflow triggers whenever someone pushes to the main branch. It runs your test suite, builds any assets that need preprocessing, and then deploys the application to the server via SSH. Create this file at .github/workflows/deploy.yml:
name: Deploy to Production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mysql, curl, zip
- name: Install Composer dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run tests
run: vendor/bin/phpunit --testdox
- name: Deploy to server
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
run: |
eval "$(ssh-agent -s)"
echo "$DEPLOY_KEY" | tr -d '\r' | ssh-add -
mkdir -p ~/.ssh
ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
./.github/workflows/deploy.sh
- name: Notify deployment
run: |
echo "Deployment completed at $(date)"
The workflow starts by checking out the code with a full Git history. This is important if your deployment script or tests rely on previous commits. The PHP setup step installs the extensions your application needs, and the test step ensures nothing broken reaches production. Only if tests pass does the deployment step run.
Writing the Deployment Script
The deploy.sh script handles the actual file transfer. Keeping it in the repository means it is version-controlled and reviewed with code changes. Place it at .github/workflows/deploy.sh and make it executable:
#!/bin/bash
set -e
export DEPLOY_HOST="${DEPLOY_HOST:?Missing DEPLOY_HOST}"
export DEPLOY_USER="${DEPLOY_USER:?Missing DEPLOY_USER}"
export DEPLOY_PATH="${DEPLOY_PATH:?Missing DEPLOY_PATH}"
rsync -avz --delete \
-e "ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key" \
--exclude='.git' \
--exclude='.github' \
--exclude='.env' \
--exclude='storage/logs/*' \
--exclude='node_modules/*' \
./ \
${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/
echo "Files synced to ${DEPLOY_HOST}:${DEPLOY_PATH}"
ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key \
${DEPLOY_USER}@${DEPLOY_HOST} \
"cd ${DEPLOY_PATH} && composer install --no-dev --optimize-autoloader --no-interaction"
echo "Deployment complete"
The rsync command transfers files from your repository to the server. The --delete flag ensures that files removed from the repository are also removed from the server, keeping both in sync. Exclude directories that should not exist on the server, such as the .git directory, workflow files, and any local environment files.
The SSH command then runs composer install on the server to handle any dependencies that are not committed to the repository. If you are setting up the server for the first time, a proper server security setup should be in place before exposing it to automated deployments.
Setting Up the Deploy Key and Repository Secrets
Generate a dedicated SSH key for deployments. Using a separate key limits the impact if the key is ever compromised, and it keeps your personal SSH key out of the deployment pipeline.
ssh-keygen -t ed25519 -f deploy_key -N "" -C "github-actions-deploy"
Add the public key to the deployment server. Log in as the deploy user and add the key to their authorized_keys file:
cat deploy_key.pub | ssh user@server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
Back in the GitHub repository, go to Settings, then Secrets and variables, then Actions. Add the following secrets:
- DEPLOY_KEY: The private key content. Paste the entire output including the header and footer lines.
- DEPLOY_HOST: The server hostname or IP address.
- DEPLOY_USER: The SSH username for the deploy user.
- DEPLOY_PATH: The absolute path on the server where files should be deployed.
GitHub Actions handles these secrets securely and makes them available as environment variables during the workflow run. Never commit these values to your repository.
Creating a Rollback Workflow
One of the practical benefits of GitOps is straightforward rollback. If a deployment causes problems, you revert the Git commit and push. The pipeline detects the revert and deploys the previous state automatically. Create a workflow at .github/workflows/rollback.yml to handle this manually when needed:
name: Rollback Deployment
on:
workflow_dispatch:
inputs:
revision:
description: 'Git revision to roll back to (short commit hash)'
required: true
jobs:
rollback:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.revision }}
- name: Deploy to server
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
run: |
eval "$(ssh-agent -s)"
echo "$DEPLOY_KEY" | tr -d '\r' | ssh-add -
mkdir -p ~/.ssh
ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
rsync -avz --delete \
-e "ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key" \
--exclude='.git' \
--exclude='.github' \
./ \
${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/
ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key \
${DEPLOY_USER}@${DEPLOY_HOST} \
"cd ${DEPLOY_PATH} && composer install --no-dev --optimize-autoloader --no-interaction"
echo "Rollback to ${{ github.event.inputs.revision }} complete"
To roll back, open the Actions tab in GitHub, select the Rollback Deployment workflow, click Run workflow, and enter the short commit hash of the revision you want to restore. The pipeline checks out that specific commit and deploys it to the server.
Handling Multiple Environments
Applications typically have separate staging and production environments. You can handle this by using different branches for each environment, with each branch triggering a different deployment:
on:
push:
branches:
- main # Deploys to staging
- production # Deploys to production
Each branch should have its own set of secrets in the repository settings. Staging deployments let you test the deployment process and catch any issues before changes reach production users. Always review what is being deployed before merging to production, and never deploy directly to production from a feature branch.
Managing Database Migrations Safely
Database migrations in a GitOps pipeline require careful planning. Running migrations automatically is convenient, but a breaking migration can leave your application unable to start. Use a migration strategy that is forward-only: new migrations add fields or tables, and existing migrations alter data without dropping columns that the previous version of the application still uses.
For Laravel applications, add a migration step to your deployment script:
ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key \
${DEPLOY_USER}@${DEPLOY_HOST} \
"cd ${DEPLOY_PATH} && php artisan migrate --force"
The --force flag runs migrations in production without prompting for confirmation. Before running migrations in production, take a database backup. If you are using a different framework, ensure your migration step is part of the deployment script and that automated backups are in place.
If you are exploring containerized approaches, it is worth considering when Docker makes sense for web applications as an alternative to traditional server deployments. Containers can simplify some deployment complexity but introduce their own operational requirements.
Automating Deployment Steps with a Bash Script
Beyond the basic file transfer, many deployments involve additional steps such as clearing caches, restarting services, or running build commands. A Bash script for application deployment can consolidate these steps into a single, repeatable process that runs automatically each time code is pushed.
For example, a PHP application might need to clear its configuration cache after deployment:
#!/bin/bash
set -e
export DEPLOY_HOST="${DEPLOY_HOST:?Missing DEPLOY_HOST}"
export DEPLOY_USER="${DEPLOY_USER:?Missing DEPLOY_USER}"
export DEPLOY_PATH="${DEPLOY_PATH:?Missing DEPLOY_PATH}"
rsync -avz --delete \
-e "ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key" \
--exclude='.git' \
--exclude='.github' \
--exclude='.env' \
./ \
${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/
ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key \
${DEPLOY_USER}@${DEPLOY_HOST} \
"cd ${DEPLOY_PATH} && \
composer install --no-dev --optimize-autoloader --no-interaction && \
php artisan config:cache && \
php artisan cache:clear && \
echo 'Deployment complete'"
Each step runs in sequence on the server after the files are synced. If any step fails, the script stops due to the set -e directive, preventing a partial deployment from leaving the application in an inconsistent state.
Monitoring and Notifications
A deployment pipeline without monitoring is incomplete. After deployment, it helps to know whether the application started correctly and whether there are any obvious errors. Consider adding health check steps to your workflow:
- name: Health check
run: |
curl -f https://yourdomain.com/health || exit 1
You can also set up notifications to Slack, email, or other channels when deployments complete or fail. GitHub Actions supports these integrations through community actions available in the GitHub Marketplace.
Keeping Your Pipeline Maintainable
As your application grows, the deployment pipeline will need maintenance. Review the workflow files periodically to ensure they reflect current application requirements. Update PHP versions, adjust excluded directories, and test the rollback workflow on a schedule.
Document any manual steps that are still required, such as DNS changes or third-party configuration updates. The goal is to make deployments as automatic as possible while keeping the process understandable for anyone on the team who needs to troubleshoot or take over.
Putting It Together
GitOps with GitHub Actions turns your deployment process into something you can version, review, and automate. The key components are the deployment workflow that triggers on pushes to main, the deploy script that handles file transfer and server-side commands, SSH deploy keys stored securely as GitHub secrets, and a rollback workflow for recovery when things go wrong.
Once the pipeline is configured, deploying becomes a matter of merging code changes. The pipeline handles the rest, from running tests to updating the server. This approach works well for PHP applications, static sites, and other web projects that run on accessible servers.
If you are evaluating deployment approaches for a new project or want to review an existing pipeline, working through the setup step by step helps identify gaps in your current process. For teams already using GitHub, building the pipeline directly within the platform keeps everything in one place and reduces the number of external tools to manage.