Skip to content

Conversation

@Kavignon
Copy link
Contributor

@Kavignon Kavignon commented Jan 26, 2025

What's being done

We continue the work established earlier for containerizing Slax for development and production.

Docker environment for Slax in development

In a previous PR (#16), we added a Docker development environment that allows Slax (a Phoenix/LiveView application) to run in Docker during development This involved:

Docker environment for Slax in production

Building on that, this PR introduces a multi-stage Docker build tailored for production Unlike the development container—which bundles all dev dependencies and results in a larger footprint—this approach produces a much smaller, more secure final image. While I'm not a security specialist, I want to minimize the attack surface and streamline the production build.

Multi-stage build

As mentioned above, the primary goals are to reduce the container's footprint and ensure minimal attack surface (within reason). A multi-stage Docker build lets you split the build process into separate phases in a single Dockerfile. Each stage focuses on a different part of your application's build lifecycle (e.g., asset compilation, release building, final runtime), and you only copy forward what's necessary for the next stage.

Multi-stage builds allow developers to separate build-time dependencies from runtime ones. Developers can now start from a full-featured build image with all the necessary components installed, perform the necessary build step, and then copy only the result of those steps to a more minimal or even an empty image, called “scratch”. With this approach, there’s no need to clean up dependencies and, as an added bonus, the build stages are also cacheable, which can considerably reduce build time.
Source: https://www.docker.com/blog/is-your-container-image-really-distroless/

1. Node/Assets stage

When running Slax in production, we don’t need Node.js or other dev tools present—just the compiled (and minified) static assets. By isolating this step, we keep Node-related dependencies out of the final image, reducing size and potential vulnerabilities.

So here is what's happening during this build stage:

  1. Installs Node dependencies under assets/.
  2. Runs esbuild to bundle and minify static files.
  3. Outputs bundled assets to priv/static.

2. Elixir build stage

Next, we build the actual Elixir/Phoenix release:

  1. Copy in the compiled assets from the Node stage
  2. Install and compile Mix dependencies (mix deps.get --only prod && mix deps.compile)
  3. Run mix phx.digest to cache-bust and fingerprint static assets
  4. Produce the final Phoenix release (in _build/prod/rel/...)

3. Distroless Runtime Stage

Why a distroless runtime?

Distroless images (by Google) include the bare minimum required to run an application—essentially just the language runtime and no shell. When Slax is compiled for production, we no longer need to preserve the compiler, ore package manager, or shell. We can copy the final compiled release from the previous stage.

Restricting what's in your runtime container to precisely what's necessary for your app is a best practice employed by Google and other tech giants that have used containers in production for many years. It improves the signal-to-noise of scanners (e.g., CVE) and reduces the burden of establishing provenance to just what you need.

Distroless images are very small. The smallest distroless image, gcr.io/distroless/static-debian12, is around 2 MiB. That's about 50% of the size of alpine (~5 MiB), and less than 2% of the size of debian (124 MiB).
Source: https://github.com/GoogleContainerTools/distroless?tab=readme-ov-file#why-should-i-use-distroless-images

What happens during this stage?

  1. Copy the compiled release from the Elixir build stage.
  2. Use a Distroless base image.
  3. Start the Phoenix application (listening on port 4000).

Because the final container only includes the compiled release and a minimal runtime, it’s significantly smaller and more secure than a single-stage image that carries all the build tools and dependencies.

The docker container for production is being built in 3 stages. The Node build installs the dependencies set in app/assets , copies what's in the assets folder and runs esbuild to bundle + minify static files. The Elixir build stage installs Hex and Rebar, fetches + compiles dependencies for production, copies compiled assets from the Node build stage and runs  The Distroless runtime stage copies the compiled production binaries in the Elixir stage into a minimal Distroless image (very lean Unix runtime to reduce attack opportunities), sets the working directory to /app and starts the Phoenix server.
@Kavignon
Copy link
Contributor Author

I'm running into issues due to esbuild. I thought it could be my Docker setup on my machine but even trying to get a little ahead of deploying to Fly, I'm still having issues

fly deploy --dockerfile Dockerfile.prod -a slax-phoenix-server                      

==> Verifying app config
Validating /Users/user/development/slax/fly.toml
✓ Configuration is valid
--> Verified app config
==> Building image
Remote builder fly-builder-falling-sky-7237 ready
Remote builder fly-builder-falling-sky-7237 ready
==> Building image with Docker
--> docker host: 24.0.7 linux x86_64
[+] Building 31.5s (21/25)                                                                                                                                                       
 => [internal] load build definition from Dockerfile.prod                                                                                                                   0.1s
 => => transferring dockerfile: 1.19kB                                                                                                                                      0.1s
 => [internal] load .dockerignore                                                                                                                                           0.1s
 => => transferring context: 268B                                                                                                                                           0.1s
 => [internal] load metadata for gcr.io/distroless/base-debian11:latest                                                                                                     0.9s
 => [internal] load metadata for docker.io/library/elixir:1.18-otp-27-alpine                                                                                                0.3s
 => [internal] load metadata for docker.io/library/node:20-slim                                                                                                             0.2s
 => [stage-2 1/3] FROM gcr.io/distroless/base-debian11:latest@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455                                       1.4s
 => => resolve gcr.io/distroless/base-debian11:latest@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455                                               0.0s
 => => sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455 1.51kB / 1.51kB                                                                              0.0s
 => => sha256:5664b15f108bf9436ce3312090a767300800edbbfd4511aa1a6d64357024d5dd 168B / 168B                                                                                  0.2s
 => => sha256:33e068de264953dfdc9f9ada207e76b61159721fd64a4820b320d05133a55fb8 122B / 122B                                                                                  0.2s
 => => sha256:2eebbdaceeec718d203fd32370a1c1960b7d9d550d6acb1bca4957ab8560e3fb 2.42kB / 2.42kB                                                                              0.0s
 => => sha256:e33bce57de289fffd2380f73997dfb7e1ec193877904bed99f28c715d071fdc4 21.19kB / 21.19kB                                                                            0.3s
 => => sha256:b6824ed73363f94b3b2b44084c51c31bc32af77a96861d49e16f91e3ab6bed71 67B / 67B                                                                                    0.3s
 => => sha256:1c56d6035a42c0a75d79cc88acf6c9d4104343639f19b8262b520c449731445d 104.12kB / 104.12kB                                                                          0.3s
 => => sha256:7c12895b777bcaa8ccae0605b4de635b68fc32d60fa08f421dc3818bf55ee212 188B / 188B                                                                                  0.3s
 => => sha256:4aa0ea1413d37a58615488592a0b827ea4b2e48fa5a77cf707d0e35f025e613f 385B / 385B                                                                                  0.2s
 => => sha256:9ef7d74bdfdf3c517b28bd694a9159e94e5f53ff1ca87b39f8ca1ac0be2ed317 320B / 320B                                                                                  0.2s
 => => sha256:2a6de77407bff82fecc48d028fde7b88366509bffff4337d0e809db77ccfdbe3 1.81kB / 1.81kB                                                                              0.0s
 => => sha256:473d8557b1b27974f7dc7c4b4e1a209df0e27e8cae1e3e33b7bb45c969b6fc7e 755.28kB / 755.28kB                                                                          0.3s
 => => sha256:27be814a09ebd97fac6fb7b82d19f117185e90601009df3fbab6f442f85cd6b3 93B / 93B                                                                                    0.2s
 => => sha256:83f8d4690e1f293d0438aef7d1075e590ce77fdec97bb4d90b1d227aeba343fd 5.85MB / 5.85MB                                                                              0.7s
 => => sha256:9112d77ee5b16873acaa186b816c3c61f5f8eba40730e729e9614a27f40211e0 122.56kB / 122.56kB                                                                          0.5s
 => => sha256:a4ba90834fb4abf3d80bbdaaaef36560ab1bb682f5279d44114d768e119639b9 2.06MB / 2.06MB                                                                              0.6s
 => => sha256:df368711b36276ed02b2040d3e3296b919042d2a05a2bbe9f758e708436c12cf 968.57kB / 968.57kB                                                                          0.6s
 => => extracting sha256:1c56d6035a42c0a75d79cc88acf6c9d4104343639f19b8262b520c449731445d                                                                                   0.0s
 => => extracting sha256:e33bce57de289fffd2380f73997dfb7e1ec193877904bed99f28c715d071fdc4                                                                                   0.0s
 => => extracting sha256:473d8557b1b27974f7dc7c4b4e1a209df0e27e8cae1e3e33b7bb45c969b6fc7e                                                                                   0.5s
 => => extracting sha256:b6824ed73363f94b3b2b44084c51c31bc32af77a96861d49e16f91e3ab6bed71                                                                                   0.0s
 => => extracting sha256:7c12895b777bcaa8ccae0605b4de635b68fc32d60fa08f421dc3818bf55ee212                                                                                   0.0s
 => => extracting sha256:33e068de264953dfdc9f9ada207e76b61159721fd64a4820b320d05133a55fb8                                                                                   0.0s
 => => extracting sha256:5664b15f108bf9436ce3312090a767300800edbbfd4511aa1a6d64357024d5dd                                                                                   0.0s
 => => extracting sha256:27be814a09ebd97fac6fb7b82d19f117185e90601009df3fbab6f442f85cd6b3                                                                                   0.0s
 => => extracting sha256:4aa0ea1413d37a58615488592a0b827ea4b2e48fa5a77cf707d0e35f025e613f                                                                                   0.0s
 => => extracting sha256:9ef7d74bdfdf3c517b28bd694a9159e94e5f53ff1ca87b39f8ca1ac0be2ed317                                                                                   0.0s
 => => extracting sha256:9112d77ee5b16873acaa186b816c3c61f5f8eba40730e729e9614a27f40211e0                                                                                   0.0s
 => => extracting sha256:83f8d4690e1f293d0438aef7d1075e590ce77fdec97bb4d90b1d227aeba343fd                                                                                   0.2s
 => => extracting sha256:a4ba90834fb4abf3d80bbdaaaef36560ab1bb682f5279d44114d768e119639b9                                                                                   0.0s
 => => extracting sha256:df368711b36276ed02b2040d3e3296b919042d2a05a2bbe9f758e708436c12cf                                                                                   0.0s
 => [build  1/10] FROM docker.io/library/elixir:1.18-otp-27-alpine@sha256:745632e8eefcd0cff05f45fb5855175c317095cf5b9659f03e21e661c7e64293                                  2.5s
 => => resolve docker.io/library/elixir:1.18-otp-27-alpine@sha256:745632e8eefcd0cff05f45fb5855175c317095cf5b9659f03e21e661c7e64293                                          0.0s
 => => sha256:745632e8eefcd0cff05f45fb5855175c317095cf5b9659f03e21e661c7e64293 7.70kB / 7.70kB                                                                              0.0s
 => => sha256:f6ed0bd9e4bb13e9cfcbe46cdbb872cebe81e4c3d64c43cdca58a42ae2d5e1ac 1.54kB / 1.54kB                                                                              0.0s
 => => sha256:7e30bb25d0b6c32930e5043bc3baa532c2103fac0ad92801a52b2d5ba993a74b 5.56kB / 5.56kB                                                                              0.0s
 => => sha256:23f3bd720b59f82235a9f5bf3fa6409799f37f7c1f81d8d3c9d1d76b9e8595c6 7.45MB / 7.45MB                                                                              0.7s
 => => sha256:66a3d608f3fa52124f8463e9467f170c784abd549e8216aa45c6960b00b4b79b 3.63MB / 3.63MB                                                                              0.5s
 => => sha256:3845e47744390803c6c9bbadcc3e772a8e0b72df4633f2453700258703486252 49.42MB / 49.42MB                                                                            1.0s
 => => extracting sha256:66a3d608f3fa52124f8463e9467f170c784abd549e8216aa45c6960b00b4b79b                                                                                   0.1s
 => => extracting sha256:3845e47744390803c6c9bbadcc3e772a8e0b72df4633f2453700258703486252                                                                                   1.1s
 => => extracting sha256:23f3bd720b59f82235a9f5bf3fa6409799f37f7c1f81d8d3c9d1d76b9e8595c6                                                                                   0.3s
 => [internal] load build context                                                                                                                                          26.5s
 => => transferring context: 81.15MB                                                                                                                                       26.5s
 => [node-build 1/6] FROM docker.io/library/node:20-slim@sha256:626b719f38532dfe02d806bc64161d94d951ec4ade80494f5d0407bed08c3f5c                                            4.0s
 => => resolve docker.io/library/node:20-slim@sha256:626b719f38532dfe02d806bc64161d94d951ec4ade80494f5d0407bed08c3f5c                                                       0.0s
 => => sha256:471e8ef6379c5755a8f4ac93688bbf020b5c31e5bbee537313ab556169b23ac4 6.54kB / 6.54kB                                                                              0.0s
 => => sha256:626b719f38532dfe02d806bc64161d94d951ec4ade80494f5d0407bed08c3f5c 6.49kB / 6.49kB                                                                              0.0s
 => => sha256:4933037da8e22aeb5112ae56e73bd22a558ab327f54a36708da06b32dc0ba62a 1.93kB / 1.93kB                                                                              0.0s
 => => sha256:af302e5c37e9dc1dbe2eadc8f5059d82a914066b541b0d1a6daa91d0cc55057d 28.21MB / 28.21MB                                                                            0.9s
 => => sha256:3d85c81068a5aaa9d61d3a57437dc3fae35b98a32bc6e300b17e1954a13a62e0 3.31kB / 3.31kB                                                                              0.3s
 => => sha256:94cbdb56e8482973c4f2cbeb46c444cee3e181b98a4d26555c7f994519c1a1f2 40.76MB / 40.76MB                                                                            1.2s
 => => sha256:8c6c2e81cc3cedbc43f82efa5535e0f1eea76358b41ccbc6f822f051f1bf7d94 1.71MB / 1.71MB                                                                              0.3s
 => => sha256:cace62cadcc9de85e19f4cbb62dc86f5cbde40f667da41e523b18914b0e245d5 448B / 448B                                                                                  0.3s
 => => extracting sha256:af302e5c37e9dc1dbe2eadc8f5059d82a914066b541b0d1a6daa91d0cc55057d                                                                                   1.5s
 => => extracting sha256:3d85c81068a5aaa9d61d3a57437dc3fae35b98a32bc6e300b17e1954a13a62e0                                                                                   0.0s
 => => extracting sha256:94cbdb56e8482973c4f2cbeb46c444cee3e181b98a4d26555c7f994519c1a1f2                                                                                   1.3s
 => => extracting sha256:8c6c2e81cc3cedbc43f82efa5535e0f1eea76358b41ccbc6f822f051f1bf7d94                                                                                   0.0s
 => => extracting sha256:cace62cadcc9de85e19f4cbb62dc86f5cbde40f667da41e523b18914b0e245d5                                                                                   0.0s
 => [stage-2 2/3] WORKDIR /app                                                                                                                                              0.2s
 => [build  2/10] RUN apk add --no-cache build-base git npm bash curl                                                                                                       9.0s
 => [node-build 2/6] WORKDIR /app/assets                                                                                                                                    0.2s
 => [build  3/10] WORKDIR /app                                                                                                                                              0.0s
 => [build  4/10] RUN mix local.hex --force && mix local.rebar --force                                                                                                      2.2s
 => [build  5/10] COPY mix.exs mix.lock ./                                                                                                                                  0.2s
 => [node-build 3/6] COPY assets/package.json assets/package-lock.json ./                                                                                                   0.2s
 => [build  6/10] RUN mix deps.get --only prod                                                                                                                              3.3s
 => [node-build 4/6] RUN npm install --force && npm install esbuild-linux-64 --force                                                                                        2.6s
 => [node-build 5/6] COPY assets/ ./                                                                                                                                        0.4s
 => ERROR [node-build 6/6] RUN npx esbuild-linux-64 js/app.js --bundle --minify --sourcemap --outdir=../priv/static/assets                                                  0.7s
 => CANCELED [build  7/10] COPY . .                                                                                                                                         0.4s
------
 > [node-build 6/6] RUN npx esbuild-linux-64 js/app.js --bundle --minify --sourcemap --outdir=../priv/static/assets:
0.614 npm error could not determine executable to run
0.616 npm error A complete log of this run can be found in: /root/.npm/_logs/2025-01-26T19_09_31_327Z-debug-0.log
------
==> Building image
✓ compatible remote builder found
Waiting for remote builder fly-builder-falling-sky-7237...
 🌍INFO Override builder host with: https://fly-builder-falling-sky-7237.fly.dev (was tcp://[fdaa:9:c825:a7b:1d3:95e8:4bc8:2]:2375)
Remote builder fly-builder-falling-sky-7237 ready
Waiting for remote builder fly-builder-falling-sky-7237...
 🌍INFO Override builder host with: https://fly-builder-falling-sky-7237.fly.dev (was tcp://[fdaa:9:c825:a7b:1d3:95e8:4bc8:2]:2375)
Remote builder fly-builder-falling-sky-7237 ready
==> Building image with Docker
--> docker host: 24.0.7 linux x86_64
[+] Building 7.7s (21/25)                                                                                                                                                        
 => [internal] load .dockerignore                                                                                                                                           0.1s
 => => transferring context: 268B                                                                                                                                           0.1s
 => [internal] load build definition from Dockerfile.prod                                                                                                                   0.1s
 => => transferring dockerfile: 1.19kB                                                                                                                                      0.1s
 => [internal] load metadata for gcr.io/distroless/base-debian11:latest                                                                                                     0.2s
 => [internal] load metadata for docker.io/library/node:20-slim                                                                                                             6.4s
 => [internal] load metadata for docker.io/library/elixir:1.18-otp-27-alpine                                                                                                6.4s
 => [stage-2 1/3] FROM gcr.io/distroless/base-debian11:latest@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455                                       0.0s
 => [build  1/10] FROM docker.io/library/elixir:1.18-otp-27-alpine@sha256:745632e8eefcd0cff05f45fb5855175c317095cf5b9659f03e21e661c7e64293                                  0.0s
 => [node-build 1/6] FROM docker.io/library/node:20-slim@sha256:626b719f38532dfe02d806bc64161d94d951ec4ade80494f5d0407bed08c3f5c                                            0.0s
 => [internal] load build context                                                                                                                                           0.5s
 => => transferring context: 587.02kB                                                                                                                                       0.5s
 => CACHED [stage-2 2/3] WORKDIR /app                                                                                                                                       0.0s
 => CACHED [node-build 2/6] WORKDIR /app/assets                                                                                                                             0.0s
 => CACHED [node-build 3/6] COPY assets/package.json assets/package-lock.json ./                                                                                            0.0s
 => CACHED [node-build 4/6] RUN npm install --force && npm install esbuild-linux-64 --force                                                                                 0.0s
 => CACHED [node-build 5/6] COPY assets/ ./                                                                                                                                 0.0s
 => CACHED [build  2/10] RUN apk add --no-cache build-base git npm bash curl                                                                                                0.0s
 => CACHED [build  3/10] WORKDIR /app                                                                                                                                       0.0s
 => CACHED [build  4/10] RUN mix local.hex --force && mix local.rebar --force                                                                                               0.0s
 => CACHED [build  5/10] COPY mix.exs mix.lock ./                                                                                                                           0.0s
 => CACHED [build  6/10] RUN mix deps.get --only prod                                                                                                                       0.0s
 => ERROR [node-build 6/6] RUN npx esbuild-linux-64 js/app.js --bundle --minify --sourcemap --outdir=../priv/static/assets                                                  0.7s
 => CANCELED [build  7/10] COPY . .                                                                                                                                         0.7s
------
 > [node-build 6/6] RUN npx esbuild-linux-64 js/app.js --bundle --minify --sourcemap --outdir=../priv/static/assets:
0.581 npm error could not determine executable to run
0.583 npm error A complete log of this run can be found in: /root/.npm/_logs/2025-01-26T19_09_42_934Z-debug-0.log
------
Error: failed to fetch an image or build from source: error building: failed to solve: process "/bin/sh -c npx esbuild-linux-64 js/app.js --bundle --minify --sourcemap --outdir=../priv/static/assets" did not complete successfully: exit code: 1

@Kavignon Kavignon marked this pull request as draft January 26, 2025 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants