#PHP #Webapplications in #NixOS are a bit special, as they commonly violate the split between configuration, data and application. Sometimes it's all in the same directory but more commonly it's a subdirectory that contains the data. Packaging the sources can be easy or complicated, depending on wether there is some build process. For Shaarli I just use their full.tar.gz and don't have to worry about that.
The package expression is very basic:
{ lib, stdenv, fetchurl, config ? null, dataDir ? "/var/lib/shaarli" }:
stdenv.mkDerivation rec {
name = "shaarli-${version}";
version = "0.11.1";
preferLocalBuild = true;
src = fetchurl {
url = "https://github.com/shaarli/Shaarli/releases/download/v0.11.1/shaarli-v${version}-full.tar.gz";
sha256 = "1psijcmi24hk0gxh1zdsm299xj11i7find2045nnx3r96cgnwjpn";
};
phases = [ "installPhase" ];
installPhase = ''
mkdir $out
tar xzf $src
cp -ra Shaarli/. $out/
find $out -type d -exec chmod 0755 {} \;
find $out -type f -exec chmod 0644 {} \;
for a in cache data pagecache tmp; do
mv $out/$a $out/$a.orig
ln -s "${dataDir}/$a" $out/$a
done
'';
meta = with stdenv.lib; {
description = "";
# License is complicated...
#license = licenses.agpl3;
homepage = "https://github.com/shaarli/Shaarli";
platforms = platforms.all;
maintainers = with stdenv.lib.maintainers; [ tokudan ];
};
}
What's uncommon is that I have two optional arguments: config
and dataDir
. config is not used in my Shaarli derivation and is just part of the boilerplate I use for PHP apps. I use it to feed in a config.php if that makes sense for the PHP app, for example my roundcube config uses it. dataDir
on the other hand is used in the installPhase.
I move away some directories to $a.orig
so the install service can setup the dataDirectory if it doesn't exist yet. It's not perfect, but works for now. Then the directories are replaced with symlinks to /var/lib/shaarli - or whatever was specified in dataDir.
This derivation gives me a package that is specific to one instance of shaarli. If I run a second instance, I need to specify a different dataDir, leading to another build of the derivation.
The second part of the equation is the system configuration. How do I include the above derivation in my system? I use nginx and phppool with specific users for each php app. Here is the part of my system configuration that uses the package:
{ config, lib, pkgs, ... }:
let
phppoolName = "shaarli_pool";
dataDir = "/var/lib/shaarli";
vhost = "shaarli.example.com";
shaarli = pkgs.callPackage ./pkg-shaarli.nix {
inherit dataDir;
};
in
{
services.nginx.virtualHosts."${vhost}" = {
forceSSL = true;
enableACME = true;
root = "${shaarli}";
extraConfig = ''
index index.php;
etag off;
add_header etag "\"${builtins.substring 11 32 shaarli}\"";
'';
locations."/robots.txt" = {
extraConfig = ''
add_header Content-Type text/plain;
return 200 "User-agent: *\nDisallow: /\n";
'';
};
locations."/" = {
extraConfig = ''
try_files $uri $uri/ index.php;
'';
};
locations."~ (index)\.php$" = {
extraConfig = ''
fastcgi_split_path_info ^(.+\.php)(/.*)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
fastcgi_pass unix:${config.services.phpfpm.pools."${vhost}".socket};
fastcgi_index index.php;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param HTTPS $https;
fastcgi_param HTTP_PROXY "";
'';
};
locations."~ \.php$" = {
extraConfig = ''
deny all;
'';
};
};
services.phpfpm.pools."${vhost}" = {
user = "shaarli";
group = "shaarli";
settings = {
"listen.owner" = "nginx";
"listen.group" = "nginx";
"user" = "shaarli";
"pm" = "dynamic";
"pm.max_children" = "75";
"pm.min_spare_servers" = "5";
"pm.max_spare_servers" = "20";
"pm.max_requests" = "10";
"catch_workers_output" = "1";
};
};
users.extraUsers.shaarli = { group = "shaarli"; };
users.extraGroups.shaarli = { };
systemd.services.shaarli-install = {
serviceConfig.Type = "oneshot";
wantedBy = [ "multi-user.target" ];
script = ''
if [ ! -d "${dataDir}" ]; then
mkdir -p ${dataDir}/{cache,data,pagecache,tmp}
cp -R ${shaarli}/data.orig/.htaccess ${dataDir}/cache/
cp -R ${shaarli}/data.orig/.htaccess ${dataDir}/data/
cp -R ${shaarli}/data.orig/.htaccess ${dataDir}/pagecache/
cp -R ${shaarli}/data.orig/.htaccess ${dataDir}/tmp/
fi
chown -Rc shaarli:shaarli ${dataDir}
find ${dataDir} -type d ! -perm 0700 -exec chmod 0700 {} \; -exec chmod g-s {} \;
find ${dataDir} -type f ! -perm 0600 -exec chmod 0600 {} \;
'';
};
}
The let block just defines some variables to be used by the expression, but there are a couple of important options I use below that:
The nginx extraConfig
contains
etag off;
add_header etag "\"${builtins.substring 11 32 shaarli}\"";
This is both nice and bad at the same time: It leaks some information to the outside world by publishing part of the hash of my Shaarli derivation. On the other hand it ensures that Browsers will refresh their caches as needed if I switch to another derivation, as they use that part of the hash to verify if the file on the server has changed and do not rely on the file modification time, which would always be the unix epoch in the nix store.
At the bottom you can see systemd.services.shaarli-install
, which is the service that sets up the data directory when the configuration is activated. Note that with its current implementation it cannot detect if the Shaarli version changed and run any update scripts, but that's hopefully not necessary for Shaarli.
This type of packaging seems to work for most php webapps. It's certainly not perfect and has a lot of redundancies, but for me it gets the job done.