Greetings, fellow coder! Welcome to our magical journey through the land of Docker, Node.js, and TypeScript. Picture this: we are brave knights on a quest to vanquish bloated Docker images and fight off lurking security vulnerabilities. Our weapons? Multistage builds and distroless images!
Multistage builds are Docker's equivalent of a transformer robot - a single Dockerfile with multiple identities! These powerful, shapeshifting Dockerfiles help us keep our final Docker image as lean as a sprinter and as clean as a whistle.
Here's the deal: in a Node.js application, there's a crowd of dependencies that are just party crashers. They join the fun during the build process but are total couch potatoes when it's time for the application to run. With multistage builds, we kick these loafers out when they're no longer needed. We install them during the build stage, let them do their thing, and then bid them adieu in the final image.
Distroless images are the superheroes of Docker images! They're here to save the day with their minimalist design and robust security. They come packed with only your application and its runtime dependencies. No package managers, shells, or other uninvited guests you'd typically find in a standard Linux distribution.
These cape-wearing images swoop in with two superpowers:
- Reduced attack surface: With fewer elements in the image, villains find fewer opportunities to exploit our container. It's like reducing the number of doors in a fortress!
- Reduced image size: Less clutter, smaller size. This means our Docker image is nimble and efficient, just like a superhero should be.
The most battle-tested and actively maintained docker images for this is maintained by Google as part of the Google Container Tools. This is what we will use for our Dockerfile, and other than Node you'll also find images for Java and Python.
Now, let's dive into the deep end of our Dockerfile and dissect it, piece by piece:
ARG BUILD_IMAGE=node:20.1.0 ARG RUN_IMAGE=gcr.io/distroless/nodejs20-debian11
Here we're setting the stage. The ARG instruction is our backstage crew, setting up BUILD_IMAGE as our build-stage costume and RUN_IMAGE as our final runtime-stage ensemble.
# Build stage FROM $BUILD_IMAGE AS build-env COPY . /app WORKDIR /app RUN npm ci && npm run build
# Prepare production dependencies FROM $BUILD_IMAGE AS deps-env COPY package.json package-lock.json ./ RUN npm ci --omit=dev
Next scene: another build stage. Here, we're like bouncers at a club, only letting in the cool, production dependencies with the --omit=dev flag. This ensures the party crashers (aka devDependencies) don't sneak into the runtime. The result? A sleek, slim Docker image.
# Create final production stage FROM $RUN_IMAGE AS run-env WORKDIR /usr/app COPY --from=deps-env /node_modules ./node_modules COPY --from=build-env /app/build ./build COPY package.json ./
Now, the final act! We're in the distroless Node.js image, building our grand production at /usr/app. We move in our essential props – node_modules from our deps-env stage and the compiled code from the build-env stage. We even slide in our package.json script for good measure.
This cunning strategy ensures only the VIPs - our essential runtime dependencies and compiled application code - make it to our final Docker image. Thus, our image remains slender, and security risks get sent packing!
ENV NODE_ENV="production" EXPOSE 8080 CMD ["build"]
Finale time! We hoist the NODE_ENV flag to "production", just as a security measure to minimize the risk of running in development mode by accident. The EXPOSE 8080 instruction is like a public service announcement to Docker that our container will be entertaining guests on port 8080 at runtime, though it does not automatically publish the port when running the container.
Curtains close! So, there you have it, fellow knight! Using these best practices, you can craft a Node.js & TypeScript application Docker image that's as swift as a cheetah, as lean as a gazelle, and as secure as a fortress. Multistage builds and distroless images are your trusted squires in this noble quest for optimal, professional, and secure deployments. Now, go forth and conquer the world of containerized applications!
Here is the complete Dockerfile:
ARG BUILD_IMAGE=node:20.1.0 ARG RUN_IMAGE=gcr.io/distroless/nodejs20-debian11 # Build stage FROM $BUILD_IMAGE AS build-env COPY . /app WORKDIR /app RUN npm ci && npm run build # Prepare production dependencies FROM $BUILD_IMAGE AS deps-env COPY package.json package-lock.json ./ RUN npm ci --omit=dev # Create final production stage FROM $RUN_IMAGE AS run-env WORKDIR /usr/app COPY --from=deps-env /node_modules ./node_modules COPY --from=build-env /app/build ./build COPY package.json ./ ENV NODE_ENV="production" EXPOSE 8080 CMD ["build"]
And here is the size of a fastify api built with this Dockerfile when hosted on AWS ECR: