LinuxParty
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
- Documentación oficial: introducción a Dockerfile
- Referencia oficial de instrucciones Dockerfile
- Buenas prácticas para construir imágenes Docker



