LinuxParty

Inicio desactivadoInicio desactivadoInicio desactivadoInicio desactivadoInicio desactivado
 

Cómo crear una imagen Docker personalizada con Dockerfile paso a paso

Aprende a construir, configurar y ejecutar tus propias imágenes Docker mediante un archivo Dockerfile, utilizando como ejemplo un servidor web Nginx personalizado

Docker permite ejecutar aplicaciones dentro de contenedores aislados, ligeros y reproducibles. Aunque podemos descargar imágenes preparadas desde Docker Hub, tarde o temprano necesitaremos crear una imagen adaptada a nuestro proyecto.

Para automatizar este proceso se utiliza un archivo llamado Dockerfile. En él indicamos la imagen de partida, los paquetes necesarios, los archivos que deben copiarse, las variables de entorno, los puertos utilizados y el comando que se ejecutará al iniciar el contenedor.

Gracias a este archivo podemos reconstruir la misma imagen tantas veces como necesitemos, utilizarla en diferentes servidores y mantener su configuración dentro de un repositorio Git.

¿Qué es un Dockerfile?

Un Dockerfile es un documento de texto que contiene una serie de instrucciones ordenadas. Docker interpreta estas instrucciones para construir una imagen.

Según la documentación oficial de Docker, cada instrucción genera o configura una parte de la imagen. El resultado final es una plantilla inmutable desde la que podemos iniciar uno o varios contenedores.

Conviene distinguir estos tres conceptos:

  • El Dockerfile contiene las instrucciones de construcción.
  • La imagen contiene el sistema de archivos y la configuración resultantes.
  • El contenedor es una instancia en ejecución de esa imagen.

Podemos construir una sola imagen y utilizarla para iniciar numerosos contenedores independientes.

Instrucciones principales de un Dockerfile

Las instrucciones se escriben normalmente en mayúsculas para distinguirlas fácilmente de sus argumentos.

Las más utilizadas son:

  • FROM: selecciona la imagen base.
  • RUN: ejecuta órdenes durante la construcción.
  • COPY: copia archivos desde el contexto de construcción.
  • ADD: añade archivos y admite algunas funciones adicionales.
  • WORKDIR: establece el directorio de trabajo.
  • ENV: define variables de entorno permanentes.
  • ARG: declara variables disponibles durante la construcción.
  • EXPOSE: documenta el puerto utilizado por la aplicación.
  • USER: selecciona el usuario que ejecutará las órdenes posteriores.
  • CMD: establece la orden predeterminada del contenedor.
  • ENTRYPOINT: define el ejecutable principal.
  • LABEL: añade metadatos a la imagen.
  • HEALTHCHECK: comprueba si la aplicación continúa funcionando.

Un Dockerfile debe comenzar normalmente con FROM, aunque antes pueden aparecer directivas del analizador, comentarios o determinados argumentos globales.

Crear el directorio del proyecto

En este ejemplo construiremos una imagen que sirva una página web mediante Nginx.

Creamos un directorio de trabajo:

mkdir -p ~/docker-linuxparty
cd ~/docker-linuxparty

Dentro guardaremos todos los archivos que Docker podrá utilizar durante la construcción.

Esta carpeta será nuestro contexto de construcción.

Crear una página web de ejemplo

Creamos el archivo index.html:

nano index.html

Introducimos el siguiente contenido:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mi primera imagen Docker</title>
</head>
<body>
    <h1>Servidor web creado con Dockerfile</h1>
    <p>Esta página se sirve desde un contenedor Docker personalizado.</p>
</body>
</html>

Guardamos mediante Ctrl+O, pulsamos Enter y salimos con Ctrl+X.

Crear el Dockerfile

Ahora creamos un archivo llamado exactamente Dockerfile, sin extensión:

nano Dockerfile

Añadimos:

# syntax=docker/dockerfile:1

FROM nginx:alpine

LABEL org.opencontainers.image.title="LinuxParty Web"
LABEL org.opencontainers.image.description="Servidor web de ejemplo"
LABEL org.opencontainers.image.vendor="LinuxParty"

COPY index.html /usr/share/nginx/html/index.html

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget -q --spider http://127.0.0.1/ || exit 1

No es necesario añadir CMD porque la imagen oficial de Nginx ya incluye la orden necesaria para iniciar el servidor en primer plano.

Qué hace cada instrucción

La primera línea indica la versión de la sintaxis que debe utilizar Docker:

# syntax=docker/dockerfile:1

Docker recomienda esta directiva para poder utilizar las características estables actuales del formato.

La siguiente instrucción selecciona la imagen base:

