Setting up a WordPress dev environment. AKA... Trying to run before I can walk

Hi, I’ve been dabbling in the NixOS world for some months now and am at the point where I’ve got my day to day setup working okay.

Part of what I do is work on WordPress sites. I used to have Apache, PHP, and MariaDB installed on my Ubuntu (now NixOS) machine. So there are directories under /var/www with full WordPress installations. I would love to be able to work in these directories and be able to manage the files myself. I mostly do plugin development there… but for several different sites.

I am currently trying to go the route of setting up a flake.nix file in the www directory and then running nix develop in the pertinent sub-directory to get everything going for that site. Nixfoo is not strong enough to get this to work (should read loooots more) so I’d love to know how any of you would approach this. Quitting is not on the table! :slight_smile:

The bare basics I need is PHP, and MariaDB. Not sure I want the WordPress package, since I think it may constrain my ability to do dev work (edit files and plugins). I have ran PHP -S in the past with good enough success, so Nginx or Apache are not a huge deal, but maybe a nice addition.

I’d share the four different files I’m tying to build, but I don’t want to get banned from this forum :sweat_smile:

Hi, I believe the easier way for this kind of dev stack is using tools like devenv or flox. I’ve only tried devenv yet, it provides php and services like mariadb and nginx, and you can start your stack using a devenv up command, just like docker compose.

You might want to dig into forums’ posts a bit. Some ppl have shared working configurations for NixOS already:
https://discourse.nixos.org/search?q=wordpress

I haven’t really found any examples on the web, or the forums, that would be for local development environments where I would be able to edit files/plugins at will. What I have found are examples of production configurations that would be better for serving on the web. I have cobbled together things from those here.

So far I’m feeling pretty close and learning a whole lot in the process, which is one of the reasons I’m trying to build this flake.

