wallabag is a self hostable application for saving web pages: Save and classify articles. Read them later. Freely.
Configurable NixOS Module for Wallabag
This module is based on dwarfmaster/home-nix's
wallabag module. It's a bit verbose and difficult to wrap the head
around, but basically what is happening is that it is generating a parameters.yml
file to configure wallabag,
synthesize some paths merging that configuration with the Wallabag
source code. This built environment is used to create a PHP
application under your dataDir
and wiring up a php-fpm
pool to that directory; finally there is
a systemd
script that ensures that
migrations are run, assets are cached, etc.
I've extended this to be somewhat configurable. I would like to figure out how to make this work without having that assertion, or whether that is even necessary.
{
config,
lib,
pkgs,
...
}:
let
pool = config.services.phpfpm.pools.wallabag;
cfg = config.services.wallabag;
wallabag = cfg.package;
parameters = {
# these can be rewritten to read from ENV with
# %env.database_driver% type of stuff, good for turning these in
# to nixos options
database_driver = cfg.database_type;
database_host = "";
database_port = config.services.postgresql.port;
database_name = "wallabag";
database_user = "wallabag";
database_password = "";
database_path = "";
database_table_prefix = "wallabag_";
database_socket = "/run/postgresql/.s.PGSQL.${toString config.services.postgresql.port}";
database_charset = "utf8";
domain_name = "https://${cfg.domain}";
mailer_dsn = "null://";
from_email = "";
locale = "en_US";
server_name = "Wallabag";
secret = "";
# A secret key that's used to generate certain security-related tokens
# two factor stuff
twofactor_auth = false;
twofactor_sender = "";
# Disable user registration
# See https://github.com/wallabag/wallabag/issues/1873
fosuser_registration = false;
fosuser_confirmation = true;
# how long the access token should live in seconds for the API
fos_oauth_server_access_token_lifetime = 3600;
# how long the refresh token should life in seconds for the API
fos_oauth_server_refresh_token_lifetime = 1209600;
rss_limit = 50;
# RabbitMQ processing
rabbitmq_host = "localhost";
rabbitmq_port = config.services.rabbitmq.port;
rabbitmq_user = "guest";
rabbitmq_password = "guest";
rabbitmq_prefetch_count = 10;
# Redis processing
redis_scheme = "unix";
redis_host = ""; # Ignored for unix scheme
redis_port = 0; # Ignored for unix scheme
redis_path = config.services.redis.servers.wallabag.unixSocket;
redis_password = null;
# sentry logging
sentry_dsn = "";
} // cfg.parameters;
parameters-json = pkgs.writeTextFile {
name = "parameters.json";
text = builtins.toJSON {inherit parameters;};
};
yaml_parameters = pkgs.runCommand
"parameters.yml" {preferLocalBuild = true;} ''
mkdir -p $out/app/config
${pkgs.remarshal}/bin/json2yaml -i ${parameters-json} -o $out/app/config/parameters.yml
'';
appDir = pkgs.buildEnv {
name = "wallabag-app-dir";
ignoreCollisions = true;
checkCollisionContents = false;
paths = [ yaml_parameters "${wallabag}" ];
pathsToLink = [
"/app" "/src" "/translations"
];
};
dataDir = cfg.dataDir;
php = cfg.php.package;
exts = cfg.php.extensions.package;
phpPkgs = cfg.php.packages.package;
# See there for available commands:
# https://doc.wallabag.org/en/admin/console_commands.html
# A user can be made admin with the fos:user:promote --super <user> command
console = pkgs.writeShellScriptBin "wallabag-console" ''
export WALLABAG_DATA="${dataDir}"
cd "${dataDir}"
${php}/bin/php ${wallabag}/bin/console --env=prod $@
'';
in
{
options.services.wallabag = with lib; {
enable = mkEnableOption (mdDoc "Wallabag read-it-later service");
package = mkOption {
type = types.package;
default = pkgs.wallabag;
};
php.package = mkOption {
type = types.package;
default = pkgs.php;
};
php.extensions.package = mkOption {
type = types.attrsOf types.package;
default = pkgs.php.extensions;
};
php.packages.package = mkOption {
type = types.attrsOf types.package;
default = pkgs.php.packages;
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/wallabag";
description = mdDoc ''
Location which Wallabag will install itself and place cache files, etc within.
'';
};
parameters = mkOption {
type = types.attrsOf types.str;
default = {};
description = mdDoc "Parameters to override from the default. See <https://doc.wallabag.org/en/admin/parameters.html> for values.";
};
database_type = mkOption {
type = types.enum [
"pdo_sqlite3"
"pdo_pgsql"
];
default = if config.services.postgresql.enable
then "pdo_pgsql"
else "pdo_sqlite3";
defaultText = ''
if config.services.postgresql.enable
then "pdo_pgsql"
else "pdo_sqlite3"
'';
description = mdDoc ''
The database engine name. Can be pdo_sqlite3 or pdo_pgsql.
'';
};
domain = mkOption {
type = types.str;
description = "Bare domain name for Wallabag";
};
virtualHost.enable = mkEnableOption (mdDoc "Define nginx virtualhost for Wallabag");
};
config =
lib.mkIf cfg.enable{
# Wallabag config yml files needs to be recreated at each update, and
# var/cache needs to be cleared between restart
# assertions = [
# {
# assertion = wallabag.version == "2.6.6";
# message = "Wallabag update to ${wallabag.version} needs manual intervention";
# }
# ];
# Install console manager
environment.systemPackages = [console];
# Inspired by https://doc.wallabag.org/ens/admin/installation/virtualhosts.html
services.nginx.virtualHosts."${cfg.domain}" = lib.mkIf cfg.virtualHost.enable {
# forceSSL = true;
# enableACME = true;
root = "${wallabag}/web";
locations."/" = {
priority = 10;
tryFiles = "$uri /app.php$is_args$args";
};
locations."~ ^/app\\.php(/|$)" = {
priority = 100;
fastcgiParams = {
SCRIPT_FILENAME = "$realpath_root$fastcgi_script_name";
DOCUMENT_ROOT = "$realpath_root";
};
extraConfig = ''
fastcgi_pass unix:${pool.socket};
include ${config.services.nginx.package}/conf/fastcgi_params;
include ${config.services.nginx.package}/conf/fastcgi.conf;
internal;
'';
};
locations."~ \\.php$" = {
priority = 1000;
return = "404";
};
};
# PHP
services.redis.servers.wallabag = {
enable = true;
user = "wallabag";
};
services.phpfpm.pools.wallabag = {
user = "wallabag";
group = "wallabag";
phpPackage = php;
phpEnv = {
WALLABAG_DATA = dataDir;
PATH = lib.makeBinPath [php];
};
settings = {
"listen.owner" = config.services.nginx.user;
"pm" = "dynamic";
"pm.max_children" = 32;
"pm.max_requests" = 500;
"pm.start_servers" = 1;
"pm.min_spare_servers" = 1;
"pm.max_spare_servers" = 5;
"php_admin_value[error_log]" = "stderr";
"php_admin_flag[log_errors]" = true;
"catch_workers_output" = true;
};
phpOptions = ''
extension=${exts.pdo}/lib/php/extensions/pdo.so
extension=${exts.pdo_pgsql}/lib/php/extensions/pdo_pgsql.so
extension=${exts.session}/lib/php/extensions/session.so
extension=${exts.ctype}/lib/php/extensions/ctype.so
extension=${exts.dom}/lib/php/extensions/dom.so
extension=${exts.simplexml}/lib/php/extensions/simplexml.so
extension=${exts.gd}/lib/php/extensions/gd.so
extension=${exts.mbstring}/lib/php/extensions/mbstring.so
extension=${exts.xml}/lib/php/extensions/xml.so
extension=${exts.tidy}/lib/php/extensions/tidy.so
extension=${exts.iconv}/lib/php/extensions/iconv.so
extension=${exts.curl}/lib/php/extensions/curl.so
extension=${exts.gettext}/lib/php/extensions/gettext.so
extension=${exts.tokenizer}/lib/php/extensions/tokenizer.so
extension=${exts.bcmath}/lib/php/extensions/bcmath.so
extension=${exts.intl}/lib/php/extensions/intl.so
extension=${exts.opcache}/lib/php/extensions/opcache.so
'';
};
# PostgreSQL Database
services.postgresql = lib.mkIf (cfg.database_type == "pdo_pgsql"){
ensureDatabases = ["wallabag"];
# Wallabag does not support passwordless login into database,
# so the database password for the user must be manually set
ensureUsers = [
{
name = "wallabag";
ensurePermissions."DATABASE wallabag" = "ALL PRIVILEGES";
}
];
};
# Data directory
systemd.tmpfiles.rules = let
user = "wallabag";
in ["d ${dataDir} 0700 ${user} ${user} - -"];
systemd.services."wallabag-setup" = {
description = "Wallabag install service";
wantedBy = ["multi-user.target"];
before = ["phpfpm-wallabag.service"];
requiredBy = ["phpfpm-wallabag.service"];
after = ["postgresql.service"];
path = [pkgs.coreutils php phpPkgs.composer];
serviceConfig = {
User = "wallabag";
Group = "wallabag";
Type = "oneshot";
RemainAfterExit = "yes";
PermissionsStartOnly = true;
Environment = "WALLABAG_DATA=${dataDir}";
};
script = ''
echo "Setting up wallabag files in ${dataDir} ..."
cd "${dataDir}"
rm -rf var/cache/*
rm -f app
ln -sf ${appDir}/app app
rm -f src
ln -sf ${appDir}/src src
rm -f translations
ln -sf ${appDir}/translations translations
ln -sf ${wallabag}/composer.{json,lock} .
if [ ! -f installed ]; then
echo "Installing wallabag"
php ${wallabag}/bin/console --env=prod wallabag:install --no-interaction
touch installed
else
php ${wallabag}/bin/console --env=prod doctrine:migrations:migrate --no-interaction
fi
php ${wallabag}/bin/console --env=prod cache:clear
'';
};
# Misc settings
services.rabbitmq.enable = false;
users.users.wallabag = {
isSystemUser = true;
group = "wallabag";
};
users.groups.wallabag = {};
};
}
Using this module in The Wobserver configuration
I tried to make that module configurable so that others could use it, but your mileage may vary. I have no intention of upstreaming this to nixpkgs because I think it needs quite some work, but it's good enough for the CCE.
{ pkgs, ... }:
{
imports = [ ./wallabag-mod.nix ./wallabag-secrets.nix ];
services.wallabag = {
enable = true;
dataDir = "/srv/wallabag";
domain = "bag.fontkeming.fail";
virtualHost.enable = true;
parameters = {
server_name = "rrix's Back-log Black-hole";
twofactor_sender = "wallabag@fontkeming.fail";
locale = "en_US";
from_email = "wallabag@fontkeming.fail";
};
};
services.nginx.virtualHosts."bag.fontkeming.fail".extraConfig = ''
error_log /var/log/nginx/wallabag_error.log;
access_log /var/log/nginx/wallabag_access.log;
'';
}
Make sure to define services.wallabag.parameters.secret
too. I won't
show you mine unless the following NOEXPORT
heading exports. ;)