Skip to content

Grav Production Deployment — Nginx, SSL, Monitoring and Scaling

DodaTech Updated 2026-06-27 9 min read

In this tutorial, you'll learn Grav production deployment — configuring Nginx and Apache for production, setting up SSL/TLS certificates, implementing monitoring and alerting, scaling Grav horizontally, and following a go-live checklist.

What You'll Learn

  • Production server configuration (Nginx, Apache)
  • SSL/TLS certificate setup with Let's Encrypt
  • Performance monitoring and alerting
  • Horizontal scaling strategies for Grav
  • Load balancing and high availability
  • Go-live checklist and pre-launch verification

Why It Matters

In WordPress, production deployment involves database migrations, plugin compatibility checks, and often complex server setups. In Grav, deployment is simpler because there is no database. But production readiness still requires proper server configuration, SSL, security headers, monitoring, and scaling plans. A well-deployed Grav site handles traffic spikes gracefully, recovers from failures automatically, and gives you visibility into performance and errors.

Real-World Use

A SaaS company's documentation site runs Grav on a 3-server cluster behind a load balancer. Each server runs Nginx with FastCGI cache. Redis handles session and page caching across all servers. Monitoring alerts the team if response time exceeds 500ms or error rate exceeds 1%. SSL is managed with auto-renewing Let's Encrypt certificates. The setup handles 50,000 daily visitors with 99.9% uptime.

Learning Path

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

Nginx Configuration

/etc/nginx/sites-available/grav:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    root /var/www/grav-site;

    # SSL configuration
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    # Max upload size
    client_max_body_size 50M;

    # Gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml text/javascript image/svg+xml;
    gzip_min_length 256;
    gzip_comp_level 6;
    gzip_vary on;

    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|webp|svg|css|js|woff|woff2|ttf)$ {
        expires 365d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Deny access to sensitive files
    location ~* \.(yaml|md|twig|log|git)$ {
        deny all;
        return 404;
    }

    location ~ /\. {
        deny all;
        return 404;
    }

    # Deny access to user data directories
    location ~ ^/user/data/(cache|images|accounts) {
        deny all;
        return 404;
    }

    # PHP processing
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root/index.php;
        fastcgi_param PATH_INFO $fastcgi_path_info;

        # FastCGI cache
        fastcgi_cache GRAV;
        fastcgi_cache_valid 200 60m;
        fastcgi_cache_use_stale error timeout updating;
        fastcgi_cache_min_uses 1;
        fastcgi_cache_lock on;
        add_header X-FastCGI-Cache $upstream_cache_status;
    }

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

    # Logs
    access_log /var/log/nginx/grav-access.log;
    error_log /var/log/nginx/grav-error.log;
}

Apache Configuration

/etc/apache2/sites-available/grav.conf:

<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/grav-site

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

    <Directory /var/www/grav-site>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    # Security headers
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "DENY"
    Header always set X-XSS-Protection "1; mode=block"

    # Deny access to sensitive files
    <FilesMatch "\.(yaml|md|twig|log|git)$">
        Require all denied
    </FilesMatch>

    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php [QSA,L]

    ErrorLog ${APACHE_LOG_DIR}/grav-error.log
    CustomLog ${APACHE_LOG_DIR}/grav-access.log combined
</VirtualHost>

SSL/TLS with Let's Encrypt

# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d example.com -d www.example.com

# Auto-renewal (Certbot adds a systemd timer)
sudo certbot renew --dry-run

Monitoring Setup

Server Monitoring with Prometheus Node Exporter

# Install Node Exporter
wget https://github.com/prometheus/node_exporter/releases/download/v1.6.0/node_exporter-1.6.0.linux-amd64.tar.gz
tar xvf node_exporter-*.tar.gz
sudo mv node_exporter-*/node_exporter /usr/local/bin/

# Run as service
cat > /etc/systemd/system/node_exporter.service << EOF
[Unit]
Description=Node Exporter

[Service]
User=nobody
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl enable node_exporter
sudo systemctl start node_exporter

Uptime Monitoring

Configure external uptime monitoring services:

# uptime-monitoring.yaml - Configuration for UptimeRobot/Pingdom
monitors:
  - name: Grav Site
    url: https://example.com
    type: http
    interval: 5
    timeout: 30
    alerts:
      - email: admin@example.com
      - sms: +1234567890

  - name: Admin Panel
    url: https://example.com/admin
    type: http
    interval: 15
    timeout: 30

  - name: SSL Certificate
    url: https://example.com
    type: ssl
    interval: 1440  # Daily
    alert_days_before: 14

Error Monitoring

// user/plugins/error-monitor/error-monitor.php
public function onShutdown()
{
    $error = error_get_last();
    if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        $this->reportError($error);
    }
}

private function reportError($error)
{
    $data = [
        'message' => $error['message'],
        'file' => $error['file'],
        'line' => $error['line'],
        'url' => $_SERVER['REQUEST_URI'] ?? 'unknown',
        'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
        'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
        'timestamp' => date('c'),
    ];

    // Send to error tracking service
    $ch = curl_init('https://api.example.com/errors');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_TIMEOUT, 3);
    curl_exec($ch);
    curl_close($ch);
}

Horizontal Scaling

Shared Filesystem (NFS)

For multi-server setups, share the user/ directory via NFS:

# Server 1 (NFS server)
sudo apt install nfs-kernel-server
echo "/var/www/grav-site/user *(rw,sync,no_subtree_check)" >> /etc/exports
sudo exportfs -a

# Server 2, 3 (NFS clients)
sudo apt install nfs-common
sudo mount -t nfs server1:/var/www/grav-site/user /var/www/grav-site/user

