NixOS from local development to try with a VM. WordPress use case

Published June 18, 2024

I wrote about how I set up my local development for this site, built on WordPress with NixOS this articles highlights how I move the website to production via NixOS as well

I am one of those persons who like to work locally when it is possible. Even when it comes to microservices environments with many processes and dependencies, if I know I am going to contribute to such a project for some time, I take the time to figure out how to run it on my laptop. It is an investment that always pack back in my case. I feel more secure about what it does, I can debug it quickly with various techniques that I am sure will work remotely as well, but I just feel more comfortable in this way.

When it comes to infrastructure development, the closest tools that sparks joy when it comes to “trying a bunch of codebases in an isolated environment that may look like production” is probably Vagrant or Packer. Nothing that exiting, so when I tried the NixOS way, I got hooked into it!

NixOS helps with that when it comes to system and infrastructure related work because in the same way you build a Docker container with Nix, or configure and deploy to a cloud provider VM you can build one that runs in QEMU for example and the iteration process and feedback look is very short. You can even write automated tests on top.

For WordPress, I did something like:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = {self, nixpkgs, flake-utils, ...}:
    flake-utils.lib.eachDefaultSystem
    (system:
    let
      pkgs = import nixpkgs { inherit system; };
    in with pkgs;
    {
      packages = {
        "0x0" = pkgs.stdenv.mkDerivation {
          name = "0x0";
          pversion = "0";
          src = ./wp-theme;
          installPhase = "mkdir -p $out; cp -R * $out/";
        };
        "smtp-mailer" = pkgs.stdenv.mkDerivation {
          name = "smtp-mailer";
          pversion = "1.1.14";
          src = pkgs.fetchzip {
            name = "smtp-mailer";
            version = "1.1.14";
            hash = "sha256-HP5sqswACiqexwep0ombdExx7avyXhOnq8zdeghTPGM=";
            url = "https://downloads.wordpress.org/plugin/smtp-mailer.zip";
          };
          installPhase = "mkdir -p $out; cp -R * $out/";
        };
        "independent-analytics" = pkgs.stdenv.mkDerivation {
          name = "independent-analytics";
          pversion = "2.5.1";
          src = pkgs.fetchzip {
            name = "independent-analytics";
            version = "2.5.1";
            hash = "sha256-mfaoFQ/+oAROFOnCscH7un+knJ0sWK4JEb5qoGfZmeY=";
            url = "https://downloads.wordpress.org/plugin/independent-analytics.2.5.1.zip";
          };
          installPhase = "mkdir -p $out; cp -R * $out/";
        };
        "wordpress-importer" = pkgs.stdenv.mkDerivation {
          name = "wordpress-importer";
          pversion = "0.8.2";
          src = pkgs.fetchzip {
            name = "wordpress-importer";
            version = "0.8.2";
            hash = "sha256-3fiiB+vAksxHFkGNWJ8B/rL/aEorHwY0FNX/BAkVsEo=";
            url = "https://downloads.wordpress.org/plugin/wordpress-importer.0.8.2.zip";
          };
          installPhase = "mkdir -p $out; cp -R * $out/";
        };
      };
      devShells.default = mkShell {
        buildInputs = [
          git
          php
          mariadb
          process-compose
        ];
      };
    })//{
      nixosConfigurations = {
        wp-qemu = nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          modules = [
            ({ pkgs, modulesPath, ... }: {
              imports = [
                (modulesPath + "/profiles/qemu-guest.nix")
                (modulesPath+"/virtualisation/qemu-vm.nix")
              ];
              services.getty.autologinUser = pkgs.lib.mkForce "root";
              networking.hostName = "shippingbytes";
              networking.firewall.allowedTCPPorts = [ 25 80 443 ];
              virtualisation.memorySize = 2024;
              virtualisation.cores = 2;
              virtualisation.diskSize = 1024 * 6;
              virtualisation.forwardPorts = [
                { from = "host"; host.port = 8025; guest.port = 25; }
                { from = "host"; host.port = 8080; guest.port = 80; }
                { from = "host"; host.port = 8443; guest.port = 443; }
              ];

              services.wordpress = {
                webserver = "nginx";
                sites."shippingbytes.dev" = {
                  themes = [
                    self.packages.x86_64-linux."0x0"
                  ];
                  plugins = [
                    self.packages.x86_64-linux."wordpress-importer"
                    self.packages.x86_64-linux."independent-analytics"
                  ];
                  settings = {
                    WPLANG = "en_US";
                  };
                };
              };
            })
          ];
        };
      };
    };
}

