Docker
Corre Pepe como contenedor, e instala dentro de él las herramientas que el agente necesite.
Cada release publica, junto a los binarios, una imagen de contenedor para
amd64 y arm64. Al hacer docker pull, se elige sola la arquitectura
correcta, sea en un Mac de la serie M o 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 termina la configuración desde el panel.
Requisitos
Hay dos ajustes obligatorios, y saltarte cualquiera de los dos falla en silencio.
Volúmenes
Son dos, y cada uno guarda un tipo distinto de cosa.
/data (el PEPE_HOME) es estado: configuración, agentes,
conversaciones, workspaces y Mnesia. Es el volumen del que debes respaldar
copias de seguridad; sin él, un docker rm te borra la instalación entera.
/tools es caché: todo lo que el agente instala por su cuenta. Está en
el PATH, y además ahí vive el directorio home del agente, en
/tools/home. Ese segundo detalle es justo lo que hace que “se instala una
sola vez” sea cierto de verdad, y tiene su propio apartado más abajo.
/tools queda deliberadamente separado de /data. Una copia de seguridad
debería llevarse el estado, no decenas de megabytes de binarios y modelos
que se pueden volver a descargar, y esos archivos además son específicos de
cada arquitectura: si restauras en una máquina amd64 un /data respaldado
en una arm64, terminarías con ejecutables en el PATH que ahí simplemente
no corren.
-v pepe-data:/data -v pepe-tools:/tools
Contraseña del panel
Un contenedor no cuenta como tu propia máquina: Pepe da por hecho que su red es pública y, sin contraseña, rechaza toda petición con un HTTP 403. El panel simplemente no se sirve.
-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 que no puede garantizar como segura. Esta regla nació de un incidente real, un servicio expuesto sin autenticación que terminó escaneado y explotado.
Secretos
Nunca pongas claves de API dentro de la imagen ni en el archivo de configuración. Deja ahí solo la referencia, y entrega el valor real en el momento de ejecutar; Pepe resuelve esa referencia al leerla, y jamás guarda el valor ya 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 corre como un usuario sin privilegios y no puede ejecutar
apt install. Esto es intencional: los comandos que ejecuta los elige un
modelo de lenguaje, y darle acceso root a ese proceso no es una decisión que
nadie deba tomar en tu nombre.
Esta restricción cuesta menos de lo que parece, porque root nunca fue la pieza que faltaba:
Todo lo que instala
aptmuere junto con el contenedor. apt escribe en/usry en/etc, que pertenecen a la capa escribible del contenedor, no a un volumen. Root te da permiso, no persistencia: lo que instala desaparece con eldocker rm, incluso corriendo como root.
La pregunta nunca es cómo hacerte root. Es dónde tiene que vivir una herramienta para sobrevivir. Hay dos respuestas posibles, y hoy la primera resuelve, ella sola, la mayoría de los casos.
Todo lo que el agente instala por su cuenta persiste
El HOME del agente es /tools/home, y eso lo coloca dentro del volumen
/tools. Ese es todo el truco: los instaladores no preguntan dónde está tu
volumen, simplemente escriben en ~/.local/bin y en ~/.cache, y en ningún
otro lado. Si el HOME vive en la capa del contenedor, todo lo que el
agente instaló se vuelve a descargar en el siguiente contenedor; si el
HOME vive en el volumen, se instala una sola vez.
La diferencia se puede medir sin esfuerzo. Un agente que transcribe un
mensaje de voz instala uv y descarga un modelo Whisper de unos 75 MB; la
primera vez tarda 27 segundos. En un contenedor completamente nuevo, esa
misma transcripción tarda 1,2 segundos, porque la caché sobrevivió al
reinicio.
Así que uv, un pip install --user, un modelo Whisper, un toolchain de
algún lenguaje, o simplemente una descarga directa:
curl -sL <url> -o /tools/op && chmod +x /tools/op
todo eso sobrevive a un docker rm y a una actualización de Pepe, sin
necesitar root ni reconstruir nada. Como /tools está en el PATH,
cualquier binario que dejes ahí queda disponible al instante desde la shell
del agente. El CLI de 1Password (op), gh, kubectl y terraform son
todos archivos únicos, y no necesitan más que esto.
Los paquetes de sistema van en la imagen
Algunas herramientas sí son paquetes de sistema de verdad. Cosas como
psql o imagemagick reparten archivos y bibliotecas compartidas por todo
el sistema de archivos, algo que un volumen no puede sostener. Esas tienen
que formar parte de la imagen.
Un argumento de build instala paquetes adicionales sin que tengas que escribir un Dockerfile:
docker build --build-arg PEPE_IMAGE_APT_PACKAGES="postgresql-client imagemagick" .
Si prefieres mantener tu propio Dockerfile, partir de nuestra imagen funciona igual de bien y sigue siendo una opción totalmente 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
Cualquiera de los dos caminos tiene el mismo costo: cada nueva release de Pepe implica reconstruir la imagen.
Por qué ffmpeg no está en la imagen
ffmpeg parece el paquete de sistema obvio para esta imagen, ya que
Telegram manda la voz en OGG/Opus y de algún lado tiene que salir la
transcripción. Pero ninguna de las dos rutas que realmente transcriben lo
necesita: una API de transcripción recibe el .ogg tal cual llega, sin
ninguna conversión, y faster-whisper decodifica a través de PyAV, que trae
sus propios códecs dentro del wheel. Esto se comprobó, no se supuso: un
archivo OGG/Opus se transcribió sin problema en un Debian limpio, sin
ffmpeg instalado en ningún lado. Solo el CLI de whisper.cpp sí llama a
ffmpeg por fuera, y esa ruta es opcional.
Incluirlo de todos modos salía mucho más caro de lo que valía. El paquete
ffmpeg de Debian arrastra 204 paquetes más y 121 MB de archivos (LLVM,
Mesa, un sintetizador de voz, un demostrador de teoremas), todo para
sostener una pila de aceleración de video por GPU que un contenedor
headless jamás va a usar. Al quitarlo, la imagen bajó de 945 MB a 408 MB,
unos 84 MB comprimidos, que es lo que en realidad te descargas por
arquitectura.
Si de todas formas quieres ffmpeg, ya sea para el CLI de whisper.cpp o
para cualquier otra cosa, instálalo con el argumento de build de arriba, o
deja un build estático de un solo archivo dentro de /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
Esto funciona, y se pierde con el siguiente docker rm. Úsalo solo para
confirmar que una herramienta resuelve tu problema, y después decide dónde
debe vivir de verdad: en el home del propio agente, si puede instalarla él
mismo, o en la imagen, si es un paquete de sistema.
Correr el contenedor como root (docker run --user root) es opcional, y
jamás el valor por defecto. Vale la pena repetirlo: eso no compra nada
duradero, porque lo que escribe apt sigue muriendo con el contenedor, así
que terminas de vuelta en una de las dos respuestas de arriba.
Compose
Pepe ya trae su propio docker-compose.yml, así que no tienes que escribir
nada:
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
puestos:
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 archivo .env al lado, nunca dentro del archivo
compose ni de 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 .env, y una línea
bajo environment: que lo nombre. Compose lee el .env para completar los
${...} del propio archivo compose, no para poblar el contenedor
directamente, así que una clave que solo esté en .env nunca llega a Pepe,
y terminas con un “no model configured” sin ninguna pista de por qué.
Agrega una línea por cada secreto que uses.
Después, desde ese mismo 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. En
cuanto agregas un dominio, TLS y un proxy inverso detrás, entran en juego
cuatro cosas más, y ninguna depende de Docker. Consulta Desplegar en un
servidor para ver 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 Compose:
docker pull ghcr.io/pepe-agent/pepe
docker rm -f pepe
docker run -d ... ghcr.io/pepe-agent/pepe # mismos volúmenes, mismas flags
La configuración, los agentes y las conversaciones vuelven junto con
/data. Las herramientas del agente, su home y toda la caché que contiene
vuelven con /tools, así que no reinstala nada en el primer mensaje. Lo
único que no vuelve son los paquetes instalados con apt, y para esos
justamente existe la imagen.
Un detalle que conviene tener claro: docker compose down detiene los
contenedores y deja los volúmenes intactos, que es lo que quieres. docker compose down -v, en cambio, también los borra, y con ellos toda la
instalación.
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 desde dentro.
Si solo necesitas un comando puntual de pepe en vez de una shell completa,
usa bin/pepe rpc con dispatch_attached/1: son los mismos comandos que
pepe corre en cualquier otro lado, pero adaptados para ejecutarse sin
riesgo contra un nodo que ya está sirviendo:
docker exec pepe bin/pepe rpc 'Mix.Tasks.Pepe.dispatch_attached(["agent", "list"])'
bin/pepe <comando> por sí solo no funciona aquí: el contenedor es una
release simple, y su despacho de CLI solo se activa en el binario Burrito,
que se distribuye aparte. bin/pepe rpc/remote es, en cualquier caso, la
puerta de entrada. dispatch_attached/1 existe porque bastantes comandos
(plugin install, eval, doctor, cron, y otros más, no solo
run/chat/tui) de otro modo terminarían cambiando ajustes globales
pensados para decidirse una sola vez, al arrancar. Eso no tiene ningún costo
en un proceso de CLI recién levantado que termina enseguida, pero sí lo
tendría en un nodo que ya está corriendo y va a seguir así.