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};
};
}