Skip to content

Distroless builds: Node-something-distroless examples + nginx + mcp server - #461

Open
cosmintanasa47 wants to merge 18 commits into
unikraft-cloud:mainfrom
cosmintanasa47:node-something-distroless
Open

cosmintanasa47 wants to merge 18 commits into
unikraft-cloud:mainfrom
cosmintanasa47:node-something-distroless

Conversation

@cosmintanasa47

Copy link
Copy Markdown

Those are the distroless examples for node distroless builds for non-httpserver examples. Details about the implementation in each modified files's commit message.

Use nodejs24-debian12 distroless image to run the server.
It fits best with the build image. Copy only necessary
executables and libraries. Out of the three executables
copied initially, only /bin/sh is mandatory because of
the entrypoint.sh scrypt that runs node.
Modify the exec command to be able to run node. It had to
match the distroless image's node path.

Previous: exec node ...
Current: exec /nodejs/bin/node ...
Slightly modify to fit new distroless version.
Slightly modify to fit new distroless version.
Unmodified files from non-distroless version.
Use bookworm for build stage to be compatible with
cc-debian12 distroless runtime image. Node's odd
versions do not have available distroless images
so used a cc image.
Slightly modify to fit new distroless version.
Slightly modify to fit new distroless version.
Unmodified files from non-distroless version.
@cosmintanasa47 cosmintanasa47 changed the title Distroless builds: Node-something-distroless examples Distroless builds: Node-something-distroless examples + nginx + mcp server Sep 14, 2026
@cosmintanasa47

Copy link
Copy Markdown
Author

Also added two nginx examples and one mcp server.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings affect startup, request isolation, cleanup, and service fallback behavior.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds 11 Unikraft distroless examples for Node.js, Playwright, Nginx, Flask/MongoDB, and MCP, with deployment documentation and end-to-end tests.

Changes:

  • Adds distroless Dockerfiles and Kraftfiles.
  • Adds application code, dependencies, configurations, and runtime wrappers.
  • Adds automated deployment and functional tests.
