Dockerize a Nuxt Application

Khimananda Oli 8 min read Programming and Languages
Dockerize a Nuxt Application

By Khimananda Oli | Last reviewed: August 2026

To Dockerize a Nuxt application effectively, you must move beyond basic tutorials and implement multi-stage builds that separate dependencies from runtime artifacts. This approach reduces image size by over 80% and eliminates build tools from your production attack surface, which is critical for compliance and security. Whether you are deploying to Kubernetes or a standalone VPS, following the patterns in this containerization fundamentals guide ensures your Nuxt app is portable, secure, and performant.

Stage 1: Builder (node:22-alpine)Install Dependencies (npm ci)Build App (npm run build)Generate .output ArtifactStage 2: Runtime (node:22-alpine)Copy .output from BuilderCreate Non-Root UserExpose Port 3000 & RunArtifact Only(No node_modules)
Multi-stage build flow isolating build dependencies from the final Nuxt runtime image

How do you write a production-ready Dockerfile to Dockerize a Nuxt application?

A common mistake when teams first Dockerize a Nuxt application is using a single-stage build that includes devDependencies, source code, and build tools in the final image. This results in 1GB+ images with unnecessary CVE exposure. The correct approach uses multi-stage builds to create a lean, secure artifact.

The Optimized Multi-Stage Dockerfile

This Dockerfile targets Nuxt 3.x with the Nitro server engine. It assumes you have a standard nuxt.config.ts and package.json in your project root.

# Stage 1: Build the application
FROM node:22-alpine AS builder

WORKDIR /app

# Copy dependency files first for layer caching
COPY package.json package-lock.json ./

# Install all dependencies including devDependencies needed for build
RUN npm ci

# Copy source code
COPY . .

# Build Nuxt - generates .output directory
RUN npm run build

# Stage 2: Production runtime
FROM node:22-alpine AS runtime

# Security: Create non-root user
RUN addgroup -g 1001 -S nuxtjs && \
    adduser -S nuxtjs -u 1001 -G nuxtjs

WORKDIR /app

# Copy only the built output from builder stage
COPY --from=builder --chown=nuxtjs:nuxtjs /app/.output .output

# Set environment variables
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000

# Switch to non-root user
USER nuxtjs

EXPOSE 3000

# Health check for orchestrators
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1

CMD ["node", ".output/server/index.mjs"]

Several details here matter for production reliability. The npm ci command ensures deterministic installs matching your lockfile exactly, unlike npm install which may resolve newer versions. The --chown flag on COPY prevents permission errors when running as a non-root user. The HEALTHCHECK instruction allows Kubernetes or Docker Swarm to detect unresponsive containers automatically without external probes.

Handling Environment Variables Correctly

Nuxt handles environment variables differently at build time versus runtime. Variables prefixed with NUXT_PUBLIC_ are embedded during build and baked into the client bundle. Server-only variables like database credentials must be injected at runtime, never during build.

  • Build-time: NUXT_PUBLIC_API_BASE, feature flags, public metadata
  • Runtime: DATABASE_URL, API_SECRET, REDIS_PASSWORD
  • Never embed: Secrets, tokens, or PII in the Docker image layers

For runtime secrets, integrate with Kubernetes secrets management or AWS Secrets Manager rather than baking them into environment files. This keeps your container images immutable and safe to promote across environments.

How do you optimize Docker image size when you Dockerize a Nuxt application?

Image size directly impacts deployment speed, cold start latency, and storage costs. A typical unoptimized Nuxt Docker image exceeds 900MB. With proper optimization, you can reduce this to under 150MB.

Optimization TechniqueSize ImpactComplexityTrade-offs
Multi-stage build (alpine)-70% to -80%LowAlpine uses musl libc; rare compatibility issues
Distroless base image-85% to -90%MediumNo shell for debugging; requires sidecar or ephemeral containers
.dockerignore exclusions-5% to -15%LowPrevents cache invalidation from irrelevant file changes
Standalone Nitro preset-10% to -20%LowBundles only required node_modules into .output
Layer ordering optimization0% (build speed)LowReduces rebuild time significantly in CI

Using the Standalone Preset

