Repository: railwayapp/nixpacks
Pull request #553 (2022-09-14 23:10:12)

Incremental caching
<!-- Thanks for sending a pull request! -->
<!-- Please add a useful description explaining what this PR does -->
NOTE: This is early PoC, feedback is welcomed!

When building a docker image, There are two type of cache content that Docker maintain between builds:
* Image layer caching (to help reduce the amount of layer downloaded from registry)
* Mount/Incremental cache (to keep the files generated between builds, like `/root/.npm`)

Both cache content are served/handled by the local Docker daemon, this means that an image built on specific Docker host. need to be built on the same host each time to take full advantage of caching capabilities 

This PR enables Nixpacks to work more smoothly in a cluster setup (Multiple Docker nodes accessing cache remotely)
After build command runs (for example `npm run build`) the directories need to be cached (like   `/root/.npm`) will be archived with `tar` and sent to a small HTTP server that has been started by Nixpacks to receive these files

These `tar` files -if available- will be copied back to any newer image build. It will extracted to the right location inside the image to help build tools find out the files needed for the build without the needed to re-download it.

From high level, these are the steps:
- Get random free port & start Nixpacks web server with
- Check if the provided incremental-cache image already exists in docker registry 
- Read cached directories from the provided image into the image we're building (using `COPY --from=...`)
- Run build steps 
- tar cached directories (in case any updates happens to) & send to Nixpacks file server
- loop on all tar files & run `docker import` against to create a "system file" image (an image that won't be started as a container) 
- Show a message that incremental cache image has been updated, to let user push it


Numbers for sample tests:

Clean build (no remote cache): `2m44s`
---
<img width="1648" alt="test2" src="https://user-images.githubusercontent.com/781863/193349564-abbe7b2a-9357-4acd-af29-50a34762c49b.png">


Clean build with remote cache (both layer cache + new incremental cache): `1m26s`
---
![test4](https://user-images.githubusercontent.com/781863/193351314-f234414e-4230-4a97-8667-cd5e268fe1a2.png)


Clean build with incremental cache only: `2m46s`
-- 
<img width="1393" alt="test 6" src="https://user-images.githubusercontent.com/781863/193353376-7102976c-8ccd-4ef1-8f60-32f84dcdce8b.png">



Another test case
Clean build (no remote cache): `1m23s`
----
![image](https://user-images.githubusercontent.com/781863/193349674-d29400ce-df9d-44de-8aa3-86c0ec473032.png)


Clean build with remote cache (both layer cache + incremental cache): `0m55s`
---

![test3](https://user-images.githubusercontent.com/781863/193350145-91f2d321-1997-42fd-a2fa-6f1860f88742.png)



Clean build with Incremental cache only: `1m29s`
---
<img width="1394" alt="test 5" src="https://user-images.githubusercontent.com/781863/193352561-5e28ebe7-bae3-4026-808f-351901ac39be.png">

<!-- PR Checklist  -->
<!-- - [ ] Tests are added/updated if needed  -->
<!-- - [ ] Docs are updated if needed  -->


Milo123459: Could we use Axum instead of Actix? 
AhmedMozaly: Could you share some context about why `Axum` maybe in favor of `Actix`?
Milo123459: It is faster. I would be willing to make the first implementation (this one) get merged and then I can later PR to use Axum, with other tokio features.
Milo123459: Could you post some screenshots of speed improvements?
AhmedMozaly: > Could you post some screenshots of speed improvements?

Will do!
coffee-cup: > It is faster.

I've seen in a few places that Actix is more production ready compared to Axum. I think we should do a few comparisons and optimize for our use case instead of switching to Axum because it is faster. This is a small HTTP server that will be doing well below 100/min so we it doesn't need to be that fast.

@Milo123459 
Milo123459: Ah, fair enough. The tokio ecosystem is arguably better, but we can always change if we need to.

PS: @AhmedMozaly any benchmarks yet?
coffee-cup: > The tokio ecosystem is arguably better

Totally fair too. I do thinking using the Tokio ecoysystem is a good idea. I just didn't want the main reason to switch to be because it is faster, because that is not exactly what we need. But a better ecosystem is worth it I think.
AhmedMozaly: > Ah, fair enough. The tokio ecosystem is arguably better, but we can always change if we need to.
> 
> PS: @AhmedMozaly any benchmarks yet?

Will add some numbers in the next 2 day, stay tuned 😄 
AhmedMozaly: @Milo123459 I got pulled into other stuff but this PR should be my primary focus next week!
Milo123459: Hate to be that guy, but any benchmarks? Sorry!
AhmedMozaly: > Hate to be that guy, but any benchmarks? Sorry!

@Milo123459 I've added numbers for two test cases
Please note that the main goal was to move cold builds to use remote cache (for both layer + incremental cache)

For Incremental cache alone (without layers cache) the goal is to keep numbers of cold builds almost as is with minimal overhead, Given the fact that we're doing more IO work (by starting Web server > sending files to > create system file. docker image with)

Thanks for your following up!

Milo123459: Great to see these performance improvements! Now I think the outstanding review comments need to be looked at and conflicts to be fixed before we can merge this.

Patch:
diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 0000000..0d4bc40
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,2 @@
+[alias]
+generate_plan_test = "test --package nixpacks --lib --test generate_plan_tests"
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b8266db..0046be1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -160,11 +160,11 @@ jobs:
       - name: Checkout sources
         uses: actions/checkout@v3
 
-      - name: Install 1.57 rust toolchain
+      - name: Install 1.59 rust toolchain
         uses: actions-rs/toolchain@v1
         with:
           profile: minimal
-          toolchain: 1.57
+          toolchain: 1.59
           override: true
 
       - uses: Swatinem/rust-cache@v2
diff --git a/Cargo.lock b/Cargo.lock
index 626e842..2ccfd6c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,202 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "actix-codec"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "actix-http"
+version = "3.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c83abf9903e1f0ad9973cc4f7b9767fd5a03a583f51a5b7a339e07987cd2724"
+dependencies = [
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "ahash",
+ "base64",
+ "bitflags",
+ "brotli",
+ "bytes",
+ "bytestring",
+ "derive_more",
+ "encoding_rs",
+ "flate2",
+ "futures-core",
+ "h2",
+ "http",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "language-tags",
+ "local-channel",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rand 0.8.5",
+ "sha1",
+ "smallvec",
+ "tracing",
+ "zstd",
+]
+
+[[package]]
+name = "actix-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "actix-router"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799"
+dependencies = [
+ "bytestring",
+ "http",
+ "regex",
+ "serde",
+ "tracing",
+]
+
+[[package]]
+name = "actix-rt"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000"
+dependencies = [
+ "futures-core",
+ "tokio",
+]
+
+[[package]]
+name = "actix-server"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0da34f8e659ea1b077bb4637948b815cd3768ad5a188fdcd74ff4d84240cd824"
+dependencies = [
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "futures-core",
+ "futures-util",
+ "mio",
+ "num_cpus",
+ "socket2",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "actix-service"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a"
+dependencies = [
+ "futures-core",
+ "paste",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-utils"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e491cbaac2e7fc788dfff99ff48ef317e23b3cf63dbaf7aaab6418f40f92aa94"
+dependencies = [
+ "local-waker",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-web"
+version = "4.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d48f7b6534e06c7bfc72ee91db7917d4af6afe23e7d223b51e68fffbb21e96b9"
+dependencies = [
+ "actix-codec",
+ "actix-http",
+ "actix-macros",
+ "actix-router",
+ "actix-rt",
+ "actix-server",
+ "actix-service",
+ "actix-utils",
+ "actix-web-codegen",
+ "ahash",
+ "bytes",
+ "bytestring",
+ "cfg-if",
+ "cookie",
+ "derive_more",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "http",
+ "itoa",
+ "language-tags",
+ "log",
+ "mime",
+ "once_cell",
+ "pin-project-lite",
+ "regex",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "smallvec",
+ "socket2",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "actix-web-codegen"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13"
+dependencies = [
+ "actix-router",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "0.7.19"
@@ -11,11 +207,26 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
 [[package]]
 name = "anyhow"
-version = "1.0.64"
+version = "1.0.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7"
+checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
 
 [[package]]
 name = "atty"
@@ -61,6 +272,27 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea27d8d5fd867b17523bf6788b1175fa9867f34669d057e9adaf76e27bcea44b"
 
+[[package]]
+name = "brotli"
+version = "3.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
 [[package]]
 name = "bstr"
 version = "0.2.17"
@@ -70,17 +302,41 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "bytes"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
+
+[[package]]
+name = "bytestring"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b6a75fd3048808ef06af5cd79712be8111960adaf89d90250974b38fc3928a"
+dependencies = [
+ "bytes",
+]
+
 [[package]]
 name = "cargo_toml"
-version = "0.11.6"
+version = "0.11.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4419e9adae9fd7e231b60d50467481bf8181ddeef6ed54683b23ae925c74c9c"
+checksum = "e72c3ff59e3b7d24630206bb63a73af65da4ed5df1f76ee84dfafb9fee2ba60e"
 dependencies = [
  "serde",
  "serde_derive",
  "toml",
 ]
 
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+dependencies = [
+ "jobserver",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -89,9 +345,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "clap"
-version = "3.2.20"
+version = "3.2.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd"
+checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750"
 dependencies = [
  "bitflags",
  "clap_lex",
@@ -132,6 +388,23 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "cookie"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
 [[package]]
 name = "cpufeatures"
 version = "0.2.5"
@@ -141,6 +414,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.11"
@@ -196,11 +478,24 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+]
+
 [[package]]
 name = "digest"
-version = "0.10.3"
+version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
 dependencies = [
  "block-buffer",
  "crypto-common",
@@ -222,18 +517,135 @@ version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
+[[package]]
+name = "encoding_rs"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
 [[package]]
 name = "fuchsia-cprng"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 
+[[package]]
+name = "futures"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
+
+[[package]]
+name = "futures-task"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
+
+[[package]]
+name = "futures-util"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.14.6"
@@ -268,6 +680,25 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "h2"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.12.3"
@@ -283,12 +714,45 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
 [[package]]
 name = "ident_case"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
+[[package]]
+name = "idna"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
 [[package]]
 name = "ignore"
 version = "0.4.18"
@@ -325,9 +789,9 @@ checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
 
 [[package]]
 name = "insta"
-version = "1.19.1"
+version = "1.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc61e98be01e89296f3343a878e9f8ca75a494cb5aaf29df65ef55734aeb85f5"
+checksum = "58a931b01c76064c5be919faa2ef0dc570e9a889dcd1e5fef08a8ca6eb4d6c0b"
 dependencies = [
  "console",
  "linked-hash-map",
@@ -345,6 +809,21 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
 
+[[package]]
+name = "jobserver"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "language-tags"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -353,9 +832,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.132"
+version = "0.2.133"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
 
 [[package]]
 name = "linked-hash-map"
@@ -363,6 +842,34 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "local-channel"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "local-waker",
+]
+
+[[package]]
+name = "local-waker"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1"
+
+[[package]]
+name = "lock_api"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
 [[package]]
 name = "log"
 version = "0.4.17"
@@ -378,10 +885,38 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
+]
+
 [[package]]
 name = "nixpacks"
-version = "0.9.4"
+version = "0.9.6"
 dependencies = [
+ "actix-web",
  "anyhow",
  "base64",
  "box_drawing",
@@ -390,13 +925,17 @@ dependencies = [
  "colored",
  "console",
  "dotenv-parser",
+ "futures",
+ "futures-util",
  "globset",
  "ignore",
  "indoc",
  "insta",
  "path-slash",
+ "portpicker",
  "rand 0.8.5",
  "regex",
+ "sanitize-filename",
  "serde",
  "serde_json",
  "serde_with",
@@ -410,11 +949,30 @@ dependencies = [
  "walkdir",
 ]
 
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "once_cell"
-version = "1.14.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
 
 [[package]]
 name = "os_str_bytes"
@@ -422,17 +980,52 @@ version = "6.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
 
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
+
 [[package]]
 name = "path-slash"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
 
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
 [[package]]
 name = "pest"
-version = "2.3.0"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4"
+checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048"
 dependencies = [
  "thiserror",
  "ucd-trie",
@@ -440,9 +1033,9 @@ dependencies = [
 
 [[package]]
 name = "pest_derive"
-version = "2.3.0"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "905708f7f674518498c1f8d644481440f476d39ca6ecae83319bba7c6c12da91"
+checksum = "502b62a6d0245378b04ffe0a7fb4f4419a4815fce813bd8a0ec89a56e07d67b1"
 dependencies = [
  "pest",
  "pest_generator",
@@ -450,9 +1043,9 @@ dependencies = [
 
 [[package]]
 name = "pest_generator"
-version = "2.3.0"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad"
+checksum = "451e629bf49b750254da26132f1a5a9d11fd8a95a3df51d15c4abd1ba154cb6c"
 dependencies = [
  "pest",
  "pest_meta",
@@ -463,13 +1056,34 @@ dependencies = [
 
 [[package]]
 name = "pest_meta"
-version = "2.3.0"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1538eb784f07615c6d9a8ab061089c6c54a344c5b4301db51990ca1c241e8c04"
+checksum = "bcec162c71c45e269dfc3fc2916eaeb97feab22993a21bcce4721d08cd7801a6"
 dependencies = [
  "once_cell",
  "pest",
- "sha-1",
+ "sha1",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "portpicker"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9"
+dependencies = [
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -480,9 +1094,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.43"
+version = "1.0.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58"
 dependencies = [
  "unicode-ident",
 ]
@@ -517,7 +1131,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
  "rand_chacha",
- "rand_core 0.6.3",
+ "rand_core 0.6.4",
 ]
 
 [[package]]
@@ -527,7 +1141,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core 0.6.3",
+ "rand_core 0.6.4",
 ]
 
 [[package]]
@@ -547,9 +1161,9 @@ checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
 
 [[package]]
 name = "rand_core"
-version = "0.6.3"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
  "getrandom",
 ]
@@ -563,6 +1177,15 @@ dependencies = [
  "rand_core 0.3.1",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
 [[package]]
 name = "regex"
 version = "1.6.0"
@@ -589,6 +1212,15 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.11"
@@ -604,20 +1236,42 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "sanitize-filename"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c"
+dependencies = [
+ "lazy_static",
+ "regex",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "semver"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
+
 [[package]]
 name = "serde"
-version = "1.0.144"
+version = "1.0.145"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
+checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.144"
+version = "1.0.145"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
+checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -635,6 +1289,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "serde_with"
 version = "1.14.0"
@@ -670,22 +1336,56 @@ dependencies = [
 ]
 
 [[package]]
-name = "sha-1"
-version = "0.10.0"
+name = "sha1"
+version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
+checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
 dependencies = [
  "cfg-if",
  "cpufeatures",
  "digest",
 ]
 
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "similar"
 version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803"
 
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "strsim"
 version = "0.10.0"
@@ -694,9 +1394,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
 [[package]]
 name = "syn"
-version = "1.0.99"
+version = "1.0.101"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
+checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -735,24 +1435,24 @@ dependencies = [
 
 [[package]]
 name = "textwrap"
-version = "0.15.0"
+version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
+checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
 
 [[package]]
 name = "thiserror"
-version = "1.0.34"
+version = "1.0.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252"
+checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.34"
+version = "1.0.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487"
+checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -768,6 +1468,72 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
+dependencies = [
+ "itoa",
+ "libc",
+ "num_threads",
+ "time-macros",
+]
+
+[[package]]
+name = "time-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "once_cell",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
 [[package]]
 name = "toml"
 version = "0.5.9"
@@ -777,6 +1543,27 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "tracing"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7"
+dependencies = [
+ "once_cell",
+]
+
 [[package]]
 name = "typenum"
 version = "1.15.0"
@@ -789,11 +1576,37 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
 
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
 [[package]]
 name = "unicode-ident"
-version = "1.0.3"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
 
 [[package]]
 name = "uuid"
@@ -867,6 +1680,49 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
 [[package]]
 name = "yaml-rust"
 version = "0.4.5"
@@ -875,3 +1731,32 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
 dependencies = [
  "linked-hash-map",
 ]
+
+[[package]]
+name = "zstd"
+version = "0.11.2+zstd.1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "5.0.2+zstd.1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
+dependencies = [
+ "libc",
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.1+zstd.1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b"
+dependencies = [
+ "cc",
+ "libc",
+]
diff --git a/Cargo.toml b/Cargo.toml
index dd203e5..8c6fdfe 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nixpacks"
-version = "0.9.4"
+version = "0.9.6"
 edition = "2021"
 license = "MIT"
 authors = ["Railway <contact@railway.app>"]
@@ -8,7 +8,7 @@ description = "Generate an OCI compliant image based off app source"
 readme = "README.md"
 homepage = "https://github.com/railwayapp/nixpacks"
 repository = "https://github.com/railwayapp/nixpacks"
-rust-version = "1.57"
+rust-version = "1.59"
 exclude = ["examples/", "docs/"]
 
 [[bin]]
@@ -48,6 +48,11 @@ cargo_toml = "0.11.6"
 rand = "0.8.5"
 path-slash = "0.2.1"
 ignore = "0.4.18"
+actix-web = "4"
+sanitize-filename = "0.4"
+futures-util = "0.3"
+futures = "0.3"
+portpicker =  "0.1.1"
 
 [dev-dependencies]
 dotenv-parser = "0.1.3"
diff --git a/README.md b/README.md
index 59baed6..63b4bee 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 [![CI](https://github.com/railwayapp/bb/actions/workflows/ci.yml/badge.svg)](https://github.com/railwayapp/bb/actions/workflows/ci.yml)
 [![Crates.io](https://img.shields.io/crates/v/nixpacks)](https://crates.io/crates/nixpacks)
-[![Rust: 1.57+](https://img.shields.io/badge/rust-1.57+-93450a)](https://blog.rust-lang.org/2021/12/02/Rust-1.57.0.html)
+[![Rust: 1.59+](https://img.shields.io/badge/rust-1.59+-93450a)](https://blog.rust-lang.org/2022/02/24/Rust-1.59.0.html)
 
 **App source + Nix packages + Docker = Image**
 
diff --git a/docs/pages/docs/install.md b/docs/pages/docs/install.md
index 13df6af..1d5510e 100644
--- a/docs/pages/docs/install.md
+++ b/docs/pages/docs/install.md
@@ -58,7 +58,7 @@ scoop install nixpacks
 
 Build and install from source using [Rust](https://www.rust-lang.org/tools/install).
 
-> Nixpacks currently requires a [Rust](https://www.rust-lang.org/tools/install) version no lower than [1.57](https://blog.rust-lang.org/2021/12/02/Rust-1.57.0.html)
+> Nixpacks currently requires a [Rust](https://www.rust-lang.org/tools/install) version no lower than [1.59](https://blog.rust-lang.org/2022/02/24/Rust-1.59.0.html)
 
 ```sh
 cargo install nixpacks
diff --git a/examples/node-canvas/index.js b/examples/node-canvas/index.js
index e8e31fe..4e96657 100644
--- a/examples/node-canvas/index.js
+++ b/examples/node-canvas/index.js
@@ -1,2 +1,11 @@
-require("canvas");
-console.log("Hello from Node canvas");
+const canvas = require('canvas');
+
+const box = canvas.createCanvas(100, 100);
+
+const fs = require('fs')
+const out = fs.createWriteStream(__dirname + '/test.png')
+const stream = box.createPNGStream()
+stream.pipe(out)
+out.on('finish', () =>  console.log('The PNG file was created.'))
+
+console.log("Hello from Node canvas")
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 035f626..c19bde7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -118,6 +118,17 @@ fn main() -> Result<()> {
                         .long("no-cache")
                         .help("Disable building with the cache"),
                 )
+                .arg(
+                    Arg::new("incremental-cache-image")
+                        .long("incremental-cache-image")
+                        .help("Image to hold the cached directories between builds.")
+                        .takes_value(true),
+                )
+                .arg(
+                    Arg::new("verbose")
+                        .long("verbose")
+                        .help("Display more info during build."),
+                )
                 .arg(
                     Arg::new("inline-cache")
                         .long("inline-cache")
@@ -273,7 +284,17 @@ fn main() -> Result<()> {
             let mut cache_key = matches.value_of("cache-key").map(ToString::to_string);
             let no_cache = matches.is_present("no-cache");
             let inline_cache = matches.is_present("inline-cache");
-            let cache_from = matches.value_of("cache-from").map(ToString::to_string);
+            let verbose = matches.is_present("verbose") || envs.contains(&"NIXPACKS_VERBOSE=1");
+
+            let cache_from = if !no_cache {
+                matches.value_of("cache-from").map(ToString::to_string)
+            } else {
+                None
+            };
+
+            let incremental_cache_image = matches
+                .value_of("incremental-cache-image")
+                .map(ToString::to_string);
 
             // Default to absolute `path` of the source that is being built as the cache-key if not disabled
             if !no_cache && cache_key.is_none() {
@@ -312,6 +333,8 @@ fn main() -> Result<()> {
                 inline_cache,
                 cache_from,
                 no_error_without_start,
+                incremental_cache_image,
+                verbose,
             };
 
             create_docker_image(path, envs, &options, build_options)?;
diff --git a/src/nixpacks/builder/docker/docker_image_builder.rs b/src/nixpacks/builder/docker/docker_image_builder.rs
index 37007cf..dfcb77e 100644
--- a/src/nixpacks/builder/docker/docker_image_builder.rs
+++ b/src/nixpacks/builder/docker/docker_image_builder.rs
@@ -1,10 +1,16 @@
 use super::{dockerfile_generation::DockerfileGenerator, DockerBuilderOptions, ImageBuilder};
 use crate::nixpacks::{
-    builder::docker::dockerfile_generation::OutputDir, environment::Environment, files,
-    logger::Logger, plan::BuildPlan,
+    builder::docker::{
+        dockerfile_generation::OutputDir,
+        file_server::FileServer,
+        incremental_cache::{IncrementalCache, IncrementalCacheDirs},
+    },
+    environment::Environment,
+    files,
+    logger::Logger,
+    plan::BuildPlan,
 };
 use anyhow::{bail, Context, Ok, Result};
-
 use std::{
     fs::{self, remove_dir_all, File},
     process::Command,
@@ -36,8 +42,19 @@ impl ImageBuilder for DockerImageBuilder {
         let name = self.options.name.clone().unwrap_or_else(|| id.to_string());
         output.ensure_output_exists()?;
 
+        let incremental_cache = IncrementalCache::default();
+        let incremental_cache_dirs = IncrementalCacheDirs::create(&output)?;
+
+        let file_server_config = if self.options.incremental_cache_image.is_some() {
+            let file_server = FileServer {};
+            let config = file_server.start(&incremental_cache_dirs);
+            Some(config)
+        } else {
+            None
+        };
+
         let dockerfile = plan
-            .generate_dockerfile(&self.options, env, &output)
+            .generate_dockerfile(&self.options, env, &output, file_server_config)
             .context("Generating Dockerfile for plan")?;
 
         // If printing the Dockerfile, don't write anything to disk
@@ -73,6 +90,13 @@ impl ImageBuilder for DockerImageBuilder {
             println!("\nRun:");
             println!("  docker run -it {}", name);
 
+            if self.options.incremental_cache_image.is_some() {
+                incremental_cache.create_image(
+                    &incremental_cache_dirs,
+                    &self.options.incremental_cache_image.clone().unwrap(),
+                )?;
+            }
+
             if output.is_temp {
                 remove_dir_all(output.root)?;
             }
@@ -113,6 +137,10 @@ impl DockerImageBuilder {
             .arg("-t")
             .arg(name);
 
+        if self.options.verbose {
+            docker_build_cmd.arg("--progress=plain");
+        }
+
         if self.options.quiet {
             docker_build_cmd.arg("--quiet");
         }
@@ -163,7 +191,7 @@ impl DockerImageBuilder {
     fn write_dockerfile(&self, dockerfile: String, output: &OutputDir) -> Result<()> {
         let dockerfile_path = output.get_absolute_path("Dockerfile");
         File::create(dockerfile_path.clone()).context("Creating Dockerfile file")?;
-        fs::write(dockerfile_path, dockerfile)?;
+        fs::write(dockerfile_path, dockerfile).context("Write Dockerfile")?;
 
         Ok(())
     }
diff --git a/src/nixpacks/builder/docker/dockerfile_generation.rs b/src/nixpacks/builder/docker/dockerfile_generation.rs
index 626a898..76a2a30 100644
--- a/src/nixpacks/builder/docker/dockerfile_generation.rs
+++ b/src/nixpacks/builder/docker/dockerfile_generation.rs
@@ -1,4 +1,6 @@
-use super::{utils, DockerBuilderOptions};
+use super::{
+    file_server::FileServerConfig, incremental_cache::IncrementalCache, utils, DockerBuilderOptions,
+};
 use crate::nixpacks::{
     app,
     environment::Environment,
@@ -81,6 +83,7 @@ pub trait DockerfileGenerator {
         options: &DockerBuilderOptions,
         env: &Environment,
         output: &OutputDir,
+        _file_server_config: Option<FileServerConfig>,
     ) -> Result<String>;
     fn write_supporting_files(
         &self,
@@ -98,6 +101,7 @@ impl DockerfileGenerator for BuildPlan {
         options: &DockerBuilderOptions,
         env: &Environment,
         output: &OutputDir,
+        file_server_config: Option<FileServerConfig>,
     ) -> Result<String> {
         let plan = self;
 
@@ -165,13 +169,12 @@ impl DockerfileGenerator for BuildPlan {
         let dockerfile_phases = phases
             .into_iter()
             .map(|phase| {
-                let phase_dockerfile =
-                    phase
-                        .generate_dockerfile(options, env, output)
-                        .context(format!(
-                            "Generating Dockerfile for phase {}",
-                            phase.get_name()
-                        ))?;
+                let phase_dockerfile = phase
+                    .generate_dockerfile(options, env, output, file_server_config.clone())
+                    .context(format!(
+                        "Generating Dockerfile for phase {}",
+                        phase.get_name()
+                    ))?;
 
                 Ok(phase_dockerfile)
             })
@@ -182,7 +185,7 @@ impl DockerfileGenerator for BuildPlan {
             .start_phase
             .clone()
             .unwrap_or_default()
-            .generate_dockerfile(options, env, output)?;
+            .generate_dockerfile(options, env, output, file_server_config)?;
 
         let base_image = plan
             .build_image
@@ -284,6 +287,7 @@ impl DockerfileGenerator for StartPhase {
         _options: &DockerBuilderOptions,
         _env: &Environment,
         _output: &OutputDir,
+        _file_server_config: Option<FileServerConfig>,
     ) -> Result<String> {
         let start_cmd = match &self.cmd {
             Some(cmd) => utils::get_exec_command(cmd),
@@ -332,6 +336,7 @@ impl DockerfileGenerator for Phase {
         options: &DockerBuilderOptions,
         env: &Environment,
         _output: &OutputDir,
+        file_server_config: Option<FileServerConfig>,
     ) -> Result<String> {
         let phase = self;
 
@@ -363,15 +368,42 @@ impl DockerfileGenerator for Phase {
         let phase_copy_cmd = utils::get_copy_command(&phase_files, APP_DIR);
 
         let cache_mount = utils::get_cache_mount(&cache_key, &phase.cache_directories);
-        let cmds_str = phase
-            .cmds
-            .clone()
-            .unwrap_or_default()
+        let cmds_str = if options.incremental_cache_image.is_some() {
+            let image = &options.incremental_cache_image.clone().unwrap();
+            let cache_copy_in_command = if IncrementalCache::is_image_exists(image)? {
+                IncrementalCache::get_copy_to_image_command(&phase.cache_directories, image)
+                    .join("\n")
+            } else {
+                "".to_string()
+            };
+
+            let cache_copy_out_command = IncrementalCache::get_copy_from_image_command(
+                &phase.cache_directories,
+                file_server_config,
+            );
+
+            let run_commands = [
+                phase.cmds.clone().unwrap_or_default(),
+                cache_copy_out_command,
+            ]
+            .concat()
             .iter()
-            .map(|s| format!("RUN {} {}", cache_mount, s))
+            .map(|s| format!("RUN {}", s))
             .collect::<Vec<_>>()
             .join("\n");
 
+            format!("{}\n{}", cache_copy_in_command, run_commands)
+        } else {
+            phase
+                .cmds
+                .clone()
+                .unwrap_or_default()
+                .iter()
+                .map(|s| format!("RUN {} {}", cache_mount, s))
+                .collect::<Vec<_>>()
+                .join("\n")
+        };
+
         let dockerfile_stmts = vec![build_path, run_path, phase_copy_cmd, cmds_str]
             .into_iter()
             .filter(|stmt| !stmt.is_empty())
@@ -414,6 +446,7 @@ mod tests {
                 &DockerBuilderOptions::default(),
                 &Environment::default(),
                 &OutputDir::default(),
+                Some(FileServerConfig::default()),
             )
             .unwrap();
 
@@ -438,6 +471,7 @@ mod tests {
                 &DockerBuilderOptions::default(),
                 &Environment::default(),
                 &OutputDir::default(),
+                Some(FileServerConfig::default()),
             )
             .unwrap();
 
diff --git a/src/nixpacks/builder/docker/file_server.rs b/src/nixpacks/builder/docker/file_server.rs
new file mode 100644
index 0000000..6794b3d
--- /dev/null
+++ b/src/nixpacks/builder/docker/file_server.rs
@@ -0,0 +1,124 @@
+// use actix_multipart::Multipart;
+use actix_web::http::header::HeaderValue;
+use actix_web::{
+    middleware, rt, web, App as ActixApp, Error as ActixError, HttpRequest, HttpResponse,
+    HttpServer,
+};
+use anyhow::Result;
+use futures_util::stream::StreamExt;
+use portpicker::pick_unused_port;
+use std::fs::File;
+use std::io::Write;
+use std::path::PathBuf;
+use std::thread;
+
+use super::incremental_cache::IncrementalCacheDirs;
+use uuid::Uuid;
+
+const NIXPACKS_SERVER_HOST: &str = "host.docker.internal";
+const NIXPACKS_SERVER_LISTEN_TO_IP: &str = "0.0.0.0";
+
+#[derive(Debug, Clone)]
+pub struct FileServer {}
+
+#[derive(Debug, Clone, Default)]
+pub struct FileServerConfig {
+    pub listen_to_ip: String,
+    pub port: u16,
+    pub access_token: String,
+    pub upload_url: String,
+    pub files_dir: PathBuf,
+}
+
+impl FileServer {
+    pub fn start(self, incremental_cache_dirs: &IncrementalCacheDirs) -> FileServerConfig {
+        let port = self.get_free_port();
+
+        let config = FileServerConfig {
+            files_dir: incremental_cache_dirs.uploads_dir.clone(),
+            access_token: Uuid::new_v4().to_string(),
+            listen_to_ip: NIXPACKS_SERVER_LISTEN_TO_IP.to_string(),
+            port,
+            upload_url: format!("http://{}:{}/upload/", NIXPACKS_SERVER_HOST, port),
+        };
+
+        let server_config = config.clone();
+        thread::spawn(move || {
+            let server_future = FileServer::run_app(server_config);
+            if let Err(e) = rt::System::new().block_on(server_future) {
+                println!("File server error: {}", e);
+            }
+        });
+
+        config
+    }
+
+    fn get_free_port(&self) -> u16 {
+        for _ in 1..3 {
+            // try 2 times
+            if let Some(port) = pick_unused_port() {
+                return port;
+            }
+        }
+
+        // last try to get free port then fail if no ports available
+        pick_unused_port().expect("No ports available")
+    }
+
+    async fn run_app(data: FileServerConfig) -> std::io::Result<()> {
+        let server_config = web::Data::new(data.clone());
+        let server = HttpServer::new(move || {
+            ActixApp::new()
+                .app_data(server_config.clone())
+                .wrap(middleware::Logger::default())
+                .service(
+                    web::resource("/health")
+                        .route(web::get().to(|| async { "Nixpacks HTTP server is up & running!" })),
+                )
+                .service(
+                    web::resource("/upload/{filename}").route(web::put().to(FileServer::upload)),
+                )
+        })
+        .bind((data.listen_to_ip, data.port))?
+        .run();
+
+        server.await
+    }
+
+    fn has_valid_access_token(token: Option<&HeaderValue>, access_token: &str) -> bool {
+        if let Some(header) = token {
+            match header.to_str() {
+                Ok(value) => value == access_token,
+                _ => false,
+            }
+        } else {
+            false
+        }
+    }
+
+    #[allow(dead_code)]
+    async fn upload(
+        mut payload: web::Payload,
+        path: web::Path<String>,
+        req: HttpRequest,
+        data: web::Data<FileServerConfig>,
+    ) -> Result<HttpResponse, ActixError> {
+        if !FileServer::has_valid_access_token(req.headers().get("t"), &data.access_token) {
+            return Ok(HttpResponse::Unauthorized().into());
+        }
+
+        let filename = path.into_inner();
+        let filepath = data.files_dir.join(sanitize_filename::sanitize(&filename));
+
+        let in_path = PathBuf::from(&filepath);
+        let mut f: File = web::block(|| File::create(in_path)).await??;
+
+        while let Some(chunk) = payload.next().await {
+            let data = chunk?;
+            f = web::block(move || f.write_all(&data).map(|_| f)).await??;
+        }
+        web::block(move || f.flush()).await??;
+
+        Ok(HttpResponse::Ok().into())
+    }
+}
diff --git a/src/nixpacks/builder/docker/incremental_cache.rs b/src/nixpacks/builder/docker/incremental_cache.rs
new file mode 100644
index 0000000..177ef7d
--- /dev/null
+++ b/src/nixpacks/builder/docker/incremental_cache.rs
@@ -0,0 +1,199 @@
+use std::{
+    fs::{self},
+    path::PathBuf,
+    process::Command,
+};
+
+use super::{dockerfile_generation::OutputDir, file_server::FileServerConfig};
+use anyhow::{bail, Context, Result};
+use std::process::Stdio;
+
+const INCREMENTAL_CACHE_DIR: &str = "incremental-cache";
+const INCREMENTAL_CACHE_UPLOADS_DIR: &str = "uploads";
+const INCREMENTAL_CACHE_IMAGE_DIR: &str = "image";
+
+#[derive(Default)]
+pub struct IncrementalCache {}
+
+#[derive(Default)]
+pub struct IncrementalCacheDirs {
+    pub uploads_dir: PathBuf,
+    pub image_dir: PathBuf,
+}
+
+impl IncrementalCacheDirs {
+    pub fn create(out_dir: &OutputDir) -> Result<IncrementalCacheDirs> {
+        let incremental_cache_root = out_dir.get_absolute_path(INCREMENTAL_CACHE_DIR);
+
+        if fs::metadata(&incremental_cache_root)
+            .context("Check if incremental-cache dir exists")
+            .is_ok()
+        {
+            fs::remove_dir_all(&incremental_cache_root)?;
+        }
+
+        let image_dir = incremental_cache_root.join(PathBuf::from(INCREMENTAL_CACHE_IMAGE_DIR));
+        if fs::metadata(&image_dir)
+            .context("Check if incremental cache image dir exists")
+            .is_ok()
+        {
+            fs::remove_dir_all(&image_dir)?;
+        }
+
+        let uploads_dir = incremental_cache_root.join(PathBuf::from(INCREMENTAL_CACHE_UPLOADS_DIR));
+        if fs::metadata(&uploads_dir)
+            .context("Check if incremental cache uploads dir exists")
+            .is_ok()
+        {
+            fs::remove_dir_all(&uploads_dir)?;
+        }
+
+        fs::create_dir_all(&image_dir).context("Create incremental cache image dir")?;
+        fs::create_dir_all(&uploads_dir).context("Creating incremental-cache uploads directory")?;
+
+        Ok(IncrementalCacheDirs {
+            uploads_dir,
+            image_dir,
+        })
+    }
+}
+
+impl IncrementalCache {
+    pub fn create_image(
+        &self,
+        incremental_cache_dirs: &IncrementalCacheDirs,
+        tag: &str,
+    ) -> Result<()> {
+        let files = fs::read_dir(&incremental_cache_dirs.uploads_dir)?;
+
+        // There are three options to create a filesystem image that contains only tar files
+        // #1 Use a Rust crate to create the image: 30+ seconds in a sample test, Also no clear winner Crate for creating OCI image
+        // #2 Create minimal Dockerfile: 6 seconds in a sample test
+        // #3 Use Docker import: Provide 3 seconds in a sample test
+        for f in files {
+            let mut docker_import_cmd = Command::new("docker");
+            docker_import_cmd.arg("import").arg(&f?.path()).arg(&tag);
+
+            let result = docker_import_cmd
+                .spawn()?
+                .wait()
+                .context("Create incremental cache image")?;
+
+            if !result.success() {
+                bail!("Creating incremental cache image failed")
+            }
+        }
+
+        println!("Incremental cache image created: {}", &tag);
+        Ok(())
+    }
+
+    pub fn is_image_exists(image_tag: &str) -> Result<bool> {
+        let mut docker_inspect_cmd = Command::new("docker");
+        docker_inspect_cmd
+            .arg("manifest")
+            .arg("inspect")
+            .arg(&image_tag)
+            .stdout(Stdio::null())
+            .stderr(Stdio::null());
+
+        let result = docker_inspect_cmd
+            .spawn()?
+            .wait()
+            .context("Check incremental cache image exists in registry")?;
+
+        Ok(result.success())
+    }
+
+    pub fn get_copy_to_image_command(
+        cache_directories: &Option<Vec<String>>,
+        incremental_cahge_image: &str,
+    ) -> Vec<String> {
+        let dirs = &cache_directories.clone().unwrap_or_default();
+        if dirs.is_empty() {
+            return vec![];
+        }
+
+        dirs.iter()
+            .flat_map(|dir| {
+                let target_cache_dir = dir.replace('~', "/root");
+                let target_cache_dir_optional = target_cache_dir
+                    .split('/')
+                    .into_iter()
+                    .filter(|c| !c.is_empty())
+                    .map(|c| format!("{}?", c))
+                    .collect::<Vec<_>>()
+                    .join("/");
+
+                vec![format!(
+                    "COPY --from={} {} {}",
+                    incremental_cahge_image, target_cache_dir_optional, target_cache_dir
+                )]
+            })
+            .collect::<Vec<String>>()
+    }
+
+    pub fn get_copy_from_image_command(
+        cache_directories: &Option<Vec<String>>,
+        file_server_config: Option<FileServerConfig>,
+    ) -> Vec<String> {
+        let container_dirs = cache_directories.clone().unwrap_or_default();
+        if container_dirs.is_empty() || file_server_config.is_none() {
+            return vec![];
+        }
+
+        let server_config = file_server_config.unwrap();
+        container_dirs
+            .iter()
+            .flat_map(|dir| {
+                let sanitized_dir = dir.replace('~', "/root");
+                let compressed_file_name = format!("{}.tar", sanitized_dir.replace('/', "%2f"));
+                vec![
+                    format!("if [ -d \"{sanitized_dir}\" ]; then tar -cf {compressed_file_name} {sanitized_dir}; fi;"),
+                    format!(
+                        "if [ -d \"{sanitized_dir}\" ]; then curl -v -T {} {} --header \"t:{}\" --retry 3 --retry-all-errors; fi;",
+                        compressed_file_name, server_config.upload_url, server_config.access_token,
+                    ),
+                    format!("if [ -d \"{sanitized_dir}\" ]; then rm -rf {}; fi", sanitized_dir),
+                ]
+            })
+            .collect::<Vec<String>>()
+    }
+}
+
+#[test]
+fn test_get_copy_from_image_command() {
+    let cmds = IncrementalCache::get_copy_from_image_command(
+        &Some(vec!["./parent_dir/child_dir".to_string()]),
+        Some(FileServerConfig {
+            listen_to_ip: "0.0.0.0".to_string(),
+            port: 1234,
+            access_token: "test_access_token".to_string(),
+            upload_url: "http://test.com/upload".to_string(),
+            files_dir: PathBuf::from("./source_dir".to_string()),
+        }),
+    );
+
+    assert_eq!(cmds.len(), 3);
+    assert_eq!(cmds[0], "if [ -d \"./parent_dir/child_dir\" ]; then tar -cf .%2fparent_dir%2fchild_dir.tar ./parent_dir/child_dir; fi;".to_string());
+    assert_eq!(cmds[1], "if [ -d \"./parent_dir/child_dir\" ]; then curl -v -T .%2fparent_dir%2fchild_dir.tar http://test.com/upload --header \"t:test_access_token\" --retry 3 --retry-all-errors; fi;".to_string());
+    assert_eq!(
+        cmds[2],
+        "if [ -d \"./parent_dir/child_dir\" ]; then rm -rf ./parent_dir/child_dir; fi".to_string()
+    );
+}
+
+#[test]
+fn test_get_copy_to_image_command() {
+    let cmds = IncrementalCache::get_copy_to_image_command(
+        &Some(vec!["./parent_dir/child_dir".to_string()]),
+        "docker.io/library/test-image",
+    );
+
+    assert_eq!(cmds.len(), 1);
+    assert_eq!(
+        cmds[0],
+        "COPY --from=docker.io/library/test-image .?/parent_dir?/child_dir? ./parent_dir/child_dir"
+            .to_string()
+    );
+}
diff --git a/src/nixpacks/builder/docker/mod.rs b/src/nixpacks/builder/docker/mod.rs
index ae0334a..4f44f31 100644
--- a/src/nixpacks/builder/docker/mod.rs
+++ b/src/nixpacks/builder/docker/mod.rs
@@ -16,9 +16,13 @@ pub struct DockerBuilderOptions {
     pub platform: Vec<String>,
     pub current_dir: bool,
     pub no_error_without_start: bool,
+    pub incremental_cache_image: Option<String>,
+    pub verbose: bool,
 }
 
 mod cache;
 pub mod docker_image_builder;
 mod dockerfile_generation;
-mod utils;
+pub mod file_server;
+pub mod incremental_cache;
+pub mod utils;
diff --git a/src/nixpacks/images.rs b/src/nixpacks/images.rs
index 2d98207..50c105e 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-1664842184";
+pub const DEFAULT_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1665446966";
 pub const DEBIAN_SLIM_IMAGE: &str = "debian:bullseye-slim";
diff --git a/src/providers/java.rs b/src/providers/java.rs
index 5181ca9..763eb03 100644
--- a/src/providers/java.rs
+++ b/src/providers/java.rs
@@ -38,7 +38,7 @@ impl Provider for JavaProvider {
 
             let gradle_exe = self.get_gradle_exe(app);
             let mut build = Phase::build(Some(format!("{} build -x check", gradle_exe)));
-            build.add_cache_directory(".gradle");
+            build.add_cache_directory("/root/.gradle");
             build
         } else {
             setup = Phase::setup(Some(vec![Pkg::new("jdk")]));
diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs
index 3bd24f2..7464209 100644
--- a/src/providers/node/mod.rs
+++ b/src/providers/node/mod.rs
@@ -73,8 +73,24 @@ impl Provider for NodeProvider {
                 "libxshmfence1".to_string(),
                 "libglu1".to_string(),
             ]);
-        } else if NodeProvider::uses_node_dependency(app, "canvas") {
+        }
+
+        if NodeProvider::uses_node_dependency(app, "canvas") {
             setup.add_pkgs_libs(vec!["libuuid".to_string(), "libGL".to_string()]);
+            setup.add_apt_pkgs(vec![
+                "build-essential".into(),
+                "libcairo2-dev".into(),
+                "libpango1.0-dev".into(),
+                "libjpeg-dev".into(),
+                "libgif-dev".into(),
+                "librsvg2-dev".into(),
+            ]);
+        }
+
+        if NodeProvider::uses_node_dependency(app, "node-gyp")
+            || NodeProvider::uses_node_dependency(app, "node-pre-gyp")
+        {
+            setup.add_nix_pkgs(&[Pkg::new("python39"), Pkg::new("gccStdenv")]);
         }
 
         // Install


Test patch:
diff --git a/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap b/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap
index e53313c..0cafdd7 100644
--- a/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap
+++ b/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap
@@ -18,7 +18,7 @@ expression: plan
         "./gradlew build -x check"
       ],
       "cacheDirectories": [
-        ".gradle"
+        "/root/.gradle"
       ]
     },
     "setup": {
diff --git a/tests/snapshots/generate_plan_tests__java_spring_boot.snap b/tests/snapshots/generate_plan_tests__java_spring_boot.snap
index c60a8b2..b7455bc 100644
--- a/tests/snapshots/generate_plan_tests__java_spring_boot.snap
+++ b/tests/snapshots/generate_plan_tests__java_spring_boot.snap
@@ -18,7 +18,7 @@ expression: plan
         "./gradlew build -x check"
       ],
       "cacheDirectories": [
-        ".gradle"
+        "/root/.gradle"
       ]
     },
     "setup": {
diff --git a/tests/snapshots/generate_plan_tests__node_canvas.snap b/tests/snapshots/generate_plan_tests__node_canvas.snap
index ca61444..981f5b3 100644
--- a/tests/snapshots/generate_plan_tests__node_canvas.snap
+++ b/tests/snapshots/generate_plan_tests__node_canvas.snap
@@ -40,7 +40,9 @@ expression: plan
       "name": "setup",
       "nixPkgs": [
         "nodejs-16_x",
-        "npm-8_x"
+        "npm-8_x",
+        "python39",
+        "gccStdenv"
       ],
       "nixLibs": [
         "libuuid",
@@ -49,7 +51,15 @@ expression: plan
       "nixOverlays": [
         "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "nixpkgsArchive": "[archive]"
+      "nixpkgsArchive": "[archive]",
+      "aptPkgs": [
+        "build-essential",
+        "libcairo2-dev",
+        "libpango1.0-dev",
+        "libjpeg-dev",
+        "libgif-dev",
+        "librsvg2-dev"
+      ]
     }
   },
   "start": {
diff --git a/tests/snapshots/generate_plan_tests__node_nx.snap b/tests/snapshots/generate_plan_tests__node_nx.snap
index 48768ed..3b4c6b1 100644
--- a/tests/snapshots/generate_plan_tests__node_nx.snap
+++ b/tests/snapshots/generate_plan_tests__node_nx.snap
@@ -45,7 +45,9 @@ expression: plan
       "name": "setup",
       "nixPkgs": [
         "nodejs-16_x",
-        "npm-8_x"
+        "npm-8_x",
+        "python39",
+        "gccStdenv"
       ],
       "nixLibs": [
         "libuuid",
@@ -54,7 +56,15 @@ expression: plan
       "nixOverlays": [
         "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
       ],
-      "nixpkgsArchive": "[archive]"
+      "nixpkgsArchive": "[archive]",
+      "aptPkgs": [
+        "build-essential",
+        "libcairo2-dev",
+        "libpango1.0-dev",
+        "libjpeg-dev",
+        "libgif-dev",
+        "librsvg2-dev"
+      ]
     }
   },
   "start": {
diff --git a/tests/snapshots/generate_plan_tests__node_yarn_berry.snap b/tests/snapshots/generate_plan_tests__node_yarn_berry.snap
index 0838564..99adcda 100644
--- a/tests/snapshots/generate_plan_tests__node_yarn_berry.snap
+++ b/tests/snapshots/generate_plan_tests__node_yarn_berry.snap
@@ -43,7 +43,9 @@ expression: plan
       "name": "setup",
       "nixPkgs": [
         "nodejs-16_x",
-        "yarn-1_x"
+        "yarn-1_x",
+        "python39",
+        "gccStdenv"
       ],
       "nixOverlays": [
         "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"