File summaries
File Summary
node24-karaoke-distroless/test_node24-karaoke-distroless.py Karaoke E2E test. Nit (1 vote): assert an application marker instead of only HTTP 200.
node24-karaoke-distroless/README.md Karaoke deployment guide.
node24-karaoke-distroless/Kraftfile Karaoke runtime configuration.
node24-karaoke-distroless/entrypoint.sh Karaoke startup script.
node24-karaoke-distroless/Dockerfile Karaoke distroless image build.
node24-karaoke-distroless/.dockerignore Build context exclusions.
node21-websocket-distroless/test_node21-websocket-distroless.py WebSocket E2E test.
node21-websocket-distroless/server.js WebSocket echo server.
node21-websocket-distroless/README.md WebSocket deployment guide.
node21-websocket-distroless/package.json WebSocket dependency metadata.
node21-websocket-distroless/package-lock.json Locked WebSocket dependencies.
node21-websocket-distroless/Kraftfile WebSocket runtime configuration.
node21-websocket-distroless/Dockerfile WebSocket distroless image build.
node18-wingsio-distroless/test_node18-wingsio-distroless.py Wings.io E2E test.
node18-wingsio-distroless/README.md Wings.io deployment guide.
node18-wingsio-distroless/Kraftfile Wings.io runtime configuration. Critical (1 vote): use an absolute /nodejs/bin/node executable path.
node18-wingsio-distroless/Dockerfile Wings.io distroless image build.
node18-agario-distroless/test_node18-agario-distroless.py Agar.io E2E test.
node18-agario-distroless/README.md Agar.io deployment guide.
node18-agario-distroless/Kraftfile Agar.io runtime configuration. Critical (1 vote): use an absolute /nodejs/bin/node executable path.
node18-agario-distroless/Dockerfile Agar.io distroless image build.
node-playwright-webkit-distroless/wrapper.sh WebKit startup wrapper.
node-playwright-webkit-distroless/test_node-playwright-webkit-distroless.py WebKit screenshot test.
node-playwright-webkit-distroless/server.js WebKit screenshot server. Critical (3 votes): keep the target URL local to each request instead of assigning undeclared global state.
node-playwright-webkit-distroless/README.md WebKit deployment guide.
node-playwright-webkit-distroless/package.json WebKit dependencies.
node-playwright-webkit-distroless/Kraftfile WebKit runtime configuration.
node-playwright-webkit-distroless/Dockerfile WebKit distroless image build.
node-playwright-webkit-distroless/.gitignore Local artifact exclusions.
node-playwright-webkit-distroless/.dockerignore Build context exclusions.
node-playwright-firefox-distroless/wrapper.sh Firefox startup wrapper.
node-playwright-firefox-distroless/test_node-playwright-firefox-distroless.py Firefox screenshot test.
node-playwright-firefox-distroless/server.js Firefox screenshot server. Critical (2 votes): update comments that incorrectly describe Chromium instead of Firefox.
node-playwright-firefox-distroless/README.md Firefox deployment guide.
node-playwright-firefox-distroless/package.json Firefox dependencies.
node-playwright-firefox-distroless/Kraftfile Firefox runtime configuration.
node-playwright-firefox-distroless/Dockerfile Firefox distroless image build.
node-playwright-firefox-distroless/.gitignore Local artifact exclusions.
node-playwright-firefox-distroless/.dockerignore Build context exclusions.
node-playwright-chromium-distroless/wrapper.sh Chromium startup wrapper.
node-playwright-chromium-distroless/test_node-playwright-chromium-distroless.py Chromium screenshot test.
node-playwright-chromium-distroless/server.js Chromium screenshot server. Critical (3 votes): keep the target URL local to each request instead of assigning undeclared global state.
node-playwright-chromium-distroless/README.md Chromium deployment guide.
node-playwright-chromium-distroless/package.json Chromium dependencies.
node-playwright-chromium-distroless/package-lock.json Locked Chromium dependencies.
node-playwright-chromium-distroless/Kraftfile Chromium runtime configuration.
node-playwright-chromium-distroless/Dockerfile Chromium distroless image build.
node-playwright-chromium-distroless/.gitignore Local artifact exclusions.
node-playwright-chromium-distroless/.dockerignore Build context exclusions.
node-code-execution-distroless/tsconfig.json TypeScript configuration.
node-code-execution-distroless/test_node-code-execution-distroless.py ROM execution E2E tests. Moderate (3 votes): reverse cleanup finalizer order. Moderate (1 vote): use instances templates delete. Moderate (1 vote): use instances templates list for readiness polling.
node-code-execution-distroless/server.ts Dynamic ROM loader server.
node-code-execution-distroless/rom2/Kraftfile TypeScript ROM configuration.
node-code-execution-distroless/rom2/fs/rom.ts TypeScript ROM handler.
node-code-execution-distroless/rom1/Kraftfile JavaScript ROM configuration.
node-code-execution-distroless/rom1/fs/rom.js JavaScript ROM handler.
node-code-execution-distroless/README.md ROM deployment guide.
node-code-execution-distroless/package.json Node and TypeScript metadata.
node-code-execution-distroless/Kraftfile ROM host runtime configuration.
node-code-execution-distroless/Dockerfile ROM host distroless image build.
nginx-flask-mongo-distroless/test_nginx-flask-mongo-distroless.py Full-stack E2E test.
nginx-flask-mongo-distroless/README.md Full-stack deployment guide.
nginx-flask-mongo-distroless/nginx/nginx.conf Reverse-proxy configuration.
nginx-flask-mongo-distroless/nginx/Kraftfile Nginx runtime configuration.
nginx-flask-mongo-distroless/nginx/entrypoint.sh Nginx configuration bootstrap.
nginx-flask-mongo-distroless/nginx/Dockerfile Nginx distroless image build.
nginx-flask-mongo-distroless/mongo/Kraftfile MongoDB runtime configuration.
nginx-flask-mongo-distroless/mongo/Dockerfile MongoDB distroless image build.
nginx-flask-mongo-distroless/flask/server.py Flask backend. Moderate (1 vote): initialize and explicitly handle an unavailable MongoDB client.
nginx-flask-mongo-distroless/flask/requirements.txt Python dependencies.
nginx-flask-mongo-distroless/flask/Kraftfile Flask runtime configuration.
nginx-flask-mongo-distroless/flask/Dockerfile Flask distroless image build.
nginx-flask-mongo-distroless/flask/.dockerignore Build context exclusions.
nginx-distroless/test_nginx-distroless.py Nginx E2E test.
nginx-distroless/rootfs/wwwroot/index.html Static welcome page.
nginx-distroless/rootfs/etc/nginx/nginx.conf Nginx configuration.
nginx-distroless/README.md Nginx deployment guide. Nit (3 votes): correct the nonexistent customization path referenced at lines 175 and 196.
nginx-distroless/Kraftfile Nginx runtime configuration.
nginx-distroless/Dockerfile Nginx distroless image build.
mcp-server-simple-distroless/test_mcp-server-simple-distroless.py MCP E2E test.
mcp-server-simple-distroless/server.py FastMCP weather server.
mcp-server-simple-distroless/requirements.txt MCP dependencies.
mcp-server-simple-distroless/README.md MCP deployment guide. Nit (2 votes): align the documented FastMCP version with fastmcp==3.2.4. Nit (2 votes): correct the setup list numbering.
mcp-server-simple-distroless/Kraftfile MCP runtime configuration.
mcp-server-simple-distroless/Dockerfile MCP distroless image build.
mcp-server-simple-distroless/client.py MCP client example.
Review details

