Intro
Cuando alguien dice que va a correr un modelo "en sandbox", muchas veces lo que termina haciendo es bastante menos serio: un docker run rápido, montando medio host, con red abierta y permisos por defecto.
Eso sirve para una demo. No necesariamente sirve para hablar de contención real.
Si querés usar Docker como baseline para aislar un modelo, un runner o una pieza auxiliar de inferencia, la pregunta no es solo cómo levantar el contenedor, sino qué querés impedir, qué permisos necesita de verdad y cuánto blast radius estás aceptando.
La buena noticia es que no hace falta arrancar con microVMs ni con una plataforma gigantesca para mejorar mucho la situación. Con unos cuantos defaults bien elegidos, Docker puede ser una base razonable para escenarios de riesgo bajo o medio.
Qué querés aislar realmente
Antes de mirar comandos, conviene ordenar el objetivo. Un sandbox mínimo serio intenta controlar al menos estas cinco superficies:
- proceso: que no corra con privilegios de más;
- filesystem: que no vea ni escriba cualquier ruta del host;
- red: que no pueda salir libremente a cualquier destino si no hace falta;
- recursos: que no se coma CPU, RAM o PIDs sin límite;
- secretos: que no herede tokens o variables sensibles por comodidad.
Si esas cinco cosas quedan abiertas, decir que "está en Docker" aporta bastante menos de lo que parece.
Receta mínima: un contenedor endurecido para inferencia local
Supongamos un caso simple: querés correr un modelo o runner en un directorio controlado, sin privilegios, con filesystem casi read-only y sin salida de red.
Primero, prepará un workspace explícito:
mkdir -p sandbox-model/{workspace,tmp,cache}
chmod 700 sandbox-model/workspace sandbox-model/tmp sandbox-model/cacheAhora, un ejemplo de ejecución endurecida:
docker run --rm \
--name model-sandbox \
--user 1000:1000 \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=512m \
--mount type=bind,src="$PWD/sandbox-model/workspace",dst=/workspace,rw \
--mount type=bind,src="$PWD/sandbox-model/cache",dst=/cache,rw \
--workdir /workspace \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
--pids-limit=256 \
--memory=8g \
--cpus=4 \
--network=none \
--ulimit nofile=4096:4096 \
ghcr.io/tu-org/tu-runner:latest \
./run-model --model /cache/model.gguf --input prompt.txtNo es magia. Pero ya hay varias decisiones buenas ahí adentro.
Qué significa cada flag importante
--user 1000:1000
Evita correr como root dentro del contenedor. No te vuelve inmune a todo, pero baja bastante el daño posible frente a errores o escapes parciales.
--read-only
Hace que el filesystem del contenedor sea de solo lectura, salvo las rutas que abras de forma explícita. Este cambio solo ya obliga a pensar mejor dónde se escribe y corta bastante desorden operativo.
--tmpfs /tmp
Le da al proceso una zona temporal en memoria para archivos efímeros, sin arrastrar basura persistente al host.
--mount ... /workspace
En vez de montar el home entero o el repo completo "porque es cómodo", montás solo el directorio de trabajo que la tarea necesita.
--cap-drop=ALL
Saca capabilities Linux innecesarias. Este debería ser el default mental: empezar sin capabilities y agregar solo si algo realmente lo exige.
--security-opt=no-new-privileges:true
Impide escaladas de privilegio vía setuid u otros mecanismos parecidos. Es una protección simple que conviene dejar prendida.
--pids-limit, --memory, --cpus
No son solo tuning. También son control de daño. Si algo entra en loop, filtra memoria o dispara procesos de más, el impacto queda acotado.
--network=none
Si la tarea no necesita internet, lo más sano es que no tenga internet. Muchísimos pseudo-sandboxes se rompen por dejar la red abierta aunque el caso de uso no lo pedía.
Variante: cuando el runner necesita descargar el modelo
Hay escenarios donde el contenedor sí necesita red, aunque sea al inicio. En ese caso, el error típico es dejar salida abierta para siempre.
Una forma más sana es separar bootstrap de ejecución.
Por ejemplo:
- descargar el modelo en una etapa controlada;
- guardarlo en un cache local deliberado;
- correr luego la inferencia sin red.
Bootstrap:
docker run --rm \
--user 1000:1000 \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=512m \
--mount type=bind,src="$PWD/sandbox-model/cache",dst=/cache,rw \
--workdir /cache \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
--pids-limit=128 \
--memory=2g \
--cpus=2 \
ghcr.io/tu-org/model-fetcher:latest \
./download-model --output /cache/model.ggufY después ejecutás la inferencia con --network=none.
Ese patrón suele ser bastante mejor que mezclar descarga, cacheo e inferencia dentro de un mismo proceso abierto de par en par.
Variante con GPU: útil, pero más delicada
Si necesitás GPU, el sandbox se complica un poco porque el contenedor va a requerir acceso adicional al runtime y dispositivos.
Un ejemplo mínimo:
docker run --rm \
--gpus all \
--name model-sandbox-gpu \
--user 1000:1000 \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=1g \
--mount type=bind,src="$PWD/sandbox-model/workspace",dst=/workspace,rw \
--mount type=bind,src="$PWD/sandbox-model/cache",dst=/cache,rw \
--workdir /workspace \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
--pids-limit=256 \
--memory=16g \
--cpus=6 \
ghcr.io/tu-org/tu-runner-gpu:latest \
./run-model --model /cache/model.gguf --input prompt.txtEsto puede ser útil, pero conviene decirlo sin vueltas: un contenedor con GPU no equivale automáticamente a un sandbox fuerte. Si además le agregás mounts amplios, credenciales largas y red libre, el aislamiento real vuelve a caer bastante.
Secretos: el error más común es la comodidad
Otro error clásico es pasarle al contenedor el mismo .env enorme que usa toda la plataforma.
Eso destruye gran parte del valor del sandbox.
Si la tarea necesita credenciales:
- que sean pocas;
- que tengan scopes mínimos;
- que duren poco;
- que entren de forma explícita y no por herencia accidental.
Si no necesita secretos, mejor todavía: no le des ninguno.
Qué no deberías montar
Hay tres atajos que suelen romper el diseño muy rápido:
Montar el home del operador
Cómodo, sí. Pero también innecesariamente riesgoso.
Montar el repo entero cuando solo hacían falta dos archivos
Eso agranda la superficie de lectura y escritura sin ningún beneficio real.
Montar sockets del host o Docker socket
Si hacés eso, dejaste una puerta demasiado grande abierta. En muchos casos, ya no tiene sentido seguir hablando de sandbox serio.
Un wrapper simple para no repetir errores
Si esta receta se va a usar seguido, conviene encapsularla en un script en vez de depender de memoria humana.
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$ROOT/workspace" "$ROOT/tmp" "$ROOT/cache"
timeout 20m docker run --rm \
--name model-sandbox \
--user 1000:1000 \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=512m \
--mount type=bind,src="$ROOT/workspace",dst=/workspace,rw \
--mount type=bind,src="$ROOT/cache",dst=/cache,rw \
--workdir /workspace \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
--pids-limit=256 \
--memory=8g \
--cpus=4 \
--network=none \
ghcr.io/tu-org/tu-runner:latest "$@"El timeout externo, aunque parezca menor, también ayuda bastante para cortar tareas colgadas o experimentos que se fueron de tema.
Checklist rápido de endurecimiento
Antes de decir que "ya está sandboxeado", conviene revisar esto:
- ¿corre como usuario no root?
- ¿el filesystem está en read-only salvo rutas concretas?
- ¿los mounts son mínimos y deliberados?
- ¿la red está deshabilitada o al menos muy acotada?
- ¿hay límites de CPU, RAM y PIDs?
- ¿los secretos son mínimos o inexistentes?
- ¿hay timeout y cleanup?
Si la respuesta a varias de esas preguntas es no, probablemente todavía no estás frente a un sandbox serio. Estás frente a un contenedor cómodo.
Cuándo Docker ya no alcanza
Docker puede ser un baseline útil, pero no sirve para todo.
Si vas a ejecutar código realmente hostil, si tenés multi-tenant duro, si el impacto potencial es alto o si el contenedor necesita demasiadas excepciones para funcionar, quizás ya sea momento de subir de nivel: workers remotos, aislamiento adicional o microVMs.
La idea importante no es enamorarse de Docker. Es usarlo bien mientras siga siendo suficiente para el riesgo que querés asumir.
La diferencia importante
Implementar un sandbox para correr un modelo con Docker no es escribir un docker run largo para quedar tranquilo. Es decidir con claridad qué puede tocar ese proceso, qué no, cuánto dura, qué recursos consume y qué pasa si se equivoca.
Cuando esas respuestas están bien resueltas, Docker puede ser una base bastante digna.
Cuando no lo están, el contenedor solo maquilla el problema.
