Un Dockerfile es un programa declarativo que transforma una imagen base y un conjunto de archivos de entrada en una nueva imagen reproducible. No describe un servidor que se configura para siempre: describe un build que debe poder repetirse y producir un artefacto destinado a ser reemplazado.
Sin Dockerfile, una imagen puede construirse mediante comandos ejecutados manualmente dentro de un contenedor y guardados con docker commit. Ese resultado presenta varios problemas:
no explica cómo se produjo;
es difícil de revisar en Git;
puede contener cambios accidentales;
no se puede reconstruir de forma confiable;
oculta versiones y decisiones de seguridad;
favorece servidores e imágenes mutables.
El Dockerfile convierte la construcción en código revisable:
Texto
cambio en source o dependencias
→ modificar archivos versionados
→ ejecutar build
→ probar resultado
→ publicar nueva image
La fuente de verdad deja de ser un contenedor modificado y pasa a ser el conjunto de inputs del build.
Fijar el digest evita que la base cambie silenciosamente, pero también significa que las correcciones no llegan hasta actualizarlo y reconstruir. Una política madura combina actualización automatizada, pruebas y revisión del digest.
Define el directorio para instrucciones posteriores y el command de runtime. Si no existe, Docker lo crea.
Es preferible a repetir:
docker
RUN mkdir -p /app
RUN cd /app && ...
Cada RUN usa un shell nuevo; un cd de una instrucción no cambia automáticamente el directorio de la siguiente. WORKDIR sí modifica la metadata de la etapa.
Utiliza rutas absolutas para evitar que una base image con WORKDIR inesperado cambie el resultado.
Para copias normales, prefiere COPY porque expresa una intención más limitada.
ADD tiene funciones adicionales, como extracción automática de ciertos archivos tar locales y soporte de fuentes específicas. Esas capacidades pueden ser útiles, pero también menos evidentes. No uses ADD como sinónimo estilístico de COPY.
Instalar un paquete con RUN lo incorpora a la imagen. Ejecutar un daemon con RUN service nginx start solo lo inicia temporalmente dentro del paso de build; no quedará ejecutándose en futuros contenedores.
Con package managers que actualizan índices, combina actualización, instalación y limpieza cuando pertenecen a una misma transacción. Separarlas puede utilizar una cache con índices antiguos o conservar archivos innecesarios en capas previas.
Cada descarga añade una dependencia de disponibilidad y mutabilidad. Fija versiones, valida checksums cuando corresponda y evita descargar “la última release” sin identidad.
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-slim
ARG define parámetros disponibles durante build. Su alcance depende de dónde se declare y de las etapas.
Diferencias centrales:
Aspecto
ARG
ENV
Disponible durante build
Sí, según alcance
Sí desde su declaración
Disponible por defecto en runtime
No
Sí
Apropiado para secretos
No
No
Puede afectar cache
Sí
Sí
Aunque ARG no quede como variable de runtime, puede aparecer en metadata, logs o contenido producido por el comando. Usa secret mounts para credenciales.
Las instalaciones de sistema suelen necesitar root. Después prepara ownership y cambia al usuario final:
docker
FROM node:22-slim
WORKDIR /app
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
Este ejemplo tiene un matiz: RUN npm ci ocurre antes de USER node, por lo que los archivos pueden quedar como root. Puede ser correcto si solo necesitan lectura, pero debes verificar permisos. Otra opción es cambiar de usuario antes o usar un stage preparado específicamente.
UID y GID explícitos pueden facilitar mounts, aunque deben coordinarse con la plataforma.
Declara un punto de mount esperado y puede causar la creación de un volume anónimo en runtime. Su uso dentro de imágenes de aplicación requiere cuidado:
el consumidor pierde parte del control del lifecycle;
pueden acumularse anonymous volumes;
el contenido posterior en esa ruta tiene comportamientos que deben entenderse;
Compose o el runtime suelen ser un lugar más explícito para decidir persistencia.
No declares VOLUME pensando que eso crea un backup o almacenamiento compartido.
Añade metadata útil para ownership, source, licencia y trazabilidad. No confíes en labels como almacenamiento de secretos o como prueba criptográfica del origen; cualquiera que construya puede escribirlas.
Permite expansión, pipes y operadores, pero introduce el shell. En CMD, puede convertir al shell en PID 1 e interferir con señales.
Para RUN, la shell form es normal cuando necesitas encadenar operaciones. Usa opciones del shell o scripts robustos cuando un pipe deba propagar fallos.
Puede depender de un archivo o descarga conservada accidentalmente. Prueba builds sin cache de forma controlada en CI, pero no uses --no-cache como solución permanente.