Redis for Shared Caching

# system.yaml on all servers
cache:
    driver: redis
    redis:
        server: redis.example.com
        port: 6379

Load Balancer (HAProxy)

/etc/haproxy/haproxy.cfg:

frontend grav-http
    bind *:80
    redirect scheme https code 301 if !{ ssl_fc }

    use_backend grav-servers

backend grav-servers
    balance roundrobin
    option httpchk GET /
    server grav1 10.0.0.1:443 check
    server grav2 10.0.0.2:443 check
    server grav3 10.0.0.3:443 check

Go-Live Checklist

Pre-Launch

  • SSL certificate installed and auto-renewal configured
  • Security headers verified (securityheaders.com)
  • Redis cache configured and tested
  • Asset pipeline enabled (CSS/JS merge + minify)
  • Image optimization: WebP, responsive sizes, lazy loading
  • File permissions set: 755 dirs, 644 files, 640 config
  • Debug mode disabled, Twig cache enabled
  • Admin panel route changed from default
  • Git repository configured with .gitignore
  • Automated backup script in place

Launch

  • DNS records configured (A record, CNAME for www)
  • CDN configured (Cloudflare)
  • Monitoring alerts configured (uptime, error, SSL expiry)
  • Cache warmed (run bin/grav cache:warmup)
  • Search engines sitemap submitted
  • Google Analytics or tracking configured
  • robots.txt configured
  • 301 redirects from old URLs mapped
  • Contact/forms tested
  • Load test completed

Post-Launch

  • Monitor error logs for 24 hours
  • Verify SSL certificate (no mixed content)
  • Check Lighthouse score (target: 90+)
  • Review analytics after 1 week
  • Set up regular maintenance schedule

Learning Path

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

Common Mistakes

  1. Not testing SSL configuration: A misconfigured SSL certificate causes browser warnings. Use SSL Labs' SSL Test to verify your configuration.

  2. Forgetting to warm the cache: The first visitor after deployment gets a slow page because the cache is cold. Always run cache warming after deployment.

  3. Not setting up monitoring: Without monitoring, you learn about downtime from users. Set up uptime monitoring (Pingdom, UptimeRobot) and error tracking before launch.

  4. Incorrect file permissions: Files with 777 permissions are a security risk. Files with 644 are not writable by the web server. Find the right balance with 755/644/640.

  5. No scaling plan: If your site gets Slashdotted, will it survive? Plan for traffic spikes with caching, CDN, and auto-scaling before you need them.

Practice Questions

  1. What is the recommended Nginx cache Strategy for Grav? Answer: Use FastCGI cache for PHP responses (60-minute TTL) with cache lock to prevent thundering herd. Add long-lived cache headers (365 days) for static assets.

  2. How do you handle shared caching across multiple Grav servers? Answer: Use a centralized Redis server that all Grav instances connect to. The Redis cache driver shares cached pages, sessions, and configuration across servers.

  3. What monitoring should be in place before a site goes live? Answer: Uptime monitoring (5-minute checks), SSL certificate expiry alerts (14 days before), error rate monitoring, server resource monitoring (CPU, memory, disk), and response time alerts.

  4. How do you automate SSL certificate renewal for a Grav site? Answer: Install Certbot which adds a systemd timer for automatic renewal. Renewal checks run twice daily and renew certificates that expire within 30 days.

  5. Challenge: Deploy a Grav site to production following a complete checklist. Set up: Nginx with SSL, FastCGI cache, and security headers, Redis for shared caching across 2 servers, load balancer with HAProxy, Cloudflare CDN, Prometheus monitoring with Grafana dashboards, Let's Encrypt auto-renewal, automated backup script running nightly, CI/CD pipeline with GitHub Actions, and load testing with 1000 concurrent users. Verify all components work together and document the architecture.

FAQ

Can Grav handle high-traffic production sites?

Yes. Grav with proper caching (Redis), CDN, and server optimization handles millions of page views per month. The flat-file architecture means no database bottlenecks.

What is the minimum server requirement for a Grav production site?

1 CPU core, 1GB RAM, PHP 8.0+, and 10GB storage. For higher traffic, add Redis cache and a CDN. Scale vertically (bigger server) or horizontally (multiple servers behind a load balancer).

How do I migrate a Grav site from one server to another?

Copy the files (excluding cache), run composer install, set up the web server, configure SSL, and point DNS to the new server. No database migration needed.

What is the recommended backup strategy for Grav?

Daily automated backups of user/ directory (pages, config, accounts, languages). Store backups off-server (S3, B2) and keep 30 days of history. Test restoration quarterly.

How do I handle a server failure?

With a multi-server setup and load balancer, failed servers are automatically removed from the pool. With a single server, restore from the most recent backup onto a new server and update DNS.

Mini Project

Goal: Deploy a Grav site to production with complete infrastructure.

  1. Set up Nginx with SSL and security headers
  2. Configure Redis for cache and sessions
  3. Set up Cloudflare CDN
  4. Implement monitoring (uptime, errors, performance)
  5. Create automated backup script
  6. Set up CI/CD pipeline with GitHub Actions
  7. Run load test with 1000 concurrent users
  8. Verify Lighthouse score (90+)
  9. Complete the go-live checklist
  10. Create a runbook for the operations team

What's Next

Congratulations on completing the Grav CMS tutorial series. You have learned everything from Grav basics and installation to theme development, plugin creation, caching, security, and production deployment. To continue your learning, explore Twig for advanced template techniques, Symfony for deeper understanding of Grav's underlying framework, and CSS for advanced theme styling. Visit getgrav.org for official documentation and community support.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro