Todos los recursos

Un skill de Claude Code que escribe un Dockerfile multi-stage pequeño, que aprovecha la caché y corre como usuario non-root con un healthcheck de verdad, para que un cambio de código no reinstale cada paquete y un escape de contenedor no le regale root a un atacante.

El skill Dockerfile Author: imágenes pequeñas, cacheadas y sin root por defecto

En resumen

  • El skill escribe un Dockerfile multi-stage con el orden de capas que cachea las dependencias aparte del código: editar código no reinstala todo.
  • Se dispara cuando contenerizas un servicio o cuando una imagen existente está inflada, tarda en construir o corre como root.
  • Non-root por defecto y un healthcheck de verdad son innegociables, no algo que le agregas después.
  • Acompaña el Dockerfile con un .dockerignore: el archivo que más rinde y que casi todos se olvidan.
  • No persigue una imagen scratch de 20 MB cuando una base slim es la concesión sensata; optimiza lo que de verdad te complica.

Cualquiera escribe un Dockerfile que construye. Escribir uno que reconstruye rápido, pesa poco y corre sin root ya es otra cosa. Y es siempre el mismo puñado de patrones, que es justo para lo que sirve un skill. El skill Dockerfile Author encapsula esos patrones para que dejes de re-deducir el orden de capas y de releer el mismo post de blog en cada servicio nuevo. Esta guía cubre qué produce, cuándo debe dispararse, cómo funciona por dentro, una invocación real con la salida que puedes esperar, la configuración que define y los detalles que separan una imagen limpia de un lastre de 1.4 GB que reinstala todo tu árbol de dependencias por un cambio de una línea.

El modelo mental que conviene tener: un Dockerfile es un programa de caché de build, no un script de instalación. Cada instrucción es una capa, las capas se cachean de arriba hacia abajo, y la primera línea que cambia invalida todo lo de abajo. Casi toda imagen lenta e inflada es esa única idea ignorada. El trabajo del skill es poner la caché a trabajar a tu favor y cerrar los dos huecos de seguridad, root y una superficie de ataque enorme, que de otro modo terminarías descubriendo en producción.

01 · Qué hace el skill

El skill Dockerfile Author toma una descripción de tu servicio (runtime, paso de build, comando de arranque) y produce una definición de imagen lista para deploy, no un esqueleto. En concreto, emite:

  1. Un Dockerfile multi-stage, una etapa de build pesada con el toolchain, y una etapa de runtime liviana que solo copia hacia adelante los artefactos compilados.
  2. Orden de capas correcto para la caché, el manifiesto copiado y las dependencias instaladas antes de copiar el código, así editar un handler no invalida la capa de instalación.
  3. Un runtime non-root, una directiva USER para que el proceso nunca corra como root dentro del contenedor.
  4. Un healthcheck de verdad, un HEALTHCHECK que realmente sondea la app, para que el orquestador distinga "corriendo" de "vivo".
  5. Un .dockerignore, emitido junto al Dockerfile, porque copiar node_modules, .git y tu .env al contexto de build es el desastre silencioso que pasa por defecto.

Lo que a propósito no hace es entregarte una imagen de una sola etapa que mete el compilador, las dependencias de desarrollo y tu historial de git dentro de lo que expones a la red. Ese es justo el antipatrón que el skill existe para jubilar.

Nota

Un Dockerfile es un programa de caché. Ordena las instrucciones de la que menos cambia a la que más: imagen base, luego paquetes del sistema, luego tu manifiesto de dependencias + install, y al final tu código. Con ese orden, un cambio de código reconstruye una capa barata en vez de todas.

02 · Cuándo debe dispararse

Echa mano del skill cuando se cumpla cualquiera de estas:

  • Estás contenerizando un servicio por primera vez y lo quieres bien hecho, no un "en mi máquina funciona".
  • Una imagen existente está inflada, cientos de megas que no logras explicar, o tarda en construir, reinstalando dependencias en cada edición.
  • Una imagen corre como root y la quieres asegurada antes de que salga a deploy.
  • Estás moviendo un servicio a un host con pocos recursos, como mi VPS único detrás de Traefik, donde el tamaño de la imagen es disco que no tienes y el tiempo de build es downtime.

No lo dispares cuando:

  • En realidad no necesitas un contenedor. Un sitio estático en un CDN o una función en una plataforma no te exigen escribir un Dockerfile.
  • El problema real es la app, no la imagen. Si el servicio va lento en runtime, un Dockerfile más afinado no lo arregla: perfila la app.
  • Persigues el número más pequeño posible por puro deporte. Reducir una imagen slim hasta scratch con una libc armada a mano rara vez compensa el costo de depurar; el skill optimiza lo que te complica, no métricas de vanidad.

Una prueba útil: si te encuentras copiando y pegando un Dockerfile de otro repo y editando la línea FROM, ese es el momento del skill: aplica los patrones que estabas por recordar a medias.

Consejo

Dile al skill el runtime y el comando de arranque, no "hazme un Dockerfile". "Node 20, build con npm run build, arranca con node dist/server.js, escucha en 3000" le da todo lo que necesita para elegir etapas, puertos y el target del healthcheck. Una entrada vaga produce una imagen genérica que vas a terminar reescribiendo.

03 · Cómo funciona por dentro

El skill razona tu build en dos pasadas. Primero divide el trabajo en etapas: qué pasos necesitan el toolchain completo (compiladores, dependencias de desarrollo, el build mismo) frente a cuáles hacen falta en runtime (el intérprete más la salida compilada). El toolchain vive en una etapa de build que nunca se despliega; la etapa de runtime solo copia los artefactos hacia adelante.

Segundo, ordena las instrucciones del runtime por frecuencia de cambio para que la caché trabaje al máximo. Esta es la forma canónica que produce para un servicio TypeScript/Node:

# ---- etapa de build: tiene el toolchain, nunca se despliega ----
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- etapa de runtime: liviana, non-root, con healthcheck ----
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD node -e "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "dist/server.js"]

Cuatro decisiones ahí cargan con el peso:

  • Manifiesto antes que código. Copiar package.json* y correr npm ci antes de COPY . . hace que una edición de código reutilice la capa de instalación cacheada. Inviertes esos dos y cada cambio de código reinstala cada paquete: la razón número uno de que los builds se sientan lentos.
  • Solo dependencias de prod en la etapa de runtime. La etapa de build instala todo para compilar; la de runtime corre npm ci --omit=dev para que la imagen desplegada no cargue compiladores, ni frameworks de test, ni herramientas de desarrollo.
  • USER node antes del CMD. La imagen oficial de Node trae un usuario non-root node; cambiar a él hace que el proceso, y cualquier cosa que lo comprometa, corra sin root dentro del contenedor.
  • Un healthcheck que pega contra la app. Sondear /health por HTTP le dice al orquestador que el proceso está sirviendo, no solo presente. "El PID existe" no es lo mismo que "los requests se responden".

El .dockerignore es la mitad de la ganancia

El contexto de build es todo lo que Docker sube antes de que corra la primera instrucción. Sin un .dockerignore, eso incluye tu node_modules, tu historial de .git, los artefactos de build y (el peligroso) cualquier archivo .env que esté en el directorio. El skill emite uno cada vez:

node_modules
.git
.gitignore
dist
*.log
.env
.env.*
Dockerfile
.dockerignore

Esto achica la subida, acelera el build y mantiene secretos e historial fuera de capas donde un COPY . . descuidado los dejaría grabados.

Atención

Nunca dejes que un secreto entre a una capa. Un token pasado por ARG o copiado desde un .env queda recuperable en el historial de la imagen para siempre, aunque una capa posterior borre el archivo. Pasa los secretos en runtime por variables de entorno que inyecte el host (la misma disciplina que uso con el vault de secretos Infuse en Agent Orchestra y ThinkTank AI). Nunca los dejes grabados en build.

04 · Invocación de ejemplo y salida esperada

Invocas el skill describiendo el servicio, como cuando le pasas un brief a un compañero:

/skill dockerfile-author

Conteneriza nuestra API. Node 20, TypeScript. Build con "npm run build" a
./dist, arranca con "node dist/server.js". Escucha en 3000 y expone
GET /health. Debe correr non-root y reconstruir rápido cuando solo cambia
el código.

Lo que vuelve no son consejos, son archivos:

  • Un Dockerfile multi-stage con la etapa de build compilando TypeScript y la de runtime cargando solo dist más las dependencias de producción.
  • Orden de capas que cachea la instalación de npm ci aparte del código, así un cambio de una línea reconstruye en segundos.
  • USER node, un EXPOSE 3000 y un HEALTHCHECK que sondea /health.
  • Un .dockerignore ajustado al proyecto para que el contexto excluya node_modules, .git y .env.
  • Una nota de una línea con el tamaño esperado de la imagen y cuál FROM cambiar si necesitas otra base.

Un ejemplo corto de la propia descripción del skill, el frontmatter que lo hace dispararse en los requests correctos y quedarse callado en el resto:

---
name: dockerfile-author
description: Write a small, cache-friendly, non-root multi-stage Dockerfile
  plus a .dockerignore. Use when containerizing a service, or when an
  existing image is bloated, slow to build, or running as root. Ask for the
  runtime, build step, start command, and port. Do NOT use when a container
  isn't needed (static site, serverless) or to micro-optimize for vanity.
---

Fíjate que pide el runtime y el comando de arranque desde el principio, porque esa es la entrada que determina cada decisión real que toma el skill.

05 · Configuración y las concesiones que toma

El skill toma algunas decisiones por ti y debería explicarte por qué:

  • Imagen base: slim, ni scratch ni la completa. Un tag -slim quita el grueso de la imagen completa pero conserva una shell de verdad y un gestor de paquetes para cuando necesites depurar. scratch y distroless bajan más, pero a cambio de toda comodidad para depurar; el skill arranca por slim y solo baja más si lo pides.
  • Versiones mayores fijadas, no :latest. node:20-slim, no node:latest. Un tag flotante hace que tu build no sea reproducible y puede romperse de un día para otro cuando upstream se mueve. Fija la versión mayor; deja que los parches entren a propósito, no por sorpresa.
  • Separación build vs. runtime. Todo lo que el runtime no ejecuta, compiladores, devDependencies, archivos de test, se queda en la etapa de build. La imagen desplegada solo carga lo que responde requests.
  • Healthcheck ajustado a la app. Intervalo, timeout y reintentos se configuran para que un arranque lento no se lea como un crash y un cuelgue real no se pase por alto. El default de 30s/3s/3 es un buen punto de partida, no una ley.

Sobre el tamaño: perseguir la imagen más pequeña posible es una trampa pasado cierto punto. Una base slim a la que puedes entrar por shell y depurar suele valer más que una imagen scratch 40 MB más pequeña e imposible de inspeccionar cuando falla a las 2 de la mañana. El skill optimiza primero el tiempo de build y la superficie de ataque, porque esos son los costos que de verdad pagas todos los días.

Importante

Fija tu imagen base y reconstruye con cierta cadencia. node:20-slim es reproducible hoy, pero los paquetes del OS de abajo igual van acumulando CVEs. Un tag fijado más una reconstrucción periódica, y un escaneo en CI, le gana tanto a un :latest flotante como a un tag que fijaste en 2024 y nunca volviste a tocar.

06 · Detalles que te van a complicar

Estas son las fallas que más veo, más o menos ordenadas por qué tan seguido aparecen:

  1. Código copiado antes de instalar dependencias. El invalidador de caché clásico. COPY . . por encima de npm ci hace que cada edición reinstale todo. Manifiesto primero, instala, luego el código, siempre.
  2. Correr como root. El default si nunca escribes USER. Con un escape de contenedor, ser root adentro es un camino mucho más corto al host que un proceso non-root.
  3. Sin .dockerignore. Todo el repo, node_modules, .git, secretos, se sube como contexto y corre el riesgo de quedar grabado en una capa. Es el archivo que más rinde y el más olvidado.
  4. Secretos en build args. Un token en ARG o un .env copiado vive en el historial de la imagen para siempre. Inyecta los secretos en runtime, nunca en build.
  5. Imágenes de una sola etapa. Desplegar el compilador y las dependencias de desarrollo infla la imagen y ensancha la superficie de ataque. Separa build de runtime.
  6. Un healthcheck que miente. Una verificación que solo confirma que el proceso existe, no que responde requests, le dice al orquestador que todo va bien mientras los usuarios reciben errores. Sondea la app de verdad.
  7. :latest en FROM. Builds no reproducibles que se rompen cuando upstream se mueve. Fija la versión mayor.

Una prueba de humo rápida que pesca casi todas: cambia una línea de código, reconstruye y mira la salida. Si Docker reinstala dependencias, tu orden de capas está mal. Después corre docker history sobre la imagen, si ahí ves tu .env, tu .git o un token de build, detente y arréglalo antes de que esa imagen llegue a ningún lado.

Vale la pena echar mano del skill Dockerfile Author en cuanto estés por copiar y pegar un Dockerfile de otro repo y editar la línea FROM. Bien usado, te da una imagen que reconstruye en segundos, pesa poco y corre sin root. Esos son los defaults aburridos y correctos que rinden en cada deploy. Mal usado (una sola etapa, root, sin .dockerignore, secretos grabados) terminas con una imagen lenta de construir, cara de alojar y a un escape de distancia de un día mucho peor.

Puntos clave

  • Trata el Dockerfile como un programa de caché: ordena las instrucciones de la que menos cambia a la que más, así una edición de código reconstruye una sola capa barata.
  • Copia siempre el manifiesto de dependencias e instala antes de copiar el código, invertir esos dos es la causa número uno de builds lentos.
  • Non-root (USER) y un HEALTHCHECK de verdad que sondee la app son defaults, no añadidos de última hora.
  • Emite un .dockerignore cada vez, y nunca dejes que un secreto entre a una capa, inyéctalos en runtime, no en build.
  • Arranca por una base slim y fijada; optimiza primero el tiempo de build y la superficie de ataque, y no persigas scratch por un número de vanidad.

Preguntas frecuentes

¿Por qué multi-stage? Mi Dockerfile de una sola etapa funciona bien.

Funciona, pero despliega tu compilador, tus dependencias de desarrollo y muchas veces tu código e historial de git dentro de la imagen que expones a la red. Eso es tamaño desperdiciado y una superficie de ataque más amplia. Un build multi-stage hace el trabajo pesado en una etapa que nunca se despliega, y luego copia solo los artefactos compilados y las dependencias de producción a una etapa de runtime liviana. Mismo resultado, imagen más pequeña y más segura.

¿Por qué importa tanto el orden de COPY y RUN?

Porque Docker cachea las capas de arriba hacia abajo y la primera instrucción que cambia invalida todas las de abajo. Si haces COPY de todo tu código antes de instalar dependencias, cada edición cambia esa capa y obliga a una reinstalación completa. Copia primero el manifiesto, instala y luego copia el código, así un cambio de una línea reutiliza la instalación cacheada y reconstruye en segundos en vez de minutos.

¿Correr como root dentro de un contenedor es de verdad un problema? Al final está aislado.

El aislamiento es una frontera, no una garantía. Si un atacante explota tu app y el proceso es root dentro del contenedor, un escape de contenedor o un mount mal configurado lo deja como root camino al host. Correr como usuario non-root hace que un compromiso arranque desde una posición mucho más débil. Te cuesta una sola línea USER, así que no hay razón para saltártelo.

¿Debo usar alpine, slim, scratch o distroless?

Arranca con slim. Es mucho más pequeña que la imagen completa pero conserva una shell y un gestor de paquetes para que de verdad puedas depurar un contenedor que falla. Alpine es aún más pequeña, pero su libc musl a veces da sorpresas con los módulos nativos. Scratch y distroless son las más pequeñas y cerradas, a costa de quedarte sin shell cuando algo se rompe. El skill arranca por slim y solo baja más si lo pides, porque poder depurar suele valer más que los últimos megabytes.

¿Cómo meto los secretos en la imagen de forma segura?

No los metes, no en build. Cualquier cosa que pases por ARG o copies de un .env vive en el historial de la imagen para siempre, aunque una capa posterior la borre. Inyecta los secretos en runtime por variables de entorno que dé el host, o tráelos de un gestor de secretos cuando el contenedor arranca. Esa es la disciplina que mantengo con el vault Infuse en mis proyectos: la imagen está hecha para ser pública, los secretos llegan al ejecutar.

¿De verdad necesito un HEALTHCHECK si mi orquestador ya reinicia los contenedores caídos?

Reiniciar al caer solo atrapa un proceso muerto. Se le escapa la falla peor: un proceso que sigue corriendo pero ya no responde requests: un deadlock, un pool de conexiones agotado, un event loop trabado. Un healthcheck que sondea la app de verdad por HTTP sí atrapa esos casos, así el orquestador puede reiniciar o dejar de mandarle tráfico. Solo asegúrate de que pegue contra un endpoint real; una verificación que solo confirma que el PID existe no te dice nada útil.

¿Prefieres que lo hagamos por ti?

Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.

Escríbenos por WhatsApp

Escríbenos por WhatsApp

Escanéalo con tu teléfono para escribirnos por WhatsApp.

Escanéalo con tu teléfono para escribirnos por WhatsApp.

¿Estás desde el teléfono y no puedes escanear? Escríbenos a info@ilustrari.com

Primera conversación gratis. Te responde el fundador.

Recursos relacionados

PromptDocker

Un prompt que endurece tu Dockerfile y te explica cada cambio

La mayoría de los Dockerfiles corren como root, están inflados y no fijan versiones, y como los arreglos son tan mecánicos, la gente pega una reescritura de 'best practices' sin entender qué resolvió. Este es un prompt completo y listo para copiar que reescribe tu Dockerfile a multi-etapa, sin root, con versiones fijadas y con las capas ordenadas para cache, y conecta cada cambio con el riesgo concreto que elimina, además de cómo adaptarlo según el lenguaje, cuáles son los placeholders que cargan el valor, qué variantes vale la pena guardar y el detalle que convierte una salida limpia del chat en un CI en rojo.

11 abr 202611 min de lectura
SkillClaude Code

El skill de revisión de código: un revisor que lee el diff, no el repo

Un revisor empaquetado de Claude Code que invocas antes de cada commit en lugar de repegar las mismas instrucciones. Lee el diff en stage, primero marca los bugs reales de corrección, luego lista aparte las limpiezas opcionales y no opina sobre detalles de estilo que el linter ya cubre. Aquí te explico cómo armarlo, conectarlo y evitar que te reescriba la función completa.

1 jun 202611 min de lectura
SkillClaude Code

Skill de auditoría de seguridad: una revisión repetible de tu código antes de que se te complique

Un skill de auditoría de seguridad le da siempre la misma revisión aburrida y estructurada a tu código (secretos filtrados, authz que falta, inyección, cripto débil) y te devuelve una lista priorizada con archivo:línea y una etiqueta de confianza. Aquí te explico cómo armar uno que de verdad ayude en vez de ahogarte en hallazgos de «quizás deberías revisar esto».

31 may 202611 min de lectura