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:
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:
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.
BENGOO G9000 Stereo Gaming Headset for PS4 PC Xbox One PS5 Controller, Noise Cancelling Over Ear Headphones with Mic, LED Light, Bass Surround, Soft Memory Earmuffs (Blue)
$25.99 (as of December 21, 2024 08:38 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)The Legend of Zelda Encyclopedia
$20.00 (as of December 21, 2024 19:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Nexus: A Brief History of Information Networks from the Stone Age to AI
$15.99 (as of December 21, 2024 19:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)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:
// 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:
# 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:
- Evaluate Current Image Sizes: Use
docker image ls
to understand the current state. - Implement Best Practices: Use a minimal base image, install only production dependencies, and clean up unnecessary files.
- Configure Next.js for Standalone Output: Optimize the build output by enabling
standalone
mode. - Next.js Standalone mode: Removes the need for the full node_modules folder to be copied over.
- 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.