Docker

Ejecuta Pepe en un contenedor e instala, dentro de él, las herramientas que el agente necesita.

Cada release publica una imagen de contenedor junto con los binarios, para amd64 y arm64. El docker pull selecciona la arquitectura correcta automáticamente, tanto en un Mac M-series como en un servidor.

docker run -d --name pepe \
  -p 4000:4000 \
  -v pepe-data:/data \
  -v pepe-tools:/tools \
  -e PEPE_DASHBOARD_PASSWORD=una-contrasena-fuerte \
  ghcr.io/pepe-agent/pepe

Abre http://localhost:4000, inicia sesión y completa la configuración desde el panel.

Requisitos

Dos ajustes son obligatorios. Omitir cualquiera de los dos falla en silencio.

Volúmenes

Son dos, y guardan cosas de naturaleza distinta.

/data (el PEPE_HOME) es estado: configuración, agentes, conversaciones, workspaces y Mnesia. Es el volumen del que haces copia de seguridad. Sin él, docker rm borra la instalación entera.

/tools es caché: todo lo que el agente instala para sí mismo. Está en el PATH y es también donde vive el directorio home del agente, en /tools/home. Ese segundo detalle es lo que hace que “se instala una vez” sea cierto de verdad, y tiene su propia sección más abajo.

/tools queda fuera de /data a propósito. Una copia de seguridad debe llevar estado, no decenas de megabytes de binarios y archivos de modelo que se pueden volver a descargar, y esos archivos son específicos de arquitectura: un /data guardado en una máquina arm64 y restaurado en una amd64 pondría en el PATH ejecutables que allí no funcionan.

-v pepe-data:/data -v pepe-tools:/tools

Contraseña del panel

Un contenedor no cuenta como tu propia máquina: Pepe trata su red como pública y, sin contraseña, rechaza todas las peticiones (HTTP 403). El panel no llega a abrirse.

-e PEPE_DASHBOARD_PASSWORD=...

Es una política deliberada, no una limitación de Docker. Pepe se niega a exponer un panel sin autenticación en una red por la que no puede responder. La regla surgió de un incidente real: un servicio expuesto, sin autenticación, fue escaneado y alguien abusó de él.

Secretos

No pongas claves de API ni en la imagen ni en el archivo de configuración. Guarda solo la referencia en la configuración y proporciona el valor real en la ejecución. Pepe resuelve la referencia en el momento de la lectura y nunca almacena el valor expandido.

# la configuración guarda solo:  "api_key": "${OPENROUTER_API_KEY}"
docker run -d ... -e OPENROUTER_API_KEY=sk-... ghcr.io/pepe-agent/pepe

Herramientas para el agente

El agente se ejecuta como usuario sin privilegios y no puede lanzar apt install. Es intencional: los comandos que ejecuta los elige un modelo de lenguaje, y concederle root a ese proceso no es una decisión que nos corresponda tomar por ti.

La restricción cuesta menos de lo que parece, porque root no es la pieza que falta:

Todo lo que apt instala muere con el contenedor. apt escribe en /usr y /etc, que pertenecen a la capa escribible del contenedor, no a un volumen. Root da permiso, no persistencia: lo instalado desaparece en el docker rm aunque se ejecute como root.

La pregunta nunca es cómo llegar a root. Es dónde tiene que vivir la herramienta para sobrevivir. Hay dos respuestas, y hoy la primera resuelve por sí sola la mayoría de los casos.

Todo lo que el agente instala para sí mismo persiste

El HOME del agente es /tools/home, es decir, queda dentro del volumen /tools. Ahí está todo el truco. Los instaladores no preguntan dónde está tu volumen: escriben en ~/.local/bin y en ~/.cache, y en ningún otro sitio. Con el HOME en la capa del contenedor, todo lo que el agente instala para sí se descarga otra vez en el contenedor siguiente. Con el HOME en el volumen, se instala una sola vez.

