CodingDevOpsIntermediate30 minSaves 30 minutes

Secure, Compact Multi-Stage Docker for Node.js APIs

For platform engineers, this guide delivers a multi-stage Docker build for Node.js applications, ensuring minimal image size and enhanced security for production deployments.

Craft a secure, lightweight multi-stage Dockerfile for Node.js APIs, targeting sub-150MB images on a distroless base. This solution provides a Dockerfile, Docker Compose setup, and essential notes on build, runtime, security, and image size optimization, helping engineers ship production-ready services efficiently.

READY-TO-USE PROMPT

Copy Prompt

prompt.txt
As an experienced Docker and Node.js build engineer, with a strong focus on operational security and image optimization, your task is to design a production-ready Docker solution.

### Context
I am deploying a Node.js API named `{{application_name}}` that needs a highly optimized and secure Docker image. The API's main entry point is `{{api_entrypoint}}`. The primary objectives are a minimal final image size (ideally under 150MB), enhanced security through a reduced attack surface, and clear documentation for deployment and maintenance.

### Task
Generate a complete Docker solution consisting of a multi-stage Dockerfile, a `docker-compose.yml` file for local development and testing, and comprehensive notes covering build specifics, runtime considerations, security best practices, and image size targets.

### Constraints
1.  **Multi-Stage Build**: Implement a multi-stage Dockerfile. The first stage (build stage) should include all development dependencies necessary for building the application. The second stage (runtime stage) must be as lean as possible, containing only the compiled application and its production dependencies.
2.  **Base Image**: Use an appropriate base image for the build stage (e.g., `node:lts-alpine` or similar). For the runtime stage, use a distroless or `alpine` base image to minimize the attack surface. Prioritize distroless if possible, or `alpine` if specific runtime tools are essential.
3.  **Image Size Target**: The final production image size must be under 150MB. Include strategies to achieve this, such as removing build artifacts, using a minimal base, and optimizing `node_modules`.
4.  **Security**: The runtime container must run as a non-root user. Include directives for creating a dedicated user and group, and setting appropriate permissions. Avoid installing unnecessary packages in the runtime image.
5.  **Environment Variables**: Use environment variables for configuration where appropriate, but ensure sensitive data is not hardcoded.
6.  **Node.js Version**: Assume a recent LTS Node.js version (e.g., Node.js 20). If specific version `{{node_version}}` is provided, use that.
7.  **Application Port**: The application listens on port `{{application_port}}`.
8.  **Output Structure**: Clearly separate the Dockerfile, `docker-compose.yml`, and the explanatory notes.

### Output
Provide the following:

1.  **Dockerfile**: The complete multi-stage Dockerfile.
2.  **`docker-compose.yml`**: A `docker-compose.yml` file for local testing that builds and runs the service.
3.  **Build and Runtime Notes**: Detailed explanations of each stage in the Dockerfile, including rationale for choices (e.g., base images, package managers, dependency handling).
4.  **Image Size Targets**: A clear statement of the expected final image size and key strategies used to achieve it.
5.  **Security Notes**: Specific security measures implemented, such as non-root user, minimal packages, and any other relevant hardening considerations.

Estimated results

DifficultyIntermediate
Setup time30 min
Time saved30 minutes
Best modelsChatGPT, Gemini, Claude
Best audienceSoftware Development, DevOps

Editor's note

Why this prompt matters

Deploying Node.js APIs to production often introduces common challenges around Docker image size and security posture. Without careful construction, images can become bloated with development dependencies, leading to slower deployment cycles, increased storage costs, and a larger attack surface. Many engineering teams struggle to balance developer convenience with the stringent requirements of a production environment, where every megabyte and every installed package can represent a potential vulnerability or operational overhead. This often results in compromises that impact either security or efficiency.

This guide addresses those critical concerns directly. It outlines a multi-stage Docker build strategy specifically tailored for Node.js applications, focusing on creating compact, hardened images. The workflow emphasizes separating build-time tools from runtime essentials, employing minimal base images, and configuring containers to run with least privilege. By systematically stripping away unnecessary components and adopting security-first principles, the resulting images are significantly smaller and more resilient against common threats.

For engineers responsible for shipping Node.js services, adopting these practices translates into tangible operational benefits. Expect faster image pulls, reduced resource consumption, and a stronger security stance for your deployed applications. This approach provides a clear path to building and deploying Node.js APIs that meet enterprise-grade standards for efficiency and security, ensuring your services are production-ready without compromise.

Anatomy

Prompt engineering breakdown

Role

As an experienced Docker and Node.js build engineer, with a strong focus on operational security and image optimization

Context

Deploying a Node.js API named `{{application_name}}` with `{{api_entrypoint}}` that needs an optimized and secure Docker image. Objectives: minimal image size (<150MB), enhanced security, clear documentation.

Goal

Generate a complete Docker solution: multi-stage Dockerfile, `docker-compose.yml`, and comprehensive notes covering build specifics, runtime, security, and image size targets.

Constraints

Multi-stage build, specific base images (distroless/alpine for runtime), <150MB final image, non-root user, environment variables for configuration, recent LTS Node.js (or `{{node_version}}`), application listens on `{{application_port}}`, structured output.

Output format

Dockerfile, `docker-compose.yml`, Build and Runtime Notes, Image Size Targets, Security Notes.

Why this structure works

The prompt uses role priming to establish expertise, guiding the model to act as a specialized engineer. Explicit constraints like the 150MB image size and non-root user ensure specific security and performance requirements are met. Finally, the structured output format guarantees all necessary components of a production-ready Docker solution are provided clearly and completely.

Pick your version

Prompt variations

BeginnerWorks with any model

When you're new to Docker optimization and need a guided solution for a Node.js app without deep dives into advanced concepts.

prompt.txt
As a Docker build assistant, help me create a basic but secure Docker setup for my Node.js application named `{{app_name}}`. I need a Dockerfile that uses two stages: one for building with development tools, and a second, smaller stage for running the application. The goal is a final image under 150MB. Please also provide a `docker-compose.yml` file for local testing that builds and starts the service. The application listens on port `{{port}}`. Use a recent Node.js LTS version. Include simple notes explaining why each part is important, especially for keeping the image small and secure by running as a non-root user.
ProfessionalBest with claude

For experienced engineers who need a comprehensive, production-grade Docker setup for a Node.js API, emphasizing security and performance.

prompt.txt
As a senior Docker and Node.js architect focused on operational excellence and security, engineer a production-grade Docker solution for a Node.js API, `{{application_name}}`, with entry point `{{api_entrypoint}}`. The solution must deliver a multi-stage Dockerfile, a `docker-compose.yml` for local validation, and comprehensive documentation. Prioritize a sub-150MB final image, a minimal attack surface via distroless/alpine runtime, and non-root user execution. Detail build, runtime, security hardening, and image size optimization strategies. Ensure all configuration is via environment variables, not hardcoded. The application runs on port `{{application_port}}`, using Node.js version `{{node_version}}` (default to LTS if not specified).
Short VersionBest with chatgpt

For quick Dockerfile generation when you already understand the underlying principles and need a concise output.

prompt.txt
Generate a multi-stage Docker solution for a Node.js API `{{app_name}}` with entry `{{entrypoint}}`. Provide a Dockerfile with a build stage and a lean runtime stage (distroless/alpine), targeting a sub-150MB final image running as a non-root user. Include a `docker-compose.yml` for local testing on port `{{port}}` and brief notes on image optimization and security hardening. Assume Node.js LTS.
EnterpriseBest with gemini

When deploying critical Node.js services in an enterprise environment, requiring strict compliance, detailed security auditing, and stakeholder reporting.

prompt.txt
As an enterprise cloud architect specializing in secure and compliant deployments, develop a comprehensive multi-stage Docker solution for our critical Node.js API, `{{application_name}}`. The solution must meet stringent internal security policies and external compliance requirements, aiming for a final image under 150MB with a minimal attack surface, running as a non-root user. Provide a detailed Dockerfile, a `docker-compose.yml` for development and security testing, and extensive notes covering build rationale, runtime hardening, and specific risk mitigation strategies. Address how this solution aligns with our `{{compliance_standard}}` standards and supports audit trails. The application runs on port `{{application_port}}`, using Node.js `{{node_version}}`.

What you'll get

Expected output

```dockerfile # Stage 1: Build application and install production dependencies FROM node:20-alpine AS builder

WORKDIR /app

# Copy package.json and package-lock.json first to cache npm ci COPY package*.json ./ RUN npm ci --omit=dev --no-progress

# Copy application source code COPY . .

# Stage 2: Create the lean production image FROM alpine:3.18

# Create a non-root user and group ARG UID=1000 ARG GID=1000 RUN addgroup -g ${GID} nodeuser && adduser -u ${UID} -G nodeuser -s /bin/sh -D nodeuser

WORKDIR /app

# Copy production dependencies and application code from the builder stage COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app ./

# Set appropriate permissions for the application directory RUN chown -R nodeuser:nodeuser /app

# Run as the non-root user USER nodeuser

EXPOSE 3000

# Command to run the application CMD ["node", "src/index.js"] ```

``yaml version: '3.8' services: my-node-api: build: context: . dockerfile: Dockerfile ports: - "3000:3000" volumes: - .:/app - /app/node_modules # Prevents host node_modules from overriding container's environment: NODE_ENV: development PORT: 3000 restart: unless-stopped ``

Build and Runtime Notes

Build Specifics: The Dockerfile uses a multi-stage build. The initial builder stage, based on node:20-alpine, handles dependency installation. npm ci --omit=dev ensures only production dependencies are installed, keeping the final image lean by excluding development tools and test frameworks. Application source code is copied into this stage for preparation.

Runtime Considerations: The production image is built from alpine:3.18. This minimal base image significantly reduces the potential attack surface. A dedicated non-root user, nodeuser, is created and assigned to run the application, adhering to the principle of least privilege. Only the essential production node_modules and compiled application code are transferred from the builder stage. The application is configured to listen on port 3000.

Security Best Practices: Running the container with a non-root user (nodeuser) is a foundational security measure, mitigating the impact of any potential compromise. The choice of an alpine base image minimizes the number of included packages, reducing the overall attack surface and potential vulnerabilities. Regular security scanning of the final image is advised. Avoid embedding sensitive data directly; use environment variables or a dedicated secrets management system.

Image Size Targets: The multi-stage build is key to achieving an image size under 150MB. By separating the build environment from the runtime, development dependencies and build tools are discarded. npm ci --omit=dev further prunes unnecessary packages. The inherently lightweight alpine base image, combined with these practices, ensures the final image contains only the absolute necessities for application execution, maintaining a small and efficient footprint.

Under the hood

Why this prompt works

This prompt works by precisely defining the AI's role and setting strict technical parameters, mirroring an engineering brief. Establishing the persona of an "experienced Docker and Node.js build engineer" immediately focuses the model on operational security and image optimization. This ensures the output reflects production-grade considerations, not just functional code.

The inclusion of specific objectives—minimal image size, enhanced security, and clear documentation—guides the AI toward desired outcomes, while detailed context (application name, entry point) provides the necessary project-specific data. The prompt's core strength lies in its hard constraints. Directives like "Multi-Stage Build," "Base Image" (distroless or Alpine), "Image Size Target" (under 150MB), and "Security" (non-root user) are non-negotiable requirements. These constraints force the AI to implement industry best practices, directly addressing common pain points for engineers deploying Node.js services. They prevent the generation of insecure or bloated images.

Finally, the explicit "Output Structure" ensures the model delivers a well-organized, directly usable solution, separating the Dockerfile, docker-compose.yml, and explanatory notes. This structured delivery is essential for engineering workflows, making the generated content immediately actionable and reducing post-generation refinement.

Model fit

Best AI models for this prompt

Claude

Claude excels at generating structured, detailed code and explanations. Its strength lies in understanding complex constraints and producing well-commented Dockerfiles and comprehensive notes. Claude generally provides reasoned choices for base images and optimization strategies. It can sometimes be overly verbose; review for conciseness. See the full Claude hub for deeper guidance.

ChatGPT

ChatGPT is effective for producing functional Dockerfiles and accompanying documentation. It typically adheres well to multi-stage build requirements and security directives like non-root users. Verify its suggestions for optimal base images and ensure the generated docker-compose.yml aligns with current best practices. See the full ChatGPT hub for deeper guidance.

Gemini

Gemini is capable of generating accurate Docker configurations, particularly for Node.js applications. It's good at integrating specific version requirements and port configurations into the solution. Pay attention to the detail in its security notes and image size optimization suggestions, as these can sometimes be less exhaustive than other models. See the full Gemini hub for deeper guidance.

When to use

  • When deploying Node.js APIs to production environments.
  • When minimizing Docker image size is critical for faster deployments and reduced registry costs.
  • When security is a primary concern, requiring a reduced attack surface by stripping unnecessary tools.
  • When standardizing Node.js build and runtime environments across multiple projects or teams.
  • When optimizing CI/CD pipeline efficiency by leveraging Docker's build cache for consistent builds.

When not to use

  • For local development where full debugging tools and frequent iteration without rebuilding are prioritized.
  • For simple scripts or one-off tasks not intended for long-term production deployment.
  • When the application relies heavily on complex native dependencies that require a full operating system environment for runtime.
  • During rapid prototyping phases where image optimization and security hardening are not immediate concerns.

Get more from it

Pro tips

  • 1

    Prioritize `npm ci` over `npm install` in build stages to ensure deterministic dependency installation, preventing build inconsistencies between environments.

  • 2

    Mount `node_modules` as a volume during local development to avoid rebuilding the image on every code change, saving significant iteration time.

  • 3

    Regularly scan your base images and application dependencies for known vulnerabilities using tools like Trivy or Clair, integrating them into your CI/CD process.

  • 4

    Use `.dockerignore` effectively to prevent unnecessary files (like `.git` or local `node_modules`) from being copied into the build context, reducing build times.

  • 5

    Pin specific versions for base images (e.g., `node:20-alpine`) rather than `lts` to ensure reproducible builds over time, avoiding unexpected updates.

  • 6

    Consider `distroless` for the runtime stage only after confirming all necessary runtime libraries are statically linked or explicitly included, avoiding runtime surprises.

  • 7

    Use Docker's build cache by ordering Dockerfile instructions from least to most frequently changing elements, speeding up subsequent builds.

Don't ship this

Common mistakes

  • Installing development dependencies (e.g., `nodemon`, testing frameworks) in the final runtime image.

    Fix — Ensure `devDependencies` are only installed in the build stage and not copied to the lean production image, significantly reducing size and attack surface.

  • Running the container as the root user in the runtime stage, granting unnecessary privileges.

    Fix — Create a non-root user and group, setting appropriate permissions and switching to this user before running the application command.

  • Copying the entire application context (`.` command) without an effective `.dockerignore` file.

    Fix — Utilize a `.dockerignore` file to exclude unnecessary files and directories (like `.git`, `dist`, local `node_modules`) from the build context.

  • Not pinning specific versions for base images or Node.js runtime, leading to inconsistent builds.

    Fix — Always specify exact image tags (e.g., `node:20.10.0-alpine3.19`) to ensure consistent builds across development, staging, and production environments.

  • Exposing sensitive environment variables directly in the Dockerfile or `docker-compose.yml` for production.

    Fix — Use Docker secrets, Kubernetes secrets, or external vault solutions for sensitive data in production environments.

  • Forgetting to remove build artifacts or temporary files from the build stage before copying to the runtime stage.

    Fix — Explicitly clean up cache directories (e.g., `npm cache clean --force`) or temporary files in the build stage to minimize the final image size.

People also ask

Frequently asked questions

Q.Why is a multi-stage build necessary for a Node.js application?

It separates build-time dependencies (compilers, dev tools) from runtime dependencies. This significantly reduces the final image size and attack surface, as development tools are not shipped to production, improving security and deployment speed.

Q.What's the primary benefit of running the container as a non-root user?

Running as non-root mitigates potential security risks. If an attacker compromises the application, they won't have root privileges within the container, limiting their ability to impact the host system or other containers.

Q.Can this multi-stage approach be used for applications with native C++ dependencies?

Yes, but it requires careful setup. The build stage must include compilers and build tools (e.g., g++, make) to compile native modules. Only the compiled binaries and production node_modules are then copied to the lean runtime stage.

Q.How should I handle environment-specific configurations (e.g., different API endpoints for dev vs. prod)?

Use environment variables passed at runtime (e.g., via docker run -e, docker-compose.yml, or Kubernetes manifests). This keeps your Docker image generic and configuration external to the image.

Q.What if my final production image size consistently exceeds the 150MB target?

Review your node_modules for large or unnecessary production dependencies using npm ls --prod. Ensure your .dockerignore is comprehensive. Consider alternative base images or pnpm for better dependency deduplication.

Q.Is it recommended to commit my `node_modules` directory to version control?

No, node_modules should be excluded from version control using .gitignore. The Dockerfile explicitly handles installing dependencies from package.json and package-lock.json, ensuring a clean and reproducible build environment.

Version 1.0Last reviewed July 20, 2026
Reviewed by PromptInFlow Editorial Team