Grav

Grav is considered a CMS and is my first choice for anything that should alow direct page edits to update content. It also makes a pretty good wiki if combining that with flat files for your content is important.

Note: This content is circa 2018 so take it with a gain of salt for config details.

Prerequisites

  • DNS Name
  • Nginx
  • PHP
  • Certbot

DNS Name

This has mostly to do with your registrar and hosting. Suffice to say, if you don’t have a domain name you won’t be able to complete the certificate steps and have meaningful SSL. Even if you don’t care about SSL, a DNS name is pretty useful.

Nginx

We’re installing just the light version as we need only the minimal set of modules.

sudo apt install nginx-light

PHP

A minimal install of Debian 9 does not include PHP, nor would it be the right version if it did. You must install php 7.1 and the grav requirements1. The docs there mention php-openssl and php-session, but these seem to be compiled in and enabled by default in the Debian packages23. You also need to install php-fpm as there’s no php module like apache4.

Note: not sure about php-zip. It may have been included by one of the other packs because I went back to add and it was already installed

sudo apt install php-common php-curl php-ctype php-dom php-gd php-json php-mbstring php-simplexml php-xml php-fpm

# possibly sudo apt install php-zip

Certbot

In this example we’re on Debian stable so we’re installing the stretch backports enabling us to install and use the current certbot, per their instruction5.

sudo echo "deb http://deb.debian.org/debian stretch-backports main" > /etc/apt/sources.list.d/stretch-backports.list
sudo apt update;sudo apt upgrade
sudo apt-get install certbot python-certbot-nginx -t stretch-backports

Configuration

Let’s configure the firewall, nginx, certbot and file permissions.

Firewall Config

We only need two ports; 80 and 443. On ubuntu you can use ufw. On debian you can issue iptables commands such as:

sudo iptables -A INPUT -p tcp -m tcp --dport 80 -m conntract --ctstate NEW,ESTABLISHED -j ACCEPT
sudo iptables -A INPUT -p tcp -m tcp --dport 443 -m conntract --ctstate NEW,ESTABLISHED -j ACCEPT

Nginx config

Nginx comes with a somewhat verbose default config. Let’s clean that up, arrange for a virtual host specific site, and block any random scans6. We can do that on the SSL site as well thanks to nginx’s support of SNI7. We also need to add a rewrite so that short-pathing works and you don’t get 404s for all sub pages8.

