Repository: railwayapp/nixpacks
Pull request #642 (2022-10-30 22:03:52)

for ruby, cache bundle install by default if Gemfile* has not changed
I'm not aware of any cases where `bundle install` uses any files except for Gemfile and Gemfile.lock

coffee-cup: The one case I know that the `Gemfile` uses other local files is when referencing local gems (The `ruby-local-deps` example does this). We should be able to check this though and include everything if no local deps are included.

For reference this was changed in this PR https://github.com/railwayapp/nixpacks/pull/477 because of an issue with local deps.
flybayer: hey, gentle nudge on this @coffee-cup :)

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5ccd746..1c10d3e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -228,6 +228,17 @@ version = "1.0.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
 
+[[package]]
+name = "async-trait"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -914,10 +925,11 @@ dependencies = [
 
 [[package]]
 name = "nixpacks"
-version = "0.12.2"
+version = "0.12.3"
 dependencies = [
  "actix-web",
  "anyhow",
+ "async-trait",
  "base64",
  "box_drawing",
  "cargo_toml",
@@ -943,6 +955,7 @@ dependencies = [
  "tempdir",
  "test-helper",
  "textwrap 0.16.0",
+ "tokio",
  "toml",
  "uuid",
  "wait-timeout",
@@ -1509,23 +1522,35 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.21.1"
+version = "1.21.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95"
+checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
 dependencies = [
  "autocfg",
  "bytes",
  "libc",
  "memchr",
  "mio",
- "once_cell",
+ "num_cpus",
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
  "socket2",
+ "tokio-macros",
  "winapi",
 ]
 
+[[package]]
+name = "tokio-macros"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "tokio-util"
 version = "0.7.4"
diff --git a/Cargo.toml b/Cargo.toml
index 536b1a2..63c4972 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nixpacks"
-version = "0.12.2"
+version = "0.12.3"
 edition = "2021"
 license = "MIT"
 authors = ["Railway <contact@railway.app>"]
@@ -53,6 +53,8 @@ sanitize-filename = "0.4"
 futures-util = "0.3"
 futures = "0.3"
 portpicker =  "0.1.1"
+tokio = { version = "1.21.2", features = ["full"] }
+async-trait = "0.1.58"
 
 [dev-dependencies]
 dotenv-parser = "0.1.3"
diff --git a/src/lib.rs b/src/lib.rs
index a18c654..31a5c84 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -98,7 +98,7 @@ pub fn get_plan_providers(
     generator.get_plan_providers(&app, &environment)
 }
 
-pub fn create_docker_image(
+pub async fn create_docker_image(
     path: &str,
     envs: Vec<&str>,
     plan_options: &GeneratePlanOptions,
@@ -112,7 +112,9 @@ pub fn create_docker_image(
 
     let logger = Logger::new();
     let builder = DockerImageBuilder::new(logger, build_options.clone());
-    builder.create_image(app.source.to_str().unwrap(), &plan, &environment)?;
+    builder
+        .create_image(app.source.to_str().unwrap(), &plan, &environment)
+        .await?;
 
     Ok(())
 }
diff --git a/src/main.rs b/src/main.rs
index 7b4d65e..74e562e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -34,7 +34,8 @@ impl PlanFormat {
     }
 }
 
-fn main() -> Result<()> {
+#[tokio::main]
+async fn main() -> Result<()> {
     const VERSION: &str = env!("CARGO_PKG_VERSION");
 
     let matches = Command::new("nixpacks")
@@ -368,7 +369,7 @@ fn main() -> Result<()> {
                 verbose,
             };
 
-            create_docker_image(path, envs, &options, build_options)?;
+            create_docker_image(path, envs, &options, build_options).await?;
         }
         _ => eprintln!("Invalid command"),
     }
diff --git a/src/nixpacks/builder/docker/docker_image_builder.rs b/src/nixpacks/builder/docker/docker_image_builder.rs
index e41434f..e533d9f 100644
--- a/src/nixpacks/builder/docker/docker_image_builder.rs
+++ b/src/nixpacks/builder/docker/docker_image_builder.rs
@@ -34,8 +34,11 @@ fn get_output_dir(app_src: &str, options: &DockerBuilderOptions) -> Result<Outpu
     }
 }
 
+use async_trait::async_trait;
+
+#[async_trait]
 impl ImageBuilder for DockerImageBuilder {
-    fn create_image(&self, app_src: &str, plan: &BuildPlan, env: &Environment) -> Result<()> {
+    async fn create_image(&self, app_src: &str, plan: &BuildPlan, env: &Environment) -> Result<()> {
         let id = Uuid::new_v4();
 
         let output = get_output_dir(app_src, &self.options)?;
diff --git a/src/nixpacks/builder/mod.rs b/src/nixpacks/builder/mod.rs
index 4d9a382..73533e7 100644
--- a/src/nixpacks/builder/mod.rs
+++ b/src/nixpacks/builder/mod.rs
@@ -1,8 +1,15 @@
 use super::{environment::Environment, plan::BuildPlan};
 use anyhow::Result;
+use async_trait::async_trait;
 
 pub mod docker;
 
+#[async_trait]
 pub trait ImageBuilder {
-    fn create_image(&self, app_source: &str, plan: &BuildPlan, env: &Environment) -> Result<()>;
+    async fn create_image(
+        &self,
+        app_source: &str,
+        plan: &BuildPlan,
+        env: &Environment,
+    ) -> Result<()>;
 }
diff --git a/src/nixpacks/images.rs b/src/nixpacks/images.rs
index d9063fc..f0bf424 100644
--- a/src/nixpacks/images.rs
+++ b/src/nixpacks/images.rs
@@ -1,2 +1,2 @@
-pub const DEFAULT_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1667261211";
+pub const DEFAULT_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1667866002";
 pub const DEBIAN_SLIM_IMAGE: &str = "debian:bullseye-slim";
diff --git a/src/providers/ruby.rs b/src/providers/ruby.rs
index 18ee130..67cd600 100644
--- a/src/providers/ruby.rs
+++ b/src/providers/ruby.rs
@@ -71,7 +71,7 @@ impl RubyProvider {
             setup.add_apt_pkgs(vec!["default-libmysqlclient-dev".to_string()]);
         }
 
-        if self.uses_gem_dep(app, "rmagick") {
+        if self.uses_gem_dep(app, "magick") {
             setup.add_apt_pkgs(vec![String::from("libmagickwand-dev")]);
             setup.add_nix_pkgs(&[Pkg::new("imagemagick")]);
         }
@@ -100,6 +100,11 @@ impl RubyProvider {
         let mut install = Phase::install(None);
         install.add_cache_directory(BUNDLE_CACHE_DIR.to_string());
 
+        if !self.uses_gem_dep(app, "local") {
+            // Only run install if Gemfile or Gemfile.lock has changed
+            install.only_include_files = Some(vec!["Gemfile*".to_string()]);
+        }
+
         install.add_cmd("bundle install".to_string());
 
         // Ensure that the ruby executable is in the PATH
@@ -112,13 +117,12 @@ impl RubyProvider {
     }
 
     fn get_build(&self, app: &App) -> Result<Option<Phase>> {
+        let mut build = Phase::build(None);
         if self.is_rails_app(app) {
-            Ok(Some(Phase::build(Some(
-                "bundle exec rake assets:precompile".to_string(),
-            ))))
-        } else {
-            Ok(None)
+            build.add_cmd("bundle exec rake assets:precompile".to_string());
         }
+
+        Ok(Some(build))
     }
 
     fn get_start(&self, app: &App) -> Result<Option<StartPhase>> {
@@ -144,6 +148,7 @@ impl RubyProvider {
                 "GEM_HOME".to_string(),
                 format!("/usr/local/rvm/gems/{}", ruby_version),
             ),
+            ("MALLOC_ARENA_MAX".to_string(), "2".to_string()),
         ]);
         if self.is_rails_app(app) {
             env_vars.insert("RAILS_LOG_TO_STDOUT".to_string(), "enabled".to_string());


Test patch:
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index f459f38..c0f620a 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -15,7 +15,7 @@ use wait_timeout::ChildExt;
 use rand::thread_rng;
 use rand::{distributions::Alphanumeric, Rng};
 
-fn get_container_ids_from_image(image: &str) -> String {
+async fn get_container_ids_from_image(image: &str) -> String {
     let output = Command::new("docker")
         .arg("ps")
         .arg("-a")
@@ -50,8 +50,8 @@ fn remove_containers(container_id: &str) {
         .unwrap();
 }
 
-fn stop_and_remove_container_by_image(image: &str) {
-    let container_ids = get_container_ids_from_image(image);
+async fn stop_and_remove_container_by_image(image: &str) {
+    let container_ids = get_container_ids_from_image(image).await;
     let container_id = container_ids.trim().split('\n').collect::<Vec<_>>()[0].to_string();
 
     stop_and_remove_container(container_id);
@@ -69,7 +69,7 @@ struct Config {
 }
 /// Runs an image with Docker and returns the output
 /// The image is automatically stopped and removed after `TIMEOUT_SECONDS`
-fn run_image(name: &str, cfg: Option<Config>) -> String {
+async fn run_image(name: &str, cfg: Option<Config>) -> String {
     let mut cmd = Command::new("docker");
     cmd.arg("run");
 
@@ -92,7 +92,7 @@ fn run_image(name: &str, cfg: Option<Config>) -> String {
     let _status_code = match child.wait_timeout(secs).unwrap() {
         Some(status) => status.code(),
         None => {
-            stop_and_remove_container_by_image(name);
+            stop_and_remove_container_by_image(name).await;
             child.wait().unwrap().code()
         }
     };
@@ -107,7 +107,7 @@ fn run_image(name: &str, cfg: Option<Config>) -> String {
 
 /// Builds a directory with default options
 /// Returns the randomly generated image name
-fn simple_build(path: &str) -> String {
+async fn simple_build(path: &str) -> String {
     let name = Uuid::new_v4().to_string();
     create_docker_image(
         path,
@@ -119,12 +119,13 @@ fn simple_build(path: &str) -> String {
             ..Default::default()
         },
     )
+    .await
     .unwrap();
 
     name
 }
 
-fn build_with_build_time_env_vars(path: &str, env_vars: Vec<&str>) -> String {
+async fn build_with_build_time_env_vars(path: &str, env_vars: Vec<&str>) -> String {
     let name = Uuid::new_v4().to_string();
     create_docker_image(
         path,
@@ -136,6 +137,7 @@ fn build_with_build_time_env_vars(path: &str, env_vars: Vec<&str>) -> String {
             ..Default::default()
         },
     )
+    .await
     .unwrap();
 
     name
@@ -323,14 +325,14 @@ fn run_mysql() -> Container {
     }
 }
 
-#[test]
-fn test_deno() {
-    let name = simple_build("./examples/deno");
-    assert!(run_image(&name, None).contains("Hello from Deno"));
+#[tokio::test]
+async fn test_deno() {
+    let name = simple_build("./examples/deno").await;
+    assert!(run_image(&name, None).await.contains("Hello from Deno"));
 }
 
-#[test]
-fn test_elixir_no_ecto() {
+#[tokio::test]
+async fn test_elixir_no_ecto() {
     let rand_64_str: String = thread_rng()
         .sample_iter(&Alphanumeric)
         .take(64)
@@ -340,96 +342,116 @@ fn test_elixir_no_ecto() {
     let name = build_with_build_time_env_vars(
         "./examples/elixir_no_ecto",
         vec![&*secret_env, "MIX_ENV=prod"],
-    );
+    )
+    .await;
 
-    assert!(run_image(&name, None).contains("Hello from Phoenix"));
+    assert!(run_image(&name, None).await.contains("Hello from Phoenix"));
 }
 
-#[test]
-fn test_node() {
-    let name = simple_build("./examples/node");
-    assert!(run_image(&name, None).contains("Hello from Node"));
+#[tokio::test]
+async fn test_node() {
+    let name = simple_build("./examples/node").await;
+    assert!(run_image(&name, None).await.contains("Hello from Node"));
 }
 
-#[test]
-fn test_node_nx_default_app() {
-    let name = simple_build("./examples/node-nx");
-    assert!(run_image(&name, None).contains("nx express app works"));
+#[tokio::test]
+async fn test_node_nx_default_app() {
+    let name = simple_build("./examples/node-nx").await;
+    assert!(run_image(&name, None)
+        .await
+        .contains("nx express app works"));
 }
 
-#[test]
-fn test_node_nx_next() {
+#[tokio::test]
+async fn test_node_nx_next() {
     let name =
-        build_with_build_time_env_vars("./examples/node-nx", vec!["NIXPACKS_NX_APP_NAME=next-app"]);
+        build_with_build_time_env_vars("./examples/node-nx", vec!["NIXPACKS_NX_APP_NAME=next-app"])
+            .await;
 
     assert!(run_image(&name, None)
+        .await
         .contains("ready - started server on 0.0.0.0:3000, url: http://localhost:3000"));
 }
 
-#[test]
-fn test_node_nx_start_command() {
+#[tokio::test]
+async fn test_node_nx_start_command() {
     let name = build_with_build_time_env_vars(
         "./examples/node-nx",
         vec!["NIXPACKS_NX_APP_NAME=start-command"],
-    );
+    )
+    .await;
 
-    assert!(run_image(&name, None).contains("nx express app works"));
+    assert!(run_image(&name, None)
+        .await
+        .contains("nx express app works"));
 }
 
-#[test]
-fn test_node_nx_no_options() {
+#[tokio::test]
+async fn test_node_nx_no_options() {
     let name = build_with_build_time_env_vars(
         "./examples/node-nx",
         vec!["NIXPACKS_NX_APP_NAME=no-options"],
-    );
+    )
+    .await;
 
-    assert!(run_image(&name, None).contains("fake start command started!"));
+    assert!(run_image(&name, None)
+        .await
+        .contains("fake start command started!"));
 }
 
-#[test]
-fn test_node_nx_start_command_production() {
+#[tokio::test]
+async fn test_node_nx_start_command_production() {
     let name = build_with_build_time_env_vars(
         "./examples/node-nx",
         vec!["NIXPACKS_NX_APP_NAME=start-command-production"],
-    );
+    )
+    .await;
 
-    assert!(run_image(&name, None).contains("nx express app works"));
+    assert!(run_image(&name, None)
+        .await
+        .contains("nx express app works"));
 }
 
-#[test]
-fn test_node_nx_node() {
+#[tokio::test]
+async fn test_node_nx_node() {
     let name =
-        build_with_build_time_env_vars("./examples/node-nx", vec!["NIXPACKS_NX_APP_NAME=node-app"]);
+        build_with_build_time_env_vars("./examples/node-nx", vec!["NIXPACKS_NX_APP_NAME=node-app"])
+            .await;
 
-    assert!(run_image(&name, None).contains("Hello from node-app!"));
+    assert!(run_image(&name, None)
+        .await
+        .contains("Hello from node-app!"));
 }
 
-#[test]
-fn test_node_nx_express() {
+#[tokio::test]
+async fn test_node_nx_express() {
     let name = build_with_build_time_env_vars(
         "./examples/node-nx",
         vec!["NIXPACKS_NX_APP_NAME=express-app"],
-    );
+    )
+    .await;
 
-    assert!(run_image(&name, None).contains("nx express app works"));
+    assert!(run_image(&name, None)
+        .await
+        .contains("nx express app works"));
 }
 
-#[test]
-fn test_node_custom_version() {
-    let name = simple_build("./examples/node-custom-version");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_node_custom_version() {
+    let name = simple_build("./examples/node-custom-version").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Node version: v18"));
 }
 
-#[test]
-fn test_node_canvas() {
-    let name = simple_build("./examples/node-canvas");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_node_canvas() {
+    let name = simple_build("./examples/node-canvas").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Node canvas"));
 }
 
-#[test]
-fn test_prisma_postgres() {
+#[tokio::test]
+async fn test_prisma_postgres() {
     // Create the network
     let n = create_network();
     let network_name = n.name.clone();
@@ -442,7 +464,7 @@ fn test_prisma_postgres() {
     attach_container_to_network(n.name, container_name.clone());
 
     // Build the Django example
-    let name = simple_build("./examples/node-prisma-postgres");
+    let name = simple_build("./examples/node-prisma-postgres").await;
 
     // Run the Rails example on the attached network
     let output = run_image(
@@ -451,7 +473,8 @@ fn test_prisma_postgres() {
             environment_variables: c.config.unwrap().environment_variables,
             network: Some(network_name.clone()),
         }),
-    );
+    )
+    .await;
 
     // Cleanup containers and networks
     stop_and_remove_container(container_name);
@@ -460,110 +483,111 @@ fn test_prisma_postgres() {
     assert!(output.contains("My post content"));
 }
 
-#[test]
-fn test_yarn_custom_version() {
-    let name = simple_build("./examples/node-yarn-custom-node-version");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_yarn_custom_version() {
+    let name = simple_build("./examples/node-yarn-custom-node-version").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Node version: v14"));
 }
 
-#[test]
-fn test_node_turborepo() {
+#[tokio::test]
+async fn test_node_turborepo() {
     let name = build_with_build_time_env_vars(
         "./examples/node-turborepo",
         vec!["NIXPACKS_TURBO_APP_NAME=web"],
-    );
+    )
+    .await;
 
-    assert!(run_image(&name, None).contains("> next start"));
+    assert!(run_image(&name, None).await.contains("> next start"));
 }
 
-#[test]
-fn test_yarn_berry() {
-    let name = simple_build("./examples/node-yarn-berry");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_yarn_berry() {
+    let name = simple_build("./examples/node-yarn-berry").await;
+    let output = run_image(&name, None).await;
 
     assert!(output.contains("Hello from Yarn v2+"));
 }
 
-#[test]
-fn test_yarn_prisma() {
-    let name = simple_build("./examples/node-yarn-prisma");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_yarn_prisma() {
+    let name = simple_build("./examples/node-yarn-prisma").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("My post content"));
 }
 
-#[test]
-fn test_pnpm() {
-    let name = simple_build("./examples/node-pnpm");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_pnpm() {
+    let name = simple_build("./examples/node-pnpm").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from PNPM"));
 }
 
-#[test]
-fn test_bun() {
-    let name = simple_build("./examples/node-bun");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_bun() {
+    let name = simple_build("./examples/node-bun").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Bun"));
 }
 
-#[test]
-fn test_bun_web_server() {
-    let name = simple_build("./examples/node-bun-web-server");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_bun_web_server() {
+    let name = simple_build("./examples/node-bun-web-server").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from a Bun web server!"));
 }
 
-#[test]
-fn test_pnpm_custom_version() {
-    let name = simple_build("./examples/node-pnpm-custom-node-version");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_pnpm_custom_version() {
+    let name = simple_build("./examples/node-pnpm-custom-node-version").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from PNPM"));
 }
 
-#[test]
-fn test_puppeteer() {
-    let name = simple_build("./examples/node-puppeteer");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_puppeteer() {
+    let name = simple_build("./examples/node-puppeteer").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from puppeteer"));
 }
 
-#[test]
-fn test_csharp() {
-    let name = simple_build("./examples/csharp-cli");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_csharp() {
+    let name = simple_build("./examples/csharp-cli").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello world from C#"));
 }
 
-#[test]
-fn test_fsharp() {
-    let name = simple_build("./examples/fsharp-cli");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_fsharp() {
+    let name = simple_build("./examples/fsharp-cli").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello world from F#"));
 }
 
-#[test]
-fn test_python() {
-    let name = simple_build("./examples/python");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_python() {
+    let name = simple_build("./examples/python").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Python"));
 }
 
-#[test]
-fn test_python_procfile() {
-    let name = simple_build("./examples/python-procfile");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_python_procfile() {
+    let name = simple_build("./examples/python-procfile").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Python"));
 }
 
-#[test]
-fn test_python_2() {
-    let name = simple_build("./examples/python-2");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_python_2() {
+    let name = simple_build("./examples/python-2").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Python 2"));
 }
 
-#[test]
-fn test_django() {
+#[tokio::test]
+async fn test_django() {
     // Create the network
     let n = create_network();
     let network_name = n.name.clone();
@@ -576,7 +600,7 @@ fn test_django() {
     attach_container_to_network(n.name, container_name.clone());
 
     // Build the Django example
-    let name = simple_build("./examples/python-django");
+    let name = simple_build("./examples/python-django").await;
 
     // Run the Django example on the attached network
     let output = run_image(
@@ -585,7 +609,8 @@ fn test_django() {
             environment_variables: c.config.unwrap().environment_variables,
             network: Some(network_name.clone()),
         }),
-    );
+    )
+    .await;
 
     // Cleanup containers and networks
     stop_and_remove_container(container_name);
@@ -594,8 +619,8 @@ fn test_django() {
     assert!(output.contains("Running migrations"));
 }
 
-#[test]
-fn test_django_mysql() {
+#[tokio::test]
+async fn test_django_mysql() {
     let n = create_network();
     let network_name = n.name.clone();
 
@@ -604,7 +629,7 @@ fn test_django_mysql() {
 
     attach_container_to_network(n.name, container_name.clone());
 
-    let name = simple_build("./examples/python-django-mysql");
+    let name = simple_build("./examples/python-django-mysql").await;
 
     let output = run_image(
         &name,
@@ -612,7 +637,8 @@ fn test_django_mysql() {
             environment_variables: c.config.unwrap().environment_variables,
             network: Some(network_name.clone()),
         }),
-    );
+    )
+    .await;
 
     stop_and_remove_container(container_name);
     remove_network(network_name);
@@ -620,29 +646,29 @@ fn test_django_mysql() {
     assert!(output.contains("Running migrations"));
 }
 
-#[test]
-fn test_python_poetry() {
-    let name = simple_build("./examples/python-poetry");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_python_poetry() {
+    let name = simple_build("./examples/python-poetry").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Python-Poetry"));
 }
 
-#[test]
-fn test_python_numpy() {
-    let name = simple_build("./examples/python-numpy");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_python_numpy() {
+    let name = simple_build("./examples/python-numpy").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Python numpy and pandas"));
 }
 
-#[test]
-fn test_python_postgres() {
-    let name = simple_build("./examples/python-postgres");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_python_postgres() {
+    let name = simple_build("./examples/python-postgres").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("psycopg2"));
 }
 
-#[test]
-fn test_rust_custom_version() {
+#[tokio::test]
+async fn test_rust_custom_version() {
     let name = Uuid::new_v4().to_string();
     create_docker_image(
         "./examples/rust-custom-version",
@@ -654,70 +680,71 @@ fn test_rust_custom_version() {
             ..Default::default()
         },
     )
+    .await
     .unwrap();
 
-    let output = run_image(&name, None);
+    let output = run_image(&name, None).await;
     assert!(output.contains("cargo 1.56.0"));
 }
 
-#[test]
-fn test_rust_ring() {
-    let name = simple_build("./examples/rust-ring");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_rust_ring() {
+    let name = simple_build("./examples/rust-ring").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from rust"));
 }
 
-#[test]
-fn test_rust_openssl() {
-    let name = simple_build("./examples/rust-openssl");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_rust_openssl() {
+    let name = simple_build("./examples/rust-openssl").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Rust openssl!"));
 }
 
-#[test]
-fn test_rust_cargo_workspaces() {
-    let name = simple_build("./examples/rust-cargo-workspaces");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_rust_cargo_workspaces() {
+    let name = simple_build("./examples/rust-cargo-workspaces").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from rust"));
 }
 
-#[test]
-fn test_rust_cargo_workspaces_glob() {
-    let name = simple_build("./examples/rust-cargo-workspaces-glob");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_rust_cargo_workspaces_glob() {
+    let name = simple_build("./examples/rust-cargo-workspaces-glob").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from rust"));
 }
 
-#[test]
-fn test_go() {
-    let name = simple_build("./examples/go");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_go() {
+    let name = simple_build("./examples/go").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Go"));
 }
 
-#[test]
-fn test_go_custom_version() {
-    let name = simple_build("./examples/go-custom-version");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_go_custom_version() {
+    let name = simple_build("./examples/go-custom-version").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from go1.18"));
 }
 
-#[test]
-fn test_haskell_stack() {
-    let name = simple_build("./examples/haskell-stack");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_haskell_stack() {
+    let name = simple_build("./examples/haskell-stack").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Haskell"));
 }
 
-#[test]
-fn test_crystal() {
-    let name = simple_build("./examples/crystal");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_crystal() {
+    let name = simple_build("./examples/crystal").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Crystal"));
 }
 
-#[test]
-fn test_cowsay() {
+#[tokio::test]
+async fn test_cowsay() {
     let name = Uuid::new_v4().to_string();
     create_docker_image(
         "./examples/shell-hello",
@@ -729,20 +756,21 @@ fn test_cowsay() {
             ..Default::default()
         },
     )
+    .await
     .unwrap();
-    let output = run_image(&name, None);
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello World"));
 }
 
-#[test]
-fn test_staticfile() {
-    let name = simple_build("./examples/staticfile");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_staticfile() {
+    let name = simple_build("./examples/staticfile").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("start worker process"));
 }
 
-#[test]
-fn test_swift() {
+#[tokio::test]
+async fn test_swift() {
     let name = Uuid::new_v4().to_string();
 
     create_docker_image(
@@ -755,71 +783,72 @@ fn test_swift() {
             ..Default::default()
         },
     )
+    .await
     .unwrap();
 
-    let output = run_image(&name, None);
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from swift"));
 }
 
-#[test]
-fn test_dart() {
-    let name = simple_build("./examples/dart");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_dart() {
+    let name = simple_build("./examples/dart").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Dart"));
 }
 
-#[test]
-fn test_java_maven() {
-    let name = simple_build("./examples/java-maven");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_java_maven() {
+    let name = simple_build("./examples/java-maven").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Built with Spring Boot"));
 }
 
-#[test]
-fn test_zig() {
-    let name = simple_build("./examples/zig");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_zig() {
+    let name = simple_build("./examples/zig").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Zig"));
 }
 
-#[test]
-fn test_zig_gyro() {
-    let name = simple_build("./examples/zig-gyro");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_zig_gyro() {
+    let name = simple_build("./examples/zig-gyro").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Zig"));
     assert!(output.contains("The URI scheme of GitHub is https."));
 }
 
-#[test]
-fn test_ruby_sinatra() {
-    let name = simple_build("./examples/ruby-sinatra/");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_ruby_sinatra() {
+    let name = simple_build("./examples/ruby-sinatra/").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Sinatra"));
 }
 
-#[test]
-fn test_ruby_node() {
-    let name = simple_build("./examples/ruby-with-node/");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_ruby_node() {
+    let name = simple_build("./examples/ruby-with-node/").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello from Ruby with Node"));
 }
 
-#[test]
-fn test_ruby_execjs() {
-    let name = simple_build("./examples/ruby-execjs/");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_ruby_execjs() {
+    let name = simple_build("./examples/ruby-execjs/").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("HELLO FROM EXECJS"));
 }
 
-#[test]
-fn test_ruby_local_deps() {
-    let name = simple_build("./examples/ruby-local-deps/");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_ruby_local_deps() {
+    let name = simple_build("./examples/ruby-local-deps/").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Hello world from Local lib"));
 }
 
-#[test]
-fn test_ruby_rails() {
+#[tokio::test]
+async fn test_ruby_rails() {
     // Create the network
     let n = create_network();
     let network_name = n.name.clone();
@@ -831,8 +860,8 @@ fn test_ruby_rails() {
     // Attach the postgres instance to the network
     attach_container_to_network(n.name, container_name.clone());
 
-    // Build the Django example
-    let name = simple_build("./examples/ruby-rails-postgres");
+    // Build the Rails example
+    let name = simple_build("./examples/ruby-rails-postgres").await;
 
     // Run the Rails example on the attached network
     let output = run_image(
@@ -841,7 +870,8 @@ fn test_ruby_rails() {
             environment_variables: c.config.unwrap().environment_variables,
             network: Some(network_name.clone()),
         }),
-    );
+    )
+    .await;
 
     // Cleanup containers and networks
     stop_and_remove_container(container_name);
@@ -850,76 +880,82 @@ fn test_ruby_rails() {
     assert!(output.contains("Rails 7"));
 }
 
-#[test]
-fn test_clojure() {
-    let name = simple_build("./examples/clojure");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_clojure() {
+    let name = simple_build("./examples/clojure").await;
+    let output = run_image(&name, None).await;
     assert_eq!(output, "Hello, World From Clojure!");
 }
 
-#[test]
-fn test_clojure_ring_app() {
-    let name = simple_build("./examples/clojure-ring-app");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_clojure_ring_app() {
+    let name = simple_build("./examples/clojure-ring-app").await;
+    let output = run_image(&name, None).await;
     assert_eq!(output, "Started server on port 3000");
 }
 
-#[test]
-fn test_cobol() {
-    let name = simple_build("./examples/cobol");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_cobol() {
+    let name = simple_build("./examples/cobol").await;
+    let output = run_image(&name, None).await;
     assert_eq!(output, "Hello from cobol! index");
 }
 
-#[test]
-fn test_cobol_src_index() {
-    let name = simple_build("./examples/cobol-src");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_cobol_src_index() {
+    let name = simple_build("./examples/cobol-src").await;
+    let output = run_image(&name, None).await;
     assert_eq!(output, "Hello from cobol! src-index");
 }
 
-#[test]
-fn test_cobol_my_app() {
+#[tokio::test]
+async fn test_cobol_my_app() {
     let name =
-        build_with_build_time_env_vars("./examples/cobol", vec!["NIXPACKS_COBOL_APP_NAME=my-app"]);
+        build_with_build_time_env_vars("./examples/cobol", vec!["NIXPACKS_COBOL_APP_NAME=my-app"])
+            .await;
 
-    assert_eq!(run_image(&name, None), "Hello from cobol! my-app");
+    assert_eq!(run_image(&name, None).await, "Hello from cobol! my-app");
 }
 
-#[test]
-fn test_cobol_src_my_app() {
+#[tokio::test]
+async fn test_cobol_src_my_app() {
     let name = build_with_build_time_env_vars(
         "./examples/cobol-src",
         vec!["NIXPACKS_COBOL_APP_NAME=my-app"],
-    );
+    )
+    .await;
 
-    assert_eq!(run_image(&name, None), "Hello from cobol! src-my-app");
+    assert_eq!(run_image(&name, None).await, "Hello from cobol! src-my-app");
 }
 
-#[test]
-fn test_cobol_free() {
+#[tokio::test]
+async fn test_cobol_free() {
     let name = build_with_build_time_env_vars(
         "./examples/cobol",
         vec![
             "NIXPACKS_COBOL_APP_NAME=cobol-free",
             "NIXPACKS_COBOL_COMPILE_ARGS=-free -x -o",
         ],
-    );
+    )
+    .await;
 
-    assert_eq!(run_image(&name, None), "Hello from cobol! cobol-free");
+    assert_eq!(run_image(&name, None).await, "Hello from cobol! cobol-free");
 }
 
-#[test]
-fn test_cobol_no_index() {
-    let name = simple_build("./examples/cobol-no-index");
+#[tokio::test]
+async fn test_cobol_no_index() {
+    let name = simple_build("./examples/cobol-no-index").await;
 
-    assert_eq!(run_image(&name, None), "Hello from cobol! cobol-no-index");
+    assert_eq!(
+        run_image(&name, None).await,
+        "Hello from cobol! cobol-no-index"
+    );
 }
 
-#[test]
-fn test_multiple_providers() {
-    let name = simple_build("./examples/multiple-providers");
-    let output = run_image(&name, None);
+#[tokio::test]
+async fn test_multiple_providers() {
+    let name = simple_build("./examples/multiple-providers").await;
+    let output = run_image(&name, None).await;
     assert!(output.contains("Python"));
     assert!(output.contains("go"));
     assert!(output.contains("deno"));
diff --git a/tests/snapshots/generate_plan_tests__ruby.snap b/tests/snapshots/generate_plan_tests__ruby.snap
index ee48faf..0645b23 100644
--- a/tests/snapshots/generate_plan_tests__ruby.snap
+++ b/tests/snapshots/generate_plan_tests__ruby.snap
@@ -9,9 +9,16 @@ expression: plan
     "BUNDLE_GEMFILE": "/app/Gemfile",
     "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
+    "MALLOC_ARENA_MAX": "2",
     "NIXPACKS_METADATA": "ruby"
   },
   "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ]
+    },
     "install": {
       "name": "install",
       "dependsOn": [
@@ -20,6 +27,9 @@ expression: plan
       "cmds": [
         "bundle install"
       ],
+      "onlyIncludeFiles": [
+        "Gemfile*"
+      ],
       "cacheDirectories": [
         "/root/.bundle/cache"
       ],
diff --git a/tests/snapshots/generate_plan_tests__ruby_execjs.snap b/tests/snapshots/generate_plan_tests__ruby_execjs.snap
index cbcbfb4..b4b1371 100644
--- a/tests/snapshots/generate_plan_tests__ruby_execjs.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_execjs.snap
@@ -9,9 +9,17 @@ expression: plan
     "BUNDLE_GEMFILE": "/app/Gemfile",
     "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
+    "MALLOC_ARENA_MAX": "2",
     "NIXPACKS_METADATA": "ruby"
   },
   "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install",
+        "node:install"
+      ]
+    },
     "install": {
       "name": "install",
       "dependsOn": [
@@ -20,6 +28,9 @@ expression: plan
       "cmds": [
         "bundle install"
       ],
+      "onlyIncludeFiles": [
+        "Gemfile*"
+      ],
       "cacheDirectories": [
         "/root/.bundle/cache"
       ],
diff --git a/tests/snapshots/generate_plan_tests__ruby_local_deps.snap b/tests/snapshots/generate_plan_tests__ruby_local_deps.snap
index efc087f..76c5961 100644
--- a/tests/snapshots/generate_plan_tests__ruby_local_deps.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_local_deps.snap
@@ -9,9 +9,16 @@ expression: plan
     "BUNDLE_GEMFILE": "/app/Gemfile",
     "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
+    "MALLOC_ARENA_MAX": "2",
     "NIXPACKS_METADATA": "ruby"
   },
   "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ]
+    },
     "install": {
       "name": "install",
       "dependsOn": [
diff --git a/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
index 90360a9..8126cce 100644
--- a/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
@@ -9,6 +9,7 @@ expression: plan
     "BUNDLE_GEMFILE": "/app/Gemfile",
     "GEM_HOME": "/usr/local/rvm/gems/3.1.2",
     "GEM_PATH": "/usr/local/rvm/gems/3.1.2:/usr/local/rvm/gems/3.1.2@global",
+    "MALLOC_ARENA_MAX": "2",
     "NIXPACKS_METADATA": "ruby",
     "RAILS_LOG_TO_STDOUT": "enabled"
   },
@@ -30,6 +31,9 @@ expression: plan
       "cmds": [
         "bundle install"
       ],
+      "onlyIncludeFiles": [
+        "Gemfile*"
+      ],
       "cacheDirectories": [
         "/root/.bundle/cache"
       ],
diff --git a/tests/snapshots/generate_plan_tests__ruby_sinatra.snap b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
index f352383..e4a272b 100644
--- a/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
@@ -9,9 +9,16 @@ expression: plan
     "BUNDLE_GEMFILE": "/app/Gemfile",
     "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
+    "MALLOC_ARENA_MAX": "2",
     "NIXPACKS_METADATA": "ruby"
   },
   "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ]
+    },
     "install": {
       "name": "install",
       "dependsOn": [
@@ -20,6 +27,9 @@ expression: plan
       "cmds": [
         "bundle install"
       ],
+      "onlyIncludeFiles": [
+        "Gemfile*"
+      ],
       "cacheDirectories": [
         "/root/.bundle/cache"
       ],
diff --git a/tests/snapshots/generate_plan_tests__ruby_with_node.snap b/tests/snapshots/generate_plan_tests__ruby_with_node.snap
index 0ffb7ad..7e64737 100644
--- a/tests/snapshots/generate_plan_tests__ruby_with_node.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_with_node.snap
@@ -9,9 +9,17 @@ expression: plan
     "BUNDLE_GEMFILE": "/app/Gemfile",
     "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
+    "MALLOC_ARENA_MAX": "2",
     "NIXPACKS_METADATA": "ruby"
   },
   "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install",
+        "node:install"
+      ]
+    },
     "install": {
       "name": "install",
       "dependsOn": [
@@ -20,6 +28,9 @@ expression: plan
       "cmds": [
         "bundle install"
       ],
+      "onlyIncludeFiles": [
+        "Gemfile*"
+      ],
       "cacheDirectories": [
         "/root/.bundle/cache"
       ],