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 Marco Lee · Reportar un error
Continúa con un curso prácticoSi 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

| Pieza | Root | USER node |
|---|---|---|
| PID 1 | peor blast | default bueno |
| Volumen sqlite | escribe siempre | uid alineado |
| apk upgrade | sí, en build | no en runtime |
| HEALTHCHECK wget | a veces | usa node |
| SIGTERM | igual si PID 1 es node | igual |
Errores comunes

| Síntoma | Causa | Fix |
|---|---|---|
EACCES /data | uid ≠ owner | chown 1000 / uid fijo |
| HEALTHCHECK unhealthy | wget ausente + USER | node -e HTTP |
| SIGTERM ignorado | npm PID 1 | CMD ["node",…] |
| “Funciona en local” | Docker Desktop rootish | mismo USER en prod |
| Entrypoint root eterno | olvidaste gosu | drop after setup |
Relación con el resto
Checklist
-
USERnumérico onodecon uid documentado - Volumen
/dataowned por ese uid -
CMDexec form anode - HEALTHCHECK sin wget en alpine
- No
apken 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 gratisContenido 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.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