# Remove the default config files and content
sudo rm /etc/nginx/sites-available/*
sudo rm /etc/nginx/sites-enabled/*
sudo rm /var/www/html/*

# Make a directory for your grav deployment and the default site
sudo mkdir /var/www/html/default
sudo mkdir /var/www/html/cms.yourdomain.com

# Create a generic SSL key so we can inspect all requests[^7] and do some basic SSL testing
sudo mkdir /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt

# Define a default site simply drops requests (return code 444) if they didn't request a specific hostname
sudo vi /etc/nginx/sites-available/default

server {
    listen      80 default_server;
    server_name _;
    return      444;
}

server {
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;
    return 444;
}

# Define the site that will host grav
sudo vi /etc/nginx/sites-available/cms.yourdomain.com

#
# Redirect requests for the site that are unencrypted
#
server {
        listen 80;
	server_name cms.yourdomain.com;
        return 301 https://$host$request_uri;
}

#
# Site cms.yourdomain.com 
#
server {
        listen 443 ssl; 
        server_name cms.yourdomain.com;
	ssl_certificate /etc/nginx/ssl/nginx.crt;
	ssl_certificate_key /etc/nginx/ssl/nginx.key;

        root /var/www/html/cms.yourdomain.com;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html index.php;

        location / {
		# if file doesn't exist (and it shouldn't unless you 
		# put a file there), pass to the index.php for routing
		try_files $uri $uri/ /index.php?$query_string;
        }



    	# Some additional security settings from the grav example
	location ~* /(\.git|cache|bin|logs|backup|tests)/.*$ { return 403; }
	location ~* /(system|vendor)/.*\.(txt|xml|md|html|yaml|yml|php|pl|py|cgi|twig|sh|bat)$ { return 403; }
	location ~* /user/.*\.(txt|md|yaml|yml|php|pl|py|cgi|twig|sh|bat)$ { return 403; }
	location ~ /(LICENSE\.txt|composer\.lock|composer\.json|nginx\.conf|web\.config|htaccess\.txt|\.htaccess) { return 403; }

        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
        }

}

Now let’s do some basic testing to make sure it’s working

# Enable the sites, test for syntax errors then load the config in nginx
sudo ln -rs /etc/nginx/sites-available/default /etc/nginx/sites-enabled
sudo ln -rs /etc/nginx/sites-available/cms.yourdomain.com /etc/nginx/sites-enabled
sudo nginx -t
sudo systemctl reload nginx

# This tests that requests without the right server name are dropped
curl http://localhost
curl -k https://localhost

# This tests that the right server name does work and that non-ssl is redirected.
curl --header 'Host: cms.yourdomain.com' http://localhost
curl --header 'Host: cms.yourdomain.com' -k https://localhost

PHP Config

One enables php by including a ’location’ snippit in the nginx config file and allowing for index php files. We snuck that in above when we configured nginx. So let’s just create a php info file and make sure it’s working. As an added bonus the contents of that web page will tell you where your php.ini files is, if you do need to make any php config changes.

sudo vi /var/www/html/cms.yourdomain.com/info.php

<?php
  phpinfo();
?>

curl --header 'Host: cms.yourdomain.com' -k https://localhost/info.php

sudo rm /var/www/html/cms.yourdomain.com/info.php

PHP Optimization

The Grav docs recommend some optional modules9 and state You should run a PHP opcache and usercache10.

Opcache has replaced apcu and should be installed and enabled by default11. Recreate the info.php from above and search for OPcache to verify

PECL-yaml is the simply the php native yaml. You can install it and test that’s it’s enabled by

php -i | grep yaml
sudo apt install php-yaml
php -i | grep yaml

In the advanced section they mention memcached and redis, but these are for distributed systems and are slower than OPcache

Cerbot Config

We’re using certbot to automate the installation and maintenance of free certificates from Let’s Encrypt. It will scan your config and ask which site you want to use. After it does it’s thing, it will ask you if you want to redirect. Choose no on that as you’ve already set that up.

When it’s done it will insert the appropriate stanza in your nginx config file and a cron job to renew the cert in your /etc/cron.d/ folder

sudo certbot --nginx
...
...
Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: cms.yourdomain.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel): 1
...
...
Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations! You have successfully enabled https://cms.yourdomain.com

You can verify this with curl by asking for the site without the insecure option

curl https://cms.yourdomain.com	

File Permissions

Editing content means modifying files under the web-root. This is done by the web server itself (for on-line editing), syncing files via sftp and direct edits from the shell.

Managing this type of access is best done with group permissions and ACLs. Using simple file group ownership and umask breaks down when you have multiple ways in.

Luckily, there is already a group for this in debian:www-data. So lets add ourself to the group, own the directory to the group and set the existing and future permissions so the group can edit files12.

sudo usermod -aG www-data allen
sudo chown www-data:www-data /var/www/html/cms.yourdomain.com
sudo apt install acl
sudo setfacl --recursive --modify "g:www-data:rwx,d:g:www-data:rwx" /var/www/html/cms.yourdomain.com

Grav Installation

Extract Grav

Grav strongly recommends moving the extracted directory and not just copying the contents. I suspect this is so hidden .htaccess files and file permissions are kept, neither of which we need. But let’s go ahead and do as they ask, then change to be like we want.

sudo apt install unzip

cd ~
wget -O grav-admin.zip https://getgrav.org/download/core/grav-admin/latest
unzip grav-admin.zip
mv grav-admin /var/www/html/cms.yourdomain.com
sudo chown -R www-data:www-data /var/www/html/cms.yourdomain.com
sudo setfacl --recursive --modify "g:www-data:rwx,d:g:www-data:rwx" /var/www/html/cms.yourdomain.com
rm -rf grav-admin

Test it

You should be able to access the default page now at cms.yourdomain.com, and the admin panel at /admin. The admin panel will ask you set set a password and from there you’re free to move on to picking themes and adding content

Resources


Last modified May 8, 2026: Fixed links (04b3f1e)