Distroless builds: Node-something-distroless examples + nginx + mcp server - #461
cosmintanasa47 wants to merge 18 commits into
Conversation
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.
|
Also added two nginx examples and one mcp server. |
There was a problem hiding this comment.
🟡 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 isrootfs/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
MongoClientconstruction raises (for example becauseMONGO_SERVER_URLis malformed), the exception is logged butclientis never assigned. Every request then raisesNameErroratclient.admin.command(...)instead of taking the intended fallback path; initialize the variable before thetryand 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 currentunikraftCLI command used throughout this repository isinstances templates delete(seescripts/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 isinstances 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
pageURL 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 butnewPage,goto, orscreenshotthrows, control jumps tocatchand this close is skipped, leaving the browser process running for each failed request. Move browser cleanup into afinallyblock that handles a failed launch as well.
node-playwright-firefox-distroless/server.js:14 remoteis 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
pageURL 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 butnewPage,goto, orscreenshotthrows, control jumps tocatchand this close is skipped, leaving the browser process running for each failed request. Move browser cleanup into afinallyblock that handles a failed launch as well.
node-playwright-webkit-distroless/server.js:21 - This public screenshot endpoint passes the caller-controlled
pageURL 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 butnewPage,goto, orscreenshotthrows, control jumps tocatchand this close is skipped, leaving the browser process running for each failed request. Move browser cleanup into afinallyblock 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.Partytitle.
- 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.
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.
1f6d569 to
c8cfdc9
Compare
|
Applied all fixes except source file fixes. |
There was a problem hiding this comment.
🟡 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, sohttp()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 deletecommand while invoking the currentunikraftCLI. The README and cleanup script useunikraft instances templates delete; with the current command this cleanup silently fails becausecheck=False, leaving the template behind.
node-code-execution-distroless/test_node-code-execution-distroless.py:79 - This also invokes the legacy
instance template lssyntax, but the test is running the currentunikraftCLI. The documented command isunikraft instances templates list, so the loop will never detect the template and will fail after the timeout.
node-playwright-chromium-distroless/server.js:14 remoteis assigned without a declaration, making it module-global. Two simultaneous requests can overwrite it beforepage.goto, so one client can receive a screenshot of another client's URL. Keep the parsed URL in a per-requestconst/letcaptured by this closure.
node-playwright-webkit-distroless/server.js:14remoteis assigned without a declaration, making it module-global. Two simultaneous requests can overwrite it beforepage.goto, so one client can receive a screenshot of another client's URL. Keep the parsed URL in a per-requestconst/letcaptured 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; comparereplywithmsgdirectly.
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
|
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. |
Those are the distroless examples for node distroless builds for non-httpserver examples. Details about the implementation in each modified files's commit message.