FROM nginx:alpine

En lugar de instalar manualmente Nginx sobre una distribución completa, utilizamos la imagen oficial preparada para ejecutar el servidor web. La variante Alpine ocupa poco espacio.

En proyectos de producción es recomendable fijar una versión concreta o incluso un resumen criptográfico. Utilizar una etiqueta genérica puede provocar que una reconstrucción futura emplee una base diferente.

Las instrucciones LABEL añaden información descriptiva:

LABEL org.opencontainers.image.title="LinuxParty Web"

La antigua instrucción MAINTAINER está obsoleta. Actualmente se recomienda utilizar etiquetas.

La orden:

COPY index.html /usr/share/nginx/html/index.html

copia nuestra página al directorio utilizado por Nginx.

Finalmente:

EXPOSE 80

documenta que la aplicación escucha en el puerto 80. EXPOSE no publica el puerto automáticamente ni abre el cortafuegos del servidor. Para acceder desde el exterior tendremos que utilizar docker run -p.

El bloque HEALTHCHECK solicita periódicamente la página principal. Si Nginx deja de responder, Docker podrá marcar el contenedor como no saludable.

Crear un archivo .dockerignore

Docker envía el contexto de construcción al constructor. Si la carpeta contiene archivos innecesarios, copias de seguridad, credenciales o directorios Git, el proceso puede resultar más lento y menos seguro.

Creamos el archivo:

nano .dockerignore

Añadimos:

.git
.gitignore
*.log
*.tmp
*.bak
.env
Dockerfile*
README.md

No siempre tendremos que excluir el propio Dockerfile, pero Docker puede seguir utilizándolo durante la construcción aunque aparezca en .dockerignore. La finalidad es impedir que termine dentro de la imagen mediante una orden amplia como COPY . ..

Nunca debemos guardar contraseñas, claves privadas ni credenciales dentro de una imagen.

Construir la imagen Docker

Desde el directorio que contiene el Dockerfile ejecutamos:

docker build -t linuxparty-web:1.0 .

La opción -t asigna un nombre y una etiqueta:

linuxparty-web:1.0

El punto final es muy importante:

.

Indica que el directorio actual será el contexto de construcción. Docker buscará allí el Dockerfile y los archivos utilizados por COPY.

Durante el proceso veremos las diferentes etapas:

[1/2] FROM docker.io/library/nginx:alpine
[2/2] COPY index.html /usr/share/nginx/html/index.html

Si todo termina correctamente, la nueva imagen quedará almacenada localmente.

Mostrar las imágenes disponibles

Podemos comprobarlo mediante:

docker image ls

También podemos buscar solamente nuestra imagen:

docker image ls linuxparty-web

Obtendremos una salida parecida a:

REPOSITORY        TAG       IMAGE ID       CREATED          SIZE
linuxparty-web    1.0       8f26d63c819a   20 seconds ago   52MB

El identificador y el tamaño exacto pueden variar.

Ejecutar el contenedor

Para iniciar el servidor web:

docker run \
    --name linuxparty-web \
    -d \
    --restart unless-stopped \
    -p 127.0.0.1:8080:80 \
    linuxparty-web:1.0

Las opciones utilizadas significan:

  • --name linuxparty-web: asigna un nombre al contenedor.
  • -d: lo ejecuta en segundo plano.
  • --restart unless-stopped: lo reinicia automáticamente salvo que lo detengamos manualmente.
  • -p 127.0.0.1:8080:80: publica el puerto 80 del contenedor como puerto 8080 local.
  • linuxparty-web:1.0: selecciona la imagen.

Podemos probar el funcionamiento mediante:

curl http://127.0.0.1:8080/

También podemos abrir en el navegador:

http://127.0.0.1:8080/

Al vincular el puerto a 127.0.0.1, el servicio solamente estará disponible desde el propio servidor. Esta configuración resulta adecuada si colocaremos Apache, Nginx, Caddy o un proxy inverso delante del contenedor.

Permitir el acceso desde otros equipos

Si queremos que el puerto escuche en todas las interfaces:

docker run \
    --name linuxparty-web \
    -d \
    --restart unless-stopped \
    -p 8080:80 \
    linuxparty-web:1.0

La página estará disponible mediante:

http://IP_DEL_SERVIDOR:8080/

Antes de exponer un servicio debemos revisar el cortafuegos, las reglas de acceso y la seguridad de la aplicación.

Publicar un puerto con Docker puede modificar reglas de red del sistema. No debemos asumir que un bloqueo genérico del cortafuegos impedirá siempre el acceso sin comprobar la configuración concreta de Docker.

