Optimizing Next.js Docker Images for Production

Optimizing Docker images is crucial for creating efficient and manageable applications. As a developer, I’ve found that even small tweaks can significantly improve performance and reduce costs. In this guide, I’ll walk you through how I optimized my Next.js Docker images to be as small and efficient as possible. We’ll cover evaluating current image sizes, implementing best practices for Dockerfile optimization, configuring Next.js for standalone output, and using multi-stage builds.

Thank me by sharing on Twitter 🙏

Evaluating Current Image Sizes

Before diving into optimizations, it’s essential to understand the current state of your Docker images. Evaluating the size of your images helps you identify potential areas for improvement.

To start, open your terminal and list all Docker images using the following command:

ShellScript
docker image ls

This command will provide a list of all Docker images on your system, along with their sizes. Here’s a sample output:

Plaintext
REPOSITORY          TAG        IMAGE ID       CREATED         SIZE
my-nextjs-app       latest     7c8d9e2f807d   2 minutes ago   450MB
node                20-alpine  5c9d9e2f807d   3 days ago      88MB

In this example, the my-nextjs-app image is significantly larger than the base node:20-alpine image. This discrepancy highlights the potential for optimization. By comparing your custom images to base images, you can better understand the impact of your application layers on the final image size.

Implementing Best Practices for Dockerfile Optimization

Optimizing your Dockerfile is the next step in reducing the size of your Docker images. Here are some best practices I’ve found effective:

Use Minimal Base Images

Choosing a minimal base image like node:20-alpine can significantly reduce the size of your final image. Alpine images are lightweight and include only the essential packages.

Use Multi-Stage Builds

Multi-stage builds are a powerful feature in Docker that allows you to separate the build environment from the runtime environment. This separation ensures that only the necessary files are included in the final image, significantly reducing its size.

Install Only Production Dependencies

When building your application, ensure that only production dependencies are included in the final image. This can be achieved by running npm prune --production after installing all dependencies.

Remove Unnecessary Files

Cleaning up unnecessary files and dependencies can further reduce image size. This includes excluding development files and documentation that aren’t needed in a production environment.

Configuring Next.js for Standalone Output

Next.js provides a standalone output mode that further optimizes the build output by including only the necessary files and dependencies. Configuring Next.js for standalone output simplifies the final build and reduces the size of the Docker image.

To enable standalone mode, modify your next.config.mjs as follows:

TypeScript
// next.config.mjs
export default {
  output: 'standalone',
  // other configurations
};

This configuration ensures that only essential files are included in the final build output, making the Docker image more efficient.

Final Result

Here’s an optimized Dockerfile incorporating these best practices:

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

# Set the working directory inside the container
WORKDIR /app

# Copy package.json and package-lock.json files to the working directory
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy the rest of the application code to the working directory
COPY . .

# Build the Next.js app
RUN npm run build && \
		npm prune --production

# Stage 2: Create the final image
FROM node:20-alpine

# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Set the working directory inside the container
WORKDIR /app

# Copy only the necessary files from the builder stage
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

# Change ownership to the non-root user
RUN chown -R appuser:appgroup /app

# Switch to the non-root user
USER appuser

# Expose port 8080 to the outside world
EXPOSE 8080

# Set environment variable to specify the port
ENV PORT 8080

# Start the Next.js app
CMD ["node", "server.js"]

Final size in my example came down to 170mb from 450mb

Key Points:

  • Minimal Base Image: Using node:20-alpine reduces the base image size.
  • Build Stage: The first stage (builder) installs dependencies and builds the application.
  • Production Stage: The second stage (runner) copies only the necessary files from the build stage, ensuring a smaller final image.
  • Production Dependencies: Running npm prune --production ensures only necessary dependencies are included.
  • Clean Up: Removing unnecessary files and dependencies reduces bloat.

Conclusion

Optimizing Docker images for a Next.js application involves several key steps: evaluating current image sizes, implementing best practices for Dockerfile optimization, configuring Next.js for standalone output, and using multi-stage builds. By following these steps, I was able to reduce the size of my Docker images, resulting in more efficient deployments and lower operational costs.

In summary:

  1. Evaluate Current Image Sizes: Use docker image ls to understand the current state.
  2. Implement Best Practices: Use a minimal base image, install only production dependencies, and clean up unnecessary files.
  3. Configure Next.js for Standalone Output: Optimize the build output by enabling standalone mode.
  4. Next.js Standalone mode: Removes the need for the full node_modules folder to be copied over.
  5. Use Multi-Stage Builds: Separate the build environment from the runtime environment to reduce image size.

These optimizations not only improve the efficiency of your Docker images but also enhance the overall performance and manageability of your Next.js applications.

Share this:

Leave a Reply