feat(opencode): añadir stack del agente de código OpenCode

OpenCode como agente de desarrollo con acceso al filesystem del host,
expuesto vía Traefik con autenticación forward auth de Authentik.
Incluye configuración de systemd para modo web y ejemplos de .env.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduardo David Paredes Vara
2026-05-25 05:49:44 +00:00
parent c484e502c2
commit d771ea9dd5
9 changed files with 267 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
TZ=Europe/Madrid
OPENCODE_DOMAIN=ocode.sherlockhomeless.net
OPENCODE_HOME_PATH=/opt/opencode/home
OPENCODE_CONFIG_PATH=/opt/opencode/config
OPENCODE_WORKSPACE_PATH=/opt/opencode/workspace
OPENCODE_SERVER_USERNAME=colleague
OPENCODE_SERVER_PASSWORD=change-me-very-long
OPENCODE_DISABLE_AUTOUPDATE=true
DEEPSEEK_API_KEY=sk-change-me
TRAEFIK_DOCKER_NETWORK=proxy
TRAEFIK_ENTRYPOINT_SECURE=websecure
TRAEFIK_CERTRESOLVER=letsencrypt
TRAEFIK_AUTH_CHAIN=ths-authentik@docker,opencode-inject-auth@file
+11
View File
@@ -0,0 +1,11 @@
FROM node:22-bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends bash git openssh-client ca-certificates \
&& npm install -g opencode-ai \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
USER 1000:1000
ENTRYPOINT ["opencode"]
+107
View File
@@ -0,0 +1,107 @@
# OpenCode Web UI
Este stack ejecuta OpenCode en un contenedor con aislamiento fuerte para exponerlo solo por HTTPS detras de Traefik + Authentik.
## Objetivo
- Dominio sandboxed: `https://ocode.sherlockhomeless.net`
- Dominio clasico host/systemd: `https://opencode.sherlockhomeless.net`
- Acceso: solo usuarios autorizados en Authentik
- Aislamiento: sin privilegios, filesystem raiz de solo lectura y workspace dedicado
## Archivos
- Compose: `opencode/docker-compose.yml`
- Variables: `opencode/.env` (copiar desde `opencode/.env.example`)
- Middleware Traefik (inyecta `Authorization`): `/opt/traefik/dynamic/opencode-auth.yml`
- Config de legado host/systemd: `opencode/systemd/` y `opencode/traefik/opencode.yml`
## Despliegue en contenedor
```bash
cp opencode/.env.example opencode/.env
mkdir -p /opt/opencode/home /opt/opencode/config /opt/opencode/workspace
docker compose --env-file opencode/.env -f opencode/docker-compose.yml up -d
```
Para SSO directo (sin segundo login de OpenCode), crea tambien este middleware dinamico en Traefik:
```yaml
http:
middlewares:
opencode-inject-auth:
headers:
customRequestHeaders:
Authorization: "Basic <base64(usuario:password-opencode)>"
```
El router sandboxed usa la cadena `ths-authentik@docker,opencode-inject-auth@file`, asi Authentik valida al usuario y Traefik pasa despues la cabecera Basic al backend.
## Endurecimiento aplicado
- `user: 1000:1000`
- `cap_drop: [ALL]`
- `security_opt: [no-new-privileges:true]`
- `read_only: true`
- `/tmp` en `tmpfs` con `exec`, necesario para que Bun/OpenCode cargue librerias temporales
- Home persistente aislada en `/opt/opencode/home`
- Workspace aislado en `/opt/opencode/workspace`, montado dentro de la home como `/home/opencode/workspace`
- Sin `docker.sock` ni montajes del host fuera de los paths definidos
## Authentik (acceso solo a esta app)
En Authentik, cada dominio necesita su propio `Proxy Provider` publicado en el `authentik Embedded Outpost`:
1. Provider/app para `ocode.sherlockhomeless.net` (sandboxed).
2. Provider/app independiente para `opencode.sherlockhomeless.net` (clasico).
3. Ambos providers deben estar anadidos al `authentik Embedded Outpost`.
4. Para aislar el acceso sandboxed, crea un grupo exclusivo, por ejemplo `opencode-ocode-users`.
5. Crea una `Policy Binding` para la app sandboxed que permita solo ese grupo.
6. Crea el usuario del colega y anadelo solo a `opencode-ocode-users`.
7. No lo anadas a grupos con acceso a otras aplicaciones.
Si el clasico devuelve una pagina `Not Found` de Authentik, normalmente significa que el provider de `opencode.sherlockhomeless.net` no esta asociado al outpost o fue sustituido por el provider de `ocode.sherlockhomeless.net`.
## Arquitectura de modelos LLM
Hay dos instancias de OpenCode con modelos distintos:
| Instancia | Tipo | Modelos |
|-------------------------------|-------------- |----------------------------|
| `opencode.sherlockhomeless.net` | Host (systemd) | DeepSeek + **Gemma 4** (Ollama) |
| `ocode.sherlockhomeless.net` | Contenedor | Solo DeepSeek (colega) |
El contenedor sandboxed (`ocode`) **no** usa Ollama ni Gemma 4. Sus modelos ya estan configurados via `DEEPSEEK_API_KEY` en su `.env`.
### Modelos Gemma 4 via Ollama (para el host OpenCode)
Ollama corre en contenedor expuesto en `127.0.0.1:11434` (solo localhost).
```bash
cp ollama/.env.example ollama/.env
mkdir -p /opt/ollama/data
docker compose --env-file ollama/.env -f ollama/docker-compose.yml up -d
```
Pull de modelos:
```bash
docker exec -it ollama ollama pull gemma4:e2b # modelo principal
docker exec -it ollama ollama pull gemma4:e4b # opcional si e2b no basta
```
Activar provider en el OpenCode del host:
```bash
sudo install -m 0644 opencode/opencode.json.example /home/felidae/.config/opencode/opencode.json
sudo systemctl restart opencode-web
```
Modelos recomendados para este VPS (8 vCPU, 22 GiB RAM, sin GPU):
| Modelo | RAM aprox | Uso |
|------------- |---------- |--------------------- |
| `gemma4:e2b` | ~3-4 GiB | Modelo principal |
| `gemma4:e4b` | ~5-6 GiB | Solo si e2b no basta |
| `gemma4:26b` | ~16 GiB | No recomendable |
| `gemma4:31b` | ~20 GiB | No recomendable |
+43
View File
@@ -0,0 +1,43 @@
services:
opencode:
build:
context: .
dockerfile: Dockerfile
image: local/opencode-sandbox:latest
container_name: opencode
restart: unless-stopped
stop_grace_period: 30s
user: "1000:1000"
command: ["web", "--hostname", "0.0.0.0", "--port", "4096"]
env_file:
- .env
environment:
HOME: /home/opencode
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /.local:size=128m,mode=1777
- /tmp:size=256m,mode=1777,exec
volumes:
- ${OPENCODE_HOME_PATH}:/home/opencode:Z
- ${OPENCODE_CONFIG_PATH}:/home/opencode/.config/opencode:Z
- ${OPENCODE_WORKSPACE_PATH}:/home/opencode/workspace:Z
working_dir: /home/opencode/workspace
networks:
- proxy
labels:
traefik.enable: "true"
traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}"
traefik.http.routers.ocode.rule: "Host(`${OPENCODE_DOMAIN}`)"
traefik.http.routers.ocode.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.ocode.tls: "true"
traefik.http.routers.ocode.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
traefik.http.routers.ocode.middlewares: "crowdsec-bouncer@file,${TRAEFIK_AUTH_CHAIN}"
traefik.http.services.ocode.loadbalancer.server.port: "4096"
networks:
proxy:
external: true
+4
View File
@@ -0,0 +1,4 @@
OPENCODE_SERVER_USERNAME=opencode
OPENCODE_SERVER_PASSWORD=change-me
OPENCODE_DISABLE_AUTOUPDATE=true
+29
View File
@@ -0,0 +1,29 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama-vps": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama VPS",
"options": {
"baseURL": "http://localhost:11434/v1",
"apiKey": "ollama"
},
"models": {
"gemma4:e2b": {
"name": "Gemma 4 E2B VPS",
"limit": {
"context": 16384,
"output": 2048
}
},
"gemma4:e4b": {
"name": "Gemma 4 E4B VPS",
"limit": {
"context": 16384,
"output": 2048
}
}
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
[Unit]
Description=OpenCode Web UI
Documentation=https://opencode.ai/docs/web/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=felidae
Group=felidae
WorkingDirectory=/home/eduardo/Portainer_repo
EnvironmentFile=/etc/opencode-web.env
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=HOME=/home/felidae
ExecStart=/usr/local/bin/opencode-host web --hostname 10.0.0.1 --port 4096 --print-logs
Restart=always
RestartSec=5
# OpenCode necesita operar sobre repos y herramientas del host; se mantiene sin
# sandboxing restrictivo de systemd a proposito.
NoNewPrivileges=false
[Install]
WantedBy=multi-user.target
+6
View File
@@ -0,0 +1,6 @@
http:
middlewares:
opencode-inject-auth:
headers:
customRequestHeaders:
Authorization: "Basic <redacted-opencode-basic-auth>"
+27
View File
@@ -0,0 +1,27 @@
http:
routers:
opencode:
rule: Host(`opencode.sherlockhomeless.net`)
entryPoints:
- websecure
tls:
certResolver: letsencrypt
middlewares:
- crowdsec-bouncer@file
- ths-authentik@docker
- opencode-basic-auth
service: opencode
middlewares:
opencode-basic-auth:
headers:
customRequestHeaders:
# En el host desplegado se genera desde /etc/opencode-web.env.
# No versionar el secreto real en el repo.
Authorization: "Basic <base64(usuario:password-opencode)>"
services:
opencode:
loadBalancer:
servers:
- url: http://host.docker.internal:4096