Nuxt's Nitro server supports a standalone preset that traces actual dependencies and bundles them into the output directory. This eliminates the need to copy node_modules entirely in some configurations.

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: 'node-server',
    // Enable standalone output for minimal footprint
    experimental: {
      wasm: false
    },
    rollupConfig: {
      output: {
        // Ensures clean chunk splitting
        manualChunks: undefined
      }
    }
  }
})

When combined with the multi-stage Dockerfile above, the standalone preset ensures only actually-imported modules end up in the final image. Unused packages in package.json are excluded automatically.

Crafting an Effective .dockerignore

Your .dockerignore file prevents unnecessary context from being sent to the Docker daemon. This speeds up builds and prevents sensitive files from accidentally entering image layers.

# Version control
.git
.gitignore

# IDE and editor files
.vscode
.idea
*.swp
*.swo

# Documentation and non-production assets
README.md
LICENSE
docs/
*.md

# Test files
__tests__/
*.test.ts
*.spec.ts
coverage/

# Local environment and secrets
.env
.env.*
!.env.example

# Build artifacts (rebuilt in container)
.output
.nuxt
dist/
node_modules/

# OS and temp files
.DS_Store
Thumbs.db
*.log
First BuildBase Image (Cached)COPY package*.jsonnpm ci (Slow)COPY Source Codenpm run buildSource Change RebuildBase Image (Cached ✓)COPY package*.json (✓)npm ci (Cached ✓)COPY Source (Rebuilt)npm run build (Rebuilt)Dependency ChangeBase Image (Cached ✓)COPY package*.json (✗)npm ci (Rebuilt)COPY Source (Rebuilt)npm run build (Rebuilt)
Docker layer caching behavior: separating dependency installation from source code maximizes cache hits

What are the security best practices when you Dockerize a Nuxt application?

Security is not optional for production containers. When you Dockerize a Nuxt application for enterprise or regulated environments, these practices are baseline requirements for SOC 2 and ISO 27001 compliance.

Non-Root User Enforcement

Running containers as root is a critical vulnerability. If an attacker exploits your Nuxt application, root access grants full container control and potential host escape vectors. Always create and switch to a dedicated non-root user as shown in the Dockerfile above.

Read-Only Filesystem

In Kubernetes, mount the root filesystem as read-only and use emptyDir volumes only for directories that require writes (like /tmp for Nitro cache). This prevents attackers from writing malicious scripts or modifying binaries post-exploitation.

# Kubernetes Pod Security Context example
securityContext:
  runAsNonRoot: true
  runAsUser: 1001
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL

Image Scanning in CI

Integrate vulnerability scanning into your pipeline before pushing images. Tools like Trivy or Grype detect known CVEs in base images and dependencies. Block deployments if critical vulnerabilities are found. This aligns with the container image scanning practices used in mature DevSecOps workflows.

Minimal Base Images

Consider gcr.io/distroless/nodejs22-debian12 instead of Alpine for maximum security. Distroless images contain only the Node.js runtime and its direct dependencies—no shell, no package manager, no utilities. This makes exploitation significantly harder but requires adjustments to health checks and debugging workflows.

How do you deploy and run a Dockerized Nuxt application in production?

Building the image is only half the work. Running it reliably requires proper orchestration, networking, and observability configuration.

Docker Compose for Local and Staging

For local development or simple staging environments, Docker Compose provides a straightforward way to run your Nuxt container alongside databases and caches.

services:
  nuxt-app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NUXT_PUBLIC_API_BASE=https://api.staging.example.com
      - DATABASE_URL=postgresql://user:pass@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/"]
      interval: 30s
      timeout: 3s
      retries: 3

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  pgdata:

Kubernetes Deployment Considerations

For production Kubernetes deployments, reference the resource limits and requests guide to right-size your Nuxt pods. Nitro servers typically need 128-256Mi memory and 100-250m CPU for moderate traffic. Always set both requests and limits to prevent noisy-neighbor issues.

Configure readiness and liveness probes separately. The readiness probe should check your application's health endpoint (/api/health or similar), while the liveness probe can use a simpler TCP check on port 3000. This distinction prevents cascading restarts during temporary load spikes.

Ingress / LBTLS TerminationNuxt Pod 1Port 3000Nuxt Pod 2Port 3000Nuxt Pod NPort 3000Service Mesh / Cluster DNSInternal Routing & mTLSPostgreSQL / RedisStateful ServicesExternal or ManagedObservability StackPrometheus + GrafanaLogs, Metrics, TracesSecrets ManagerVault / AWS SMRuntime Injection
Production topology for Dockerized Nuxt with horizontal scaling, observability, and externalized state

