Skip to content

Grav Git Workflow — Git-Based Deployment, CI/CD and Multi-Environment

DodaTech Updated 2026-06-27 8 min read

In this tutorial, you'll learn Grav Git workflow — setting up Git-based deployment pipelines, integrating with CI/CD services, creating multi-environment workflows (development, staging, production), and managing team collaboration with flat-file content.

What You'll Learn

  • Why Git is a natural fit for Grav's flat-file architecture
  • Setting up a Git repository for a Grav site
  • Multi-environment configuration (dev, staging, prod)
  • Git-based deployment strategies
  • CI/CD integration with GitHub Actions
  • Team collaboration workflows with Git

Why It Matters

In WordPress, Git workflows are complex because content is in a database. You need plugins to sync the database, and deployment involves both files and SQL dumps. In Grav, everything is files — pages, configuration, plugins, themes. This means your entire site can live in Git. Branch, merge, pull request, and deploy just like any software project. This is the modern way to manage a CMS.

Real-World Use

A team of 5 developers and 3 content editors works on a documentation site. Developers create branches for new features. Content editors create branches for new articles. Pull requests are reviewed and merged. On merge to main, a GitHub Action deploys the site to production. The entire workflow is Git-based — no database syncs, no Migration scripts, no manual file transfers.

Learning Path