Comprobar el estado del contenedor

Para mostrar los contenedores en ejecución:

docker ps

Para incluir también los detenidos:

docker ps -a

Después de unos segundos, la columna STATUS debería indicar que el contenedor está saludable:

Up 35 seconds (healthy)

Podemos consultar el resultado detallado de las comprobaciones:

docker inspect \
    --format='{{json .State.Health}}' \
    linuxparty-web

Consultar los registros

Para mostrar los mensajes generados por Nginx:

docker logs linuxparty-web

Para seguirlos en tiempo real:

docker logs -f linuxparty-web

Podemos limitar la salida a las últimas líneas:

docker logs --tail 50 linuxparty-web

Y añadir marcas de tiempo:

docker logs --timestamps --tail 50 linuxparty-web

Ejecutar una orden dentro del contenedor

Podemos abrir una shell dentro del contenedor:

docker exec -it linuxparty-web /bin/sh

La imagen Alpine utiliza normalmente /bin/sh, no Bash.

Una vez dentro podemos comprobar los archivos:

ls -la /usr/share/nginx/html/

Para salir:

exit

Los cambios realizados manualmente dentro de un contenedor no deben considerarse permanentes. Si eliminamos el contenedor y creamos otro desde la misma imagen, esos cambios desaparecerán.

Cualquier modificación que deba conservarse tendrá que incorporarse al Dockerfile, guardarse en un volumen o almacenarse fuera del contenedor.

Detener y eliminar el contenedor

Para detenerlo:

docker stop linuxparty-web

Para volver a iniciarlo:

docker start linuxparty-web

Para eliminarlo después de detenerlo:

docker rm linuxparty-web

También podemos detenerlo y eliminarlo mediante una sola orden:

docker rm -f linuxparty-web

Esta última orden fuerza la eliminación, por lo que debemos asegurarnos de que no contiene datos necesarios que no estén guardados en un volumen.

La imagen seguirá disponible aunque eliminemos el contenedor.

Para eliminar la imagen:

docker image rm linuxparty-web:1.0

Modificar y reconstruir la imagen

Si cambiamos index.html, tendremos que reconstruir la imagen:

docker build -t linuxparty-web:1.1 .

Después eliminamos el contenedor anterior:

docker rm -f linuxparty-web

Y creamos uno nuevo:

docker run \
    --name linuxparty-web \
    -d \
    --restart unless-stopped \
    -p 127.0.0.1:8080:80 \
    linuxparty-web:1.1

Utilizar etiquetas como 1.0, 1.1 o 2.0 nos permite identificar cada versión y regresar a una imagen anterior si la nueva presenta problemas.

No conviene depender exclusivamente de latest, porque esa etiqueta no explica qué versión contiene realmente la imagen.

Diferencia entre RUN, CMD y ENTRYPOINT

Estas tres instrucciones suelen provocar confusión.

RUN

Se ejecuta mientras se construye la imagen:

RUN apk add --no-cache curl

El resultado queda guardado en una capa de la imagen. No se vuelve a ejecutar cada vez que iniciamos un contenedor.

CMD

Establece la orden o los argumentos predeterminados al iniciar el contenedor:

CMD ["nginx", "-g", "daemon off;"]

El usuario puede reemplazar fácilmente este comando desde docker run.

ENTRYPOINT

Define el ejecutable principal de la imagen:

ENTRYPOINT ["/usr/local/bin/mi-programa"]

Los argumentos escritos al final de docker run normalmente se añaden al ENTRYPOINT.

En muchas imágenes se combinan:

ENTRYPOINT ["python3", "aplicacion.py"]
CMD ["--puerto", "8000"]

En este caso, ENTRYPOINT establece la aplicación y CMD proporciona argumentos predeterminados.

La forma JSON, también llamada forma ejecutable, suele ser preferible:

CMD ["nginx", "-g", "daemon off;"]

Permite que el proceso reciba correctamente señales como SIGTERM cuando detenemos el contenedor.

Diferencia entre COPY y ADD

Para copiar archivos locales deberíamos utilizar normalmente COPY:

COPY aplicacion.php /var/www/html/

ADD incorpora comportamientos adicionales, como la extracción automática de determinados archivos comprimidos y la incorporación de recursos remotos.

Si no necesitamos esas funciones, COPY expresa mejor nuestra intención y evita resultados inesperados.

Además, descargar archivos durante la construcción suele ser más transparente mediante RUN curl o RUN wget, verificando posteriormente su suma criptográfica.

Instalar paquetes correctamente

