~sergio
ARTIGOSImaxes docker mínimas
05-09-2023
Cando se comezan a usar contenedores un sorpréndese do tamaño que teñen. Aínda que solo se engada a un contenedor unha simple aplicación «hello world» este acaba tendo varios centos de megas. Débese a que se usa como base unha imaxen dun sistema operativo completo, como pode ser Debian ou Ubuntu. A primeira solución que se ven á cabeza é usar unha base o máis pequena posible como pode ser Alpine, pero hai outra solución para cando a aplicación que queremos publicar se pode compilar.
A «maxia» está no compilado estático, nas «multi-stage builds» e na imaxen «scratch».
O compilado estático xenera executables que funcionan sin dependencias, como un artefacto completo. As «multi-stage builds» permiten crear imaxes usadas para compilar o binario e logo copialo a outra imaxen de menor tamaño. Por último, a imaxen «scratch» é a máis pequena, a usada como base inicial para crear contenedores.
Aplicación de exemplo
A modo de demostración, vamos a usar a seguinte aplicación:
package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", HelloServer) http.ListenAndServe(":8080", nil) } func HelloServer(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:]) }
Sin usar docker para o seguinte, podemos comprobar que se pode compilar estáticamente a aplicación e comprobalo con «ldd»:
go build -ldflags "-linkmode external -extldflags -static" -a main.go
$ ldd main not a dynamic executable
Dockerfile
Este é un dockerfile multi-stage, tendo unha primeira fase para facer o compilado estático e unha segunda fase na que se crea a imaxen mínima basada na «scratch» e copiando o binario nela.
# Compilado FROM golang:1.21.0 as builder WORKDIR / COPY . . RUN go build -ldflags "-linkmode external -extldflags -static" -a main.go # Execución FROM scratch COPY --from=builder /main /main EXPOSE 8080 CMD ["./main"]
Nótese o «as builder» para poñerlle un alias a esta fase e logo poder facer o «--from=builder» á hora de copiar o binario.
Usando este dockerfile conseguiremos unha imaxen moi, moi reducida en tamaño, de apenas 8MB.
docker build -t exemplo:0.0.1 .
$ docker images | grep exemplo exemplo 0.0.1 b9fc98ba8b2d 9 minutes ago 7.72MB
Contenedor e contido
Pola curiosidade, podemos ver qué é o que contén ese contenedor. Para elo, primeiro executamos a imaxen creando así un contenedor basado nela:
docker run -p 8080:8080 --name exemplo exemplo:0.0.1E ver que funciona:
$ curl http://localhost:8080/manolo Hello, manolo!
Así, pódese extraer o contido do contenedor:
docker export exemplo > /tmp/exemplo.tar
E verase que é moi sinxelo tendo apenas o binario e un par de cousiñas máis:
$ mkdir /tmp/foo && cd /tmp/foo && tar xvf ../exemplo.tar $ tree . . ├── dev │ ├── console │ ├── pts │ └── shm ├── etc │ ├── hostname │ ├── hosts │ ├── mtab -> /proc/mounts │ └── resolv.conf ├── main ├── proc └── sys
Queda, desta maneira, visto como crear imaxes docker mínimas. Non sempre vai ser posible, depende de que a aplicación se poida compilar estáticamente e hai veces que as dependencias non se deixan; para estes casos, sempre nos quedará basarnos en unha imaxen pequena como pode ser Alpine, instalándolle as dependencias necesarias, e aínda así quedará unha imaxen docker máis pequena que unha basada, por exemplo, en Ubuntu.