Introduction
AI applications often look small from the outside. A Node.js service calls a model, connects to a few tools, stores some state, and returns a response. The codebase may be much smaller than a traditional enterprise application.
The security surface is not small.
A modern Node.js AI app may use model provider APIs, MCP servers, browser automation, Redis or Postgres, private npm packages, GitHub tokens, internal APIs, and local files. An agent may read repository code, open a browser, inspect logs, summarize customer data, or call tools that perform real actions. That means the container running the app is not just serving HTTP traffic. It is sitting near credentials, tools, data, and execution paths.
This is why the Docker image matters. The base image, dependency install process, runtime user, filesystem permissions, SBOM, vulnerability scanning, and secret handling are all part of the AI application architecture.
Many AI tutorials skip this layer. They show how to call a model, build an agent loop, or connect a tool. In production, the question is different: what exactly are we shipping, where did it come from, what can it access, and how much damage can it do if compromised?
Why AI Apps Increase Supply Chain Risk
Traditional Node.js applications already have supply chain risk. They depend on npm packages, operating system packages, base images, CI pipelines, and deployment configuration. AI applications add more moving pieces.
An AI agent may use MCP servers as tool adapters. Each MCP server has its own dependencies, permissions, and credentials. A local LLM workflow may pull model artifacts from registries. A browser automation tool may bring large system dependencies. A code-review agent may need GitHub access. A support assistant may access customer-like data. A test-generation agent may read and write files.
The application may still be "just a Node.js service," but the dependency graph is much wider than it looks.
Docker's 2026 supply chain write-up on Trivy and KICS is a useful reminder of the risk. Docker described two incidents where stolen publisher credentials were used to push malicious images through legitimate publishing flows. Docker stated that its infrastructure was not breached, but anyone who pulled the compromised tags was temporarily exposed through the software supply chain.
That story matters for AI apps because agents often rely on tools they did not build. A compromised image, package, MCP server, or build step can become a path to credentials, source code, cloud systems, or sensitive data.
The Risky Dockerfile
A common Dockerfile for a Node.js AI app may look like this:
FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
ENV OPENAI_API_KEY=sk-example
ENV GITHUB_TOKEN=ghp-example
EXPOSE 3000
CMD ["npm", "start"]
This Dockerfile has several problems.
It uses node:latest, which can change over time and make builds less predictable. It copies the entire local directory into the image, which may accidentally include .env, .npmrc, test artifacts, or local files. It uses npm install instead of a lockfile-based install. It bakes secrets into the image through environment variables. It runs as the default user. It does not separate build dependencies from runtime dependencies.
For a demo, this might work. For an AI app with access to tools and credentials, it is too loose.
A Safer Dockerfile Pattern
A better pattern uses a specific base image, a multi-stage build, production-only dependencies, a non-root user, and no embedded secrets:
# syntax=docker/dockerfile:1.7
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM node:22-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
RUN groupadd --system appgroup \
&& useradd --system --gid appgroup --home /app appuser \
&& chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
This is already much safer. It pins the Node major version instead of using latest. It uses npm ci for reproducible dependency installation. It keeps build tooling out of the runtime stage. It installs only production dependencies in the final image. It runs as a non-root user.
This is not perfect security, but it is a better foundation.
Where Docker Hardened Images Fit
Docker Hardened Images take the base-image part of this problem further. Docker describes Docker Hardened Images as secure, minimal, production-ready images, and in December 2025 Docker announced that they were made free and open source under the Apache 2.0 license. Docker also stated that it had hardened more than 1,000 images and Helm charts in the catalog.
The key idea is that the base image should not be an afterthought. A hardened image reduces unnecessary packages, narrows the attack surface, and gives teams a stronger starting point than a general-purpose image.
The Dockerfile pattern stays mostly the same. The base image changes to the hardened equivalent available in your registry and organization:
FROM <your-hardened-node-image>@sha256:<digest> AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM <your-hardened-node-image>@sha256:<digest> AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER 10001
EXPOSE 3000
CMD ["node", "dist/index.js"]
The exact image name depends on how your team accesses Docker Hardened Images and how your registry is configured. The important habits are to use a trusted base image, avoid latest, and pin the image by digest when you need stronger reproducibility.
Docker's Hardened Images product page also emphasizes drop-in replacements, continuous updates, secure customization, and provenance-preserving workflows.
Do Not Bake Secrets Into the Image
AI apps usually need secrets, but the image should not contain them.
Model provider keys, GitHub tokens, MCP credentials, database passwords, and OAuth tokens should be provided at runtime through your deployment environment or secret manager. They should not appear in the Dockerfile.
services:
agent:
image: node-ai-agent:secure
environment:
NODE_ENV: production
OPENAI_API_KEY: ${OPENAI_API_KEY}
GITHUB_TOKEN: ${GITHUB_TOKEN}
DATABASE_URL: ${DATABASE_URL}
This is acceptable for local development when values come from a local .env file that is not committed. In production, prefer a managed secret system such as Kubernetes Secrets, AWS Secrets Manager, Google Secret Manager, Azure Key Vault, HashiCorp Vault, or your platform's built-in secret store.
Build-time secrets are different. If the image build needs temporary access to a private npm package or private Git repository, use Docker Build Secrets or SSH mounts instead of ARG or ENV. Docker's build secrets documentation explains that secret mounts and SSH mounts are designed for sensitive data needed during the build, and that the process has two steps: pass the secret to docker build, then consume it in the Dockerfile.
RUN --mount=type=secret,id=npm_token \
npm config set //registry.npmjs.org/:_authToken="$(cat /run/secrets/npm_token)" \
&& npm ci \
&& npm config delete //registry.npmjs.org/:_authToken
Then build with:
docker build \
--secret id=npm_token,env=NPM_TOKEN \
-t node-ai-agent:secure .
Docker's GitHub Actions documentation also supports secret mounts and SSH mounts in CI, where secret mounts appear as files under /run/secrets by default.
Add Runtime Guardrails
A secure image is only one layer. The container should also run with limited permissions.
For a Node.js AI app, a practical Compose configuration may look like this:
services:
agent:
image: node-ai-agent:secure
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: 1g
cpus: 1
environment:
NODE_ENV: production
OPENAI_API_KEY: ${OPENAI_API_KEY}
DATABASE_URL: ${DATABASE_URL}
A read-only filesystem prevents the app from writing to unexpected places. A temporary filesystem gives it a safe place for temporary files. Dropping Linux capabilities reduces what the container can do. no-new-privileges prevents privilege escalation. CPU and memory limits reduce the blast radius of a bad loop, runaway browser process, or unexpected agent behavior.
These settings may need adjustment. Browser automation, file-processing tools, and some native dependencies may require additional permissions or writable directories. The goal is not to blindly copy every restriction. The goal is to start restrictive and open only what the application actually needs.
Isolate Tooling and Data Paths
AI agents often call tools. Those tools should not all run with the same permissions.
A GitHub MCP server may need network access to GitHub but should not need write access to the local filesystem. A filesystem tool may need read access to a specific workspace but should not see the entire host machine. A browser automation tool may need temporary writable space but should not need database credentials.
A simple architecture separates the app, tools, and data:
This separation matters because compromise should not mean total access. If the browser tool is compromised, it should not automatically get database credentials. If the filesystem tool is compromised, it should not automatically get a model provider key. If the GitHub tool is compromised, it should have the smallest useful token scope.
Use SBOMs and Scanning
You cannot secure what you cannot see. A Software Bill of Materials, or SBOM, lists the components inside your image. Docker Scout uses SBOMs to understand the components in an image and cross-reference them with vulnerability data. Docker's Scout documentation describes it as a supply chain security solution that analyzes images, creates an inventory of components, and matches that inventory against vulnerability databases.
A basic scan can be part of your CI workflow:
name: Docker Security Scan
on:
pull_request:
push:
branches:
- main
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t node-ai-agent:ci .
- name: Scan image with Docker Scout
uses: docker/scout-action@v1
with:
image: node-ai-agent:ci
command: cves
only-severities: critical,high
Docker Scout's image analysis documentation explains that image analysis extracts the SBOM and image metadata, then evaluates it against vulnerability data from security advisories.
Scanning does not prove an image is safe. It gives you visibility. That visibility is especially important for AI apps because the dependency surface includes npm packages, system packages, browser dependencies, tool servers, and sometimes model artifacts.
Pin What Matters
Tags are convenient, but they can move. For production images, pin important base images by digest:
FROM node:22-slim@sha256:<digest>
This makes builds more predictable because the digest identifies a specific image. You can still update regularly, but updates become intentional instead of accidental.
The same thinking applies to MCP server images and other tool containers. Avoid pulling random latest images in production workflows. Use a known version or digest. Review the source. Keep an update process.
This is especially important for agent systems because tools can perform actions. A compromised or unexpectedly changed tool image is more dangerous than a broken static asset build.
A Practical Security Checklist
Before shipping a Node.js AI app in Docker, check the basics:
- Use a trusted base image, preferably a hardened image when available
- Avoid
latestfor production and pin critical images by digest - Use a multi-stage Dockerfile so build tools do not ship in the runtime image
- Install dependencies with
npm ciand ship only production dependencies - Run the container as a non-root user
- Do not copy
.env,.npmrc, local logs, test reports, or unnecessary files into the image - Use Docker Build Secrets for private package installation
- Pass model provider keys, GitHub tokens, database credentials, and MCP credentials at runtime through a secret manager
- Run image scanning in CI and review high and critical findings
- Use read-only filesystems, dropped capabilities, resource limits, and narrow network access where possible
- Give every MCP server and tool the smallest useful permission set
This checklist is not glamorous, but it is the work that makes AI systems safer to operate.
Conclusion
Your AI agent has a supply chain. It starts with the base image, continues through npm packages and build steps, extends into MCP servers and browser tools, and reaches all the way to model artifacts, credentials, and runtime permissions.
Docker gives Node.js teams practical controls for this problem. Docker Hardened Images provide a stronger starting point. Multi-stage builds reduce what ships. Build secrets keep private tokens out of image layers. Runtime restrictions limit damage. SBOMs and Docker Scout improve visibility. Digest pinning makes updates intentional.
None of this makes an AI app perfectly secure. It does create defense in depth. That matters because AI agents are not passive services. They read, call, summarize, browse, and sometimes act. The more capable the agent becomes, the more important its container boundary becomes.
A good AI architecture is not only about prompts, tools, and models. It is also about what the application is allowed to run, what it is allowed to access, where its dependencies came from, and what happens when something goes wrong.
Secure the supply chain before the agent becomes part of someone else's.

Top comments (0)