Cómo logré utilizar DeepSeek Harness (dsh) seguro y escalable en Docker y Kubernetes (Parte 1)
Cuando lo quise correr en Kubernetes, la pregunta no era cómo levantarlo, sino qué necesita para no romperse, poder escalar y por sobre todo poder ejecutar el container de la forma más segura posible.
Cómo logré utilizar DeepSeek Harness (dsh) seguro y escalable en Docker y Kubernetes (Parte 1)
🛠️
Serie: Explorando una forma segura de correr DeepSeek Harness: cómo lo implementé en Kubernetes. Parte 1 de 2.
Dejarías que un agente de IA, que lee tus archivos, lanza subprocesos, lee claves de API de proveedores, utiliza shells, puede alterar configuraciones de sistema y que además es de código propietario, se ejecute libremente en tus máquinas? (cof, cof... claude) Yo, al menos, no.
Es por eso que puse mi atención en DeepSeek Harness (DSH): Es un harness para agentes autónomos, open source (licencia MIT) y que se basa en el concepto de "todo es un plugin", entre otras características que no vienen al caso en este momento y claramente puedes ir a ver tu mismo. Detalle importante: las versiones actuales se consideran como developer preview; por lo tanto los cambios incompatibles son parte anunciada del recorrido.
Cuando lo quise correr en Kubernetes, la pregunta no era cómo levantarlo, sino qué necesita para no romperse, poder escalar y por sobre todo poder ejecutar el container de la forma más segura posible. De ahí salió sredevopsorg/deepseek-kubernetes-sandbox, el repo que mantenemos con la organización.
Su artefacto principal es una imagen de DSH Web (dsh web), basada en debian trixie, slim, sin root y multiarquitectura (amd64 y arm64). Todo lo demás existe para sostener esa imagen o para mostrar una implementación sobre ella.
Esta primera parte es la imagen: cómo correrla, por qué su build se fía de digests y no de tags, y el contrato de runtime que la mantiene fuera de un crash loop. En la futura parte 2 explicaremos cómo desplegarla en Kubernetes, con aislamiento mucho más sólido y la posibilidad de utilizar oAuth2 para la gestión de distintos usuarios con su propio entorno preconfigurado.
-e DEEPSEEK_API_KEY toma el valor desde el entorno de tu shell, así la clave nunca queda en la imagen ni en tu historial como argumento. El volumen con nombre guarda tanto el estado de DSH como el workspace del agente.
Abre http://localhost:3080 y recibes un 401. No es un bug. DSH Web no sirve para nada en un / pelado sin su cookie de token de lanzamiento, y el token de un solo uso aparece únicamente en los logs de arranque. Abre una vez la URL impresa con ?token=...; DSH setea su cookie de autenticación y la UI funciona normal después. Si el puerto 3080 está tomado en tu equipo, mapea otro y define DSH_PUBLIC_URL, o la URL que aparece en el log va a apuntar a un puerto donde no escucha nadie.
Ese token trátalo como contraseña. Vive en tus logs, y los logs terminan en tres lugares que ya olvidaste.
Un build que se niega a fiarse de los tags
El Dockerfile es un build de dos etapas, y las dos parten de imágenes nodefijadas por digest en versions.env. La razón es simple: un tag se puede repuntar a otro build sin avisarte. El script de verificación del repo rechaza cualquier referencia a imagen base sin un @sha256:, y la etapa de build además clona DSH upstream en un ref fijo y comprueba que el HEAD del clon sea exactamente DSH_COMMIT. Si upstream mueve el tag, el build falla ruidosamente en vez de empaquetar en silencio otro código.
Después vienen las dependencias con pnpm (activado vía corepack en una versión fija), el build de producción y el CLI @deepseek-ai/dsh empaquetado con pnpm pack. En la etapa de runtime copio el resultado a /app sobre node:24-trixie-slim, también fijada por digest, y sumo las herramientas que los agentes de verdad usan: git, ripgrep, jq, yq, curl, wget, gh, uv y tini, más bubblewrap como runner preferido del sandbox de comandos de DSH.
make image es el punto de entrada canónico; cada pin vive en versions.env y make check falla cuando uno se desvía. Las labels de la imagen registran el ref de DSH upstream y la revisión de release, así puedes ir de un contenedor corriendo a un commit sin adivinar.
Los tags son promesas mutables; los digests y los commits SHA son contratos. Este repo confía en los contratos auditables.
El contrato de runtime
Un volumen, dos hogares
El estado de DSH — conversaciones, sesiones, configuración, credenciales — vive en la raíz de /home/node/.dsh (DSH_HOME). El workspace del agente es su subdirectorio workspace/, que también es el HOME y el WORKDIR de la imagen.
Ese layout lo puse a propósito. El selector de carpetas de DSH abre en $HOME y no tiene ajuste de ruta inicial, así que apuntar HOME dentro del volumen hace que persistan el selector, tus ejecuciones, las cachés de npm/uv/git y los skills globales de DSH en ~/.agents. Monta un volumen en DSH_HOME y todo sobrevive a los reinicios.
0:00
/1:00
ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.2-rc.2 ejecutandose en Podman local, validando entorno de ejecución y programas disponibles
En Kubernetes, las plantillas que trae el repo agregan un segundo montaje del subPath workspace del mismo PVC en /home/node/.dsh/workspace. No es un segundo volumen: es un truco para que el kubelet cree el directorio y le cambie el dueño antes de que arranque el contenedor. Sin eso, el runtime crea el directorio de trabajo faltante como root, y el usuario node sin privilegios (UID/GID 1000) no puede escribir en su propio home.
Docker tiene la trampa espejo: un volumen con nombre creado por una imagen vieja no se repuebla desde la nueva. Crea workspace/ una vez con dueño 1000:1000 antes de arrancar. Un volumen nuevo copia el directorio de la imagen automáticamente, que es lo que la semántica de volúmenes de Docker busca.
Probes, puertos y señales
/ no es un endpoint de salud. Usa /favicon.svg: no requiere autenticación, lo sirve la capa web y prueba que el servidor HTTP está arriba sin disparar el flujo de auth. Un liveness probe contra / va a mandar tu pod a crash loop con una constancia admirable.
Por lo mismo le puse al startupProbe dos minutos de presupuesto: la activación de plugins puede pasarse de un liveness por defecto.
El resto del contrato: un solo puerto (3080) sirve la UI, los assets, la API, SSE y WebSocket. tini corre como PID 1, cosecha zombies y reenvía SIGTERM; el entrypoint hace exec de dsh web para que las señales lleguen de verdad a Node. El proceso no es root, DSH_HOST detecta sola la IP del pod cuando queda vacío y NODE_ENV=production está definido. Detrás de un proxy, agrega tu hostname público a DSH_TRUSTED_HOSTS: DSH rechaza los Host que no conoce, y solo localhost y 127.0.0.1 son siempre de confianza.
Aquí está la parte que casi todas las demos se saltan. DSH mete sus propios comandos bash en un sandbox, una segunda capa de aislamiento dentro del sandbox de contenedor que ya corres. Sus runners en Linux son bubblewrap y luego Landlock, y falla cerrado con SANDBOX_UNAVAILABLE en vez de correr un comando fuera del sandbox.
Opciones para DSH_PERMISSION_MODE
DSH_PERMISSION_MODE define la política: read-only, workspace-write (el default de la imagen) o danger-full-access, que permite ejecutar los procesos con los mismos permisos del usuario de la máquina, además desactiva las solicitudes de aprobación.
Elige el último sólo cuando el pod o la VM que lo ejecuta, sea la "frontera de aislamiento" en la que de verdad confías. En éste caso, si estás ejecutando la imagen en tu máquina local, sin Docker rootless, no es la mejor idea. En mi caso, utilizo Podman, que al menos en versiones recientes de Linux, se instala por defecto en modo rootless, por lo tanto me da un pequeño boost de aislamiento (insisto, PEQUEÑO), para testear los distintos
Y ojo con esto: el workspace vive dentro de DSH_HOME, así que un shell que se pasea por el directorio padre puede llegar a .credentials.yaml. El contenedor nunca fue un secreto para el agente.
En la parte 2 muestro la respuesta con gVisor y NetworkPolicy a ese párrafo incómodo.
Credenciales: inyectadas, nunca baked
DSH resuelve las API Keys desde los nombres estándar del entorno donde se ejecuta, en el caso que quieras usar algún otro provider, ejemplos: DEEPSEEK_API_KEY, OPENROUTER_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY (no GOOGLE_API_KEY), MISTRAL_API_KEY, HF_TOKEN y el resto de su catálogo de proveedores. Las inyecto con -e en Docker/Podman o con un Secret con envFrom en Kubernetes. Nunca dentro de una imagen ni en un commit.
Un detalle de orden importa durante la rotación: una clave guardada en el almacén de credenciales de DSH ($DSH_HOME/.credentials.yaml) tiene prioridad sobre el entorno. Rota solo el Secret y puede que no cambie nada. Si falta una clave tampoco se rompe la UI; solo fallan las llamadas a modelos.
Releases inmutables, digests fijados
La identidad de release es el ref de DSH upstream más una revisión del repositorio: DSH_VERSION=dsh-v0.2.2-rc.2 con RELEASE_REVISION=r3 da dsh-v0.2.2-rc.2-r3. Los tags son inmutables; un release roto lo arreglo publicando -r(N+1), nunca re-etiquetando.
Publico la imagen multiarquitectura (linux/amd64, linux/arm64) en GitHub Container Registry:
Tag
Significado
dsh-v0.2.2-rc.2
Identidad de release actual; no se mueve una vez publicada
latest
Release más reciente; mismo digest que el tag de identidad
main
Canal de desarrollo; no es un release, no es para producción
Los despliegues en producción deberían fijar el digest, y los builds de release publican attestations de SBOM y de procedencia: metadatos sobre qué estás corriendo y cómo se construyó, no un nombre que esperas que siga significando lo mismo que ayer:
docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.2-rc.2
# pin in your manifests:
# image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox@sha256:<digest>
Qué veremos en la Parte 2?
Un contenedor es una demo; la plataforma es el objetivo. En la parte 2 construyo sobre esta imagen con kubernetes-sigs/agent-sandbox: una instancia de DSH por usuario aprovisionada desde un warm pool, suspendida cuando está ociosa con el estado intacto en PVCs, y al frente un gateway de navegador que hace el handshake del token de lanzamiento del lado del servidor y expone GitHub SSO en el borde. Encima de eso, el hardening opt-in con gVisor y NetworkPolicy al que este post no deja de apuntar.
Parte por el ejemplo de agents-as-a-service. Mientras tanto: fija tus digests, hazle probe al favicon y no dejes que el primer acto de tu agente sea leer sus propias credenciales.
NVIDIA Kubernetes Engine (NVKE), un sistema de gestión de Kubernetes open source que un operador de fábrica de IA instala para crear y operar clústeres en su propia infraestructura de GPUs NVIDIA.