Saltar al contenido
Guía9 min

Usuario no-root en el contenedor del agente IA

Cómo no correr el bot como root: USER en el Dockerfile, uid fijo, volumen con el mismo uid, HEALTHCHECK antes de dropear privilegios, y por qué npm start como PID 1 rompe SIGTERM. Root no hace el modelo más listo; solo agranda el radio de daño.

Por · Reportar un error

Continúa con un curso práctico

Si el proceso del agente es root y un tool escribe donde no debe, el daño es el contenedor entero (y a veces el socket de Docker). Sandbox de coding cubre el agente que toca repos. Esta guía cubre uid del runtime en prod. Fuentes oficiales consultadas el 3 de septiembre de 2026.

La regla

USER no-root después de apk/apt y después del HEALTHCHECK que necesite binarios. El volumen /data es de ese uid. Root no acelera el modelo.

node:22-alpine trae usuario node (uid 1000). Úsalo. No inventes appuser sin uid fijo: el volumen de ayer era 1000, el de hoy 1001, sqlite “permission denied”.

Dockerfile

USER node

USER fija el usuario (o UID) y, opcional, el grupo para el resto del stage: RUN que sigue, ENTRYPOINT y CMD. Si pones grupo, el proceso solo tiene ese grupo; el resto de memberships se ignora. Si el usuario no tiene grupo primario, el stage corre con el grupo root. Eso no es un rumor: está en la página de USER.

Instala deps como root, copia archivos, chown al uid, luego USER. COPY sin --chown crea archivos con UID/GID 0. --chown=1000:1000 (números) no necesita /etc/passwd; un nombre (node) sí, y el build falla si no está. Windows containers no soportan --chown.

HEALTHCHECK corre dentro del contenedor, con el usuario vigente. Defaults oficiales: --interval=30s, --timeout=30s, --start-period=0s, --start-interval=5s (Engine 25+), --retries=3. Exit 0 = healthy, 1 = unhealthy, 2 = reservado. Solo el último HEALTHCHECK del Dockerfile cuenta. En alpine no hay wget/curl: node -e http.get. Un probe que pasa de --timeout muere con SIGKILL.

CMD ["node","server.js"] no npm start: npm como PID 1 se come SIGTERM (graceful). En Node, SIGTERM/SIGINT sin listener salen con código 128 + número de señal. Si instalas listener, el default se quita; el evento es asíncrono y el proceso puede morir antes de que el handler corra si el event loop está vacío.

Volúmenes

Fly/Railway/Compose: el bind mount entra con el uid del host o del volume. Si el proceso es 1000 y los files son root, no escribe. chown en entrypoint como root y luego gosu/su-exec a node. Un entrypoint eterno como root anula el USER.

Caps / read-only

El daemon arranca contenedores con un set restringido de capabilities (allowlist, no denylist). Best practice documentada: quitar todas salvo las que el proceso pide. Un webhook de Telegram no necesita NET_ADMIN ni bind <1024: TLS lo termina Caddy/Fly; el agente escucha 3000.

cap_drop: [ALL] + read_only: true + tmpfs en /tmp. Si un tool “necesita root”, ese tool no va en el contenedor del webhook.

User namespaces (daemon 1.10+): el root dentro se mapea a un uid ≠ 0 en el host. No viene encendido. No sustituye USER node; es otra capa si el VPS lo habilita. Rootless mode evita que el daemon sea root; el proceso del bot igual no debe ser uid 0.

La página de Engine security cierra con esto: los contenedores son razonablemente seguros sobre todo si el proceso no es privilegiado. El resto (cgroups, namespaces) no perdona un PID 1 root con un tool que escribe /.

Resource constraints (mem/cpu) no sustituyen el uid. Pon los tres: OOM, CPU, USER.

Tabla

Root versus usuario node en el mismo contenedor

PiezaRootUSER node
PID 1peor blastdefault bueno
Volumen sqliteescribe siempreuid alineado
apk upgradesí, en buildno en runtime
HEALTHCHECK wgeta vecesusa node
SIGTERMigual si PID 1 es nodeigual

Errores comunes

sqlite permission denied tras dropear a USER node

SíntomaCausaFix
EACCES /datauid ≠ ownerchown 1000 / uid fijo
HEALTHCHECK unhealthywget ausente + USERnode -e HTTP
SIGTERM ignoradonpm PID 1CMD ["node",…]
“Funciona en local”Docker Desktop rootishmismo USER en prod
Entrypoint root eternoolvidaste gosudrop after setup

Relación con el resto

Checklist

  • USER numérico o node con uid documentado
  • Volumen /data owned por ese uid
  • CMD exec form a node
  • HEALTHCHECK sin wget en alpine
  • No apk en runtime
  • Ensayo: docker exec id → uid ≠ 0

FAQ

¿Root “solo para bind 443”? No. TLS lo termina Caddy/Fly. El agente escucha 3000.

¿Kubernetes runAsNonRoot? Misma idea. Esta guía es Compose/Fly/Railway.

¿Distroless? Ya viene sin root shell. Sigue necesitando uid y volumen.

Fly y Railway

Fly corre lo que el Dockerfile diga. Si olvidas USER, eres root en la Machine. Railway igual. No hay un toggle “non-root” en el dashboard que arregle el Dockerfile. El uid del volumen de Fly es el de la Machine: alinea 1000 o documenta el que uses.

Workers y Vercel Functions no tienen USER de Linux. Esta guía no aplica ahí. No copies USER node a un wrangler.toml.

Compose: user: "1000:1000" en el servicio además del Dockerfile evita que un override local te devuelva a root. Si solo está en el Dockerfile y alguien hace docker run -u 0, perdiste.

El ensayo: docker exec <cid> id muestra uid=1000. Un archivo nuevo en /data es de 1000, no de root.

Root en el contenedor no es “más simple para el primer deploy”. Es el default que dejas y nunca quitas. Cámbialo en el mismo PR del Dockerfile, no “luego”.

Si el agente genera archivos como root un día y USER al siguiente, el volumen queda mixto. Un chown -R en el entrypoint (una vez) y listo; no lo dejes en cada request. El chown de cada turno es CPU y disco tirados (CPU).

Siguiente paso: si el uid ya es 1000 y igual no escribe, disco y logs. Sin runtime: curso.

De la guía a la práctica

Elige un curso para tu proyecto

Continúa con una ruta paso a paso: instalación, programación o uso de un asistente personal, según tu experiencia.

Explorar cursos gratis

Contenido gratis, sin registro. Los servicios externos pueden tener costos.

Avisos cuando actualicemos cursos y guías

Recibe actualizaciones por correo. Es opcional: puedes seguir leyendo y abrir cualquier curso sin suscribirte.

Al suscribirte, solicitas avisos editoriales. Consulta la política de privacidad. Puedes pedir la baja en contacto.