There are three parts that I want to highlights and those are Flake outputs: devShell, packages and nixosConfigurations.

We already discussed devShell in the article I linked at the beginning of this posts.

NixOS Configurations

nixosConfigurations are derivation of the NixOS operating system with what you need on top. In my case, I added WordPress via NixOS WordPress module and a few things related to how my VM should look like:

              imports = [
                (modulesPath + "/profiles/qemu-guest.nix")
                (modulesPath+"/virtualisation/qemu-vm.nix")
              ];
              services.getty.autologinUser = pkgs.lib.mkForce "root";
              networking.hostName = "shippingbytes";
              networking.firewall.allowedTCPPorts = [ 25 80 443 ];
              virtualisation.memorySize = 2024;
              virtualisation.cores = 2;
              virtualisation.diskSize = 1024 * 6;
              virtualisation.forwardPorts = [
                { from = "host"; host.port = 8025; guest.port = 25; }
                { from = "host"; host.port = 8080; guest.port = 80; }
                { from = "host"; host.port = 8443; guest.port = 443; }
              ];

I imported QEMU specifics suck kernel drivers and the guest agent. I exposed a few ports, increased memory, CPU and diskSize.

If you don’t know what a NixOS module is, take a look at the official Wiki, the TLDR is:

Think about NixOS modules like Ansible Playbooks because a package is not enough. Knowing how to package WordPress is helpful, but the modules have utilities to spin up httpd virtual host or nginx one. It configures and start MariaDB or MySQL if you don’t want to do it on your own.

In practice, modules are utilities that guide you to how a process should work. It can configure firewall rules, dependencies, default arguments, create users so you don’t run everything as root and so on. It is hard to know what a module does, it depends on how mature it is, who wrote it and so on. You can obviously create your own as well.

To build the VM I use the command:

$ nixos-rebuild build-vm --flake .#wp-qemu
Done.  The virtual machine can be started by running /nix/store/sj0jx3k02kr2mbpb9l7w6p7pv27yah0g-nixos-vm/bin/run-shippingbytes-vm

Since this is nothing more than a derivation, it gets built like any other package, the output will have a symlink to ./result and you can run it with:

$ ./result/bin/run-shippingbytes-vm

What you get is QEMU running your NixOS derivation and WordPress exposed at localhost:8080 because the module installed, started an nginx server, and we port forwarded port 80 to 8080.

Packaging

The packaging part is not that exciting at least for this use case but since I am developing my own theme and I want a few plugins installed I had to package them so they can get installed at the right place as part of the WordPress codebase. How does it happen? The module handles all of that. Since NixOS deals with immutable file system, if you handle WordPress with it, you need to figure out a way to install plugins and themes at build time. This is not the only way, you can install WordPress without the NixOS module, and you will end up where you want. But a pillar I want for this installation is stability, and we know that WordPress is not really the problem, complicated now well written theme or plugins installed with little to zero care increase the attack surface. So an immutable installation with just the right amount of things should be helpful.

Conclusion

The next step at this point when you are happy with what you have is to reuse the configuration you wrote to generated something else, like an ISO, or a AMI for AWS, or to mutate an existing server.

Are you having trouble figuring out your way to building automation, release and troubleshoot your software? Let's get actionables lessons learned straight to you via email.