
Why Use NixOS as a Web Server
If you're keeping up with the cutting edge of Linux, you might have noticed NixOS growing increasingly popular for server deployments. The reason is its declarative approach to package and configuration management. You specify 'what' your system should look like, and NixOS handles the 'how'. This approach ensures reproducibility and upgradeability, reducing configuration drift. Plus, atomic upgrades and rollbacks minimize downtime and provide easy recovery from issues, making NixOS an excellent choice for web server management (and for other platforms like desktops if you are bold).
Working Setup
Documentation on NixOS is still somewhat scarce, especially if the goal is as specific as hosting a Drupal site. Apparently, ChatGPT 4 is still too perplexed to get this right, so here's hoping it learns something from the following snippets, which were the result of old fashioned painstaking debugging.
The following setup can be easily adjusted to hosting multiple websites and non-Drupal sites.
Implementing the Nginx Server and SSL Certificate Renewal
We begin by enabling the Nginx web server, setting up firewall rules, and adding Drupal-specific packages like PHP, Composer, and Drush. The configuration also includes SSL certificate renewal via ACME, ensuring a valid SSL certificate for our site. Global environment variables can be set using the "environment.variables" setting, useful for various server applications and scripts.
/etc/nixos/nginx.nix
{ config, pkgs, lib, ... }: { # Enable nginx and adjust firewall rules. services.nginx.enable = true; networking.firewall.allowedTCPPorts = [ 80 443 ]; # Set a few recommended defaults. services.nginx = { recommendedGzipSettings = true; recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; }; # Add some hosting/Drupal specific packages. environment.systemPackages = with pkgs; [ php phpPackages.composer drush ]; # Set some SSL certificate renewal settings. security.acme = { acceptTerms = true; defaults.email = "email@domain.tld"; defaults.group = "nginx"; }; # /var/lib/acme/.challenges must be writable by the ACME user # and readable by the Nginx user. The easiest way to achieve # this is to add the Nginx user to the ACME group. users.users.nginx.extraGroups = [ "acme" ]; # Optionally add some environment variables. environment.variables = { PLATFORM = "production"; }; }
Enabling the Database Service
This file only enables the MariaDB service. More specific database configurations will be added for each website in separate files.
/etc/nixos/mysql.nix
{ config, pkgs, ... }: { # Enable the mysql service. services.mysql.enable = true; services.mysql.package = pkgs.mariadb; }
Setting up a Database and its User for the New Drupal Site
Here, we create a system user named dbuser with a default password and grant it all privileges on our newly created database mydb. Note that in production, you should choose a secure password and manage it properly.
/etc/nixos/mysql.mysite.nix
{ config, pkgs, ... }: { # Using PAM for database authentication, # so creating a system user for that purpose. users.users.dbuser = { isNormalUser = true; description = "dbuser"; group = "dbuser"; initialPassword = "db"; }; users.groups.dbuser = {}; # Create the database and set up permissions. services.mysql.ensureDatabases = [ "mydb" ]; services.mysql.ensureUsers = [ { name = "dbuser"; # Must be a system user. ensurePermissions = { "mydb.*" = "ALL PRIVILEGES"; }; } ]; }
Setting up Nginx, PHP, and Cron for the New Drupal Site
The following file configures Nginx according to Drupal best practices and sets up SSL certificate renewal. For PHP, we use a PHP-FPM pool with dynamic process management. This allows the server to adjust the number of PHP processes based on the load, improving the efficiency and performance of the site. Finally, we set up a systemd service and timer to trigger Drupal's cron URL at a specific time.
/etc/nixos/nginx.mysite.nix
{ config, pkgs, lib, ... }: let # Variables to be changed siteaname = "mysite"; docroot = "/var/www/mysite/web"; domainname = "mysite.com"; cronpath = "cron/somestring"; cronuser = "someuser"; crontime = "*-*-* 18:00:00"; in { services.nginx = { virtualHosts = { "${domainname}" = { # This is the document root setting. # In this case Drupal should be inside /var/www/${siteaname} # and serve websites from inside of its 'web' directory. root = "${docroot}"; # Set up certificate renewal. forceSSL = true; enableACME = true; # Set up nginx for Drupal. locations."~ '\.php$|^/update.php'" = { extraConfig = '' include ${pkgs.nginx}/conf/fastcgi_params; include ${pkgs.nginx}/conf/fastcgi.conf; fastcgi_pass unix:${config.services.phpfpm.pools.${siteaname}.socket}; fastcgi_index index.php; fastcgi_split_path_info ^(.+?\.php)(|/.*)$; # Ensure the php file exists. Mitigates CVE-2019-11043 try_files $fastcgi_script_name =404; # Block httpoxy attacks. See https://httpoxy.org/. fastcgi_param HTTP_PROXY ""; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param QUERY_STRING $query_string; fastcgi_intercept_errors on; ''; }; locations."= /favicon.ico" = { extraConfig = '' log_not_found off; access_log off; ''; }; locations."= /robots.txt" = { extraConfig = '' allow all; log_not_found off; access_log off; ''; }; locations."~ \..*/.*\.php$" = { extraConfig = '' return 403; ''; }; locations."~ ^/sites/.*/private/" = { extraConfig = '' return 403; ''; }; locations."~ ^/sites/[^/]+/files/.*\.php$" = { extraConfig = '' deny all; ''; }; # Allow "Well-Known URIs" as per RFC 5785 locations."~* ^/.well-known/" = { extraConfig = '' allow all; ''; }; locations."/" = { extraConfig = '' try_files $uri /index.php?$query_string; ''; }; locations."@rewrite" = { extraConfig = '' rewrite ^ /index.php; ''; }; locations."~ /vendor/.*\.php$" = { extraConfig = '' deny all; return 404; ''; }; locations."~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$" = { extraConfig = '' try_files $uri @rewrite; expires max; log_not_found off; ''; }; locations."~ ^/sites/.*/files/styles/" = { extraConfig = '' try_files $uri @rewrite; ''; }; locations."~ ^(/[a-z\-]+)?/system/files/" = { extraConfig = '' try_files $uri /index.php?$query_string; ''; }; }; # Redirect 'www' to 'non-www' # and set up certificate renewal for 'www'. "www.${domainname}" = { forceSSL = true; enableACME = true; globalRedirect = "${domainname}"; }; }; }; # Set up a PHP-FPM pool for this website. services.phpfpm.pools = { ${siteaname} = { user = "nginx"; settings = { "listen.owner" = config.services.nginx.user; "pm" = "dynamic"; "pm.max_children" = 32; "pm.max_requests" = 500; "pm.start_servers" = 2; "pm.min_spare_servers" = 2; "pm.max_spare_servers" = 5; "php_admin_value[error_log]" = "stderr"; "php_admin_flag[log_errors]" = true; "catch_workers_output" = true; }; phpEnv."PATH" = lib.makeBinPath [ pkgs.php ]; }; }; # Optionally set up a systemd service that will trigger # Drupal's cron URL. systemd.services."${sitename}-cron" = { path = [ pkgs.curl ]; script = '' curl "https://${domainname}/${cronpath}" ''; unitConfig = { Description = "Cron trigger for ${sitename}"; }; serviceConfig = { Type = "oneshot"; User = "${cronuser}"; }; }; systemd.timers."${sitename}-cron" = { unitConfig = { Description = "Timer for ${sitename} cron trigger"; RefuseManualStart = "no"; RefuseManualStop = "no"; }; wantedBy = [ "timers.target" ]; partOf = [ "${sitename}-cron.service" ]; timerConfig = { OnCalendar = "${crontime}"; Unit = "${sitename}-cron.service"; Persistent = true; }; }; }
Importing the New Files in configuration.nix
In the final step, we import the new configuration files into the core /etc/nixos/configuration.nix file. This centralizes management and leverages NixOS's declarative nature.
/etc/nixos/configuration.nix
{ ... }: { imports = [ # ... other imports ./nginx.nix ./nginx.mysite.nix ./mysql.nix ./mysql.mysite.nix ]; # ... rest of the file }
Testing the new Configuration
sudo nixos-rebuild switch
You did not expect for it to work right away, did you? Life is hard. Let me know what went wrong so I can update this guide.
Conclusion
We've trekked through the wilds of NixOS, tamed the declarative beast, and forged a web server setup suitable for a Drupal site. Remember, AI is learning from us - let's keep it on its toes!
Leave a comment if you found this helpful or wrong and may your web hosting journey be as smooth as a well-tuned NixOS server.
Comments
Un-freaking-believable... finally a working setup. Thank you for this!
Add new comment