# flake.nix
{
  description = "WordPress dev environment!";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
  };

  outputs = { self, nixpkgs, ... }@inputs:
    let
      flakeName = "WordPress Dev";
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
      
      # Set default values for the command-line arguments
      URL = "localhost:8089";
      PHP_VERSION = "8.1";
      WORDPRESS_VERSION = "5.9";
      DOWNLOAD_WORDPRESS = false;

      wpConfig = builtins.readFile ./wp-config.php;
      db_name = builtins.head (builtins.match ".*define\\([[:space:]]?['\"]DB_NAME['\"], ['\"]([^']+)['\"][[:space:]]?\\);.*" "${wpConfig}");
      db_user = builtins.head (builtins.match ".*define\\([[:space:]]?['\"]DB_USER['\"], ['\"]([^']+)['\"][[:space:]]?\\);.*" "${wpConfig}");
      db_password = builtins.head (builtins.match ".*define\\([[:space:]]?['\"]DB_PASSWORD['\"], ['\"]([^']+)['\"][[:space:]]?\\);.*" "${wpConfig}");
      host = "localhost";

      # Create a SQLite database file
      # Not working, may look into this later... Using a full MySQL db for now.
      # sqlite_db = nixpkgs.fetchurl {
        # url = "https://raw.githubusercontent.com/aaemnnosttv/wp-sqlite-db/master/src/db.php";
      # };
      # sqliteDbFile = builtins.storeFile "${wordpress}/wp-content/db.php" sqlite_db;

      # Download WordPress if the flag is set
      wordpress = if DOWNLOAD_WORDPRESS then
      (import (fetchTarball {
        url = "https://wordpress.org/wordpress-${WORDPRESS_VERSION}.tar.gz";
        # sha256 = nix-prefetch-url --print-sha256 "https://wordpress.org/wordpress-${WORDPRESS_VERSION}.tar.gz";
      }))./.
      else
      ./.;

      services.mysql = {
        enable = true;
        dataDir = "./db";
        package = pkgs.mariadb_110;
        initialScript =
          let
            # https://sourcegraph.com/github.com/vlktomas/nix-examples/-/blob/web/Laravel/laravel/cd.nix?L112&subtree=true
            # databasePassword = lib.removePrefix "DB_PASSWORD=" (lib.fileContents databasePasswordFile);
            databasePassword = db_password;
          in
            pkgs.writeText "initial-script" ''
              CREATE DATABASE IF NOT EXISTS `${db_name}`;
              CREATE USER IF NOT EXISTS '${db_user}'@'${host}' IDENTIFIED WITH ${db_password};
              ALTER USER '${db_user}'@'${host}' IDENTIFIED BY '${databasePassword}';
              GRANT ALL PRIVILEGES ON `${db_name}`.* TO '${db_user}'@'${host}';
            '';
        ensureDatabases = [
          db_name
        ];
        ensureUsers = [
          {
            name = db_user;
            ensurePermissions = {
              "${db_user}.*" = "ALL PRIVILEGES";
              "*.*" = "ALL PRIVILEGES";
            };
          }
        ];
      };

      # Initialize the SQLite database
      # initSqliteDb = ''
        # sqlite3 ${sqliteDbFile} < ${wordpress}/wp-admin/includes/schema.php
      # '';

      # Start the PHP development server
      startPhpServer = ''
        php -S ${URL} -t ${wordpress} &
      '';

      # Start the SQLite database server
      # startSqliteServer = ''
        # sqlite3 ${sqliteDbFile} &
      # '';

    in {
      devShells.x86_64-linux.default  = pkgs.mkShell {
        name = "wordpress-dev";

        # Define the dependencies
        nativeBuildInputs = with pkgs; [
          mariadb_110
          php
          php82Extensions.xdebug
          # sqlite
          wp-cli
        ];


        # Parse command-line arguments
        # May try this later, not working...
        # args = builtins.removeAttrs (builtins.parseArgs {
          # options = {
            # url = opts: URL = opts.arg;
            # php-version = opts: PHP_VERSION = opts.arg;
            # wordpress-version = opts: WORDPRESS_VERSION = opts.arg;
            # download-wordpress = opts: DOWNLOAD_WORDPRESS = true;
          # };
        # }) ["_"];

        # Start the services when entering the shell
        shellHook = ''
          PS1="[${flakeName}] $PS1"
          ${startPhpServer}
          echo "WordPress development environment started. Accessible at http://${URL}"
        '';
      };
    };
}

However I’m getting an error on the browser:

And this in the command line:

[sergio@samara:~/www/test]$ nix develop
WordPress development environment started. Accessible at http://localhost:8089
[WordPress Dev] 
[sergio@samara:~/www/test]$ [Sat Jun 22 12:29:17 2024] Failed to listen on localhost:8089 (reason: Address already in use)

[1]+  Exit 1                  php -S localhost:8089 -t /nix/store/cax9pphk7ir9zpabzm9z4fz3r405yliw-2x3n2ji5y6yr7h6rh2ksvli8kcwsnqbz-source
[WordPress Dev] 

I’m pretty sure I’m missing something with the database creation also the store pat seems different on the WordPress error page and the command line. Seems this may be running twice?

Still working on it… Any ideas or advice would be appreciated.

There’s a lot wrong with the flake

  • services.mysql is never used. Even if it was, such options are used in NixOS configurations, not development shells. You probably expect it to start a mysql server, but it will not.
  • builtins.parseArgs is not a thing as far as I know (is this an LLM hallucination?). I don’t think flakes even have any way to take arguments in the command line.
  • builtins.storeFile is not a thing either I think?
  • You probably need something like php82Extensions.mysqli for WordPress to be able to connect to mysql (if it were running). And you might need to use it in php.buildEnv as described in the wiki.

I would suggest starting with a smaller example without a bunch of unused and invalid code.