Si utilizamos una imagen basada en Debian o Ubuntu, debemos ejecutar apt-get update y la instalación dentro de la misma instrucción:

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        curl \
        ca-certificates \
    && rm -rf /var/lib/apt/lists/*

No es recomendable separarlo así:

RUN apt-get update
RUN apt-get install -y curl

La caché de construcción podría reutilizar una lista de paquetes antigua.

La eliminación de /var/lib/apt/lists/* reduce el tamaño de la imagen final.

En Alpine podemos utilizar:

RUN apk add --no-cache curl

Utilizar un usuario sin privilegios

Siempre que la aplicación lo permita, no debería ejecutarse como root dentro del contenedor.

Un ejemplo genérico sería:

RUN addgroup --system aplicacion \
    && adduser --system --ingroup aplicacion aplicacion

WORKDIR /app

COPY --chown=aplicacion:aplicacion . /app

USER aplicacion

Después de USER, las instrucciones siguientes y el proceso principal se ejecutarán con esa cuenta.

Cambiar el usuario no elimina todos los riesgos, pero reduce las consecuencias de una vulnerabilidad dentro de la aplicación.

Utilizar construcciones multietapa

Las construcciones multietapa permiten compilar una aplicación en una imagen que contiene herramientas de desarrollo y copiar solamente el resultado a una imagen final más pequeña.

Un ejemplo simplificado sería:

# syntax=docker/dockerfile:1

FROM node:alpine AS compilador

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine

COPY --from=compilador /app/dist/ /usr/share/nginx/html/

EXPOSE 80

La imagen final no contiene Node.js, npm, el código fuente ni las dependencias utilizadas durante la compilación. Solamente incluye Nginx y los archivos generados.

Esto reduce el tamaño y la superficie de ataque.

Buenas prácticas al crear imágenes Docker

Para obtener imágenes más seguras, pequeñas y reproducibles conviene seguir estas recomendaciones:

  • Utilizar imágenes base oficiales o procedentes de proveedores fiables.
  • Seleccionar una versión concreta de la imagen base.
  • Mantener actualizado Docker y el sistema anfitrión.
  • Crear un archivo .dockerignore.
  • No copiar archivos innecesarios mediante COPY . ..
  • No guardar contraseñas o claves dentro del Dockerfile.
  • Utilizar secretos de BuildKit cuando sean necesarios durante la construcción.
  • Ejecutar la aplicación con un usuario sin privilegios.
  • Instalar solamente los paquetes imprescindibles.
  • Eliminar cachés y archivos temporales en la misma instrucción.
  • Utilizar construcciones multietapa.
  • Añadir etiquetas y versiones comprensibles.
  • Reconstruir periódicamente las imágenes para incorporar actualizaciones.
  • Analizar las imágenes en busca de vulnerabilidades.
  • Mantener los datos persistentes fuera de la capa escribible del contenedor.

Podemos solicitar una reconstrucción actualizada de la imagen base mediante:

docker build --pull -t linuxparty-web:1.1 .

Para construirla ignorando completamente la caché:

docker build --no-cache --pull \
    -t linuxparty-web:1.1 .

No es necesario desactivar la caché en cada construcción. La caché acelera considerablemente el proceso y resulta segura cuando comprendemos cómo se invalidan sus capas.

Una configuración reproducible y fácil de desplegar

Un Dockerfile convierte la preparación manual de una aplicación en un proceso automatizado.

En lugar de iniciar un contenedor, instalar paquetes manualmente y modificar sus archivos desde dentro, declaramos el estado deseado en un documento que puede revisarse, versionarse y reconstruirse.

El flujo básico se resume en cuatro órdenes:

mkdir mi-proyecto
cd mi-proyecto
nano Dockerfile
docker build -t mi-imagen:1.0 .

Después podemos iniciar un contenedor:

docker run --name mi-contenedor -d -p 8080:80 mi-imagen:1.0

Este sistema facilita el desarrollo, las pruebas, la automatización y el despliegue en servidores Linux. Una vez comprendidas instrucciones como FROM, RUN, COPY, WORKDIR, USER, EXPOSE y CMD, podemos empaquetar prácticamente cualquier aplicación dentro de una imagen Docker personalizada.

Fuentes

No estás registrado para postear comentarios



Redes:



   

 

Suscribete / Newsletter

Suscribete a nuestras Newsletter y periódicamente recibirás un resumen de las noticias publicadas.

Donar a LinuxParty

 

Dona a LInuxParty

Donar 10

Donar 20

 

 

Tutorial de Linux

Formulario de acceso