Skip to content

Commit 44a5b1b

Browse files
committed
nixos/grav: init module
1 parent f555744 commit 44a5b1b

File tree

6 files changed

+367
-0
lines changed

6 files changed

+367
-0
lines changed

nixos/doc/manual/release-notes/rl-2505.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@
123123

124124
- [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).
125125

126+
- [grav](https://getgrav.org/), a modern flat-file CMS. Available with [services.grav](options.html#opt-services.grav.enable).
127+
126128
- [duckdns](https://www.duckdns.org), free dynamic DNS. Available with [services.duckdns](options.html#opt-services.duckdns.enable)
127129

128130
- [victorialogs][https://docs.victoriametrics.com/victorialogs/], log database from VictoriaMetrics. Available as [services.victorialogs](#opt-services.victorialogs.enable)

nixos/modules/module-list.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,7 @@
15001500
./services/web-apps/glance.nix
15011501
./services/web-apps/gotify-server.nix
15021502
./services/web-apps/gotosocial.nix
1503+
./services/web-apps/grav.nix
15031504
./services/web-apps/grocy.nix
15041505
./services/web-apps/pixelfed.nix
15051506
./services/web-apps/goatcounter.nix
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
8+
let
9+
10+
inherit (lib)
11+
generators
12+
mapAttrs
13+
mkDefault
14+
mkEnableOption
15+
mkIf
16+
mkPackageOption
17+
mkOption
18+
types
19+
;
20+
21+
cfg = config.services.grav;
22+
23+
yamlFormat = pkgs.formats.yaml { };
24+
25+
poolName = "grav";
26+
27+
servedRoot = pkgs.runCommand "grav-served-root" { } ''
28+
cp --reflink=auto --no-preserve=mode -r ${cfg.package} $out
29+
30+
for p in assets images user system/config; do
31+
rm -rf $out/$p
32+
ln -sf /var/lib/grav/$p $out/$p
33+
done
34+
'';
35+
36+
systemSettingsYaml = yamlFormat.generate "grav-settings.yaml" cfg.systemSettings;
37+
38+
in
39+
{
40+
options.services.grav = {
41+
enable = mkEnableOption "grav";
42+
43+
package = mkPackageOption pkgs "grav" { };
44+
45+
root = mkOption {
46+
type = types.path;
47+
default = "/var/lib/grav";
48+
description = ''
49+
Root of the application.
50+
'';
51+
};
52+
53+
pool = mkOption {
54+
type = types.str;
55+
default = "${poolName}";
56+
description = ''
57+
Name of existing phpfpm pool that is used to run web-application.
58+
If not specified a pool will be created automatically with
59+
default values.
60+
'';
61+
};
62+
63+
virtualHost = mkOption {
64+
type = types.nullOr types.str;
65+
default = "grav";
66+
description = ''
67+
Name of the nginx virtualhost to use and setup. If null, do not setup
68+
any virtualhost.
69+
'';
70+
};
71+
72+
phpPackage = mkPackageOption pkgs "php" { };
73+
74+
maxUploadSize = mkOption {
75+
type = types.str;
76+
default = "128M";
77+
description = ''
78+
The upload limit for files. This changes the relevant options in
79+
{file}`php.ini` and nginx if enabled.
80+
'';
81+
};
82+
83+
systemSettings = mkOption {
84+
type = yamlFormat.type;
85+
default = {
86+
log = {
87+
handler = "syslog";
88+
};
89+
};
90+
description = ''
91+
Settings written to {file}`user/config/system.yaml`.
92+
'';
93+
};
94+
};
95+
96+
config = mkIf cfg.enable {
97+
services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
98+
${poolName} = {
99+
user = "grav";
100+
group = "grav";
101+
102+
phpPackage = cfg.phpPackage.buildEnv {
103+
extensions =
104+
{ all, enabled }:
105+
with all;
106+
[
107+
apcu
108+
ctype
109+
curl
110+
dom
111+
exif
112+
filter
113+
gd
114+
mbstring
115+
opcache
116+
openssl
117+
session
118+
simplexml
119+
xml
120+
yaml
121+
zip
122+
];
123+
124+
extraConfig = generators.toKeyValue { mkKeyValue = generators.mkKeyValueDefault { } " = "; } {
125+
output_buffering = "0";
126+
short_open_tag = "Off";
127+
expose_php = "Off";
128+
error_reporting = "E_ALL";
129+
display_errors = "stderr";
130+
"opcache.interned_strings_buffer" = "8";
131+
"opcache.max_accelerated_files" = "10000";
132+
"opcache.memory_consumption" = "128";
133+
"opcache.revalidate_freq" = "1";
134+
"opcache.fast_shutdown" = "1";
135+
"openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt";
136+
catch_workers_output = "yes";
137+
138+
upload_max_filesize = cfg.maxUploadSize;
139+
post_max_size = cfg.maxUploadSize;
140+
memory_limit = cfg.maxUploadSize;
141+
"apc.enable_cli" = "1";
142+
};
143+
};
144+
145+
phpEnv = {
146+
GRAV_ROOT = toString servedRoot;
147+
GRAV_SYSTEM_PATH = "${servedRoot}/system";
148+
GRAV_CACHE_PATH = "/var/cache/grav";
149+
GRAV_BACKUP_PATH = "/var/lib/grav/backup";
150+
GRAV_LOG_PATH = "/var/log/grav";
151+
GRAV_TMP_PATH = "/var/tmp/grav";
152+
};
153+
154+
settings = mapAttrs (name: mkDefault) {
155+
"listen.owner" = config.services.nginx.user;
156+
"listen.group" = config.services.nginx.group;
157+
"listen.mode" = "0600";
158+
"pm" = "dynamic";
159+
"pm.max_children" = 75;
160+
"pm.start_servers" = 10;
161+
"pm.min_spare_servers" = 5;
162+
"pm.max_spare_servers" = 20;
163+
"pm.max_requests" = 500;
164+
"catch_workers_output" = 1;
165+
};
166+
};
167+
};
168+
169+
services.nginx = mkIf (cfg.virtualHost != null) {
170+
enable = true;
171+
virtualHosts = {
172+
${cfg.virtualHost} = {
173+
root = "${servedRoot}";
174+
175+
locations = {
176+
"= /robots.txt" = {
177+
priority = 100;
178+
extraConfig = ''
179+
allow all;
180+
access_log off;
181+
'';
182+
};
183+
184+
"~ \\.php$" = {
185+
priority = 200;
186+
extraConfig = ''
187+
fastcgi_split_path_info ^(.+\.php)(/.+)$;
188+
fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
189+
fastcgi_index index.php;
190+
'';
191+
};
192+
193+
"~* /(\\.git|cache|bin|logs|backup|tests)/.*$" = {
194+
priority = 300;
195+
extraConfig = ''
196+
return 403;
197+
'';
198+
};
199+
200+
# deny running scripts inside core system folders
201+
"~* /(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)$" =
202+
{
203+
priority = 300;
204+
extraConfig = ''
205+
return 403;
206+
'';
207+
};
208+
209+
# deny running scripts inside user folder
210+
"~* /user/.*\\.(txt|md|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$" = {
211+
priority = 300;
212+
extraConfig = ''
213+
return 403;
214+
'';
215+
};
216+
217+
# deny access to specific files in the root folder
218+
"~ /(LICENSE\\.txt|composer\\.lock|composer\\.json|nginx\\.conf|web\\.config|htaccess\\.txt|\\.htaccess)" =
219+
{
220+
priority = 300;
221+
extraConfig = ''
222+
return 403;
223+
'';
224+
};
225+
226+
# deny all files and folder beginning with a dot (hidden files & folders)
227+
"~ (^|/)\\." = {
228+
priority = 300;
229+
extraConfig = ''
230+
return 403;
231+
'';
232+
};
233+
234+
"/" = {
235+
priority = 400;
236+
index = "index.php";
237+
extraConfig = ''
238+
try_files $uri $uri/ /index.php?$query_string;
239+
'';
240+
};
241+
};
242+
243+
extraConfig = ''
244+
index index.php index.html /index.php$request_uri;
245+
add_header X-Content-Type-Options nosniff;
246+
add_header X-XSS-Protection "1; mode=block";
247+
add_header X-Download-Options noopen;
248+
add_header X-Permitted-Cross-Domain-Policies none;
249+
add_header X-Frame-Options sameorigin;
250+
add_header Referrer-Policy no-referrer;
251+
client_max_body_size ${cfg.maxUploadSize};
252+
fastcgi_buffers 64 4K;
253+
fastcgi_hide_header X-Powered-By;
254+
gzip on;
255+
gzip_vary on;
256+
gzip_comp_level 4;
257+
gzip_min_length 256;
258+
gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
259+
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;
260+
'';
261+
};
262+
};
263+
};
264+
265+
systemd.tmpfiles.rules =
266+
let
267+
datadir = "/var/lib/grav";
268+
in
269+
map (dir: "d '${dir}' 0750 grav grav - -") [
270+
"/var/cache/grav"
271+
"${datadir}/assets"
272+
"${datadir}/backup"
273+
"${datadir}/images"
274+
"${datadir}/system/config"
275+
"${datadir}/user/accounts"
276+
"${datadir}/user/config"
277+
"${datadir}/user/data"
278+
"/var/log/grav"
279+
]
280+
++ [ "L+ ${datadir}/user/config/system.yaml - - - - ${systemSettingsYaml}" ];
281+
282+
systemd.services = {
283+
"phpfpm-${poolName}" = mkIf (cfg.pool == "${poolName}") {
284+
restartTriggers = [
285+
servedRoot
286+
systemSettingsYaml
287+
];
288+
289+
serviceConfig = {
290+
ExecStartPre = pkgs.writeShellScript "grav-pre-start" ''
291+
function setPermits() {
292+
chmod -R o-rx "$1"
293+
chown -R grav:grav "$1"
294+
}
295+
296+
tmpDir=/var/tmp/grav
297+
dataDir=/var/lib/grav
298+
299+
mkdir $tmpDir
300+
setPermits $tmpDir
301+
302+
for path in config/site.yaml pages plugins themes; do
303+
fullPath="$dataDir/user/$path"
304+
if [[ ! -e $fullPath ]]; then
305+
cp --reflink=auto --no-preserve=mode -r \
306+
${cfg.package}/user/$path $fullPath
307+
fi
308+
setPermits $fullPath
309+
done
310+
311+
systemConfigDir=$dataDir/system/config
312+
if [[ ! -e $systemConfigDir/system.yaml ]]; then
313+
cp --reflink=auto --no-preserve=mode -r \
314+
${cfg.package}/system/config/* $systemConfigDir/
315+
fi
316+
setPermits $systemConfigDir
317+
'';
318+
};
319+
};
320+
};
321+
322+
users.users.grav = {
323+
isSystemUser = true;
324+
description = "Grav service user";
325+
home = "/var/lib/grav";
326+
group = "grav";
327+
};
328+
329+
users.groups.grav = {
330+
members = [ config.services.nginx.user ];
331+
};
332+
};
333+
}

nixos/tests/all-tests.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ in {
414414
grafana = handleTest ./grafana {};
415415
grafana-agent = handleTest ./grafana-agent.nix {};
416416
graphite = handleTest ./graphite.nix {};
417+
grav = runTest ./web-apps/grav.nix;
417418
graylog = handleTest ./graylog.nix {};
418419
greetd-no-shadow = handleTest ./greetd-no-shadow.nix {};
419420
grocy = handleTest ./grocy.nix {};

nixos/tests/web-apps/grav.nix

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{ pkgs, ... }:
2+
{
3+
name = "grav";
4+
5+
nodes = {
6+
machine =
7+
{ pkgs, ... }:
8+
{
9+
services.grav.enable = true;
10+
};
11+
};
12+
13+
testScript = ''
14+
start_all()
15+
machine.wait_for_unit("phpfpm-grav.service")
16+
machine.wait_for_open_port(80)
17+
18+
# The first request to a fresh install should result in a redirect to the
19+
# admin page, where the user is expected to set up an admin user.
20+
actual = machine.succeed("curl -v --stderr - http://localhost/", timeout=10).splitlines()
21+
expected = "< Location: /admin"
22+
assert expected in actual, \
23+
f"unexpected reply from Grav: '{actual}'"
24+
'';
25+
}

0 commit comments

Comments
 (0)