Honestly, I haven’t used devenv or flox as recommended by others, but they seem like they might be more fitting for this purpose. Nix shells do not provide any service management (as NixOS configuration does), but e.g. a combination of services and processes in devenv might let you achieve a dev environment with pretty high level code. With those, you might be able to just services.mysql.enable = true;, define your own process to run php -S, et voila.

I think flakes is great for packaging your app and even providing a nixosModule to run it, but to run a dev stack with multiple processes and a database devenv makes it super easy. I just tried with a fresh wordpress install and got it working with this simple devenv.nix:

{ pkgs, ... }:

{
  packages = with pkgs; [ git wp-cli ];

  services.mysql.enable = true;
  services.mysql.package = pkgs.mariadb;
  services.mysql.initialDatabases = [{ name = "wordpress"; }];
  services.mysql.ensureUsers = [{
    name = "wordpress";
    password = "wordpress";
    ensurePermissions = { "wordpress.*" = "ALL PRIVILEGES"; };
  }];

  languages.javascript.enable = true;
  languages.php.enable = true;

  process.before = "npm install";
  processes.wordpress.exec = "wp server";
  processes.wordpress-assets.exec = "npm run dev";
}

When running devenv up it spawns all the processes using process compose and you get this TUI:

Tip: with this setup when configuring WP you need to use 127.0.0.1 for DB host (not localhost). Also if you want to use phpfm this blog post should help.

3 Likes

Great points all around!

  1. didn’t know that (good to know).
  2. yup, was stuck with something and asked the robots… shoulda known better
  3. same… :robot: :poop:
  4. I’ll look into that, but it seems like I won’t be able to start the database server anyway?

So I won’t be able to define and start a service from a flake, this means I’ll need to (at the very least) configure services in my configuration file.

Just setting this up, I’m learning a whole lot about how Nix works. I’m going to continue down this path for a bit. Assuming it actually is possible… :sweat_smile:

Why not using docker and “translate” the setup with nix available docker tools?
https://nixos.wiki/wiki/Docker

Thanks for the info. Trying this now but… This may seem like a very basic question, but I’m getting complains of a package.json missing and I have no clue what should be in this file.

And if I comment out the lines 18 and 20 it connects to the db… but I now don’t know how I can access wp-cli…

I swear I’ve been working on this kind of thing for years :sweat_smile:

you’re getting some nice feedback in this thread on how to change your workflow, though maybe at this point you don’t want to change your workflow quite yet and you just want your LAMP stack back and working in a way that is simple for you to understand given your past experiences… to become productive in the way you used to be on ubuntu, but on NixOS instead?

while personally i would recommend devenv for php development for wordpress i can understand how that might be a bit much at that point… and if so, just say so and i can write up a quick and easy LAMP on NixOS configuration which will be familiar to you, given what you described in your initial post

I very much appreciate your offer! Yes, that LAMP configuration would be great to get me coding again, while I sort this out further. Running into a bit of a time crunch with some projects and I don’t really want to spin up a vm for this.

The goal is definitely to dive more into the more use of flakes and devenv for my day-to-day workflows. But it may be a dash premature… Even if I am loving all the new options. There is lots of reading and trial/error ahead of me.

you can pop the following config in something like lamp.nix and then include in your system configuration:

{ config, pkgs, lib, ... }:
let
  # php 8.1 is the easiest option - if you need  php 7.x then we can discuss https://github.com/fossar/nix-phps/ as an option
  php' = pkgs.php81.buildEnv {
    # any customizations to your `php.ini` go here
    extraConfig = ''
      memory_limit = 1024M
    '';
  };
in
{
  networking.hosts = {
    # convenient if you're going to work on multiple sites
    "127.0.0.1" = [ "example.org" ];
  };

  services.mysql.enable = true;
  services.mysql.package = pkgs.mariadb;
  services.mysql.ensureDatabases = [
    # list a database for every site you want and they will be automatically created
    "example"
  ];
  services.mysql.ensureUsers = [
    # NOTE: it is important that `name` matches your `$USER` name, this allows us to avoid password authentication
    { name = "aaron";
      ensurePermissions = {
        "*.*" = "ALL PRIVILEGES";
      };
    }
  ];

  services.phpfpm.pools."example.org" = {
    user = "aaron";
    group = "users";
    phpPackage = php';
    settings = {
      "listen.owner" = config.services.caddy.user;
      "listen.group" = config.services.caddy.group;
      "pm" = "dynamic";
      "pm.max_children" = 5;
      "pm.start_servers" = 2;
      "pm.min_spare_servers" = 1;
      "pm.max_spare_servers" = 5;
    };
  };

  services.caddy.enable = true;
  # we'll keep it simple and stick to plain http for now, though caddy supports https relatively easily
  services.caddy.virtualHosts."http://example.org:80".extraConfig = ''
    root * /var/www/example.org
    php_fastcgi unix/${config.services.phpfpm.pools."example.org".socket}
    file_server
  '';

  # automatically create a directory for each site you will work on with appropriate ownership+permissions
  systemd.tmpfiles.rules = [
    "d /var/www/example.org 0755 aaron users"
  ];
}
  • rebuild your system
  • unzip wordpress into /var/www/example.org such that index.php, wp-cron.php, etc… are in that directory
  • head to http://example.org in your browser
  • fill out wordpress installation web form… see screen capture for database details


to be clear i rebuilt my system against the exact configuration i provided you, it wasn’t a “for example” snippet of config, it will work exactly like that if your user account is aaron… so just search+replace aaron with your system username and you should be up and running :+1:

let me know if you have any issues

2 Likes

Maybe a bit out of topic, but any pros / cons using devenv?

For personal experience, 1 stack for all tend to be quite messy on the long term for WP, especially when dealing with multiple sites and instances.

How would you manage

  • compatibility testing for both PHP (e.g 7.4, 8.1, 8.2, etc), WP (5.*, 6.*, etc), db, etc
  • more “modern” approach with wp-env for development, nginx to server, etc
  • sites “encapsulation” to respect clients privacy policies

I’m production with NixOS you can run as many different versions of php side by side for each site as you like, you can run each site entirely sandboxes from each other as separate users with restrictive permissions, and separate database with restrictive permissions as well

NixOS makes for an incredibly powerful php experience both in development and production

1 Like

Thank you for putting that together! I really appreciate the help. After a couple of tweaks, it seems to be working fine. Had to move my www directory to /var/www, it didn’t work if I changed the path…

One main thing remains for me, that is to get xdebug working from configuragion.nix. Instead I have to also add it to lamp.nix.

#configuration.nix
  environment.systemPackages = with pkgs; [
    devenv
    direnv
    php83Packages.php-codesniffer
    (php83.buildEnv {
      extensions = ({ enabled, all }: enabled ++ (with all; [
        xdebug
        imagick
      ]));
      extraConfig = ''
        xdebug.mode = debug
        xdebug.start_with_request = yes
        xdebug.idekey = gdbp
      '';
    })
    spacevim
    wget
    wmutils-core
    wp-cli
  ];

#lamp.nix
  php' = pkgs.php83.buildEnv {
    extensions = ({ enabled, all }: enabled ++ (with all; [
      xdebug
      imagick
    ]));
    # any customizations to your `php.ini` go here
    extraConfig = ''
      memory_limit = 1024M
      xdebug.mode = debug
      xdebug.start_with_request = yes
      xdebug.idekey = gdbp
    '';

Looking online for a while now and continuing to dig into it. If you know of how, it would be great! I’ll share what I’ve done soon, as I’m adding some “extra” things. Mainly wanting to import existing sites/dbs, which I managed. :smile:

was it under /home? the phpfpm module has some systemd hardening which blocks that - if it is a real hassle we can undo that, let me know

maybe this can help you:

  php' = pkgs.php81.buildEnv {
    extensions = { enabled, all }: with all; enabled ++ [ xdebug ];
    extraConfig = ''
      memory_limit = 1024M

      xdebug.show_error_trace = 1
      xdebug.show_local_vars = 1
      xdebug.remote_enable = 1
      xdebug.remote_autostart = 1
    '';
  };

looking forward to it

I was editing my last reply with an update as you replied. Got it working, but had a question still…

I figures as much, no problem at all. I just moved the files…

Back again… I am trying to see if I can have a single, say, php.nix file (module?) and call it from lamp.nix and configuration.nix. But that’s just icing on the cake. I’ve had a productive day developing with this working WordPress setup. So, thank you!!!

I am having one issue that relates to the site paths… I moved most of my files to a server in my LAN and connected through NFS. It’s working fine, but now the sites aren’t loading. I’m assuming it’s the same systemd hardening issue you mentioned? The NFS mount is at /run/media/sergio/vault/www/example.org. I did try to find the solution online, but nothing yet…

sorry, can you provide context, i don’t follow what you mean?

i would imagine if you undo these lines your issue would be resolved:

something like this:

systemd.services."phpfpm-example.org".serviceConfig = {
  PrivateDevices = lib.mkForce false;
  PrivateTmp = lib.mkForce false;
  ProtectSystem = lib.mkForce "off";
  ProtectHome = lib.mkForce false;
};

This gives me the same 403 error when going to the site. Here is what I have for that part:

  services.phpfpm.pools."example.org" = {
    user = "sergio";
    group = "users";
    phpPackage = php';
    settings = {
      "listen.owner" = config.services.caddy.user;
      "listen.group" = config.services.caddy.group;
      "pm" = "dynamic";
      "pm.max_children" = 5;
      "pm.start_servers" = 2;
      "pm.min_spare_servers" = 1;
      "pm.max_spare_servers" = 5;
    };
  };

  services.caddy.enable = true;
  # we'll keep it simple and stick to plain http for now, though caddy supports https relatively easily
  services.caddy.virtualHosts."http://example.org:80".extraConfig = ''
    root * /run/media/sergio/vault/www/example.org
    php_fastcgi unix/${config.services.phpfpm.pools."example.org".socket}
    file_server
  '';

  # automatically create a directory for each site you will work on with appropriate ownership+permissions
  systemd.tmpfiles.rules = [
    "d /run/media/sergio/vault/www/example.org 0755 sergio users"
  ];

  systemd.services."phpfpm-example.org".serviceConfig = {
    PrivateDevices = lib.mkForce false;
    PrivateTmp = lib.mkForce false;
    ProtectSystem = lib.mkForce "off";
    ProtectHome = lib.mkForce false;
  };
}

sorry, can you provide context, i don’t follow what you mean?

The other thing I was meaning is that I am declaring(?) PHP and it’s extraConfigs twice. Was looking to have it in one, maybe, php.nix config and import it… Just a learning bit, not critical at all.

#configuration.nix
  environment.systemPackages = with pkgs; [
    devenv
    direnv
    php83Packages.php-codesniffer
    (php83.buildEnv {
      extensions = ({ enabled, all }: enabled ++ (with all; [
        xdebug
        imagick
      ]));
      extraConfig = ''
        xdebug.mode = debug
        xdebug.start_with_request = yes
        xdebug.idekey = gdbp
      '';
    })
    spacevim
    wget
    wmutils-core
    wp-cli
  ];
#lamp.nix
  php' = pkgs.php83.buildEnv {
    extensions = ({ enabled, all }: enabled ++ (with all; [
      xdebug
      imagick
    ]));
    # any customizations to your `php.ini` go here
    extraConfig = ''
      memory_limit = 1024M
      xdebug.mode = debug
      xdebug.start_with_request = yes
      xdebug.idekey = gdbp
    '';