Introduction:
Welcome to a comprehensive exploration of multistage Dockerfiles using a Golang application as our focal point. Multistage builds offer optimization and security benefits by segregating the build environment from the final deployable image. Let's meticulously dissect each stage to unravel its significance.
Key Advantages of Multistage Dockerfiles:
While reducing image size is a fundamental benefit, multistage builds go beyond mere size optimization:
Tailored Runtime Environments: Distinct and optimized environments for building and running applications can be established, ensuring each stage fulfills its specific purpose.
Enhanced Maintainability: By separating build tools and dependencies from the final image, codebases become cleaner, and image maintenance significantly lessens.
Security-First Approach: Employing non-root users and minimizing attack surfaces are cornerstones of secure containerization, both of which are actively promoted by multistage builds.
Optimized Rebuilds: Layer caching significantly boosts rebuild efficiency, especially for projects with extensive dependency trees.
How to Set Up the Project
Basic directory structure
After completing the following steps, our application directory structure will look like this:
simple-http-server-GO/
├── docker-compose.yml
├── Dockerfile
├── go.mod
├── go-server.exe
├── main.go
├── README.md
└── static/
├── form.html
└── index.html
This Docker image contains a simple Go web server that serves static content and handles form submissions. The default behavior is to display "Your Docker Image is Running!" when accessed at the root.
For your reference, You can checkout the GitHub repo for this project.
GitHub Link: https://github.com/panchanandevops/Building-Dockerfiles/tree/main/simple-http-server-GO
Dockerfile for GoLang application
# Stage 1: Build Stage
# Use a specific version of the official Golang image as the base image
FROM golang:1.21-bullseye AS build
# Create a non-root user for running the application
RUN useradd -u 1001 nonroot
# Set the working directory inside the container
WORKDIR /app
# Copy only the go.mod file to install dependencies efficiently and leverage layer caching
COPY go.mod ./
# Set the GIN_MODE environment variable to release
ENV GIN_MODE=release
# Use cache mounts to speed up the installation of existing dependencies
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
# Copy the entire application source code
COPY . .
# Compile the application during build and statically link the binary
RUN go build \
-ldflags="-linkmode external -extldflags -static" \
-tags netgo \
-o go-web-server
# Stage 2: Deployable Image
# Use a minimal scratch image as the base image for the final image
FROM scratch
# Copy the /etc/passwd file from the build stage to provide non-root user information
COPY --from=build /etc/passwd /etc/passwd
# Copy the compiled application binary from the build stage to the final image
COPY --from=build /app/go-web-server /go-web-server
# Use the non-root user created in the build stage
USER nonroot
# Expose the port the application will run on
EXPOSE 8080
# Define the command to run the application when the container starts
CMD ["./go-web-server"]
Breaking down each stage helps us understand multistage Dockerfiles easily. When we look at each stage carefully, we can see why each instruction is there and what it does. This makes it clear why each line in the Dockerfile is important.
Stage 1: Build Stage
# Use a specific version of the official Golang image as the base
FROM golang:1.21-bullseye AS build
# Create a non-root user for running the application
RUN useradd -u 1001 nonroot
# Set working directory inside the container
WORKDIR /app
# Copy only go.mod for efficient dependency installation
COPY go.mod ./
# Set GIN_MODE to release
ENV GIN_MODE=release
# Use cache mounts for faster dependency installation
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
# Copy the entire application source code
COPY . .
# Compile the application with static linking
RUN go build \
-ldflags="-linkmode external -extldflags -static" \
-tags netgo \
-o go-web-server
Explanation:
Base Image: We start with the official Golang image (version 1.21-bullseye) as our foundation.
Non-root User: Enhancing security by creating a non-root user (nonroot) to run the application.
Working Directory: Setting the working directory to /app within the container, providing a clean space for operations.
Optimized go.mod Copy: We initially copy only the go.mod file to leverage Docker caching for efficient dependency installation.
Environment Variable: Setting GIN_MODE to release optimizes the behavior of our Golang application.
Cache Mounts: Utilizing cache mounts for /go/pkg/mod and /root/.cache/go-build to speed up the installation of existing dependencies.
Application Compilation: Compiling the Golang application with specific flags to enable static linking.
Stage 2: Deployable Image
# Use a minimal scratch image as the base
FROM scratch
# Copy /etc/passwd for non-root user information
COPY --from=build /etc/passwd /etc/passwd
# Copy the compiled application binary
COPY --from=build /app/go-web-server /go-web-server
# Use the non-root user
USER nonroot
# Expose the application port
EXPOSE 8080
# Define the command to run the application
CMD ["./go-web-server"]
Explanation:
Minimal Scratch Image: For the final deployable image, we switch to a minimal scratch image, providing a clean slate.
User Information: Copying /etc/passwd from the build stage provides user information for the non-root user, enhancing security.
Application Binary: We copy the compiled application binary from the build stage (/app/go-web-server) to the final image.
Non-root User: Switching to the non-root user for enhanced container security.
Port Exposure: Exposing port 8080 ensures external access to our Golang application.
Command Definition: Specifying the command to run the application when the container starts.
Building and Running the Docker Image:
Build the Docker image:
docker build -t panchanandevops:v1.0.0 .
Run the container in detach mode:
docker run -d --name my-go-server -p 8080:8080 panchanandevops:v1.0.0
Conclusion:
This deep dive into multistage Dockerfiles equips you with the knowledge to optimize Golang application images for efficiency and security. By leveraging cache mounts, minimal base images, and non-root users, you enhance your deployment pipeline.
As you incorporate these concepts into your Docker workflows, experiment with different optimizations, and explore advanced build techniques. May your containers be streamlined, secure, and effortlessly scalable. Happy containerizing! 🎉😊
Top comments (2)
Excellent article, exactly what I need to remember how to create a multi-stage dockerfile for golang. Much thanks!
You're welcome! I'm glad the article was helpful. If you have any more questions or need further clarification on creating multi-stage Dockerfiles for Go, feel free to ask. Happy coding! 🚀