Home  >  Article  >  Backend Development  >  Go + Docker: How to create the best Docker images for Golang applications

Go + Docker: How to create the best Docker images for Golang applications

WBOY
WBOYOriginal
2024-08-14 22:49:02430browse

Who has never had doubts about whether they were creating the correct Docker image for their application? Well, I've had this question several times and I almost always didn't know if I was doing it right or wrong.

So in this article, we will explore advanced practices for creating efficient and optimized Docker images for your Go applications. We will compare different approaches, such as using the alpine and scratch base images, and discuss the benefits of each, with code examples and performance analyses.

Project structure

First, let's establish a typical project structure for a containerized Go application.

As an example, I'm using this application which is a URL shortener:

url_shortener
├── cmd
│   └── main.go
├── internal
│   ├── monitoring
│   │   └── prometheus.go
│   ├── server
│   │   └── server.go
│   └── shortener
│       ├── model.go
│       ├── repository.go
│       ├── service.go
│       └── service_test.go
├── pkg
│   ├── api
│   │   └── shortener
│   │       ├── handler.go
│   │       └── handler_integration_test.go
│   └── utils
│       └── base62
│           ├── hash.go
│           └── hash_test.go
├── Dockerfile
├── Dockerfile.alpine
├── Dockerfile.golang
├── README.md
├── compose.yml
├── go.mod
├── go.sum
└── prometheus.yml

1. Writing the Dockerfile

What few people know is that we don't need a "complete" image running in production. For example, Ubuntu with all packages, sources, extensions and an SDK for our language. We can simply build our app within a system with an SDK and then copy the build to a smaller, optimized image that will only run that build. And that’s where Multi-Stage comes in.

Multi-Stage Builds

In the Dockerfile, you can define multiple build stages, each starting with a FROM statement. The first stage can be used to compile the code, install dependencies, run tests, etc. In subsequent stages, you can copy only the necessary artifacts (such as compiled binaries) to the final image, discarding everything that is not needed to run the application.

# syntax=docker/dockerfile:1

# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o url-shortener ./cmd

# Final stage
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/url-shortener .
CMD ["./url-shortener"]

Damn Rafa, but what are you doing with "these two images"? First I use an image as a builder, which is where we create the application executable:

First Stage (Build stage):

  • Uses golang:1.22-alpine as base image.
  • Copies the source code to the container.
  • Download the dependencies and compile the Go application binary.

Right then, I use a smaller image that will just run this executable that we generated in the first step:

Second Stage (Final Stage):

  • Uses alpine:latest as base image, which is much smaller and only needs to have the necessary tools to run the binary.
  • Copies the compiled binary from the previous stage (builder) to the new image.
  • Defines the application execution command.

If we want to confirm what really happens, within Docker Desktop there is the possibility of analyzing the hierarchy of an image. Inside we can see what he uses:

Go + Docker: Como criar as melhores imagens Docker para aplicações Golang

Now we can also analyze the size of this image we just generated, in this case the url-shortener:alpine which was ~30mb:

$ docker images
REPOSITORY            TAG         IMAGE ID       CREATED              SIZE
url-shortener                 alpine          aa99d6a2c028   3 minutes ago    29.9MB

But what is Alpine?

Alpine is a minimalist, secure and lightweight Linux distribution, widely used in container environments due to its efficiency and simplicity. It provides a solid foundation for building scalable applications without the overhead of other heavier Linux distributions.

Some of the advantages of using Alpine in our app are mainly linked to these 3 pillars:

  • Flexibility: The alpine image (~5MB) is small and includes an apk package manager, allowing you to install additional required dependencies.
  • Compatibility: Support for dynamic libraries, making it suitable for applications that depend on external libraries.
  • Security: Regularly updated, alpine includes security patches, reducing the risk of vulnerabilities.

Okay, but what if I use the same SDK version, in this case golang:1.22-alpine. How big is my application?

REPOSITORY                    TAG             IMAGE ID       CREATED         SIZE
url-shortener                 golang-alpine   d615d75c3aff   25 minutes ago   251MB

Well, in this case we ended up with an image with ~250mb... While compared to the alpine, we purely went to ~30mb, it's already a big difference. And can it be improved even further?

The answer is YES, and let’s get into the details

2. Advanced Optimizations

2.1 Scratch

Scratch is a special and very minimalist image in Docker. It's actually the simplest and most empty possible base image you can use. It contains absolutely nothing: no operating system, no libraries, no tools — it's literally an empty container.

Essa abordagem minimalista traz benefícios significativos, especialmente em termos de segurança. Ao usar Scratch, você minimiza drasticamente a superfície de ataque, já que não há pacotes ou ferramentas adicionais que possam introduzir vulnerabilidades. Seu contêiner contém apenas o essencial para a execução do aplicativo, garantindo um ambiente imutável e previsível em qualquer situação.

# syntax=docker/dockerfile:1

# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o url-shortener ./cmd

# Final stage with Scratch
FROM scratch
WORKDIR /app
COPY --from=builder /app/url-shortener .
CMD ["./url-shortener"]

E o resultado após criar as 3 imagens, nosso ranking de menor imagem ficou assim:

  1. url-shortener:scratch - 21.1MB
  2. url-shortener:alpine - 29.9MB
  3. url-shortener:golang-alpine - 251MB

O Scratch conseguiu baixar mais alguns megas no tamanho final de nossa aplicação. Isso impacta no tamanho de arquivos transferidos pela rede, hoje algumas empresas cobram pela banda que trafegamos dentro dos servidores, e também pode influenciar em nosso Horizontal Scaling da aplicação.

