Construye tu imagen de Go
Resumen
En esta sección vas a construir una imagen de contenedor. La imagen incluye todo lo que necesitas para ejecutar tu aplicación: el archivo binario compilado de la aplicación, el entorno de ejecución (runtime), las bibliotecas y todos los demás recursos requeridos por tu aplicación.
Software requerido
Para completar este tutorial, necesitas lo siguiente:
- Docker ejecutándose localmente. Sigue las instrucciones para descargar e instalar Docker.
- Un IDE o un editor de texto para editar archivos. Visual Studio Code es una opción gratuita y popular, pero puedes usar cualquier otro con el que te sientas cómodo.
- Un cliente de Git. Esta guía utiliza un cliente de
gitbasado en línea de comandos, pero eres libre de usar el que prefieras. - Una aplicación de terminal de línea de comandos. Los ejemplos mostrados en este módulo son de la terminal de Linux, pero deberían funcionar en PowerShell, el Símbolo del sistema de Windows (Command Prompt) o la Terminal de OS X con cambios mínimos, si es que requieren alguno.
Conoce la aplicación de ejemplo
La aplicación de ejemplo es una caricatura de un microservicio. Es intencionadamente trivial para mantener el enfoque en aprender los conceptos básicos de la contenedorización para aplicaciones de Go.
La aplicación ofrece dos endpoints HTTP:
- Responde con una cadena que contiene un símbolo de corazón (
<3) a las solicitudes a/. - Responde con el JSON
{"Status" : "OK"}a una solicitud a/health.
Responde con un error HTTP 404 a cualquier otra solicitud.
La aplicación escucha en un puerto TCP definido por el valor de la variable de entorno PORT. El valor predeterminado es 8080.
La aplicación no tiene estado (stateless).
El código fuente completo de la aplicación está en GitHub: github.com/docker/docker-gs-ping. Te animamos a hacer un fork y experimentar con él tanto como quieras.
Para continuar, clona el repositorio de la aplicación en tu máquina local:
$ git clone https://github.com/docker/docker-gs-ping
El archivo main.go de la aplicación es bastante sencillo, si estás familiarizado con Go:
package main
import (
"net/http"
"os"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", func(c echo.Context) error {
return c.HTML(http.StatusOK, "Hello, Docker! <3")
})
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
})
httpPort := os.Getenv("PORT")
if httpPort == "" {
httpPort = "8080"
}
e.Logger.Fatal(e.Start(":" + httpPort))
}
// Implementación simple de un mínimo de enteros
// Adaptado de: https://gobyexample.com/testing-and-benchmarking
func IntMin(a, b int) int {
if a < b {
return a
}
return b
}Crea un Dockerfile para la aplicación
Para construir una imagen de contenedor con Docker, se requiere un Dockerfile con instrucciones de construcción.
Comienza tu Dockerfile con la línea de directiva del analizador (opcional) que indica a BuildKit que interprete tu archivo de acuerdo con las reglas gramaticales para la versión especificada de la sintaxis.
Luego, le indicas a Docker qué imagen base deseas utilizar para tu aplicación:
# syntax=docker/dockerfile:1
FROM golang:1.19Las imágenes de Docker se pueden heredar de otras imágenes. Por lo tanto, en lugar de crear tu propia imagen base desde cero, puedes usar la imagen oficial de Go que ya tiene todas las herramientas y bibliotecas necesarias para compilar y ejecutar una aplicación de Go.
NoteSi tienes curiosidad sobre cómo crear tus propias imágenes base, puedes consultar la siguiente sección de esta guía: creación de imágenes base. Ten en cuenta, sin embargo, que esto no es necesario para continuar con la tarea que tienes entre manos.
Ahora que has definido la imagen base para tu próxima imagen de contenedor, puedes comenzar a construir sobre ella.
Para facilitar las cosas al ejecutar el resto de tus comandos, crea un directorio dentro de la imagen que estás construyendo. Esto también le indica a Docker que use este directorio como el destino predeterminado para todos los comandos posteriores. De esta manera, no tienes que escribir rutas de archivo completas en el Dockerfile; las rutas relativas se basarán en este directorio.
WORKDIR /appNormalmente, lo primero que haces una vez que has descargado un proyecto escrito en Go es instalar los módulos necesarios para compilarlo. Ten en cuenta que la imagen base ya tiene el conjunto de herramientas (toolchain), pero tu código fuente aún no está en ella.
Por lo tanto, antes de que puedas ejecutar go mod download dentro de tu imagen, necesitas copiar tus archivos go.mod y go.sum en ella. Utiliza el comando COPY para hacer esto.
En su forma más simple, el comando COPY toma dos parámetros. El primer parámetro le dice a Docker qué archivos deseas copiar en la imagen. El último parámetro le dice a Docker dónde deseas que se copie ese archivo.
Copia los archivos go.mod y go.sum en el directorio de tu proyecto /app el cual, debido al uso de WORKDIR, es el directorio actual (./) dentro de la imagen. A diferencia de algunas terminales modernas que parecen ser indiferentes al uso de la barra diagonal final (/) y pueden deducir lo que el usuario quería decir (la mayoría de las veces), el comando COPY de Docker es bastante sensible en su interpretación de la barra diagonal final.
COPY go.mod go.sum ./NoteSi deseas familiarizarte con el tratamiento de la barra diagonal final por parte del comando
COPY, consulta la referencia de Dockerfile. Esta barra diagonal final puede causar problemas de más formas de las que te imaginas.
Ahora que tienes los archivos de módulo dentro de la imagen de Docker que estás construyendo, puedes usar el comando RUN para ejecutar también allí el comando go mod download. Esto funciona exactamente igual que si estuvieras ejecutando go localmente en tu máquina, pero esta vez estos módulos de Go se instalarán en un directorio dentro de la imagen.
RUN go mod downloadEn este punto, tienes una versión del conjunto de herramientas de Go 1.19.x y todas tus dependencias de Go instaladas dentro de la imagen.
Lo siguiente que debes hacer es copiar tu código fuente dentro de la imagen. Utilizarás el comando COPY al igual que lo hiciste antes con tus archivos de módulo.
COPY *.go ./Este comando COPY utiliza un comodín para copiar todos los archivos con la extensión .go ubicados en el directorio actual en el host (el directorio donde se encuentra el Dockerfile) al directorio actual dentro de la imagen.
Ahora, para compilar tu aplicación, utiliza el conocido comando RUN:
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-pingEsto debería resultarte familiar. El resultado de ese comando será un binario de aplicación estático llamado docker-gs-ping y ubicado en la raíz del sistema de archivos de la imagen que estás construyendo. Podrías haber colocado el binario en cualquier otro lugar que desees dentro de esa imagen; el directorio raíz no tiene ningún significado especial a este respecto. Simplemente es conveniente usarlo para mantener las rutas de los archivos cortas y mejorar la legibilidad.
Ahora, todo lo que queda por hacer es decirle a Docker qué comando ejecutar cuando se use tu imagen para iniciar un contenedor.
Esto lo haces con el comando CMD:
CMD ["/docker-gs-ping"]Aquí está el Dockerfile completo:
# syntax=docker/dockerfile:1
FROM golang:1.19
# Establece el destino para COPY
WORKDIR /app
# Descarga los módulos de Go
COPY go.mod go.sum ./
RUN go mod download
# Copia el código fuente. Ten en cuenta la barra diagonal al final, como se explica en
# https://docs-docker.esdocu.com/reference/dockerfile/#copy
COPY *.go ./
# Construcción
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping
# Opcional:
# Para vincular a un puerto TCP, se deben suministrar parámetros de ejecución al comando docker.
# Pero podemos documentar en el Dockerfile en qué puertos
# va a escuchar la aplicación por defecto.
# https://docs-docker.esdocu.com/reference/dockerfile/#expose
EXPOSE 8080
# Ejecución
CMD ["/docker-gs-ping"]El Dockerfile también puede contener comentarios. Siempre comienzan con el símbolo # y deben estar al principio de una línea. Los comentarios están ahí para tu conveniencia para permitirte documentar tu Dockerfile.
También existe el concepto de directivas de Dockerfile, como la directiva syntax que agregaste. Las directivas siempre deben estar en la parte superior del Dockerfile, por lo que al agregar comentarios, asegúrate de que sigan después de cualquier directiva que hayas utilizado:
# syntax=docker/dockerfile:1
# Un microservicio de ejemplo en Go empaquetado en una imagen de contenedor.
FROM golang:1.19
# ...Construye la imagen
Ahora que has creado tu Dockerfile, construye una imagen a partir de él. El comando docker build crea imágenes de Docker a partir del Dockerfile y un contexto. Un contexto de construcción es el conjunto de archivos ubicados en la ruta o URL especificada. El proceso de construcción de Docker puede acceder a cualquiera de los archivos ubicados en el contexto.
El comando de construcción toma opcionalmente una bandera --tag. Esta bandera se utiliza para etiquetar la imagen con un valor de cadena de texto, que es fácil de leer y reconocer por los humanos. Si no pasas una bandera --tag, Docker usará latest como el valor predeterminado.
Construye tu primera imagen de Docker.
$ docker build --tag docker-gs-ping .
El proceso de construcción imprimirá algunos mensajes de diagnóstico a medida que avanza por los pasos de construcción. El siguiente es solo un ejemplo de cómo se verían estos mensajes.
[+] Building 2.2s (15/15) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 701B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1 1.1s
=> CACHED docker-image://docker.io/docker/dockerfile:1@sha256:39b85bbfa7536a5feceb7372a0817649ecb2724562a38360f4d6a7782a409b14 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load metadata for docker.io/library/golang:1.19 0.7s
=> [1/6] FROM docker.io/library/golang:1.19@sha256:5d947843dde82ba1df5ac1b2ebb70b203d106f0423bf5183df3dc96f6bc5a705 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 6.08kB 0.0s
=> CACHED [2/6] WORKDIR /app 0.0s
=> CACHED [3/6] COPY go.mod go.sum ./ 0.0s
=> CACHED [4/6] RUN go mod download 0.0s
=> CACHED [5/6] COPY *.go ./ 0.0s
=> CACHED [6/6] RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:ede8ff889a0d9bc33f7a8da0673763c887a258eb53837dd52445cdca7b7df7e3 0.0s
=> => naming to docker.io/library/docker-gs-ping 0.0s
Tu salida exacta variará, pero siempre que no haya errores, deberías ver la palabra FINISHED en la primera línea de la salida. Esto significa que Docker ha construido con éxito tu imagen llamada docker-gs-ping.
Visualiza las imágenes locales
Para ver la lista de imágenes que tienes en tu máquina local, tienes dos opciones. Una es usar la CLI y la otra es usar Docker Desktop. Dado que actualmente estás trabajando en la terminal, echemos un vistazo a cómo listar imágenes con la CLI.
Para listar imágenes, ejecuta el comando docker image ls (o el atajo docker images):
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-gs-ping latest 7f153fbcc0a8 2 minutes ago 1.11GB
...
Tu salida exacta puede variar, pero deberías ver la imagen docker-gs-ping con la etiqueta latest. Debido a que no especificaste una etiqueta personalizada cuando construiste tu imagen, Docker asumió que la etiqueta sería latest, que es un valor especial.
Etiqueta imágenes
El nombre de una imagen se compone de componentes de nombre separados por barras diagonales. Los componentes de nombre pueden contener letras minúsculas, dígitos y separadores. Un separador se define como un punto, uno o dos guiones bajos, o uno o más guiones. Un componente de nombre no puede comenzar ni terminar con un separador.
Una imagen se compone de un manifiesto y una lista de capas. En términos simples, una etiqueta apunta a una combinación de estos artefactos. Puedes tener múltiples etiquetas para la imagen y, de hecho, la mayoría de las imágenes tienen múltiples etiquetas. Crea una segunda etiqueta para la imagen que construiste y echa un vistazo a sus capas.
Usa el comando docker image tag (o el atajo docker tag) para crear una nueva etiqueta para tu imagen. Este comando toma dos argumentos; el primer argumento es la imagen de origen y el segundo es la nueva etiqueta a crear. El siguiente comando crea una nueva etiqueta docker-gs-ping:v1.0 para la imagen docker-gs-ping:latest que construiste:
$ docker image tag docker-gs-ping:latest docker-gs-ping:v1.0
El comando tag de Docker crea una nueva etiqueta para la imagen. No crea una nueva imagen. La etiqueta apunta a la misma imagen y es simplemente otra forma de hacer referencia a la imagen.
Ahora ejecuta el comando docker image ls nuevamente para ver la lista actualizada de imágenes locales:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-gs-ping latest 7f153fbcc0a8 6 minutes ago 1.11GB
docker-gs-ping v1.0 7f153fbcc0a8 6 minutes ago 1.11GB
...
Puedes ver que tienes dos imágenes que comienzan con docker-gs-ping. Sabes que son la misma imagen porque si miras la columna IMAGE ID, puedes ver que los valores son los mismos para ambas imágenes. Este valor es un identificador único que Docker utiliza internamente para identificar la imagen.
Elimina la etiqueta que acabas de crear. Para hacer esto, usarás el comando docker image rm, o el atajo docker rmi (que significa "remove image"):
$ docker image rm docker-gs-ping:v1.0
Untagged: docker-gs-ping:v1.0
Observa que la respuesta de Docker te indica que la imagen no se ha eliminado, sino que solo se le ha quitado la etiqueta (untagged).
Verifícalo ejecutando el siguiente comando:
$ docker image ls
Verás que la etiqueta v1.0 ya no está en la lista de imágenes guardadas por tu instancia de Docker.
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-gs-ping latest 7f153fbcc0a8 7 minutes ago 1.11GB
...La etiqueta v1.0 se ha eliminado, pero aún tienes la etiqueta docker-gs-ping:latest disponible en tu máquina, por lo que la imagen sigue ahí.
Builds multi-etapa (Multi-stage builds)
Es posible que hayas notado que tu imagen docker-gs-ping pesa más de un gigabyte, lo cual es mucho para una pequeña aplicación de Go compilada. También te estarás preguntando qué pasó con todo el conjunto de herramientas de Go, incluido el compilador, después de haber construido tu imagen.
La respuesta es que el conjunto completo de herramientas (toolchain) sigue estando allí, en la imagen del contenedor. Esto no solo es inconveniente debido al gran tamaño del archivo, sino que también puede presentar un riesgo de seguridad cuando se despliega el contenedor.
Estos dos problemas se pueden resolver utilizando builds multi-etapa.
En pocas palabras, una construcción multi-etapa (multi-stage build) puede transferir los artefactos de una etapa de construcción a otra, y cada etapa de construcción se puede instanciar a partir de una imagen base diferente.
Por lo tanto, en el siguiente ejemplo, vas a utilizar una imagen oficial de Go completa para construir tu aplicación. Luego, copiarás el binario de la aplicación en otra imagen cuya base sea muy ligera y no incluya el conjunto de herramientas de Go ni otros componentes opcionales.
El archivo Dockerfile.multistage en el repositorio de la aplicación de ejemplo tiene el siguiente contenido:
# syntax=docker/dockerfile:1
# Construye la aplicación a partir del código fuente
FROM golang:1.19 AS build-stage
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping
# Ejecuta las pruebas en el contenedor
FROM build-stage AS run-test-stage
RUN go test -v ./...
# Despliega el binario de la aplicación en una imagen ligera
FROM gcr.io/distroless/base-debian11 AS build-release-stage
WORKDIR /
COPY --from=build-stage /docker-gs-ping /docker-gs-ping
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/docker-gs-ping"]Dado que ahora tienes dos Dockerfiles, debes decirle a Docker qué Dockerfile deseas usar para construir la imagen. Etiqueta la nueva imagen con multistage. Esta etiqueta (como cualquier otra, aparte de latest) no tiene un significado especial para Docker, es simplemente algo que elegiste.
$ docker build -t docker-gs-ping:multistage -f Dockerfile.multistage .
Al comparar los tamaños de docker-gs-ping:multistage y docker-gs-ping:latest, verás una diferencia de varios órdenes de magnitud.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-gs-ping multistage e3fdde09f172 About a minute ago 28.1MB
docker-gs-ping latest 336a3f164d0f About an hour ago 1.11GB
Esto se debe a que la imagen base "distroless" que has utilizado en la segunda etapa de la construcción es muy básica y está diseñada para despliegues ligeros de binarios estáticos.
Hay mucho más sobre las construcciones multi-etapa, incluyendo la posibilidad de construcciones multi-arquitectura, así que siéntete libre de consultar sobre builds multi-etapa. Sin embargo, esto no es esencial para tu progreso aquí.
Siguientes pasos
En este módulo, conociste tu aplicación de ejemplo y construiste una imagen de contenedor para ella.
En el próximo módulo, verás cómo ejecutar tu imagen como un contenedor.