La diferencia es fácil de medir. El agente que transcribe un mensaje de voz instala uv y descarga un modelo Whisper, unos 75 MB. La primera vez tarda 27 segundos. En un contenedor recién creado, esa misma transcripción tarda 1,2 segundos, porque la caché sobrevivió.

Así que uv, un pip install --user, un modelo Whisper, un toolchain de lenguaje o una descarga simple:

curl -sL <url> -o /tools/op && chmod +x /tools/op

sobreviven al docker rm y a una actualización de Pepe, sin root y sin reconstruir ninguna imagen. /tools está en el PATH, así que un ejecutable dejado ahí queda disponible al instante en la shell del agente. El CLI de 1Password (op), gh, kubectl y terraform son todos un único archivo y no necesitan nada más que esto.

Los paquetes de sistema van en la imagen

Algunas herramientas son paquetes de sistema de verdad. psql, imagemagick y similares reparten archivos y bibliotecas compartidas por todo el sistema de archivos, y un volumen no da abasto con eso. Tienen que formar parte de una imagen.

Un build arg instala paquetes adicionales sin que tengas que escribir un Dockerfile:

docker build --build-arg PEPE_IMAGE_APT_PACKAGES="postgresql-client imagemagick" .

Si prefieres mantener un Dockerfile propio, derivar de nuestra imagen funciona igual de bien y sigue siendo una opción perfectamente válida:

FROM ghcr.io/pepe-agent/pepe
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
      postgresql-client \
  && rm -rf /var/lib/apt/lists/*
USER pepe
docker build -t mi-pepe .
docker run -d -p 4000:4000 -v pepe-data:/data -v pepe-tools:/tools \
  -e PEPE_DASHBOARD_PASSWORD=... mi-pepe

Los dos caminos tienen el mismo coste: con cada nueva release de Pepe, reconstruyes la imagen.

Por qué ffmpeg no está en la imagen

ffmpeg parece el paquete de sistema evidente para esta imagen, ya que Telegram envía la voz en OGG/Opus y la transcripción tiene que salir de algún sitio. Ninguna de las dos rutas que transcriben de verdad lo necesita. La API de transcripción acepta el fichero .ogg tal y como llega, sin conversión alguna, y faster-whisper descodifica a través de PyAV, que lleva sus propios códecs dentro del wheel. Esto se midió, no se dio por hecho: un fichero OGG/Opus se transcribió en un Debian limpio, sin ningún ffmpeg instalado. Solo el CLI de whisper.cpp llama a ffmpeg por fuera, y esa ruta es opt-in.

Incluirlo de todos modos salía carísimo. El paquete ffmpeg de Debian arrastra 204 paquetes y 121 MB de archivos (LLVM, Mesa, un sintetizador de voz, un demostrador de teoremas), todo para sostener una pila de aceleración de vídeo por GPU que un contenedor headless no va a tocar jamás. Quitarlo dejó la imagen en 408 MB, frente a los 945 MB anteriores, unos 84 MB comprimidos, que es lo que de verdad te descargas por arquitectura.

Si aun así quieres ffmpeg, ya sea para el CLI de whisper.cpp o para cualquier otra cosa, instálalo con el build arg de arriba o deja un build estático de un solo fichero en /tools, que está en el PATH y vive en un volumen.

Probar una herramienta

docker exec -u root pepe apt-get update
docker exec -u root pepe apt-get install -y jq

Funciona, y se descarta en el siguiente docker rm. Úsalo para confirmar que la herramienta resuelve tu problema y solo después decide dónde vive: en el home del propio agente, si él mismo puede instalarla, o en la imagen, si es un paquete de sistema.

Arrancar el contenedor como root (docker run --user root) es opt-in y nunca el valor por defecto. Conviene repetirlo: no compra nada duradero, porque lo que apt escribe sigue muriendo con el contenedor, y acabas de vuelta en las dos respuestas de arriba.

Compose

Pepe ya incluye un docker-compose.yml listo para usar, así que no hay nada que escribir:

curl -O https://raw.githubusercontent.com/pepe-agent/pepe/master/docker-compose.yml

Es el mismo docker run de arriba, con los dos volúmenes y la contraseña ya en su sitio:

services:
  pepe:
    image: ghcr.io/pepe-agent/pepe:latest
    restart: unless-stopped
    ports:
      - "4000:4000"
    volumes:
      - pepe-data:/data # estado: configuración, agentes, conversaciones. De este haces copia de seguridad.
      - pepe-tools:/tools # el home del agente y todo lo que él mismo instala.
    environment:
      # El `:?` es deliberado: sin contraseña, el panel respondería 403 a cada petición
      # (un contenedor no es loopback), así que compose se niega a arrancar en lugar de
      # entregarte un contenedor que se ejecuta y no sirve nada.
      PEPE_DASHBOARD_PASSWORD: ${PEPE_DASHBOARD_PASSWORD:?set this in a .env file}
      OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
      TZ: UTC

volumes:
  pepe-data:
  pepe-tools:

Los secretos van en un fichero .env al lado, nunca en el fichero de compose y nunca en la imagen. La configuración de Pepe los referencia por nombre ("api_key": "${OPENROUTER_API_KEY}") y los resuelve al leerlos, de modo que el valor real solo existe en el entorno:

# .env
PEPE_DASHBOARD_PASSWORD=una-contrasena-fuerte
OPENROUTER_API_KEY=sk-...

Cada secreto necesita las dos mitades: el valor en el .env y una línea en environment: que lo nombre. Compose lee el .env para rellenar los ${...} del propio fichero compose, no para poblar el contenedor. Una clave que solo esté en el .env nunca llega a Pepe, y obtienes un “no model configured” sin ninguna pista del motivo. Añade una línea por cada secreto que uses.

Después, desde ese directorio:

docker compose up -d
docker compose logs -f          # sigue el log
docker compose exec pepe bin/pepe remote   # una shell IEx en el nodo en ejecución

Ponerlo en un servidor

Todo lo anterior corre en una sola máquina, alcanzable en localhost. Detrás de un dominio, con TLS y proxy inverso, cuatro cosas más pasan a importar, y ninguna es culpa de Docker. Consulta Desplegar en un servidor para Compose detrás de Caddy, Docker Swarm detrás de Traefik, y Kamal.

Actualización

Con Compose:

docker compose pull
docker compose up -d

Sin él:

docker pull ghcr.io/pepe-agent/pepe
docker rm -f pepe
docker run -d ... ghcr.io/pepe-agent/pepe   # mismos volúmenes, mismas flags

Configuración, agentes y conversaciones vuelven con /data. Las herramientas del agente, su home y todas las cachés que hay dentro vuelven con /tools, así que no reinstala nada en el primer mensaje. Los paquetes instalados con apt no vuelven, y para esos está la imagen.

Un detalle importante: docker compose down detiene los contenedores y deja los volúmenes intactos, que es lo que quieres. docker compose down -v los borra también y, con ellos, la instalación entera.

Acceso al nodo

docker exec -it pepe bin/pepe remote            # o: docker compose exec pepe bin/pepe remote

Abre una shell IEx conectada a la release en ejecución, para inspeccionar el sistema por dentro.

Para un solo comando pepe en vez de una shell completa, usa bin/pepe rpc con dispatch_attached/1, los mismos comandos que pepe ejecuta en cualquier lugar, pero seguros de ejecutar contra un nodo que ya está sirviendo:

docker exec pepe bin/pepe rpc 'Mix.Tasks.Pepe.dispatch_attached(["agent", "list"])'

bin/pepe <comando> solo no funciona aquí: el contenedor es una release simple, y su dispatch de CLI solo se activa en el binario Burrito, distribuido por separado. bin/pepe rpc/remote es el camino de entrada de cualquier forma. dispatch_attached/1 existe porque varios comandos (plugin install, eval, doctor, cron, y otros, no solo run/chat/tui) cambian ajustes globales que deberían decidirse solo al arrancar. Eso es inofensivo en un proceso CLI recién iniciado que va a salir enseguida, pero no en un nodo que ya está en pie y va a seguir así.