Deixei os 3 Dockerfiles dentro do repositório do github caso você queira testar em seu próprio pc ?

Quando devo usar o scratch

A resposta mais tranquila para essa é "quase sempre", uma dica de cara é: ele vai muito bem com linguagens como Go, Rust, ou C/C++. Mas qui estão alguns pontos para levar em consideração na hora de escolher se deve ou não usar o scratch:

  • Aplicações Statically Linked: Se sua aplicação não depende de bibliotecas dinâmicas e pode ser compilada de forma estática, Scratch é uma excelente escolha.
  • Segurança: Quando a segurança é uma prioridade e você quer minimizar a quantidade de software no contêiner.
  • Eficiência: Para criar imagens Docker extremamente pequenas e eficientes.

2.2 Reduzindo o Tempo de Build (Cache)

Usar o cache do Docker para otimizar o tempo de build é uma técnica essencial para evitar recompilar ou baixar dependências desnecessariamente em cada build. O Docker armazena em cache as camadas de cada etapa do Dockerfile, reutilizando-as sempre que possível.

Em projetos Go, baixar dependências com go mod download pode ser um processo demorado, especialmente se houver muitas dependências. Se você recompilar todas as dependências em cada build, isso aumenta significativamente o tempo de build.

Como arrumar isso?

Ao copiar apenas os arquivos go.mod e go.sum em uma etapa separada antes de copiar o código-fonte completo, você permite que o Docker use o cache dessa etapa se os arquivos go.mod e go.sum não tiverem mudado. Veja como fica nosso Docker file com as mudanças:

# syntax=docker/dockerfile:1

# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -o url-shortener ./cmd

# Final stage
FROM scratch
WORKDIR /app
COPY --from=builder /app/url-shortener .
CMD ["./url-shortener"]

Só de fazer esta pequena mudança já ganhamos dois pontos bem interessantes quando se trata de desenvolvimento de software, que são:
Menor tempo de build: Se não houver alterações nos arquivos go.mod e go.sum, o Docker reutiliza o cache, evitando o download das dependências e economizando tempo.
Eficiência no CI/CD: Em pipelines de integração contínua, essa técnica reduz o tempo de execução dos pipelines, aumentando a eficiência do desenvolvimento e entrega.

Então bora usar isso a nosso favor no dia-a-dia :)

2.3 Melhorando a Segurança (Docker Scout)

Docker Scout é uma ferramenta da Docker integrada ao Docker Desktop que analisa suas imagens para identificar vulnerabilidades de segurança. Ele fornece insights sobre as dependências presentes em suas imagens e alerta sobre possíveis problemas, permitindo que você tome medidas corretivas antes de implantar suas aplicações.

Por que é importante? Manter suas imagens Docker seguras é fundamental para proteger suas aplicações contra ataques e exploração de vulnerabilidades. O Docker Scout automatiza o processo de análise, tornando mais fácil manter suas imagens seguras e atualizadas.

Como funciona?

O Scout funciona praticamente com 2 passos, ele examina a imagem Docker, mapeia todas as dependências incluídas na imagem e verifica essas dependências em uma base de dados de vulnerabilidades conhecidas. Por fim, classifica as vulnerabilidades encontradas por severidade e fornece recomendações para corrigir ou mitigar os problemas.

Practical Example: Using Docker Scout on Docker Desktop

  1. Access Docker Desktop: Open Docker Desktop and go to the "Images" tab. Here you will see a list of all the images you have locally. In this example I used the postgres:16-alpine image, as it contains vulnerabilities that we can use as an example. Go + Docker: Como criar as melhores imagens Docker para aplicações Golang
  2. Perform Vulnerability Analysis: Select an image you want to analyze. Docker Scout will automatically display known vulnerabilities in the selected image. You will see a security status icon next to each image, indicating whether there are vulnerabilities to be addressed. Go + Docker: Como criar as melhores imagens Docker para aplicações Golang
  3. View Vulnerability Details: Click the image to see a detailed dashboard of vulnerabilities found. This includes the affected package version, CVE description, and severity. You can also view the history of scans and changes, helping you track how image security has evolved over time. Go + Docker: Como criar as melhores imagens Docker para aplicações Golang
  4. Apply Fixes: Based on Docker Scout recommendations, you can decide to update packages, rebuild the image with a more secure base, or take other mitigation actions. Fixes can be applied directly to your Dockerfile or CI/CD pipeline. Go + Docker: Como criar as melhores imagens Docker para aplicações Golang

And in this way, we will have proactive prevention that will identify and correct vulnerabilities before the image is deployed into production, helps protect against possible security exploits and we also gain operational efficiency, what do I mean by that? We can automate vulnerability analysis, allowing the DevOps or security team to focus on corrective actions rather than manual investigations.

Conclusion

With the practices we explored, you now have a clear path to building Docker images for your Go applications. Using techniques like Multi-Stage Builds, which reduce image size, choosing base images like alpine or scratch to improve security and efficiency, and by using Docker Scout to monitor vulnerabilities, you can ensure that your applications are running efficiently and securely.

These practices not only improve technical performance, but also bring direct benefits to your daily life and to the company, saving time and resources.

So the next time you're building a Docker image, keep these strategies in mind. Apply them and observe the results. ?

The above is the detailed content of Go + Docker: How to create the best Docker images for Golang applications. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Previous article:How to use go mongoNext article:How to use go mongo