nixos/grav: init module

Changed files
+367
nixos
doc
manual
release-notes
modules
services
web-apps
tests
pkgs
by-name
gr
+2
nixos/doc/manual/release-notes/rl-2505.section.md
···
- [nfc-nci](https://github.com/StarGate01/ifdnfc-nci), an alternative NFC stack and PC/SC driver for the NXP PN54x chipset, commonly found in Lenovo systems as NXP1001 (NPC300). Available as [hardware.nfc-nci](#opt-hardware.nfc-nci.enable).
+
- [grav](https://getgrav.org/), a modern flat-file CMS. Available with [services.grav](options.html#opt-services.grav.enable).
+
- [duckdns](https://www.duckdns.org), free dynamic DNS. Available with [services.duckdns](options.html#opt-services.duckdns.enable)
- [victorialogs][https://docs.victoriametrics.com/victorialogs/], log database from VictoriaMetrics. Available as [services.victorialogs](#opt-services.victorialogs.enable)
+1
nixos/modules/module-list.nix
···
./services/web-apps/glance.nix
./services/web-apps/gotify-server.nix
./services/web-apps/gotosocial.nix
+
./services/web-apps/grav.nix
./services/web-apps/grocy.nix
./services/web-apps/pixelfed.nix
./services/web-apps/goatcounter.nix
+333
nixos/modules/services/web-apps/grav.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
+
let
+
+
inherit (lib)
+
generators
+
mapAttrs
+
mkDefault
+
mkEnableOption
+
mkIf
+
mkPackageOption
+
mkOption
+
types
+
;
+
+
cfg = config.services.grav;
+
+
yamlFormat = pkgs.formats.yaml { };
+
+
poolName = "grav";
+
+
servedRoot = pkgs.runCommand "grav-served-root" { } ''
+
cp --reflink=auto --no-preserve=mode -r ${cfg.package} $out
+
+
for p in assets images user system/config; do
+
rm -rf $out/$p
+
ln -sf /var/lib/grav/$p $out/$p
+
done
+
'';
+
+
systemSettingsYaml = yamlFormat.generate "grav-settings.yaml" cfg.systemSettings;
+
+
in
+
{
+
options.services.grav = {
+
enable = mkEnableOption "grav";
+
+
package = mkPackageOption pkgs "grav" { };
+
+
root = mkOption {
+
type = types.path;
+
default = "/var/lib/grav";
+
description = ''
+
Root of the application.
+
'';
+
};
+
+
pool = mkOption {
+
type = types.str;
+
default = "${poolName}";
+
description = ''
+
Name of existing phpfpm pool that is used to run web-application.
+
If not specified a pool will be created automatically with
+
default values.
+
'';
+
};
+
+
virtualHost = mkOption {
+
type = types.nullOr types.str;
+
default = "grav";
+
description = ''
+
Name of the nginx virtualhost to use and setup. If null, do not setup
+
any virtualhost.
+
'';
+
};
+
+
phpPackage = mkPackageOption pkgs "php" { };
+
+
maxUploadSize = mkOption {
+
type = types.str;
+
default = "128M";
+
description = ''
+
The upload limit for files. This changes the relevant options in
+
{file}`php.ini` and nginx if enabled.
+
'';
+
};
+
+
systemSettings = mkOption {
+
type = yamlFormat.type;
+
default = {
+
log = {
+
handler = "syslog";
+
};
+
};
+
description = ''
+
Settings written to {file}`user/config/system.yaml`.
+
'';
+
};
+
};
+
+
config = mkIf cfg.enable {
+
services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
+
${poolName} = {
+
user = "grav";
+
group = "grav";
+
+
phpPackage = cfg.phpPackage.buildEnv {
+
extensions =
+
{ all, enabled }:
+
with all;
+
[
+
apcu
+
ctype
+
curl
+
dom
+
exif
+
filter
+
gd
+
mbstring
+
opcache
+
openssl
+
session
+
simplexml
+
xml
+
yaml
+
zip
+
];
+
+
extraConfig = generators.toKeyValue { mkKeyValue = generators.mkKeyValueDefault { } " = "; } {
+
output_buffering = "0";
+
short_open_tag = "Off";
+
expose_php = "Off";
+
error_reporting = "E_ALL";
+
display_errors = "stderr";
+
"opcache.interned_strings_buffer" = "8";
+
"opcache.max_accelerated_files" = "10000";
+
"opcache.memory_consumption" = "128";
+
"opcache.revalidate_freq" = "1";
+
"opcache.fast_shutdown" = "1";
+
"openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt";
+
catch_workers_output = "yes";
+
+
upload_max_filesize = cfg.maxUploadSize;
+
post_max_size = cfg.maxUploadSize;
+
memory_limit = cfg.maxUploadSize;
+
"apc.enable_cli" = "1";
+
};
+
};
+
+
phpEnv = {
+
GRAV_ROOT = toString servedRoot;
+
GRAV_SYSTEM_PATH = "${servedRoot}/system";
+
GRAV_CACHE_PATH = "/var/cache/grav";
+
GRAV_BACKUP_PATH = "/var/lib/grav/backup";
+
GRAV_LOG_PATH = "/var/log/grav";
+
GRAV_TMP_PATH = "/var/tmp/grav";
+
};
+
+
settings = mapAttrs (name: mkDefault) {
+
"listen.owner" = config.services.nginx.user;
+
"listen.group" = config.services.nginx.group;
+
"listen.mode" = "0600";
+
"pm" = "dynamic";
+
"pm.max_children" = 75;
+
"pm.start_servers" = 10;
+
"pm.min_spare_servers" = 5;
+
"pm.max_spare_servers" = 20;
+
"pm.max_requests" = 500;
+
"catch_workers_output" = 1;
+
};
+
};
+
};
+
+
services.nginx = mkIf (cfg.virtualHost != null) {
+
enable = true;
+
virtualHosts = {
+
${cfg.virtualHost} = {
+
root = "${servedRoot}";
+
+
locations = {
+
"= /robots.txt" = {
+
priority = 100;
+
extraConfig = ''
+
allow all;
+
access_log off;
+
'';
+
};
+
+
"~ \\.php$" = {
+
priority = 200;
+
extraConfig = ''
+
fastcgi_split_path_info ^(.+\.php)(/.+)$;
+
fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
+
fastcgi_index index.php;
+
'';
+
};
+
+
"~* /(\\.git|cache|bin|logs|backup|tests)/.*$" = {
+
priority = 300;
+
extraConfig = ''
+
return 403;
+
'';
+
};
+
+
# deny running scripts inside core system folders
+
"~* /(system|vendor)/.*\\.(txt|xml|md|html|htm|shtml|shtm|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$" =
+
{
+
priority = 300;
+
extraConfig = ''
+
return 403;
+
'';
+
};
+
+
# deny running scripts inside user folder
+
"~* /user/.*\\.(txt|md|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$" = {
+
priority = 300;
+
extraConfig = ''
+
return 403;
+
'';
+
};
+
+
# deny access to specific files in the root folder
+
"~ /(LICENSE\\.txt|composer\\.lock|composer\\.json|nginx\\.conf|web\\.config|htaccess\\.txt|\\.htaccess)" =
+
{
+
priority = 300;
+
extraConfig = ''
+
return 403;
+
'';
+
};
+
+
# deny all files and folder beginning with a dot (hidden files & folders)
+
"~ (^|/)\\." = {
+
priority = 300;
+
extraConfig = ''
+
return 403;
+
'';
+
};
+
+
"/" = {
+
priority = 400;
+
index = "index.php";
+
extraConfig = ''
+
try_files $uri $uri/ /index.php?$query_string;
+
'';
+
};
+
};
+
+
extraConfig = ''
+
index index.php index.html /index.php$request_uri;
+
add_header X-Content-Type-Options nosniff;
+
add_header X-XSS-Protection "1; mode=block";
+
add_header X-Download-Options noopen;
+
add_header X-Permitted-Cross-Domain-Policies none;
+
add_header X-Frame-Options sameorigin;
+
add_header Referrer-Policy no-referrer;
+
client_max_body_size ${cfg.maxUploadSize};
+
fastcgi_buffers 64 4K;
+
fastcgi_hide_header X-Powered-By;
+
gzip on;
+
gzip_vary on;
+
gzip_comp_level 4;
+
gzip_min_length 256;
+
gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
+
gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
+
'';
+
};
+
};
+
};
+
+
systemd.tmpfiles.rules =
+
let
+
datadir = "/var/lib/grav";
+
in
+
map (dir: "d '${dir}' 0750 grav grav - -") [
+
"/var/cache/grav"
+
"${datadir}/assets"
+
"${datadir}/backup"
+
"${datadir}/images"
+
"${datadir}/system/config"
+
"${datadir}/user/accounts"
+
"${datadir}/user/config"
+
"${datadir}/user/data"
+
"/var/log/grav"
+
]
+
++ [ "L+ ${datadir}/user/config/system.yaml - - - - ${systemSettingsYaml}" ];
+
+
systemd.services = {
+
"phpfpm-${poolName}" = mkIf (cfg.pool == "${poolName}") {
+
restartTriggers = [
+
servedRoot
+
systemSettingsYaml
+
];
+
+
serviceConfig = {
+
ExecStartPre = pkgs.writeShellScript "grav-pre-start" ''
+
function setPermits() {
+
chmod -R o-rx "$1"
+
chown -R grav:grav "$1"
+
}
+
+
tmpDir=/var/tmp/grav
+
dataDir=/var/lib/grav
+
+
mkdir $tmpDir
+
setPermits $tmpDir
+
+
for path in config/site.yaml pages plugins themes; do
+
fullPath="$dataDir/user/$path"
+
if [[ ! -e $fullPath ]]; then
+
cp --reflink=auto --no-preserve=mode -r \
+
${cfg.package}/user/$path $fullPath
+
fi
+
setPermits $fullPath
+
done
+
+
systemConfigDir=$dataDir/system/config
+
if [[ ! -e $systemConfigDir/system.yaml ]]; then
+
cp --reflink=auto --no-preserve=mode -r \
+
${cfg.package}/system/config/* $systemConfigDir/
+
fi
+
setPermits $systemConfigDir
+
'';
+
};
+
};
+
};
+
+
users.users.grav = {
+
isSystemUser = true;
+
description = "Grav service user";
+
home = "/var/lib/grav";
+
group = "grav";
+
};
+
+
users.groups.grav = {
+
members = [ config.services.nginx.user ];
+
};
+
};
+
}
+1
nixos/tests/all-tests.nix
···
grafana = handleTest ./grafana {};
grafana-agent = handleTest ./grafana-agent.nix {};
graphite = handleTest ./graphite.nix {};
+
grav = runTest ./web-apps/grav.nix;
graylog = handleTest ./graylog.nix {};
greetd-no-shadow = handleTest ./greetd-no-shadow.nix {};
grocy = handleTest ./grocy.nix {};
+25
nixos/tests/web-apps/grav.nix
···
+
{ pkgs, ... }:
+
{
+
name = "grav";
+
+
nodes = {
+
machine =
+
{ pkgs, ... }:
+
{
+
services.grav.enable = true;
+
};
+
};
+
+
testScript = ''
+
start_all()
+
machine.wait_for_unit("phpfpm-grav.service")
+
machine.wait_for_open_port(80)
+
+
# The first request to a fresh install should result in a redirect to the
+
# admin page, where the user is expected to set up an admin user.
+
actual = machine.succeed("curl -v --stderr - http://localhost/", timeout=10).splitlines()
+
expected = "< Location: /admin"
+
assert expected in actual, \
+
f"unexpected reply from Grav: '{actual}'"
+
'';
+
}
+5
pkgs/by-name/gr/grav/package.nix
···
stdenvNoCC,
lib,
fetchzip,
+
nixosTests,
}:
let
···
cp -R . $out/
runHook postInstall
'';
+
+
passthru.tests = {
+
grav = nixosTests.grav;
+
};
meta = with lib; {
description = "Fast, simple, and flexible, file-based web platform";