Image from Dockerfile
Views: 15
After the Docker Container Introduction, the next step is to deploy a sample project in a Docker container. For this, we create a basic NestJS service with a simple GET /
route that returns Hello World!
.
Simple NestJS Project
First download and install Node.js and npm, they also install the nice little tool npx
, then create a project test-nestjs-app
by running:
npx @nestjs/cli new test-nestjs-app --package-manager npm
Inside the project directory, build and start it:
cd test-nestjs-app npm i npm start
Then load: http://localhost:3000/
to get Hello World!
Now let’s deploy this in a Docker container!
Dockerfile
Create a file named Dockerfile
in the project path, with multi-stage builds:
# Stage 1: Build the application FROM mwaeckerlin/nodejs-build AS build COPY --chown=${BUILD_USER} . . RUN npm install RUN npm run build ENV NODE_ENV 'production' RUN rm -rf node_modules RUN npm install # Stage 2: Production FROM mwaeckerlin/nodejs AS production COPY --from=build /app/dist /app/dist COPY --from=build /app/node_modules /app/node_modules EXPOSE 3000 CMD ["/usr/bin/node", "/app/dist/main"]
ENV NODE_ENV 'production'
– This line sets the environment variable NODE_ENV
to production
. This is crucial for Node.js applications as it tells the app to run in production mode. Production mode typically means optimizations such as better performance, less verbose logging, and more efficient memory usage. It also prevents development dependencies from being installed or used, which could otherwise bloat the container and introduce security risks.
RUN rm -rf node_modules
– This command deletes the entire node_modules
folder. In the build stage, both production and development dependencies are installed, but for the final production image, only the production dependencies are necessary. Removing node_modules
ensures that all dependencies will be reinstalled correctly with the appropriate production flags, minimizing the final image size.
RUN npm install
– This command is used to install only the production dependencies after the rm -rf node_modules
command has cleared out the old installation. Since NODE_ENV
is set to production
, this command will skip the development dependencies, ensuring that the final image only contains what’s necessary for running the application in production, thus making the image leaner and more secure.
This Dockerfile first builds the application in a separate stage using mwaeckerlin/nodejs-build
. The final production image uses the smaller and more secure mwaeckerlin/nodejs
image, which contains only Node.js without unnecessary shell access, reducing the attack surface.
Simple Build
You can test your Dockerfile
by building the image:
docker build -t hello-world .
This executes all steps in your Dockerfile
and end with:
Successfully built 7832a2178747 Successfully tagged hello-world:latest
Where the random number is the name of your image generated by Docker. With option -t hello-world
, you created an image name hello-world
, with no repository and default tag name latest
.
Run in Docker
Now you may run your image in a new container:
docker run --name hello-world --init -it --rm -p 5000:3000 hello-world
Option --rm
removes the container when it is stopped and options --init -it
allow you to stop with -p 5000:3000
maps port 5000
of localhost
to port 3000
of your application, so you can access the app in your image with http://localhost:5000
in your browser.
Security Rules
Creating secure and minimal Docker images is crucial for production environments. By optimizing the base images and eliminating unnecessary components, you can reduce the attack surface while keeping the image size small. In this section, we will explore how to create images that are smaller than Google’s Distroless images, using examples from mwaeckerlin/nodejs
, mwaeckerlin/nginx
, and mwaeckerlin/python
. Additionally, we will discuss the benefits of using mwaeckerlin/scratch
as an improved empty base image.
mwaeckerlin/nodejs
The mwaeckerlin/nodejs
image is designed to be as minimal as possible, containing only Node.js and its shared libraries. This image excludes unnecessary components like a shell or package manager, which significantly reduces its size. In fact, it is even smaller than the popular gcr.io/distroless/nodejs
image:
- Google Distroless: ~164MB
- mwaeckerlin/nodejs: ~56MB
By focusing on including only the essential dependencies, the mwaeckerlin/nodejs
image reduces both the size and potential attack surface, making it ideal for production environments. The default CMD
for running a Node.js app can be set to match the build output (e.g., CMD ["/usr/bin/node", "/app/dist/main"]
).
mwaeckerlin/nginx
Similarly, the mwaeckerlin/nginx
image is a minimal Nginx web server build, stripped of unnecessary modules and components. This reduces the size and complexity of the image, while still providing a full Nginx server for production use. By removing unused modules and configurations, the image is kept small, but still fully functional.
mwaeckerlin/python
The mwaeckerlin/python
image is another example of a highly optimized and minimal base. It includes only the Python runtime and its dependencies, without additional utilities or shell access. This image is significantly smaller than typical Python Docker images, making it ideal for lightweight Python-based microservices.
Using mwaeckerlin/scratch
mwaeckerlin/scratch
is an improved version of the scratch
base image, which is often used as the foundation for minimal containers. Unlike the standard scratch
image, mwaeckerlin/scratch
includes a pre-configured non-root user and essential environment variables that help ensure best practices are followed in container security.
The inclusion of a non-root user is important for security, as it prevents containers from running with elevated privileges, reducing the risk of container breakout attacks. Additionally, environment variables can be predefined to ensure consistency and security across various environments.
Security Best Practices
- Run as Non-Root: Ensure all processes in your container run as a non-root user, as done by default in
mwaeckerlin/scratch
. - Minimal Dependencies: Avoid unnecessary dependencies, keeping your image small and reducing the attack surface.
- No Shell Access: By removing shell access in images like
mwaeckerlin/nodejs
andmwaeckerlin/nginx
, you significantly lower the risk of privilege escalation. - Multi-Stage Builds: Use multi-stage builds to separate build-time dependencies from the final production image.
By following these practices and using optimized base images like mwaeckerlin/scratch
, mwaeckerlin/nodejs
, and mwaeckerlin/nginx
, you can create secure, minimal Docker images for your applications.
Creating a Minimal Nginx Docker Image with Dependencies
The following code efficiently creates a minimal Nginx Docker image
by packaging only the necessary files and libraries required to run Nginx. The goal is to reduce the image size while keeping it secure.
RUN tar cph \
/app /etc/nginx /usr/lib/nginx/modules /var/lib/nginx \
/run/nginx /var/log/nginx \
$(which nginx) \
$(for f in $(which nginx) /usr/lib/nginx/modules/*; do \
ldd $f | sed -n 's,.* => \([^ ]*\) .*,\1,p'; \
done 2> /dev/null) 2> /dev/null \
| tar xpC /root/
tar Command: The Tape Archive tar
compresses and transfers only the relevant files from the Nginx installation, such as:
- Nginx Binary: The executable (
$(which nginx)
) that runs Nginx. - Nginx Modules: Libraries used by Nginx (
/usr/lib/nginx/modules
). - Configs & Logs: Configuration files (
/etc/nginx
) and runtime directories (/run/nginx
,/var/lib/nginx
).
Dependency Extraction: The ldd
command is used to extract all dynamic library dependencies required by Nginx and its modules. Nginx dynamically loads libraries at runtime. The ldd
command shows which libraries are needed by the binary and modules, so they can be included in the final image.
For example, ldd $(which nginx)
lists all shared libraries (like libc.so
, libpcre.so
) necessary for the binary to function.
The extracted list is piped into tar
, which copies only those required libraries into the final image, ensuring the container has everything it needs to run, without any excess.
Benefits of this Approach
- Minimalism: The image is built using the
scratch
base, which starts completely empty. This ensures that only what is explicitly copied viatar
ends up in the final image. - No Shell or Extra Tools: By avoiding common image components like shells or package managers, the attack surface is reduced, making the image more secure.
- Runtime User: The image runs as a non-root user, further enhancing security by limiting permissions.
Result
The final Nginx image is highly optimized, lightweight, and secure, containing only the necessary binaries, libraries, and configuration files to run the Nginx server. This reduces not only the image size but also the potential for vulnerabilities, making it safer for production use.