mixRelease: RPC failed with reason :noconnection

I’ve just packaged an Elixir Phoenix Liveview project, made a nix module, and can run it.
I’d like to access this production project, but it fails.
Is anyone able to send commands to a running instance? What are you doing differently?

> my_phoenix_project rpc "MyPhoenixProject.Release.seed_production()"
--rpc-eval : RPC failed with reason :noconnection

> my_phoenix_project restart   
--rpc-eval : RPC failed with reason :noconnection

> my_phoenix_project stop     
--rpc-eval : RPC failed with reason :noconnection

> my_phoenix_project pid 
--rpc-eval : RPC failed with reason :noconnection

All give the same error.

package.ex

_: {
  perSystem =
    {
      lib,
      pkgs,
      system,
      ...
    }:
    let
      elixir = pkgs.beam.packages.erlang_28.elixir_1_19;
      beamPackages = pkgs.beam.packagesWith pkgs.beam.interpreters.erlang_28;
      src = lib.fileset.toSource rec {
        root = ../../.;
        fileset = lib.fileset.unions [
          (root + /assets)
          (root + /config)
          (root + /lib)
          (root + /mix.exs)
          (root + /mix.lock)
          (root + /priv)
          (root + /VERSION)
        ];
      };

      version = builtins.readFile "${src}/VERSION";
      mixExs = builtins.readFile "${src}/mix.exs";
      appName = builtins.head (builtins.match ".*app:[[:space:]]*:([a-zA-Z0-9_]+).*" mixExs);
      pname = lib.strings.stringAsChars (x: if x == "_" then "-" else x) appName;

      mixNixDeps = pkgs.callPackages ../../deps.nix {
        inherit lib beamPackages;
      };
      my_phoenix_project =
        with pkgs;
        beamPackages.mixRelease {
          inherit
            pname
            version
            elixir
            src
            mixNixDeps
            ;

          removeCookie = false;

          DATABASE_URL = "";
          DATABASE_USER = "";
          DATABASE_PASS = "";
          DATABASE_HOST = "";
          DATABASE_NAME = "";
          SECRET_KEY_BASE = "";

          postBuild = ''
            tailwind_path="$(mix do \
              app.config --no-deps-check --no-compile, \
              eval 'Tailwind.bin_path() |> IO.puts()')"
            esbuild_path="$(mix do \
              app.config --no-deps-check --no-compile, \
              eval 'Esbuild.bin_path() |> IO.puts()')"

            ln -sfv ${pkgs.tailwindcss_4}/bin/tailwindcss "$tailwind_path"
            ln -sfv ${pkgs.esbuild}/bin/esbuild "$esbuild_path"
            ln -sfv ${mixNixDeps.heroicons} deps/heroicons

            mix do \
              app.config --no-deps-check --no-compile, \
              assets.deploy --no-deps-check

            mix do deps.loadpaths --no-deps-check, assets.deploy
          '';

          meta.mainProgram = "my_phoenix_project";
        };
    in
    {
      options = {
        my_phoenix_project.elixir = lib.mkOption {
          type = lib.types.package;
          readOnly = true;
        };
      };

      config = {
        my_phoenix_project = { inherit elixir; };

        packages = {
          default = my_phoenix_project;
        };
      };
    };
}

module.ex

{ self }:
{
  config,
  lib ? pkgs.lib,
  pkgs,
  ...
}:
let
  phoenix = self.packages.${pkgs.system}.default;

  my-env = "prod";

  mixExs = builtins.readFile ../mix.exs;
  moduleName = builtins.head (builtins.match ".*defmodule[[:space:]]*([a-zA-Z0-9]+)\..*" mixExs);
  appName = builtins.head (builtins.match ".*app:[[:space:]]*:([a-zA-Z0-9_]+).*" mixExs);
  pname = lib.strings.stringAsChars (x: if x == "_" then "-" else x) appName;

  cfg = config.services."${pname}";
  releaseName = env: "${appName}_${env}";
  releaseName31 = env: "${builtins.substring 0 26 appName}_${env}";
  workingDirectory = env: "/home/${releaseName env}";

  nginxHosts =
    env: envConfig:
    let
      port = toString envConfig.port;
      host = toString envConfig.host;
    in
    {
      enable = true;
      recommendedProxySettings = true;
      recommendedTlsSettings = true;
      recommendedOptimisation = true;
      recommendedGzipSettings = true;
      virtualHosts = {
        "${host}" = {
          addSSL = envConfig.ssl;
          enableACME = envConfig.ssl;
          locations = {
            "/" = {
              proxyPass = "http://0.0.0.0:${port}";
              recommendedProxySettings = true;
              proxyWebsockets = true;
            };
          };
        };
      };
    };

  postgresDatabases = env: _envConfig: {
    enable = true;
    ensureDatabases = [ (releaseName env) ];
    settings.port = 5432;
    authentication = lib.mkOverride 10 ''
      #...
      #type database DBuser origin-address auth-method
      local all       all     trust
      # ipv4
      host  all      all     127.0.0.1/32   trust
      # ipv6
      host all       all     ::1/128        trust
    '';
  };

  users = env: _envConfig: {
    "${(releaseName31 env)}" = {
      isNormalUser = true;
      home = workingDirectory env;
      extraGroups = [
        "tty"
      ];
      homeMode = "755";
    };
  };

  serviceDescription =
    env: envConfig:
    let
      port = toString envConfig.port;
      host = toString envConfig.host;
      releaseTmp = "RELEASE_TMP='${workingDirectory env}'";
      workDir = workingDirectory env;
      envReleaseName = releaseName env;
      PhoenixService = "${envReleaseName}.service";
      migrationService = "${envReleaseName}_migration.service";
      path = [
        pkgs.bash
      ]
      ++ (if envConfig.runtimePackages != [ ] then envConfig.runtimePackages else cfg.runtimePackages);
      environment = [
        "PHX_HOST=${host}"
        "PORT=${port}"
        releaseTmp
        "PHX_SERVER=true"
        "DATABASE_USER=postgres"
        "DATABASE_PASS=postgres"
        "DATABASE_HOST=localhost"
        "DATABASE_PORT=5432"
        "DATABASE_NAME=${envReleaseName}"
        "SECRET_KEY_BASE=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        "RELEASE_COOKIE=${envConfig.releaseCookie}"
      ];
    in
    {
      "${envReleaseName}_migration" = {
        inherit path;
        unitConfig = {
          Description = "${pname} ${env} migrator";
          PartOf = [ PhoenixService ];
          Requires = [
            "postgresql.service"
          ];
          After = [
            "postgresql.service"
          ];
        };
        serviceConfig = {
          Type = "oneshot";
          ExecStart = ''${lib.getExe phoenix} eval "${envConfig.migrateCommand}"'';
          StandardOutput = "journal";
          StandardError = "journal";
          User = releaseName31 env;
          Group = "users";
          WorkingDirectory = workDir;
          Environment = environment;
        };
      };
      "${envReleaseName}" = {
        inherit path;
        wantedBy = [ "multi-user.target" ];
        unitConfig = {
          Description = "${pname} ${env}";
          # requires bash
          Requires = [
            migrationService
          ];
          After = [
            migrationService
          ];
          StartLimitInterval = 10;
        };
        serviceConfig = {
          Type = "exec";
          ExecStart = "${lib.getExe phoenix} start";
          ExecStop = "${lib.getExe phoenix} stop";
          ExecReload = "${lib.getExe phoenix} reload";
          StandardOutput = "journal";
          StandardError = "journal";
          Restart = "on-failure";
          RestartSec = 5;
          StartLimitBurst = 3;
          User = releaseName31 env;
          Group = "users";
          WorkingDirectory = workDir;
          Environment = environment;
        };
      };
    };

  runtimePackages = lib.mkOption {
    type = lib.types.listOf lib.types.package;
    default = [ ];
    description = "The list of packages to include in the service";
  };
  migrateCommand = lib.mkOption {
    type = lib.types.str;
    default = "${moduleName}.Release.migrate";
    description = "The command to run when migrating the database";
  };
in
{
  options = {
    services.${pname} = {
      inherit migrateCommand runtimePackages;
      enable = lib.mkEnableOption "${pname} service";
      package = lib.mkPackageOption pkgs "${pname}" { };
      environments."${my-env}" = {

        enable = lib.mkEnableOption "${pname} service";
        host = lib.mkOption {
          type = lib.types.str;
          description = "The host for this environment";
        };
        port = lib.mkOption {
          type = lib.types.port;
          default = 4000;
          description = "The port on which this service will listen";
        };
        ssl = lib.mkEnableOption "Whether to use SSL or not";
        branch = lib.mkOption {
          type = lib.types.str;
          description = "The branch to use for this environment, will default to the environment name";
        };
        commit = lib.mkOption {
          type = lib.types.str;
          default = "";
          description = "The commit to deploy for this environment";
        };
        runtimePackages = runtimePackages // {
          default = config.services."${pname}".runtimePackages;
        };
        migrateCommand = migrateCommand // {
          default = config.services."${pname}".migrateCommand;
        };
        databaseUrlFile = lib.mkOption {
          type = lib.types.path;
          default = "ecto://postgres:postgres@localhost/${appName}_${my-env}";
          description = ''
            A file containing the URL to use to connect to the database.
            FILE_CONTENT_EXAMPLE : ecto://postgres:postgres@localhost/app_name_prod
            databaseUrlFile = config.age.secrets."databaseurl".path;
          '';
        };
        secretKeyBaseFile = lib.mkOption {
          type = lib.types.path;
          default = "aWEQaiBBr0qZU8oOT8frDE7AlSQNdJSeCK1ajq0/q3LSeAn0Mo7CqijCZmlMDa/t";
          description = ''
            A file containing the Phoenix Secret Key Base. This should be secret, and not kept in the nix store.
            Generate by running: `mix phx.gen.secret`
            FILE_CONTENT_EXAMPLE : abcdef...
            secretKeyBaseFile = config.age.secrets."secretkeybase".path;
          '';
        };
        releaseCookie = lib.mkOption {
          type = lib.types.str;
          default = "YOUR_SUPER_SECRET_COOKIE_THAT_YOU_SHOULD_CHANGE";
          description = "Release cookie to use with Phoenix";
        };
      };
    };
  };

  config = lib.mkIf cfg.enable {
    environment.systemPackages = [
      phoenix # my_phoenix_project
    ];

    systemd.services = serviceDescription my-env config.services.${pname}.environments.${my-env};
    systemd.tmpfiles.rules = tmpFiles my-env config.services.${pname}.environments.${my-env};
    users.users = users my-env config.services.${pname}.environments.${my-env};
    services.nginx = nginxHosts my-env config.services.${pname}.environments.${my-env};
    services.postgresql = postgresDatabases my-env config.services.${pname}.environments.${my-env};
  };
}
1 Like

For now I found this workaround.

DATABASE_USER=postgres \
   DATABASE_HOST=localhost \
   DATABASE_PORT=5432 \
   DATABASE_NAME=my_phoenix_project_prod \
   my_phoenix_project eval "MyPhoenixProject.Release.import_csv(Elixir.MyPhoenixProject.Repo, \"$HOME/test.csv\")"