Files not reviewed (2)

  • node-playwright-chromium-distroless/package-lock.json: Generated file
  • node21-websocket-distroless/package-lock.json: Generated file

Suppressed comments (15)

nginx-distroless/README.md:196

  • This path does not exist in this example: the Dockerfile copies rootfs/ into /, and the changed file is rootfs/etc/nginx/nginx.conf. Following this customization instruction edits nothing used by the deployed image.
If required, you can also customize the configuration of Nginx in `rootfs/conf/nginx.conf`.

nginx-flask-mongo-distroless/flask/server.py:18

  • If MongoClient construction raises (for example because MONGO_SERVER_URL is malformed), the exception is logged but client is never assigned. Every request then raises NameError at client.admin.command(...) instead of taking the intended fallback path; initialize the variable before the try and explicitly handle the unavailable-client case in the route.
try:
    client = MongoClient(host=client_host,
                         directConnection=True,
                         serverSelectionTimeoutMS=2000,
                         appname="unikraft")
except Exception as e:
    print(f"Error creating MongoDB client: {e}")

node-code-execution-distroless/test_node-code-execution-distroless.py:51

  • This cleanup invokes instance template delete, but the current unikraft CLI command used throughout this repository is instances templates delete (see scripts/cleanup-test-resources.sh:24). As written, template cleanup can be rejected and leave the template behind; use the current plural command.
    node-code-execution-distroless/test_node-code-execution-distroless.py:77
  • The readiness loop calls instance template ls, but the CLI command documented and used by the repository is instances templates list. If this alias is not accepted, every poll returns non-zero and the fixture always times out before building the ROMs.
    node-playwright-chromium-distroless/server.js:21
  • This public screenshot endpoint passes the caller-controlled page URL directly to Playwright. An unauthenticated caller can therefore make the browser request loopback, private, link-local, or cloud-metadata addresses, turning the example into an SSRF proxy. Validate destinations (including resolved IPs) or protect the endpoint before exposing it publicly.
    node-playwright-chromium-distroless/server.js:38
  • The error path returns HTTP 200 with a text error body, so callers and monitoring cannot distinguish a failed screenshot from a successful one. Return an error status such as 502 for navigation/rendering failures.
    node-playwright-chromium-distroless/server.js:24
  • If launch() succeeds but newPage, goto, or screenshot throws, control jumps to catch and this close is skipped, leaving the browser process running for each failed request. Move browser cleanup into a finally block that handles a failed launch as well.
    node-playwright-firefox-distroless/server.js:14
  • remote is assigned as an undeclared global. Concurrent requests can overwrite it while an earlier Playwright task is awaiting navigation, causing one request to screenshot another request's URL. Keep the target URL local to this request.
    node-playwright-firefox-distroless/server.js:21
  • This public screenshot endpoint passes the caller-controlled page URL directly to Playwright. An unauthenticated caller can therefore make the browser request loopback, private, link-local, or cloud-metadata addresses, turning the example into an SSRF proxy. Validate destinations (including resolved IPs) or protect the endpoint before exposing it publicly.
    node-playwright-firefox-distroless/server.js:38
  • The error path returns HTTP 200 with a text error body, so callers and monitoring cannot distinguish a failed screenshot from a successful one. Return an error status such as 502 for navigation/rendering failures.
    node-playwright-firefox-distroless/server.js:24
  • If launch() succeeds but newPage, goto, or screenshot throws, control jumps to catch and this close is skipped, leaving the browser process running for each failed request. Move browser cleanup into a finally block that handles a failed launch as well.
    node-playwright-webkit-distroless/server.js:21
  • This public screenshot endpoint passes the caller-controlled page URL directly to Playwright. An unauthenticated caller can therefore make the browser request loopback, private, link-local, or cloud-metadata addresses, turning the example into an SSRF proxy. Validate destinations (including resolved IPs) or protect the endpoint before exposing it publicly.
    node-playwright-webkit-distroless/server.js:38
  • The error path returns HTTP 200 with a text error body, so callers and monitoring cannot distinguish a failed screenshot from a successful one. Return an error status such as 502 for navigation/rendering failures.
    node-playwright-webkit-distroless/server.js:24
  • If launch() succeeds but newPage, goto, or screenshot throws, control jumps to catch and this close is skipped, leaving the browser process running for each failed request. Move browser cleanup into a finally block that handles a failed launch as well.
    node24-karaoke-distroless/test_node24-karaoke-distroless.py:30
  • The test only checks for HTTP 200, so any process that returns an empty or unrelated successful response would pass even if the AllKaraoke bundle was not copied or served. Assert a stable application marker from the documented page, such as its AllKaraoke.Party title.
  • Files reviewed: 84/86 changed files
  • Comments generated: 10
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread node-code-execution-distroless/Kraftfile Outdated
Comment thread node-playwright-chromium-distroless/server.js
Comment thread node-playwright-firefox-distroless/server.js
Comment thread node-playwright-webkit-distroless/server.js
Comment thread node18-agario-distroless/Kraftfile Outdated
Comment thread node18-wingsio-distroless/Kraftfile Outdated
Comment thread mcp-server-simple-distroless/README.md Outdated
Comment thread mcp-server-simple-distroless/README.md Outdated
Comment thread nginx-distroless/README.md Outdated
Modify dockerfile so that it has a "runtime" stage that is
based on a distroless node18 image from google.