Monitoring and Observability

Containerized Nuxt applications require structured logging and metrics exposure. Configure Nitro to output JSON logs for parsing by Fluent Bit or Vector. Expose a /metrics endpoint using the nitro-prometheus module for scraping. Refer to the four golden signals guide to identify which metrics actually matter for your Nuxt workload: latency, traffic, errors, and saturation.

Deploy Your Dockerized Nuxt Application with Confidence

When you Dockerize a Nuxt application correctly, you gain reproducible deployments, improved security posture, and infrastructure portability across cloud providers. The multi-stage build pattern, non-root execution, and proper secret handling form the foundation of production-grade containerization. Start with the Dockerfile provided here, integrate image scanning into your CI pipeline, and validate your setup against the security benchmarks before going live. If you need help architecting a compliant, scalable Nuxt deployment or auditing your existing container setup, reach out to discuss your infrastructure needs.

Frequently Asked Questions

Use node:22-alpine as the standard base image for 2026. It provides the latest LTS Node.js runtime with a minimal footprint, reducing attack surface and build times compared to full Debian images while maintaining compatibility with modern Nuxt 3 dependencies.

Define separate stages for installing dependencies, building assets, and running the production server. Copy only the .output directory and node_modules production dependencies into the final runtime stage to exclude source code, TypeScript configs, and dev tools from the deployed container image.

Yes, always enable nitro preset node-server or standalone in nuxt.config.ts. This bundles all necessary runtime dependencies into the .output folder, eliminating the need to install node_modules in the production container and significantly reducing final image size and startup latency.

Port 3000.

Pass runtime variables via ENV instructions or docker compose environment keys rather than build args. Nuxt reads process.env at runtime for server routes and useRuntimeConfig, ensuring secrets stay out of the image layers and can be changed per deployment without rebuilding.

Alpine images lack native build tools required by some npm packages. Install python3, make, g++, and vips-dev in the build stage, then copy compiled binaries to the runtime stage. Alternatively, switch to node:22-slim if native compilation issues persist despite adding dependencies.

Only for static SSG builds using nuxi generate. SSR and hybrid rendering require the Node.js Nitro server to handle dynamic requests, API routes, and server middleware. Use Nginx solely as a reverse proxy in front of the Node container for SSL termination and caching.

Enable standalone output, use alpine base images, implement multi-stage builds, and run npm ci with production flag. Clean npm cache after installation and avoid copying unnecessary files like tests, docs, or git history into the final runtime stage to achieve sub-200MB images.

Use /api/_health or create a custom server route returning 200 OK. Configure Docker HEALTHCHECK with curl against this endpoint to enable orchestrators like Kubernetes or Docker Swarm to detect unresponsive instances and trigger automatic restarts during production deployments.

Mount source code as a volume and set CHOKIDAR_USEPOLLING=true in the development container. Linux file watchers fail across Docker bind mounts on macOS and Windows hosts, so polling ensures filesystem changes trigger Vite HMR correctly during local development workflows.

No, never run production containers as root. Add a non-root user in the Dockerfile, change ownership of the app directory, and use the USER directive before CMD. This limits damage from potential exploits and satisfies security compliance requirements for containerized workloads in 2026.

Avoid baking secrets into images. Use NUXT_PUBLIC_ prefixed env vars for client config and runtime env vars for server config. Build-time arguments should only contain non-sensitive values like feature flags that genuinely require compile-time embedding into the JavaScript bundle.

Containers cannot reach each other via localhost. Use the Docker Compose service name as the hostname in connection strings. Ensure both services share the same network and the database container has finished initialization before Nuxt attempts connection by using depends_on with healthcheck conditions.

Override the entrypoint with sh or run docker exec to inspect logs and environment. Check if required runtime env vars are missing, verify the .output directory exists and has correct permissions, and test the node server command manually to isolate configuration versus code issues.

No, containerization adds negligible overhead. SSR performance depends on Node.js runtime efficiency, not the packaging method. Ensure adequate CPU and memory allocation, enable HTTP compression in your reverse proxy, and use CDN caching to maintain optimal Core Web Vitals for search rankings.