flowchart LR
    A["Security"] --> B["Git Workflow
← You are here"]:::current B --> C["CLI Tools"] C --> D["Production Deployment"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Why Git + Grav Works

Grav's flat-file structure maps directly to Git's file-based model:

grav-site/
├── user/
│   ├── pages/        # Content pages (Markdown)
│   ├── config/       # Site configuration (YAML)
│   ├── plugins/      # Plugin code (PHP, YAML)
│   ├── themes/       # Theme code (Twig, CSS, JS)
│   ├── accounts/     # User accounts (YAML)
│   └── languages/    # Translations (YAML)
├── .gitignore
└── README.md

Every change is a file change. Every file change is tracked by Git.

Setting Up Git

cd /var/www/grav-site

git init
git add .
git commit -m "Initial Grav site"

# Add remote
git remote add origin git@github.com:yourorg/grav-site.git
git push -u origin main

.gitignore

Create .gitignore:

# Grav generated files
user/data/cache/*
user/data/images/*
user/data/tmp/*
user/data/logs/*
!user/data/logs/.gitkeep

# User configuration (contains secrets)
user/config/system.yaml
user/config/site.yaml
user/config/themes/*.yaml
user/config/plugins/*.yaml

# Environment-specific
.env
.env.local

# OS files
.DS_Store
Thumbs.db

# IDE files
.vscode/
.idea/
*.swp
*.swo

Tracking Config Securely

Use .env for secrets and a template for config:

# Create config templates
cp user/config/system.yaml user/config/system.yaml.example
cp user/config/site.yaml user/config/site.yaml.example

# Track the templates, not the actual config
git add user/config/system.yaml.example
git add user/config/site.yaml.example

Multi-Environment Setup

Directory Structure

grav-site/
├── .env                  # Environment variables (not tracked)
├── .env.example          # Environment template (tracked)
├── bootstrap.php         # Environment loader
├── user/
│   └── config/
│       └── environments/  # Environment-specific config
│           ├── dev/
│           │   └── system.yaml
│           ├── staging/
│           │   └── system.yaml
│           └── prod/
│               └── system.yaml

Environment Configuration

user/config/environments/dev/system.yaml:

cache:
    enabled: false
    check:
        pages: true
        yaml: true
        twig: true

twig:
    cache: false
    debug: true
    auto_reload: true

debugger:
    enabled: true

user/config/environments/prod/system.yaml:

cache:
    enabled: true
    driver: redis
    lifetime: 604800
    check:
        yaml: false
        twig: false

twig:
    cache: true
    auto_reload: false

debugger:
    enabled: false

Environment Detection

// bootstrap.php
$env = getenv('GRAV_ENV') ?: 'prod';

switch ($env) {
    case 'dev':
    case 'development':
        $grav['environment'] = 'dev';
        break;
    case 'staging':
        $grav['environment'] = 'staging';
        break;
    default:
        $grav['environment'] = 'prod';
}

Git-Based Deployment

GitHub Actions Workflow

.github/workflows/deploy.yml:

name: Deploy Grav Site

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
          extensions: curl, mbstring, xml, zip, gd

      - name: Install dependencies
        run: |
          curl -sS https://getcomposer.org/installer | php
          php composer.phar install --no-dev --optimize-autoloader

      - name: Build assets
        run: |
          php bin/grav cache --clear
          php bin/grav install

      - name: Warm cache
        run: php bin/grav cache:warmup

      - name: Deploy to production
        uses: appleboy/scp-action@v0.1.4
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          source: "."
          target: "/var/www/grav-site"
          strip_components: 0

      - name: Post-deploy
        uses: appleboy/ssh-action@v0.1.5
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /var/www/grav-site
            php bin/grav cache --clear
            chmod -R 755 .
            chmod -R 775 user/data user/accounts user/config
            php bin/grav cache:warmup

Team Collaboration Workflow

Branch Strategy

main           → Production-ready code
├── develop    → Integration branch
│   ├── feature/new-article    → Content changes
│   ├── feature/new-template   → Theme changes
│   ├── fix/broken-link        → Bug fixes
│   └── chore/update-plugins   → Maintenance

Content Editor Workflow

# Content editor creates a new article
git checkout -b feature/new-guide

# Create the page folder
mkdir -p user/pages/06.guides/01.new-guide

# Create the Markdown file
echo "---
title: New Guide
---
Content here..." > user/pages/06.guides/01.new-guide/default.md

# Commit and push
git add .
git commit -m "Add new guide: how to use Grav"
git push -u origin feature/new-guide

Then create a pull request on GitHub. After review, merge to main.

Deployment Script

deploy.sh:

#!/bin/bash
set -e

echo "Deploying Grav site..."

# Pull latest code
git pull origin main

# Install dependencies
composer install --no-dev --optimize-autoloader

# Set production environment
export GRAV_ENV=prod

# Clear cache
php bin/grav cache --clear

# Install plugins/themes
php bin/grav install

# Set permissions
chmod -R 755 .
chmod -R 775 user/data
chmod -R 775 user/accounts
chmod -R 775 user/config

# Warm cache
php bin/grav cache:warmup

echo "Deployment complete!"

Learning Path

flowchart LR
    A["Security"] --> B["Git Workflow
← You are here"]:::current B --> C["CLI Tools"] C --> D["Production Deployment"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Common Mistakes

  1. Tracking secrets in Git: Never commit system.yaml, site.yaml, or any file containing passwords, API keys, or database credentials. Use .env files and template configs.

  2. Not ignoring cache directories: Cache files change constantly and should never be in Git. Add user/data/cache/*, user/data/images/*, and user/data/tmp/* to .gitignore.

  3. Merging without testing: Always test changes in a staging environment before merging to production. A broken template or config error can take down the live site.

  4. No environment-specific configuration: Using the same cache settings in development (where you want no cache) and production (where you want aggressive cache) causes problems. Use environment-specific config.

  5. Forgetting dependencies: The vendor/ directory should be in .gitignore. Run composer install during deployment. Team members need to run it after cloning.

Practice Questions

  1. Why is Grav a natural fit for Git-based workflows? Answer: Grav stores all content, configuration, and code as files. Unlike database-driven CMSs, there is no database to sync. The entire site can be version-controlled with Git.

  2. What files should be in .gitignore for a Grav project? Answer: Cache files (user/data/cache/*, user/data/images/*, user/data/tmp/*), actual config files containing secrets (system.yaml, site.yaml), the vendor/ directory, and environment files (.env).

  3. How do you manage environment-specific configuration? Answer: Use an environment loader (like Bootstrap.php) that detects the active environment and loads configuration from user/config/environments/{env}/.

  4. What is the recommended branch strategy for a Grav team? Answer: Main branch for production-ready code, develop for integration, feature branches for new content or features, and fix branches for bug fixes. Use pull requests for review.

  5. Challenge: Set up a complete Git workflow for a Grav site with 3 environments. Create the repository with proper .gitignore and config templates. Set up GitHub Actions for CI/CD that: runs on push to main, installs dependencies, warms the cache, deploys to production via SSH, and runs post-deployment tasks. Create environment-specific configuration for dev (no cache, debug on) and prod (Redis cache, debug off). Add a staging environment that deploys on push to the develop branch. Include a content editor workflow with branches, pull requests, and automated preview deployments.

FAQ

Should I track the `vendor/` directory in Git?

No. Add vendor/ to .gitignore. Run composer install as part of your deployment process. This keeps the repository lean and avoids dependency conflicts.

How do content editors without Git knowledge contribute?

Use Grav's Admin panel for Git-free editing. The Admin panel edits files directly. Changes are detected by Git and can be committed by a developer.

What is the best CI/CD service for Grav?

GitHub Actions works well because Grav is PHP-based. GitLab CI and Bitbucket Pipelines are also good options. All support PHP, SSH deployments, and Composer.

How do I handle merge conflicts in Markdown files?

Merge conflicts happen when two people edit the same page. Use standard Git conflict resolution. Markdown conflicts are easier to resolve than code conflicts because the content is mostly plain text.

Can I deploy only changed files instead of the entire site?

Yes. Use rsync with --update flag, or a deployment tool that tracks changes. For most sites, deploying everything is fast enough (Grav is small — typically under 50MB without cache).

Mini Project

Goal: Set up a complete Git-based deployment pipeline for Grav.

  1. Initialize a Git repository with proper .gitignore
  2. Create config templates (system.yaml.example, site.yaml.example)
  3. Set up environment-specific configuration (dev, staging, prod)
  4. Create a GitHub Actions workflow for CI/CD
  5. Set up a staging environment for testing
  6. Create branch strategy documentation
  7. Implement post-deployment cache warming
  8. Set up environment detection in bootstrap.php
  9. Create a deployment script with proper permissions
  10. Test the full workflow: create branch, make changes, PR, merge, auto-deploy

What's Next

Now you have a modern Git-based workflow. Next, learn CLI tools:

Continue to Lesson 39: CLI Tools — bin/grav commands, automated tasks, and maintenance operations.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro