How to deploy Laravel app to nixos machine?

Hello, I am struggling with deployment of Laravel app to nixos machine. During development I have used php artisan serve to work with my app. Now I want to deploy it to production, but I am stuck. Nginx nor Apache didn’t work for me. Has anyone experience with Laravel deployment, or deployment of any php app using LAMP or LEMP stack?

Did you want to build and deploy your laravel application declaratively entirely with nix/nixos … or did you simply want to host your application on nixos and manage the code imperatively?

For begining it would be enough to host and manage it imperatively. To have it entirely managed by nix/nixos would be great bonus, but it is not necessary for now.

I’ll try to keep this simple, descriptive-but-assuming-you-know-some-basics, and clear.

{ config, pkgs, lib, ... }:
let
  # declare a custom php package with any php extensions and tweaks we need, like memcached for example
  php' = pkgs.php.buildEnv {
    extensions = { enabled, all }: with all; enabled ++ [ memcached ];
    extraConfig = ''
      memory_limit = 256M
    '';
  };
in
{
  networking.firewall.allowedTCPPorts = [ 80 443 ];

  services.httpd.enable = true;
  services.httpd.adminAddr = "webmaster@example.org";
  services.httpd.extraModules = [ "proxy_fcgi" ];

  services.httpd.virtualHosts."example.org" = {
    documentRoot = "/var/www/example.org/public";
    # want ssl + a let's encrypt certificate? add `forceSSL = true;` right here
    extraConfig = ''
      # https://laravel.com/docs/8.x/deployment#nginx
      Header always append X-Frame-Options "SAMEORIGIN"
      Header always set X-XSS-Protection "1; mode=block"
      Header always set X-Content-Type-Options "nosniff"

      <Directory /var/www/example.org/public>
        AllowOverride all
        DirectoryIndex index.php

        <FilesMatch "\.php$">
          <If "-f %{REQUEST_FILENAME}">
            SetHandler "proxy:unix:${config.services.phpfpm.pools.example.socket}|fcgi://localhost/"
          </If>
        </FilesMatch>
      </Directory>
    '';
  };

  services.mysql.enable = true;
  services.mysql.package = pkgs.mariadb;

  # configure a php-fpm pool to run our web application
  services.phpfpm.pools.example = {
    user = "example";
    group = "example";
    phpPackage = php';
    settings = {
      "listen.owner" = config.services.httpd.user;
      "listen.group" = config.services.httpd.group;

      # you should probably take some time to understand these values, see https://www.php.net/manual/en/install.fpm.configuration.php
      "pm" = "dynamic";
      "pm.max_children" = 8;
      "pm.start_servers" = 2;
      "pm.min_spare_servers" = 2;
      "pm.max_spare_servers" = 4;
      "pm.max_requests" = 500;
      "request_terminate_timeout" = 300;
    };
  };

  # you might need a laravel scheduler service to run
  systemd.services.example-laravel-scheduler = {
    description = "example laravel scheduler";
    startAt = "minutely";

    # ensure this service doesn't run unless the app is properly installed
    unitConfig = {
      ConditionPathExists = "/var/www/example.org/.env";
      ConditionDirectoryNotEmpty = "/var/www/example.org/vendor";
    };

    serviceConfig = {
      Type = "oneshot";
      User = "example";
      Group = "example";
      SyslogIdentifier = "example-laravel-scheduler";
      WorkingDirectory = "/var/www/example.org";
      ExecStart = "${php'}/bin/php artisan schedule:run -v";
    };
  };

  # create the basic directory structure we need
  systemd.tmpfiles.rules = [
    "d /var/www"
    "d /var/www/example.org 0750 example example"
  ];

  # because we probably want free ssl certificates
  security.acme.acceptTerms = true;
  security.acme.email = "webmaster@example.org";

  # create a separate user to run php as for security, isolation, better reproducibility, etc...
  users.users.example = {
    description = "php-fpm user for our example laravel web application";
    isNormalUser = true;
    shell = pkgs.bash;

    # packages needed to install your laravel application, etc...
    packages = [
      php'
      php'.packages.composer

      pkgs.git
      pkgs.nodejs
    ];

    openssh.authorizedKeys.keys = [
      # maybe a deploy key goes here and hooks into your CI/CD pipeline?
    ];
  };

  users.groups.example = {};

  users.users.wwwrun.extraGroups = [
    "acme"
    "example"
  ];
}

From here you rebuild, hop on your server, and become the “example” user via sudo -iu example (or whatever):

[example@lamp:~]$ cd /var/www/example.org/
[example@lamp:~]$ git clone git@github.com:adamzivcak/example.git .
[example@lamp:~]$ # hack up your .env file with the details of your install
[example@lamp:~]$ nano .env
[example@lamp:~]$ # prepare your web app for production, see https://laravel.com/docs/8.x/deployment for details
[example@lamp:~]$ composer install --optimize-autoloader --no-dev
[example@lamp:~]$ php artisan config:cache
[example@lamp:~]$ php artisan route:cache
[example@lamp:~]$ php artisan view:cache

I haven’t got into anything more detailed like laravel queues via redis or beanstalkd, but if you want me to feel free to ask about it. A small part of my job involves deploying a large number of web applications written in php, a non trivial amount of which are laravel. Almost all of these web apps are deployed on NixOS.

I hope this helps.

ping @stianlagstad because of Tutorial for setting up the LAMP stack on a NixOS server?

3 Likes

It works perfectly. Many thanks.

I have one more issue with php in my app, maybe you can help me.

I need to call some system tools like git or nixops from my app. I use exec($command, $result, $resultcode), or shell_exec($command). But problem is, that php in my app has no idea, where to find theese packages used in $command, and fails with sh: nixops: command not found. When I specify full path like /nix/store/vikh.....dfkif-user-environment/bin/nixops - then it works. But it fails when this package has dependencies / needs to call another package from nix/store/.

Have you any idea how to fix this?

You can create a customized PATH variable in your php-fpm pool like so:

  services.phpfpm.pools.example = {
    user = "example";
    group = "example";
    phpPackage = php';
    phpEnv.PATH = with pkgs; lib.makeBinPath [ git nixops ];
    settings = {
      "listen.owner" = config.services.httpd.user;
      "listen.group" = config.services.httpd.group;

      # you should probably take some time to understand these values, see https://www.php.net/manual/en/install.fpm.configuration.php
      "pm" = "dynamic";
      "pm.max_children" = 8;
      "pm.start_servers" = 2;
      "pm.min_spare_servers" = 2;
      "pm.max_spare_servers" = 4;
      "pm.max_requests" = 500;
      "request_terminate_timeout" = 300;
    };
  };
2 Likes