Kraftfile is modified so that node's path is the one
at which node is present in the distroless image.

Slightly modify test file and README to fit this
distroless version.
Add Dockerfile with an additional stage that is based
on a distroless node18 image from google.

Modify Kraftfile's node path so that it matches
distroless image's path to nodejs.

Slightly modify test file and README so they fit for
this new distroless version.
Modify Dockerfile so that it builds the second stage
with a distroless node22 image from google. This
eliminated the copying of node binary and libraries.

Modify Kraftfile so the node binary path fits distroless
image's node path.

Slightly modify test file and README to fit new distroless
version.

Kept unmodified from the original non-distroless version:
dockerignore, gitignore, package.json,wrapper.sh, server.js.
Modify Dockerfile so that it builds the second stage
with a distroless node22 image from google. This
eliminated the copying of node binary and libraries.

Modify Kraftfile so the node binary path fits distroless
image's node path.

Slightly modify test file and README to fit new distroless
version.

Kept unmodified from the original non-distroless version:
dockerignore, gitignore, package.json,wrapper.sh, server.js.
Modify Dockerfile so that it builds the second stage
with a distroless node22 image from google. This
eliminated the copying of node binary and libraries.

Modify Kraftfile so the node binary path fits distroless
image's node path.

Slightly modify test file and README to fit new distroless
version.

Kept unmodified from the original non-distroless version:
dockerignore, gitignore, package.json,wrapper.sh, server.js.
Modify Dockerfile to have a distroless "runtime" stage that
is based on a distroless node25 image.

Modify Kraftfile's node binary path and source file
path to fit to the disign of the used distroless image.

Slightly modify test file and README to fit new distroless version.

Didn't modify other files from non-distroless version.
Switch from a two-stage Dockerfile to a one-stage Dockerfile
so it uses a distroless nginx image that eliminates the copying
of system and nginx libraries.

Modify Kraftfile so nginx binary path respects the distroless
image's nginx binary path.

Slightly modify test file and README so it fits for the new
distroless version.

Didn't modify the other files added from the original
non-distroless version.
Modify Dockerfiles for each part to run on a distroless image.
For mongo and flask used a cc image because mongo do not
have a distroless docker image and python3.12 also do not
have an image, there are images compatible only with
python3.11 and python3.13.

Modify KRaftfiles to point to the exact paths for it's
binaries (nginx, python and mongod) considering the
distroless images. Same for entrypoint.sh.

Modify test file and README to fit new distroless version.
Modify Dockerfile so the server runs on a distroless image.
This excluded the copying of system and python libraries,
but also python interpretor and other libraries. The distroless
stage just copies the dependencies resolved in the build stage
and with a compatible interpretor it runs the python mcp server.

Modify README and test file so it fits the new distroless version.

Kraftfile and the rest of the files weren't modified. Those are
the same as the ones in the non-distroless example.
@cosmintanasa47
cosmintanasa47 force-pushed the node-something-distroless branch from 1f6d569 to c8cfdc9 Compare September 17, 2026 21:32
@cosmintanasa47

Copy link
Copy Markdown
Author

Applied all fixes except source file fixes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical build, runtime, security, test, and cleanup issues remain.

Get a fresh assessment by requesting another Copilot review.

Review details

Files not reviewed (2)

  • node-playwright-chromium-distroless/package-lock.json: Generated file
  • node21-websocket-distroless/package-lock.json: Generated file

Suppressed comments (7)

nginx-flask-mongo-distroless/test_nginx-flask-mongo-distroless.py:86

  • Only the NGINX instance is awaited here. MongoDB and Flask may still be starting when the first request arrives; Flask returns HTTP 200 with MongoDB server not available... in that case, so http() will not retry and this assertion can fail intermittently. Wait for both dependencies to be ready or retry until the expected body is observed.
    wait_instance(extract_instance_name(nginx_instance), "running")

    # 5. Verify the response from the full stack.
    resp = http(url)
    assert resp.status_code == 200
    assert "Hello from the MongoDB client!" in resp.text

node-code-execution-distroless/test_node-code-execution-distroless.py:52

  • The test uses the legacy instance template delete command while invoking the current unikraft CLI. The README and cleanup script use unikraft instances templates delete; with the current command this cleanup silently fails because check=False, leaving the template behind.
    node-code-execution-distroless/test_node-code-execution-distroless.py:79
  • This also invokes the legacy instance template ls syntax, but the test is running the current unikraft CLI. The documented command is unikraft instances templates list, so the loop will never detect the template and will fail after the timeout.
    node-playwright-chromium-distroless/server.js:14
  • remote is assigned without a declaration, making it module-global. Two simultaneous requests can overwrite it before page.goto, so one client can receive a screenshot of another client's URL. Keep the parsed URL in a per-request const/let captured by this closure.
    node-playwright-webkit-distroless/server.js:14
  • remote is assigned without a declaration, making it module-global. Two simultaneous requests can overwrite it before page.goto, so one client can receive a screenshot of another client's URL. Keep the parsed URL in a per-request const/let captured by this closure.
    node21-websocket-distroless/test_node21-websocket-distroless.py:52
  • The messages are sent as WebSocket text frames, so recv() returns a Python string and has no .decode() method. This makes every round-trip in this loop fail; compare reply with msg directly.
    node24-karaoke-distroless/test_node24-karaoke-distroless.py:30
  • This only checks that the endpoint returned HTTP 200; any unrelated 200 response (including an error page) would pass even if the AllKaraoke bundle was not served. Assert an application marker such as the expected page title/content as the analogous Wings/Agar.io tests do.
  • Files reviewed: 84/86 changed files
  • Comments generated: 17
  • Review effort level: Lite

Comment thread mcp-server-simple-distroless/Dockerfile
Comment thread nginx-flask-mongo-distroless/flask/Dockerfile
Comment thread nginx-flask-mongo-distroless/mongo/Dockerfile
Comment thread node-playwright-chromium-distroless/server.js
Comment thread node-playwright-firefox-distroless/server.js
Comment thread node-playwright-chromium-distroless/server.js
Comment thread node-playwright-firefox-distroless/server.js
Comment thread node-playwright-firefox-distroless/server.js
Comment thread node-playwright-webkit-distroless/server.js
Comment thread node-playwright-webkit-distroless/server.js
@cosmintanasa47

cosmintanasa47 commented Sep 17, 2026 •

Copy link
Copy Markdown
Author

The observations made by Copilot are either about source code, and I don't modify it, or examples that crash or do not start and this is false so Copilot is wrong. The added examples were manually tested before PR.

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