Repository: railwayapp/nixpacks
Pull request #487 (2022-08-31 19:15:39)

Use CMD exec form with an ENTRYPOINT
This PR updates how the containers are started by default

The entrypoint is

```
ENTRYPOINT ["/bin/bash", "-l", "-c"]
```

And the `CMD` is specified in exec form. https://docs.docker.com/engine/reference/builder/#cmd

The reasoning for this change is that some providers add environment variables or startup scripts to `~/.profile` which should be loaded when the container starts (e.g. rvm info or the `LD_LIBRARY_PATH` variable). It is important that these are setup properly when the container starts.

If the default start command that nixpacks specifies is used, then all is okay. But if a custom command is used to start the container (e.g. on Railway), then we need a custom entrypoint, otherwise there will be some inconsistencies in how the command is interpreted.



Patch:
diff --git a/docs/pages/docs/providers/clojure.md b/docs/pages/docs/providers/clojure.md
index dd03ab9..5e25739 100644
--- a/docs/pages/docs/providers/clojure.md
+++ b/docs/pages/docs/providers/clojure.md
@@ -23,11 +23,13 @@ The version can be overriden by
 
 If `lein-ring` plugin detected
 
-```lein ring uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then mv /app/target/uberjar/standalone.jar /app/target/*standalone.jar; fi
+```
+lein ring uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then mv /app/target/uberjar/standalone.jar /app/target/*standalone.jar; fi
 ```
 
 Default
-```lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then mv /app/target/uberjar/standalone.jar /app/target/*standalone.jar; fi
+```
+lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then mv /app/target/uberjar/standalone.jar /app/target/*standalone.jar; fi
 ```
 
 
diff --git a/docs/pages/docs/providers/elixir.md b/docs/pages/docs/providers/elixir.md
index b1aaed2..33f022b 100644
--- a/docs/pages/docs/providers/elixir.md
+++ b/docs/pages/docs/providers/elixir.md
@@ -7,7 +7,24 @@ title: Elixir
 Elixir is detected if a `mix.exs` file is found.
 
 ## Setup
+The following Elixir versions are available
+
+- `latest`  (Default)
+- `1.12`
+- `1.11`
+- `1.10`
+- `1.9`
+
+The version can be overriden by
+
+- Setting the `NIXPACKS_ELIXIR_VERSION` environment variable
+- Setting the version in a `.elixir-version` file
+
+
+
 ```
+MIX_ENV=prod 
+
 mix local.hex --force
 mix local.rebar --force
 mix deps.get --only prod
diff --git a/examples/elixir-ecto/.formatter.exs b/examples/elixir-ecto/.formatter.exs
new file mode 100644
index 0000000..d2cda26
--- /dev/null
+++ b/examples/elixir-ecto/.formatter.exs
@@ -0,0 +1,4 @@
+# Used by "mix format"
+[
+  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
+]
diff --git a/examples/elixir-ecto/.gitignore b/examples/elixir-ecto/.gitignore
new file mode 100644
index 0000000..2c442b1
--- /dev/null
+++ b/examples/elixir-ecto/.gitignore
@@ -0,0 +1,26 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where third-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Ignore package tarball (built via "mix hex.build").
+friends-*.tar
+
+# Temporary files, for example, from tests.
+/tmp/
diff --git a/examples/elixir-ecto/README.md b/examples/elixir-ecto/README.md
new file mode 100644
index 0000000..8fe96bd
--- /dev/null
+++ b/examples/elixir-ecto/README.md
@@ -0,0 +1,21 @@
+# Friends
+
+**TODO: Add description**
+
+## Installation
+
+If [available in Hex](https://hex.pm/docs/publish), the package can be installed
+by adding `friends` to your list of dependencies in `mix.exs`:
+
+```elixir
+def deps do
+  [
+    {:friends, "~> 0.1.0"}
+  ]
+end
+```
+
+Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
+and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
+be found at <https://hexdocs.pm/friends>.
+
diff --git a/examples/elixir-ecto/config/config.exs b/examples/elixir-ecto/config/config.exs
new file mode 100644
index 0000000..c66a74f
--- /dev/null
+++ b/examples/elixir-ecto/config/config.exs
@@ -0,0 +1,15 @@
+import Config
+
+database_url =
+  System.get_env("DATABASE_URL") ||
+    raise """
+    environment variable DATABASE_URL is missing.
+    For example: ecto://USER:PASS@HOST/DATABASE
+    """
+    
+config :friends, Friends.Repo,
+  url: database_url,
+  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
+
+
+config :friends, ecto_repos: [Friends.Repo]
diff --git a/examples/elixir-ecto/lib/friends.ex b/examples/elixir-ecto/lib/friends.ex
new file mode 100644
index 0000000..f098cd9
--- /dev/null
+++ b/examples/elixir-ecto/lib/friends.ex
@@ -0,0 +1,18 @@
+defmodule Friends do
+  @moduledoc """
+  Documentation for `Friends`.
+  """
+
+  @doc """
+  Hello world.
+
+  ## Examples
+
+      iex> Friends.hello()
+      :world
+
+  """
+  def hello do
+    :world
+  end
+end
diff --git a/examples/elixir-ecto/lib/friends/application.ex b/examples/elixir-ecto/lib/friends/application.ex
new file mode 100644
index 0000000..556baa4
--- /dev/null
+++ b/examples/elixir-ecto/lib/friends/application.ex
@@ -0,0 +1,21 @@
+defmodule Friends.Application do
+  # See https://hexdocs.pm/elixir/Application.html
+  # for more information on OTP Applications
+  @moduledoc false
+
+  use Application
+
+  @impl true
+  def start(_type, _args) do
+    children = [
+      # Starts a worker by calling: Friends.Worker.start_link(arg)
+      # {Friends.Worker, arg}
+      Friends.Repo,
+    ]
+
+    # See https://hexdocs.pm/elixir/Supervisor.html
+    # for other strategies and supported options
+    opts = [strategy: :one_for_one, name: Friends.Supervisor]
+    Supervisor.start_link(children, opts)
+  end
+end
diff --git a/examples/elixir-ecto/lib/friends/person.ex b/examples/elixir-ecto/lib/friends/person.ex
new file mode 100644
index 0000000..a3a027c
--- /dev/null
+++ b/examples/elixir-ecto/lib/friends/person.ex
@@ -0,0 +1,15 @@
+defmodule Friends.Person do
+  use Ecto.Schema
+
+  schema "people" do
+    field :first_name, :string
+    field :last_name, :string
+    field :age, :integer
+  end
+
+  def changeset(person, params \\ %{}) do
+    person
+    |> Ecto.Changeset.cast(params, [:first_name, :last_name, :age])
+    |> Ecto.Changeset.validate_required([:first_name, :last_name])
+  end
+end
\ No newline at end of file
diff --git a/examples/elixir-ecto/lib/friends/repo.ex b/examples/elixir-ecto/lib/friends/repo.ex
new file mode 100644
index 0000000..428b72b
--- /dev/null
+++ b/examples/elixir-ecto/lib/friends/repo.ex
@@ -0,0 +1,5 @@
+defmodule Friends.Repo do
+  use Ecto.Repo,
+    otp_app: :friends,
+    adapter: Ecto.Adapters.Postgres
+end
diff --git a/examples/elixir-ecto/mix.exs b/examples/elixir-ecto/mix.exs
new file mode 100644
index 0000000..7b5e04d
--- /dev/null
+++ b/examples/elixir-ecto/mix.exs
@@ -0,0 +1,31 @@
+defmodule Friends.MixProject do
+  use Mix.Project
+
+  def project do
+    [
+      app: :friends,
+      version: "0.1.0",
+      elixir: "~> 1.13",
+      start_permanent: Mix.env() == :prod,
+      deps: deps()
+    ]
+  end
+
+  # Run "mix help compile.app" to learn about applications.
+  def application do
+    [
+      extra_applications: [:logger],
+      mod: {Friends.Application, []}
+    ]
+  end
+
+  # Run "mix help deps" to learn about dependencies.
+  defp deps do
+    [
+      # {:dep_from_hexpm, "~> 0.3.0"},
+      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
+      {:ecto_sql, "~> 3.8.3"},
+      {:postgrex, ">= 0.16.4"}
+    ]
+  end
+end
diff --git a/examples/elixir-ecto/mix.lock b/examples/elixir-ecto/mix.lock
new file mode 100644
index 0000000..b17be3d
--- /dev/null
+++ b/examples/elixir-ecto/mix.lock
@@ -0,0 +1,9 @@
+%{
+  "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
+  "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"},
+  "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
+  "ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"},
+  "ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"},
+  "postgrex": {:hex, :postgrex, "0.16.4", "26d998467b4a22252285e728a29d341e08403d084e44674784975bb1cd00d2cb", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "3234d1a70cb7b1e0c95d2e242785ec2a7a94a092bbcef4472320b950cfd64c5f"},
+  "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
+}
diff --git a/examples/elixir-ecto/priv/repo/migrations/20220831094119_create_people.exs b/examples/elixir-ecto/priv/repo/migrations/20220831094119_create_people.exs
new file mode 100644
index 0000000..a7d6135
--- /dev/null
+++ b/examples/elixir-ecto/priv/repo/migrations/20220831094119_create_people.exs
@@ -0,0 +1,11 @@
+defmodule Friends.Repo.Migrations.CreatePeople do
+  use Ecto.Migration
+
+  def change do
+    create table(:people) do
+      add :first_name, :string
+      add :last_name, :string
+      add :age, :integer
+    end
+  end
+end
diff --git a/examples/elixir-ecto/priv/repo/seeds.exs b/examples/elixir-ecto/priv/repo/seeds.exs
new file mode 100644
index 0000000..76b1a00
--- /dev/null
+++ b/examples/elixir-ecto/priv/repo/seeds.exs
@@ -0,0 +1,1 @@
+# Add database seeds here
\ No newline at end of file
diff --git a/examples/elixir_no_ecto/config/config.exs b/examples/elixir_no_ecto/config/config.exs
index a617915..8c92051 100644
--- a/examples/elixir_no_ecto/config/config.exs
+++ b/examples/elixir_no_ecto/config/config.exs
@@ -46,4 +46,4 @@ config :phoenix, :json_library, Jason
 
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
-import_config "#{config_env()}.exs"
+import_config "#{config_env()}.exs"
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index c74b513..947e270 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -81,6 +81,12 @@ fn main() -> Result<()> {
                         )
                         .takes_value(true),
                 )
+                .arg(
+                    Arg::new("current-dir")
+                        .long("current-dir")
+                        .help("Output Nixpacks related files to the current directory ")
+                        .takes_value(false),
+                )
                 .arg(
                     Arg::new("no-cache")
                         .long("no-cache")
@@ -199,6 +205,7 @@ fn main() -> Result<()> {
             let path = matches.value_of("PATH").expect("required");
             let name = matches.value_of("name").map(ToString::to_string);
             let out_dir = matches.value_of("out").map(ToString::to_string);
+            let current_dir = matches.is_present("current-dir");
             let mut cache_key = matches.value_of("cache-key").map(ToString::to_string);
             let no_cache = matches.is_present("no-cache");
 
@@ -233,6 +240,7 @@ fn main() -> Result<()> {
                 no_cache,
                 platform,
                 print_dockerfile,
+                current_dir,
             };
 
             create_docker_image(path, envs, plan_options, build_options)?;
diff --git a/src/nixpacks/builder/docker/docker_image_builder.rs b/src/nixpacks/builder/docker/docker_image_builder.rs
index 739fd47..aa938b1 100644
--- a/src/nixpacks/builder/docker/docker_image_builder.rs
+++ b/src/nixpacks/builder/docker/docker_image_builder.rs
@@ -17,19 +17,23 @@ pub struct DockerImageBuilder {
     options: DockerBuilderOptions,
 }
 
+fn get_output_dir(options: &DockerBuilderOptions) -> Result<OutputDir> {
+    if let Some(value) = &options.out_dir {
+        OutputDir::new(value.into(), false)
+    } else if options.current_dir {
+        Ok(OutputDir::default())
+    } else {
+        let tmp = TempDir::new("nixpacks").context("Creating a temp directory")?;
+        OutputDir::new(tmp.into_path(), true)
+    }
+}
+
 impl ImageBuilder for DockerImageBuilder {
     fn create_image(&self, app_src: &str, plan: &BuildPlan, env: &Environment) -> Result<()> {
         let id = Uuid::new_v4();
 
-        let dir = match &self.options.out_dir {
-            Some(dir) => dir.into(),
-            None => {
-                let tmp = TempDir::new("nixpacks").context("Creating a temp directory")?;
-                tmp.into_path()
-            }
-        };
+        let output = get_output_dir(&self.options)?;
         let name = self.options.name.clone().unwrap_or_else(|| id.to_string());
-        let output = OutputDir::new(dir.clone())?;
         output.ensure_output_exists()?;
 
         let dockerfile = plan
@@ -64,7 +68,9 @@ impl ImageBuilder for DockerImageBuilder {
             println!("\nRun:");
             println!("  docker run -it {}", name);
 
-            remove_dir_all(dir)?;
+            if output.is_temp {
+                remove_dir_all(output.root)?;
+            }
         } else {
             println!("\nSaved output to:");
             println!("  {}", output.root.to_str().unwrap());
@@ -132,7 +138,11 @@ impl DockerImageBuilder {
     }
 
     fn write_app(&self, app_src: &str, output: &OutputDir) -> Result<()> {
-        files::recursive_copy_dir(app_src, &output.root)
+        if output.is_temp {
+            files::recursive_copy_dir(app_src, &output.root)
+        } else {
+            Ok(())
+        }
     }
 
     fn write_dockerfile(&self, dockerfile: String, output: &OutputDir) -> Result<()> {
diff --git a/src/nixpacks/builder/docker/dockerfile_generation.rs b/src/nixpacks/builder/docker/dockerfile_generation.rs
index f626d7b..f2e6760 100644
--- a/src/nixpacks/builder/docker/dockerfile_generation.rs
+++ b/src/nixpacks/builder/docker/dockerfile_generation.rs
@@ -24,18 +24,23 @@ pub const APP_DIR: &str = "/app/";
 pub struct OutputDir {
     pub root: PathBuf,
     pub asset_root: PathBuf,
+    pub is_temp: bool,
 }
 
 impl OutputDir {
-    pub fn new(root: PathBuf) -> Result<Self> {
+    pub fn new(root: PathBuf, is_temp: bool) -> Result<Self> {
         let root = root;
         let asset_root = PathBuf::from(NIXPACKS_OUTPUT_DIR);
 
-        Ok(Self { root, asset_root })
+        Ok(Self {
+            root,
+            asset_root,
+            is_temp,
+        })
     }
 
-    pub fn from(root: &str) -> Result<Self> {
-        Self::new(PathBuf::from(root))
+    pub fn from(root: &str, is_temp: bool) -> Result<Self> {
+        Self::new(PathBuf::from(root), is_temp)
     }
 
     /// Ensure that the output directory and all necessary subdirectories exist.
@@ -65,7 +70,7 @@ impl OutputDir {
 
 impl Default for OutputDir {
     fn default() -> Self {
-        Self::from(".").unwrap()
+        Self::from(".", false).unwrap()
     }
 }
 
@@ -157,7 +162,10 @@ impl DockerfileGenerator for BuildPlan {
 
         let dockerfile = formatdoc! {"
             FROM {base_image}
+
+            ENTRYPOINT [\"/bin/bash\", \"-l\", \"-c\"]
             WORKDIR {APP_DIR}
+
             {assets_copy_cmd}
 
             {dockerfile_phases_str}
@@ -223,9 +231,7 @@ impl DockerfileGenerator for StartPhase {
         _output: &OutputDir,
     ) -> Result<String> {
         let start_cmd = match &self.cmd {
-            Some(cmd) => {
-                format!("CMD {}", cmd)
-            }
+            Some(cmd) => utils::get_exec_command(cmd),
             None => "".to_string(),
         };
 
diff --git a/src/nixpacks/builder/docker/mod.rs b/src/nixpacks/builder/docker/mod.rs
index cb6afd1..fc7e38c 100644
--- a/src/nixpacks/builder/docker/mod.rs
+++ b/src/nixpacks/builder/docker/mod.rs
@@ -1,6 +1,7 @@
 use super::ImageBuilder;
 
 #[derive(Clone, Default, Debug)]
+#[allow(clippy::struct_excessive_bools)]
 pub struct DockerBuilderOptions {
     pub name: Option<String>,
     pub out_dir: Option<String>,
@@ -11,6 +12,7 @@ pub struct DockerBuilderOptions {
     pub cache_key: Option<String>,
     pub no_cache: bool,
     pub platform: Vec<String>,
+    pub current_dir: bool,
 }
 
 mod cache;
diff --git a/src/nixpacks/builder/docker/utils.rs b/src/nixpacks/builder/docker/utils.rs
index b3acdc2..21fa835 100644
--- a/src/nixpacks/builder/docker/utils.rs
+++ b/src/nixpacks/builder/docker/utils.rs
@@ -46,6 +46,12 @@ pub fn get_copy_from_command(from: &str, files: &[String], app_dir: &str) -> Str
     }
 }
 
+pub fn get_exec_command(command: &str) -> String {
+    let params = command.replace('\"', "\\\"");
+
+    format!("CMD [\"{}\"]", params)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -99,4 +105,22 @@ mod tests {
             get_copy_from_command(from, &files, app_dir)
         );
     }
+
+    #[test]
+    fn test_get_exec_cmd() {
+        assert_eq!(
+            "CMD [\"command1\"]".to_string(),
+            get_exec_command("command1")
+        );
+
+        assert_eq!(
+            "CMD [\"command1 command2\"]".to_string(),
+            get_exec_command("command1 command2")
+        );
+
+        assert_eq!(
+            "CMD [\"command1 command2 -l \\\"asdf\\\"\"]".to_string(),
+            get_exec_command("command1 command2 -l \"asdf\"")
+        );
+    }
 }
diff --git a/src/providers/elixir.rs b/src/providers/elixir.rs
index 4c366ed..868ffa4 100644
--- a/src/providers/elixir.rs
+++ b/src/providers/elixir.rs
@@ -1,7 +1,7 @@
 use super::Provider;
 use crate::nixpacks::{
     app::App,
-    environment::Environment,
+    environment::{Environment, EnvironmentVariables},
     nix::pkg::Pkg,
     plan::{
         phase::{Phase, StartPhase},
@@ -9,6 +9,8 @@ use crate::nixpacks::{
     },
 };
 use anyhow::Result;
+use regex::{Match, Regex};
+const DEFAULT_ELIXIR_PKG_NAME: &str = "elixir";
 
 pub struct ElixirProvider {}
 
@@ -21,10 +23,16 @@ impl Provider for ElixirProvider {
         Ok(app.includes_file("mix.exs"))
     }
 
-    fn get_build_plan(&self, _app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
+    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
         let mut plan = BuildPlan::default();
 
-        let setup_phase = Phase::setup(Some(vec![Pkg::new("elixir")]));
+        plan.add_variables(EnvironmentVariables::from([(
+            "MIX_ENV".to_string(),
+            "prod".to_string(),
+        )]));
+
+        let elixir_pkg = ElixirProvider::get_nix_elixir_package(app, env)?;
+        let setup_phase = Phase::setup(Some(vec![elixir_pkg]));
         plan.add_phase(setup_phase);
 
         // Install Phase
@@ -35,11 +43,17 @@ impl Provider for ElixirProvider {
 
         // Build Phase
         let mut build_phase = Phase::build(Some("mix compile".to_string()));
-        build_phase.add_cmd("mix assets.deploy".to_string());
-        plan.add_phase(build_phase);
+        let mix_exs_content = app.read_file("mix.exs")?;
 
-        // TODO: Detect if this needs to be run
-        // MIX_ENV=prod mix ecto.migrate
+        if mix_exs_content.contains("assets.deploy") {
+            build_phase.add_cmd("mix assets.deploy".to_string());
+        }
+
+        if mix_exs_content.contains("postgrex") && mix_exs_content.contains("ecto") {
+            build_phase.add_cmd("mix ecto.migrate");
+            build_phase.add_cmd("mix run priv/repo/seeds.exs");
+        }
+        plan.add_phase(build_phase);
 
         // Start Phase
         let start_phase = StartPhase::new("mix phx.server".to_string());
@@ -48,3 +62,59 @@ impl Provider for ElixirProvider {
         Ok(Some(plan))
     }
 }
+
+impl ElixirProvider {
+    fn get_nix_elixir_package(app: &App, env: &Environment) -> Result<Pkg> {
+        fn as_default(v: Option<Match>) -> &str {
+            match v {
+                Some(m) => m.as_str(),
+                None => "_",
+            }
+        }
+
+        let mix_exs_content = app.read_file("mix.exs")?;
+        let custom_version = env.get_config_variable("ELIXIR_VERSION");
+
+        let mix_elixir_version_regex = Regex::new(r#"(elixir:[\s].*[> ])([0-9|\.]*)"#)?;
+
+        // If not from env variable, get it from the .elixir-version file then try to parse from mix.exs
+        let custom_version = if custom_version.is_some() {
+            custom_version
+        } else if custom_version.is_none() && app.includes_file(".elixir-version") {
+            Some(app.read_file(".elixir-version")?)
+        } else {
+            mix_elixir_version_regex
+                .captures(&mix_exs_content)
+                .map(|c| c.get(2).unwrap().as_str().to_owned())
+        };
+
+        // If it's still none, return default
+        if custom_version.is_none() {
+            return Ok(Pkg::new(DEFAULT_ELIXIR_PKG_NAME));
+        }
+        let custom_version = custom_version.unwrap();
+
+        // Regex for reading Elixir versions (e.g. 1.8 or 1.12)
+        let elixir_version_regex =
+            Regex::new(r#"^(?:[\sa-zA-Z-"']*)(\d*)(?:\.*)(\d*)(?:\.*\d*)(?:["']?)$"#)?;
+
+        // Capture matches
+        let matches = elixir_version_regex.captures(custom_version.as_str().trim());
+
+        // If no matches, just use default
+        if matches.is_none() {
+            return Ok(Pkg::new(DEFAULT_ELIXIR_PKG_NAME));
+        }
+        let matches = matches.unwrap();
+        let parsed_version = (as_default(matches.get(1)), as_default(matches.get(2)));
+
+        // Match major and minor versions
+        match parsed_version {
+            ("1", "9") => Ok(Pkg::new("elixir_1_9")),
+            ("1", "10") => Ok(Pkg::new("elixir_1_10")),
+            ("1", "11") => Ok(Pkg::new("elixir_1_11")),
+            ("1", "12") => Ok(Pkg::new("elixir_1_12")),
+            _ => Ok(Pkg::new(DEFAULT_ELIXIR_PKG_NAME)),
+        }
+    }
+}


Test patch:
diff --git a/tests/snapshots/generate_plan_tests__elixir_ecto.snap b/tests/snapshots/generate_plan_tests__elixir_ecto.snap
new file mode 100644
index 0000000..f75808c
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__elixir_ecto.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "nixpacksVersion": "[version]",
+  "buildImage": "[build_image]",
+  "variables": {
+    "MIX_ENV": "prod"
+  },
+  "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ],
+      "commands": [
+        "mix local.hex --force",
+        "mix local.rebar --force",
+        "mix deps.get --only prod"
+      ]
+    },
+    {
+      "name": "setup",
+      "nixPackages": [
+        {
+          "name": "elixir"
+        }
+      ]
+    },
+    {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "commands": [
+        "mix compile",
+        "mix ecto.migrate",
+        "mix run priv/repo/seeds.exs"
+      ]
+    }
+  ],
+  "startPhase": {
+    "cmd": "mix phx.server"
+  }
+}
diff --git a/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap b/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap
index 5315912..c1ed2c1 100644
--- a/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap
+++ b/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap
@@ -5,6 +5,9 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
+  "variables": {
+    "MIX_ENV": "prod"
+  },
   "phases": [
     {
       "name": "install",
@@ -21,7 +24,7 @@ expression: plan
       "name": "setup",
       "nixPackages": [
         {
-          "name": "elixir"
+          "name": "elixir_1_12"
         }
       ]
     },