mirror of
https://github.com/RGJorge/ContainerFlow.git
synced 2026-08-03 07:21:42 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc26e7dc1c |
@@ -14,7 +14,5 @@ data/
|
|||||||
|
|
||||||
# Local notes / marketing — not part of the public repo
|
# Local notes / marketing — not part of the public repo
|
||||||
linkdin.md
|
linkdin.md
|
||||||
recomendaciones.md
|
|
||||||
docs/reddit.md
|
|
||||||
# AI assistant context — internal, not for public repo
|
# AI assistant context — internal, not for public repo
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
|||||||
-432
@@ -1,432 +0,0 @@
|
|||||||
# ContainerFlow
|
|
||||||
|
|
||||||
[](https://github.com/RGJorge/ContainerFlow/actions/workflows/ci.yml)
|
|
||||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
||||||
[](https://github.com/RGJorge/containerflow/tags)
|
|
||||||

|
|
||||||

|
|
||||||
[](https://github.com/RGJorge/containerflow/commits/main)
|
|
||||||
|
|
||||||
**Léelo en otros idiomas**: [English](./README.md)
|
|
||||||
|
|
||||||
Real-time Docker architecture visualizer. Displays services, connections and metrics from all your Docker Compose projects in an interactive dashboard.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> *"Build what docker doesn't have the vision to build, and that Railway won't bring to local, without having to become either."*
|
|
||||||
>
|
|
||||||
> — u/dashingsauce, [on the launch thread](https://www.reddit.com/r/coolgithubprojects/comments/1ta8kak/comment/olecbxl/)
|
|
||||||
|
|
||||||
## Por qué ContainerFlow
|
|
||||||
|
|
||||||
Las herramientas existentes te muestran números. ContainerFlow además:
|
|
||||||
|
|
||||||
- **Visualiza arquitectura** — grafo interactivo con conexiones (app→db, app→cache, proxy→app) detectadas automáticamente, no solo una lista plana
|
|
||||||
- **Detecta config sub-óptima** — banners cuando un container corre sin límite de memoria, sin límite de CPU, o sin `restart: unless-stopped`. Te enseña buenas prácticas mientras lo usas
|
|
||||||
- **Mide memoria real** — resta page cache (active + inactive), no solo inactive como `docker stats`. Tu DB con buffers Postgres no muestra 98% falso
|
|
||||||
- **Multi-usuario seguro** — variable `ALLOWED_PATHS` para servidores compartidos: ves todo, solo tocas lo tuyo
|
|
||||||
- **80 MB de RAM, startup en 500ms** — Bun + Hono. Pesa una fracción de Portainer y arranca antes que Grafana
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/RGJorge/containerflow.git
|
|
||||||
cd containerflow
|
|
||||||
cp .env.example .env
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Abre `http://localhost:9470`. Listo.
|
|
||||||
|
|
||||||
Para desarrollo nativo (hot reload): `bun install && bun run dev`.
|
|
||||||
|
|
||||||
## Documentación
|
|
||||||
|
|
||||||
- **[docs/docker-guide.md](./docs/docker-guide.md)** — Guía rápida de Docker explicado para usar ContainerFlow: qué hace cada acción (Start, Stop, Restart, Recreate, Rebuild, Remove, Exec), restart policies, resource limits, volúmenes, healthchecks y preguntas frecuentes.
|
|
||||||
- **[docs/roadmap.md](./docs/roadmap.md)** — Roadmap del proyecto: qué está completo, qué viene, qué se descartó y por qué.
|
|
||||||
|
|
||||||
## Requisitos
|
|
||||||
|
|
||||||
- [Bun](https://bun.sh) >= 1.0
|
|
||||||
- Docker corriendo con acceso al socket (`/var/run/docker.sock`)
|
|
||||||
|
|
||||||
## Instalacion
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/RGJorge/containerflow.git
|
|
||||||
cd containerflow
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuracion
|
|
||||||
|
|
||||||
Copiar el archivo de ejemplo y editar:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Variables disponibles:
|
|
||||||
|
|
||||||
| Variable | Default | Descripcion |
|
|
||||||
|---|---|---|
|
|
||||||
| `PORT` | `9470` | Puerto del servidor |
|
|
||||||
| `AUTH_TOKEN` | _(vacio)_ | Token de autenticacion. Vacio = sin auth, solo localhost. Con valor = auth activado, acceso remoto |
|
|
||||||
| `DATA_DIR` | `./data` | Directorio para persistencia: SQLite de historial (`.dockerflow-stats.db`), config Discord (`.dockerflow-discord.json`), overrides por contenedor (`.dockerflow-container-settings.json`), posiciones de nodos y env file overrides. Se crea automaticamente al startup. En Docker se monta en `/app/data` via volumen `containerflow-data`. |
|
|
||||||
| `HOST_PROJECTS_DIR` | _(vacio)_ | Path adicional a montar para que `rebuild`/`remove` puedan leer compose files fuera de los defaults (`/home`, `/opt`, `/srv`, `/root`). Solo necesario para rutas no estandar (ej. `/data/apps`). |
|
|
||||||
| `ALLOWED_PATHS` | _(vacio)_ | **Vacio = todo accionable** (modo permisivo). Con valores = lista separada por `:` de prefijos; solo containers cuyo compose file este bajo alguno de estos paths pueden ejecutar acciones, el resto aparece con candado. Ver seccion [Seguridad](#seguridad). |
|
|
||||||
| `ALLOW_NON_COMPOSE` | `false` | **Solo aplica cuando `ALLOWED_PATHS` esta activo.** Si `ALLOWED_PATHS` esta vacio, esta variable no tiene efecto. Cuando aplica: `false` bloquea acciones sobre containers no-compose (corridos con `docker run` directo); `true` las permite. |
|
|
||||||
|
|
||||||
## Uso
|
|
||||||
|
|
||||||
### Desarrollo (hot reload)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Abre `http://localhost:9420` (Vite dev con hot reload, proxea API al backend en puerto 9470).
|
|
||||||
|
|
||||||
### Produccion (Docker)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Abre `http://localhost:9470`.
|
|
||||||
|
|
||||||
### Produccion (manual)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run build
|
|
||||||
bun run start
|
|
||||||
```
|
|
||||||
|
|
||||||
Abre `http://localhost:9470`.
|
|
||||||
|
|
||||||
### Modos de visualizacion
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver TODOS los containers Docker
|
|
||||||
bun run start -- --all
|
|
||||||
|
|
||||||
# Ver solo proyectos especificos
|
|
||||||
bun run start -- --projects=mi-proyecto,otro-proyecto
|
|
||||||
|
|
||||||
# Auto-detectar desde el directorio actual
|
|
||||||
bun run start
|
|
||||||
```
|
|
||||||
|
|
||||||
## Funcionalidades
|
|
||||||
|
|
||||||
- **Descubrimiento automatico** — detecta servicios via Docker socket, agrupa por proyecto o compose file
|
|
||||||
- **Conexiones inteligentes** — detecta relaciones app→database, app→cache, proxy→app, worker→broker
|
|
||||||
- **Metricas en tiempo real** — CPU y memoria por container, actualizado cada 3 segundos
|
|
||||||
- **Eventos Docker** — flash visual cuando un container inicia, para o reinicia
|
|
||||||
- **Panel de detalle** — click en un container para ver info, stats, variables de entorno y configuracion en tabs separados
|
|
||||||
- **Logs de containers** — logs en tiempo real con scroll automatico, filtro por stream (stdout/stderr) y opcion de copiar
|
|
||||||
- **Acciones sobre containers** — start, stop, restart, rebuild y remove directamente desde el panel
|
|
||||||
- **Ejecutar comandos** — terminal inline (`docker exec`) desde el DetailPanel con output, sin abrir SSH ni terminal externa
|
|
||||||
- **Toast de errores** — cuando una accion falla (rebuild que rompe, exec con exit code != 0, etc.) aparece un toast top-right con el error completo, copiable al clipboard
|
|
||||||
- **Control de acceso por path** — variable `ALLOWED_PATHS` permite restringir acciones a containers cuyo compose file este bajo rutas especificas. Ideal para servidores compartidos: ves todo, solo tocas lo tuyo. Los containers fuera de las rutas aparecen con candado
|
|
||||||
- **Recomendaciones de configuracion Docker** — banners de aviso en el DetailPanel cuando un container tiene config sub-optima: sin limite de memoria, sin limite de CPU, sin restart policy (`unless-stopped` recomendado). Ayuda al usuario a adoptar mejores practicas de Docker sin tener que recordarlas
|
|
||||||
- **Volumenes y mounts** — DetailPanel lista cada mount del container: tipo (bind / volume / tmpfs), source en el host, destination en el container, modo rw/ro. Util para debugging ("donde estan mis datos?", "es read-only?", "es persistente?")
|
|
||||||
- **Filtro de proyectos** — dropdown para mostrar/ocultar proyectos, persiste entre sesiones
|
|
||||||
- **Autenticacion** — pantalla de login con AUTH_TOKEN para acceso remoto seguro
|
|
||||||
- **Leyenda de conexiones** — colores por tipo: Database (azul), Cache (rojo), Broker (naranja), Proxy (verde)
|
|
||||||
- **Grupos visuales** — recuadros por proyecto/compose con titulo, archivo compose y conteo de containers
|
|
||||||
- **Menu contextual** — click derecho en un nodo para acciones rapidas
|
|
||||||
- **Pagina de monitoring** — historial de CPU/RAM por servicio (1h, 6h, 24h, 7d) persistido en SQLite, gráficas con sparkline, expand por contenedor, filtros por proyecto/servicio y feed de eventos Docker
|
|
||||||
- **Notificaciones Discord** — webhook configurable que avisa cambios de estado, alertas de recursos, acciones manuales y errores
|
|
||||||
- **Umbrales por contenedor** — overrides personalizados de CPU/MEM (con fallback a umbrales globales) y toggle de notificaciones por servicio
|
|
||||||
- **Pagina de settings** — configuracion de la aplicacion (auth, Discord, hosts Docker)
|
|
||||||
|
|
||||||
## Mejores prácticas
|
|
||||||
|
|
||||||
ContainerFlow no solo monitorea: detecta configuración sub-óptima de Docker y la marca con un banner ámbar en el DetailPanel del container afectado. La idea es ayudarte a adoptar buenas prácticas sin tener que recordarlas tú.
|
|
||||||
|
|
||||||
### Recomendaciones activas (warnings automáticos)
|
|
||||||
|
|
||||||
| Detección | Por qué importa | Cómo se ve en ContainerFlow |
|
|
||||||
|---|---|---|
|
|
||||||
| **Sin `memory_limit`** | Un container sin tope de RAM puede acaparar toda la memoria del host y tumbar a los demás (incluido el daemon). El kernel hace OOM kill aleatorio bajo presión. | Banner: "Sin límite de memoria configurado en Docker" |
|
|
||||||
| **Sin `cpu_quota`** | Similar al de memoria — un container puede saturar todos los núcleos. En multi-tenant esto es crítico, en single-tenant degrada la responsividad del host. | Banner: "Sin límite de CPU configurado en Docker" |
|
|
||||||
| **`restart: no` o vacío** | Si el proceso muere, el container queda muerto. En producción casi siempre quieres `unless-stopped` (reinicia si crashea, **NO** si lo paraste manualmente). | Banner: "Restart policy: none — el contenedor no se reiniciará automáticamente si se detiene" |
|
|
||||||
|
|
||||||
### Configuración recomendada (template)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml — buenas prácticas
|
|
||||||
services:
|
|
||||||
mi-app:
|
|
||||||
image: mi-app:latest
|
|
||||||
restart: unless-stopped # ← reinicia tras crashes, respeta stops manuales
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "0.5" # ← máximo medio núcleo
|
|
||||||
memory: 256M # ← tope absoluto, evita OOM del host
|
|
||||||
healthcheck: # ← detecta apps "vivas pero rotas"
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 30s
|
|
||||||
```
|
|
||||||
|
|
||||||
### Por qué ContainerFlow hace esto
|
|
||||||
|
|
||||||
La mayoría de tutoriales de Docker no mencionan estas configuraciones porque "funciona sin ellas". Pero en producción son la diferencia entre:
|
|
||||||
|
|
||||||
- **Sin límites**: un memory leak en un servicio tumba a TODO el servidor
|
|
||||||
- **Con límites**: el container se mata a sí mismo, el resto sigue vivo, las restart policies lo reviven
|
|
||||||
|
|
||||||
ContainerFlow te lo recuerda visualmente cada vez que abres el DetailPanel — no es spam, es contexto educativo solo donde aplica.
|
|
||||||
|
|
||||||
### En roadmap
|
|
||||||
|
|
||||||
- **Healthcheck recommendations**: detectar containers sin `HEALTHCHECK` y sugerir uno contextual según la imagen (postgres → `pg_isready`, redis → `redis-cli ping`, http app → `curl /health`, etc.)
|
|
||||||
- **Mounts no persistentes**: warning cuando una DB usa `tmpfs` o bind a directorio efímero
|
|
||||||
- **Versión latest**: warning cuando un container usa `image:latest` (no reproducible)
|
|
||||||
|
|
||||||
## Monitoreo e historial
|
|
||||||
|
|
||||||
ContainerFlow guarda un historial de métricas y notifica eventos importantes a Discord.
|
|
||||||
|
|
||||||
### Historial de métricas
|
|
||||||
|
|
||||||
- **Persistencia** — stats de CPU y memoria se almacenan en SQLite (`.dockerflow-stats.db`) cada vez que se hace polling de Docker (~3s)
|
|
||||||
- **Rangos** — `1h`, `6h`, `24h`, `7d` con buckets agregados (30s / 60s / 5min / 30min) para rendimiento
|
|
||||||
- **Retención** — auto-limpieza horaria descarta datos con más de 7 días y compacta la base con `VACUUM`
|
|
||||||
- **API** —
|
|
||||||
- `GET /api/stats/history?range=1h` — historial de todos los servicios
|
|
||||||
- `GET /api/stats/history/:uid?range=1h` — historial de un servicio específico
|
|
||||||
- **UI** — la página de monitoring (`MonitoringPage.tsx`) muestra una tarjeta por servicio con sparkline de CPU y MEM, valor actual, promedio y línea de umbral. Cada tarjeta puede expandirse para ver una gráfica más grande, y se filtra por proyecto y/o servicio (los filtros son acumulativos).
|
|
||||||
|
|
||||||
### Notificaciones Discord
|
|
||||||
|
|
||||||
Configurables desde **Settings → Discord Notifications**. Requiere un webhook URL que empiece por `https://discord.com/api/webhooks/`.
|
|
||||||
|
|
||||||
Eventos soportados (cada uno se puede activar/desactivar):
|
|
||||||
|
|
||||||
| Evento | Cuándo dispara |
|
|
||||||
|---|---|
|
|
||||||
| **Container State Changes** | `start`, `stop`, `die` (crash), `restart`, `health_status`. Los eventos `stop`/`die` se debouncean 15s para detectar reinicios y enviar un solo mensaje "Container Restarted" en lugar de stop+start separados |
|
|
||||||
| **Resource Alerts** | CPU o memoria de un contenedor supera el umbral (global o por-container) |
|
|
||||||
| **UI Actions** | Acción manual disparada desde el panel: start/stop/restart/rebuild/remove |
|
|
||||||
| **Action Errors** | Falló una acción ejecutada desde la UI (incluye el mensaje de error) |
|
|
||||||
|
|
||||||
Mecanismos anti-spam:
|
|
||||||
|
|
||||||
- **Cooldown global** — minutos mínimos entre alertas del mismo tipo+servicio (default `5 min`, configurable `1-60`)
|
|
||||||
- **Down reminder** — si un contenedor sigue caído, reenvía un recordatorio "Container Still Down" cada N minutos (default `5 min`)
|
|
||||||
- **Cola con rate limit** — 500ms mínimo entre webhooks; si Discord responde `429`, respeta el `Retry-After` y reintenta
|
|
||||||
- **Debounce de stop/die** — buffer de 15s para colapsar restart/redeploy en una sola notificación
|
|
||||||
|
|
||||||
Umbrales:
|
|
||||||
|
|
||||||
- **Globales** — CPU% y MEM% en Settings (default 50% / 60%)
|
|
||||||
- **Por contenedor** — desde la página de monitoring, click en el ícono ⚙️ de un servicio para abrir el panel inline. Permite:
|
|
||||||
- Activar/desactivar notificaciones para ese contenedor
|
|
||||||
- Override del umbral de CPU (drag del slider)
|
|
||||||
- Override del umbral de memoria
|
|
||||||
- Reset al valor global (X)
|
|
||||||
- Los overrides se persisten en `.dockerflow-container-settings.json` y se auto-guardan con debounce de 400ms
|
|
||||||
|
|
||||||
Botón **Test** en Settings envía un embed de prueba al webhook para verificar que funciona antes de habilitarlo.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
El proyecto usa [Vitest](https://vitest.dev/) para tests unitarios.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Correr todos los tests
|
|
||||||
bun run test
|
|
||||||
|
|
||||||
# Correr en modo watch (re-ejecuta al guardar)
|
|
||||||
bun run test:watch
|
|
||||||
|
|
||||||
# Verificar tipos TypeScript
|
|
||||||
bun run typecheck
|
|
||||||
```
|
|
||||||
|
|
||||||
Los tests cubren:
|
|
||||||
|
|
||||||
- **Logica de processing** (`src/client/hooks/processing.test.ts`) — sincronizacion de estados cuando se ejecutan acciones sobre containers (start/stop/restart), incluyendo manejo de estados crashed/dead, timeouts y minDuration
|
|
||||||
- **Deteccion de conexiones** (`src/server/docker.test.ts`) — descubrimiento de relaciones entre servicios por red compartida, clasificacion de servicios (infra, proxy, worker) y deduplicacion
|
|
||||||
|
|
||||||
## CI
|
|
||||||
|
|
||||||
GitHub Actions ejecuta automaticamente en cada push/PR a `main`:
|
|
||||||
|
|
||||||
1. Typecheck (errores de tipos)
|
|
||||||
2. Tests (Vitest)
|
|
||||||
3. Build (produccion)
|
|
||||||
|
|
||||||
Ver `.github/workflows/ci.yml`.
|
|
||||||
|
|
||||||
## Seguridad
|
|
||||||
|
|
||||||
### Red y autenticación
|
|
||||||
|
|
||||||
- **HTTPS obligatorio en produccion** — el token de autenticacion viaja en headers HTTP. Sin HTTPS, es texto plano visible en la red. Usa un reverse proxy con TLS (nginx, Caddy, Cloudflare Tunnel) delante de ContainerFlow.
|
|
||||||
- **Rate limiting** — incluido por defecto: 5 intentos fallidos por minuto por IP. Despues del limite, retorna `429 Too Many Requests`. Aplica tanto a la API REST como a la autenticacion WebSocket.
|
|
||||||
- **Acceso local por defecto** — sin `AUTH_TOKEN`, el servidor solo escucha en `127.0.0.1`. Con `AUTH_TOKEN`, escucha en `0.0.0.0` para acceso remoto.
|
|
||||||
|
|
||||||
### Privilegios del container
|
|
||||||
|
|
||||||
ContainerFlow es una herramienta privilegiada por diseño:
|
|
||||||
|
|
||||||
- **Docker socket** (`/var/run/docker.sock`) — acceso completo al daemon Docker. Equivalente a root en el host: puede crear containers privilegiados, montar cualquier path, leer/escribir el filesystem completo. Si ContainerFlow se compromete, el host está comprometido.
|
|
||||||
- **Mounts read-only del host** — el `docker-compose.yml` monta `/home`, `/opt`, `/srv` y `/root` como `:ro` para que las acciones `rebuild` y `exec` puedan leer compose files. Permite **lectura** de archivos en esos directorios (incluyendo SSH keys, git credentials, etc. de cualquier usuario en el sistema).
|
|
||||||
|
|
||||||
**Implicaciones en servidor multi-usuario:** si varios usuarios (`/home/jorge`, `/home/israel`, `/home/pedro`) tienen sus proyectos en el mismo host, ContainerFlow puede leer los archivos de todos ellos. El acceso al socket Docker hace que esto sea ruido relativo (cualquiera con el socket ya tiene acceso total al host), pero conviene estar consciente.
|
|
||||||
|
|
||||||
### Setup recomendado para single-user
|
|
||||||
|
|
||||||
Defaults actuales — convenientes y suficientes:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- containerflow-data:/app/data
|
|
||||||
- /home:/home:ro
|
|
||||||
- /opt:/opt:ro
|
|
||||||
- /srv:/srv:ro
|
|
||||||
- /root:/root:ro
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setup recomendado para multi-user / producción
|
|
||||||
|
|
||||||
Limita los mounts a directorios específicos donde tienes proyectos:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- containerflow-data:/app/data
|
|
||||||
# En vez de /home completo, solo tus proyectos
|
|
||||||
- /home/jorge/git:/home/jorge/git:ro
|
|
||||||
- /srv/apps:/srv/apps:ro
|
|
||||||
```
|
|
||||||
|
|
||||||
Esto reduce el blast radius si hay un bug que filtre paths.
|
|
||||||
|
|
||||||
### Setup recomendado para deploys compartidos: `ALLOWED_PATHS`
|
|
||||||
|
|
||||||
Si varios admins comparten un servidor y cada uno solo debe interactuar con sus propios containers, configura la variable `ALLOWED_PATHS` en `.env`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env
|
|
||||||
ALLOWED_PATHS=/home/jorge:/srv/myapp # rutas separadas por ":"
|
|
||||||
ALLOW_NON_COMPOSE=false # opcional, default false
|
|
||||||
```
|
|
||||||
|
|
||||||
**Comportamiento:**
|
|
||||||
|
|
||||||
- `ALLOWED_PATHS` vacío (default) → modo permisivo: todas las acciones disponibles para todos los containers
|
|
||||||
- `ALLOWED_PATHS` con valores → modo estricto:
|
|
||||||
- **Visualización, stats y logs:** siempre disponibles para todos los containers (la visibilidad viene del Docker socket)
|
|
||||||
- **Acciones** (start/stop/restart/rebuild/remove/exec): solo permitidas si el compose file del container está bajo una ruta permitida
|
|
||||||
- Los containers fuera de las rutas aparecen con un **ícono de candado 🔒** y todas sus acciones quedan deshabilitadas
|
|
||||||
- El menú contextual y el panel de detalle muestran un badge "View-only"
|
|
||||||
|
|
||||||
**`ALLOW_NON_COMPOSE`** controla qué pasa con containers corridos manualmente (`docker run` sin labels de compose):
|
|
||||||
|
|
||||||
- `false` (default): bloquea acciones — view-only para containers no-compose
|
|
||||||
- `true`: permite acciones sobre containers no-compose (útil si tienes containers utilitarios como Portainer agent, Watchtower, etc.)
|
|
||||||
|
|
||||||
**Ejemplo multi-usuario:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Servidor compartido con jorge, israel, pedro, nayeli
|
|
||||||
# Cada uno corre su propia instancia de ContainerFlow en puerto distinto
|
|
||||||
# El de jorge:
|
|
||||||
ALLOWED_PATHS=/home/jorge
|
|
||||||
|
|
||||||
# El de israel:
|
|
||||||
ALLOWED_PATHS=/home/israel
|
|
||||||
```
|
|
||||||
|
|
||||||
Cada uno ve **todos** los containers del servidor, pero solo puede hacer rebuild/restart/exec sobre los suyos.
|
|
||||||
|
|
||||||
**Endpoint relevante:** `GET /api/config` devuelve la config activa (consumido por el frontend para deshabilitar botones).
|
|
||||||
|
|
||||||
## Stack
|
|
||||||
|
|
||||||
| Componente | Tecnologia |
|
|
||||||
|---|---|
|
|
||||||
| Runtime | Bun |
|
|
||||||
| Server | Hono |
|
|
||||||
| Frontend | React 19 + Vite 6 |
|
|
||||||
| Grafos | @xyflow/react 12 |
|
|
||||||
| Estilos | Tailwind CSS 4 |
|
|
||||||
| Iconos | Lucide React |
|
|
||||||
| Docker API | dockerode |
|
|
||||||
| Comunicacion | WebSocket nativo |
|
|
||||||
| Tests | Vitest |
|
|
||||||
|
|
||||||
## Estructura
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
server/
|
|
||||||
index.ts — servidor Hono + WebSocket + CLI args + REST API
|
|
||||||
docker.ts — descubrimiento de servicios y conexiones
|
|
||||||
watcher.ts — polling de stats + stream de eventos Docker
|
|
||||||
stats-db.ts — SQLite de historial de stats (insert, query por rango, cleanup 7d)
|
|
||||||
discord.ts — webhooks Discord (state changes, resource alerts, cooldown, debounce, queue)
|
|
||||||
container-settings.ts — overrides por contenedor (umbrales y toggle de notificaciones)
|
|
||||||
client/
|
|
||||||
App.tsx — dashboard principal + login screen
|
|
||||||
main.tsx — entry point React
|
|
||||||
index.css — Tailwind + animaciones custom
|
|
||||||
nodes/
|
|
||||||
ServiceNode.tsx — nodo visual por container
|
|
||||||
GroupNode.tsx — header de grupo (proyecto/compose)
|
|
||||||
hooks/
|
|
||||||
useDocker.ts — hook WebSocket para datos en tiempo real + toast de errores de accion
|
|
||||||
useServerConfig.ts — fetch /api/config + helper canInteract() para ALLOWED_PATHS
|
|
||||||
useStatsHistory.ts — fetch del historial de stats por rango (1h/6h/24h/7d)
|
|
||||||
useStatsStore.ts — store en memoria para stats live
|
|
||||||
processing.ts — logica pura de estados processing
|
|
||||||
engine/
|
|
||||||
layout.ts — layout de grupos + grid + edges
|
|
||||||
components/
|
|
||||||
HeaderBar.tsx — barra superior con navegacion
|
|
||||||
EdgeLegend.tsx — leyenda de tipos de conexion
|
|
||||||
LoginScreen.tsx — pantalla de autenticacion
|
|
||||||
NodeContextMenu.tsx — menu contextual de nodos (con disable cuando locked)
|
|
||||||
OffsetEdge.tsx — edge custom con offset para evitar superposicion
|
|
||||||
Sparkline.tsx — gráfica de línea ligera para historial de stats
|
|
||||||
StatsCard.tsx — tarjeta de métrica con sparkline, hover, promedio y umbral
|
|
||||||
ThresholdBar.tsx — slider de umbral por contenedor con override/reset
|
|
||||||
ActionErrorToast.tsx — stack de toasts top-right para errores de acciones
|
|
||||||
panels/
|
|
||||||
DetailPanel.tsx — panel lateral con info, stats, env, config y logs
|
|
||||||
LogPanel.tsx — panel de logs por container
|
|
||||||
pages/
|
|
||||||
MonitoringPage.tsx — historial de CPU/RAM, eventos Docker y umbrales por contenedor
|
|
||||||
SettingsPage.tsx — configuracion (auth, Discord webhook, eventos, umbrales globales)
|
|
||||||
shared/
|
|
||||||
types.ts — tipos compartidos server/client
|
|
||||||
```
|
|
||||||
|
|
||||||
## Comunidad y contribuciones
|
|
||||||
|
|
||||||
ContainerFlow está en desarrollo activo (`v0.x`).
|
|
||||||
|
|
||||||
- 🐛 **Bug?** Abre un [issue](https://github.com/RGJorge/containerflow/issues/new?template=bug_report.md)
|
|
||||||
- 💡 **Idea?** Abre un [feature request](https://github.com/RGJorge/containerflow/issues/new?template=feature_request.md)
|
|
||||||
- 🔒 **Vulnerabilidad de seguridad?** Reporta privadamente — ver [SECURITY.md](SECURITY.md)
|
|
||||||
- 📜 **Code of Conduct** — ver [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
||||||
- 🛠 **Quiero contribuir código** — ver [CONTRIBUTING.md](CONTRIBUTING.md). Actualmente solo aceptamos issues; PRs se abrirán cuando el proyecto madure.
|
|
||||||
|
|
||||||
Si ContainerFlow te resulta útil, una ⭐ en GitHub ayuda a la visibilidad del proyecto.
|
|
||||||
|
|
||||||
## Licencia
|
|
||||||
|
|
||||||
Copyright (C) 2026 Jorge Gonzalez D. (RGJorge)
|
|
||||||
|
|
||||||
Este proyecto esta licenciado bajo **GNU Affero General Public License v3.0** (AGPL-3.0). Ver el archivo [LICENSE](LICENSE) para los terminos completos.
|
|
||||||
|
|
||||||
Para uso comercial con codigo cerrado, contactar para una licencia comercial: alteonx.servicios@gmail.com
|
|
||||||
@@ -1,31 +1,25 @@
|
|||||||
# ContainerFlow
|
# ContainerFlow
|
||||||
|
|
||||||
[](https://github.com/RGJorge/ContainerFlow/actions/workflows/ci.yml)
|
[](https://github.com/RGJorge/containerflow/actions/workflows/ci.yml)
|
||||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||||
[](https://github.com/RGJorge/containerflow/tags)
|
[](https://github.com/RGJorge/containerflow/tags)
|
||||||

|

|
||||||

|

|
||||||
[](https://github.com/RGJorge/containerflow/commits/main)
|
[](https://github.com/RGJorge/containerflow/commits/main)
|
||||||
|
|
||||||
**Read this in other languages**: [Español](./README.es.md)
|
|
||||||
|
|
||||||
Real-time Docker architecture visualizer. Displays services, connections and metrics from all your Docker Compose projects in an interactive dashboard.
|
Real-time Docker architecture visualizer. Displays services, connections and metrics from all your Docker Compose projects in an interactive dashboard.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
> *"Build what docker doesn't have the vision to build, and that Railway won't bring to local, without having to become either."*
|
## Por qué ContainerFlow
|
||||||
>
|
|
||||||
> — u/dashingsauce, [on the launch thread](https://www.reddit.com/r/coolgithubprojects/comments/1ta8kak/comment/olecbxl/)
|
|
||||||
|
|
||||||
## Why ContainerFlow
|
Las herramientas existentes te muestran números. ContainerFlow además:
|
||||||
|
|
||||||
Existing tools show you numbers. ContainerFlow also:
|
- **Visualiza arquitectura** — grafo interactivo con conexiones (app→db, app→cache, proxy→app) detectadas automáticamente, no solo una lista plana
|
||||||
|
- **Detecta config sub-óptima** — banners cuando un container corre sin límite de memoria, sin límite de CPU, o sin `restart: unless-stopped`. Te enseña buenas prácticas mientras lo usas
|
||||||
- **Visualizes architecture** — interactive graph with connections (app→db, app→cache, proxy→app) auto-detected, not just a flat list
|
- **Mide memoria real** — resta page cache (active + inactive), no solo inactive como `docker stats`. Tu DB con buffers Postgres no muestra 98% falso
|
||||||
- **Detects sub-optimal config** — banners when a container runs without memory limits, CPU limits, or `restart: unless-stopped`. Teaches good practices while you use it
|
- **Multi-usuario seguro** — variable `ALLOWED_PATHS` para servidores compartidos: ves todo, solo tocas lo tuyo
|
||||||
- **Measures real memory** — subtracts full page cache (active + inactive), not just inactive like `docker stats`. Your Postgres DB with hot buffers no longer reports a false 98%
|
- **80 MB de RAM, startup en 500ms** — Bun + Hono. Pesa una fracción de Portainer y arranca antes que Grafana
|
||||||
- **Multi-tenant via path scoping** — `ALLOWED_PATHS` env var for shared servers: see everything, only touch what's yours
|
|
||||||
- **80 MB RAM, ~500ms startup** — Bun + Hono. A fraction of Portainer's footprint, starts faster than Grafana
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@@ -36,21 +30,21 @@ cp .env.example .env
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://localhost:9470`. Done.
|
Abre `http://localhost:9470`. Listo.
|
||||||
|
|
||||||
For native development (hot reload): `bun install && bun run dev`.
|
Para desarrollo nativo (hot reload): `bun install && bun run dev`.
|
||||||
|
|
||||||
## Documentation
|
## Documentación
|
||||||
|
|
||||||
- **[docs/docker-guide.md](./docs/docker-guide.md)** — Docker quick guide for using ContainerFlow: what each action does (Start, Stop, Restart, Recreate, Rebuild, Remove, Exec), restart policies, resource limits, volumes, healthchecks and FAQs. (Currently in Spanish; English translation in progress.)
|
- **[docs/docker-guide.md](./docs/docker-guide.md)** — Guía rápida de Docker explicado para usar ContainerFlow: qué hace cada acción (Start, Stop, Restart, Recreate, Rebuild, Remove, Exec), restart policies, resource limits, volúmenes, healthchecks y preguntas frecuentes.
|
||||||
- **[docs/roadmap.md](./docs/roadmap.md)** — Project roadmap: what's done, what's coming, what was discarded and why.
|
- **[docs/roadmap.md](./docs/roadmap.md)** — Roadmap del proyecto: qué está completo, qué viene, qué se descartó y por qué.
|
||||||
|
|
||||||
## Requirements
|
## Requisitos
|
||||||
|
|
||||||
- [Bun](https://bun.sh) >= 1.0
|
- [Bun](https://bun.sh) >= 1.0
|
||||||
- Docker running with socket access (`/var/run/docker.sock`)
|
- Docker corriendo con acceso al socket (`/var/run/docker.sock`)
|
||||||
|
|
||||||
## Installation
|
## Instalacion
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/RGJorge/containerflow.git
|
git clone https://github.com/RGJorge/containerflow.git
|
||||||
@@ -58,115 +52,115 @@ cd containerflow
|
|||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuracion
|
||||||
|
|
||||||
Copy the example file and edit:
|
Copiar el archivo de ejemplo y editar:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Available variables:
|
Variables disponibles:
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Descripcion |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PORT` | `9470` | Server port |
|
| `PORT` | `9470` | Puerto del servidor |
|
||||||
| `AUTH_TOKEN` | _(empty)_ | Auth token. Empty = no auth, localhost only. Set = auth enabled, remote access allowed |
|
| `AUTH_TOKEN` | _(vacio)_ | Token de autenticacion. Vacio = sin auth, solo localhost. Con valor = auth activado, acceso remoto |
|
||||||
| `DATA_DIR` | `./data` | Persistence directory: stats history (`.dockerflow-stats.db`), Discord config (`.dockerflow-discord.json`), per-container overrides (`.dockerflow-container-settings.json`), node positions and env file overrides. Auto-created on startup. In Docker, mounted at `/app/data` via the `containerflow-data` volume. |
|
| `DATA_DIR` | `./data` | Directorio para persistencia: SQLite de historial (`.dockerflow-stats.db`), config Discord (`.dockerflow-discord.json`), overrides por contenedor (`.dockerflow-container-settings.json`), posiciones de nodos y env file overrides. Se crea automaticamente al startup. En Docker se monta en `/app/data` via volumen `containerflow-data`. |
|
||||||
| `HOST_PROJECTS_DIR` | _(empty)_ | Additional path to mount so `rebuild`/`remove` can read compose files outside the defaults (`/home`, `/opt`, `/srv`, `/root`). Only needed for non-standard paths (e.g. `/data/apps`). |
|
| `HOST_PROJECTS_DIR` | _(vacio)_ | Path adicional a montar para que `rebuild`/`remove` puedan leer compose files fuera de los defaults (`/home`, `/opt`, `/srv`, `/root`). Solo necesario para rutas no estandar (ej. `/data/apps`). |
|
||||||
| `ALLOWED_PATHS` | _(empty)_ | **Empty = everything actionable** (permissive mode). With values = `:`-separated list of prefixes; only containers whose compose file lives under one of these paths can execute actions, the rest appear with a lock icon. See [Security](#security) section. |
|
| `ALLOWED_PATHS` | _(vacio)_ | **Vacio = todo accionable** (modo permisivo). Con valores = lista separada por `:` de prefijos; solo containers cuyo compose file este bajo alguno de estos paths pueden ejecutar acciones, el resto aparece con candado. Ver seccion [Seguridad](#seguridad). |
|
||||||
| `ALLOW_NON_COMPOSE` | `false` | **Only applies when `ALLOWED_PATHS` is active.** If `ALLOWED_PATHS` is empty, this has no effect. When applicable: `false` blocks actions on non-compose containers (started with `docker run` directly); `true` allows them. |
|
| `ALLOW_NON_COMPOSE` | `false` | **Solo aplica cuando `ALLOWED_PATHS` esta activo.** Si `ALLOWED_PATHS` esta vacio, esta variable no tiene efecto. Cuando aplica: `false` bloquea acciones sobre containers no-compose (corridos con `docker run` directo); `true` las permite. |
|
||||||
|
|
||||||
## Usage
|
## Uso
|
||||||
|
|
||||||
### Development (hot reload)
|
### Desarrollo (hot reload)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Opens `http://localhost:9420` (Vite dev with hot reload, proxies API to the backend on port 9470).
|
Abre `http://localhost:9420` (Vite dev con hot reload, proxea API al backend en puerto 9470).
|
||||||
|
|
||||||
### Production (Docker)
|
### Produccion (Docker)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Opens `http://localhost:9470`.
|
Abre `http://localhost:9470`.
|
||||||
|
|
||||||
### Production (manual)
|
### Produccion (manual)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run build
|
bun run build
|
||||||
bun run start
|
bun run start
|
||||||
```
|
```
|
||||||
|
|
||||||
Opens `http://localhost:9470`.
|
Abre `http://localhost:9470`.
|
||||||
|
|
||||||
### Visualization modes
|
### Modos de visualizacion
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# View ALL Docker containers
|
# Ver TODOS los containers Docker
|
||||||
bun run start -- --all
|
bun run start -- --all
|
||||||
|
|
||||||
# View only specific projects
|
# Ver solo proyectos especificos
|
||||||
bun run start -- --projects=my-project,another-project
|
bun run start -- --projects=mi-proyecto,otro-proyecto
|
||||||
|
|
||||||
# Auto-detect from current directory
|
# Auto-detectar desde el directorio actual
|
||||||
bun run start
|
bun run start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Funcionalidades
|
||||||
|
|
||||||
- **Automatic discovery** — detects services via Docker socket, groups by project or compose file
|
- **Descubrimiento automatico** — detecta servicios via Docker socket, agrupa por proyecto o compose file
|
||||||
- **Smart connections** — detects app→database, app→cache, proxy→app, worker→broker relationships
|
- **Conexiones inteligentes** — detecta relaciones app→database, app→cache, proxy→app, worker→broker
|
||||||
- **Real-time metrics** — CPU and memory per container, refreshed every 3 seconds
|
- **Metricas en tiempo real** — CPU y memoria por container, actualizado cada 3 segundos
|
||||||
- **Docker events** — visual flash when a container starts, stops or restarts
|
- **Eventos Docker** — flash visual cuando un container inicia, para o reinicia
|
||||||
- **Detail panel** — click a container to see info, stats, env vars and config in separate tabs
|
- **Panel de detalle** — click en un container para ver info, stats, variables de entorno y configuracion en tabs separados
|
||||||
- **Container logs** — real-time logs with auto-scroll, stream filter (stdout/stderr) and copy option
|
- **Logs de containers** — logs en tiempo real con scroll automatico, filtro por stream (stdout/stderr) y opcion de copiar
|
||||||
- **Container actions** — start, stop, restart, rebuild, recreate and remove directly from the panel
|
- **Acciones sobre containers** — start, stop, restart, rebuild y remove directamente desde el panel
|
||||||
- **Execute commands** — inline terminal (`docker exec`) from the DetailPanel with output, no SSH or external terminal needed
|
- **Ejecutar comandos** — terminal inline (`docker exec`) desde el DetailPanel con output, sin abrir SSH ni terminal externa
|
||||||
- **Error toasts** — when an action fails (broken rebuild, exec with non-zero exit code, etc.) a top-right toast shows the full error, copyable to clipboard
|
- **Toast de errores** — cuando una accion falla (rebuild que rompe, exec con exit code != 0, etc.) aparece un toast top-right con el error completo, copiable al clipboard
|
||||||
- **Path-based access control** — `ALLOWED_PATHS` env var lets you restrict actions to containers whose compose file lives under specific paths. Ideal for shared servers: see everything, only touch what's yours. Containers outside the paths appear with a lock icon
|
- **Control de acceso por path** — variable `ALLOWED_PATHS` permite restringir acciones a containers cuyo compose file este bajo rutas especificas. Ideal para servidores compartidos: ves todo, solo tocas lo tuyo. Los containers fuera de las rutas aparecen con candado
|
||||||
- **Docker config recommendations** — warning banners in the DetailPanel when a container has sub-optimal config: no memory limit, no CPU limit, no restart policy (`unless-stopped` recommended). Helps users adopt good Docker practices without having to remember them
|
- **Recomendaciones de configuracion Docker** — banners de aviso en el DetailPanel cuando un container tiene config sub-optima: sin limite de memoria, sin limite de CPU, sin restart policy (`unless-stopped` recomendado). Ayuda al usuario a adoptar mejores practicas de Docker sin tener que recordarlas
|
||||||
- **Volumes and mounts** — DetailPanel lists each mount on the container: type (bind / volume / tmpfs), source on the host, destination in the container, rw/ro mode. Useful for debugging ("where's my data?", "is this read-only?", "is it persistent?")
|
- **Volumenes y mounts** — DetailPanel lista cada mount del container: tipo (bind / volume / tmpfs), source en el host, destination en el container, modo rw/ro. Util para debugging ("donde estan mis datos?", "es read-only?", "es persistente?")
|
||||||
- **Project filter** — dropdown to show/hide projects, persists across sessions
|
- **Filtro de proyectos** — dropdown para mostrar/ocultar proyectos, persiste entre sesiones
|
||||||
- **Authentication** — login screen with AUTH_TOKEN for secure remote access
|
- **Autenticacion** — pantalla de login con AUTH_TOKEN para acceso remoto seguro
|
||||||
- **Connection legend** — color-coded by type: Database (blue), Cache (red), Broker (orange), Proxy (green)
|
- **Leyenda de conexiones** — colores por tipo: Database (azul), Cache (rojo), Broker (naranja), Proxy (verde)
|
||||||
- **Visual groups** — boxes per project/compose with title, compose file and container count
|
- **Grupos visuales** — recuadros por proyecto/compose con titulo, archivo compose y conteo de containers
|
||||||
- **Context menu** — right-click on a node for quick actions
|
- **Menu contextual** — click derecho en un nodo para acciones rapidas
|
||||||
- **Monitoring page** — CPU/RAM history per service (1h, 6h, 24h, 7d) persisted in SQLite, sparkline charts, expand per container, filters by project/service, and a Docker events feed
|
- **Pagina de monitoring** — historial de CPU/RAM por servicio (1h, 6h, 24h, 7d) persistido en SQLite, gráficas con sparkline, expand por contenedor, filtros por proyecto/servicio y feed de eventos Docker
|
||||||
- **Discord notifications** — configurable webhook for state changes, resource alerts, manual actions and errors
|
- **Notificaciones Discord** — webhook configurable que avisa cambios de estado, alertas de recursos, acciones manuales y errores
|
||||||
- **Per-container thresholds** — custom CPU/MEM overrides (with fallback to global thresholds) and notification toggle per service
|
- **Umbrales por contenedor** — overrides personalizados de CPU/MEM (con fallback a umbrales globales) y toggle de notificaciones por servicio
|
||||||
- **Settings page** — application configuration (auth, Discord, Docker hosts)
|
- **Pagina de settings** — configuracion de la aplicacion (auth, Discord, hosts Docker)
|
||||||
|
|
||||||
## Best practices
|
## Mejores prácticas
|
||||||
|
|
||||||
ContainerFlow doesn't just monitor: it detects sub-optimal Docker configuration and flags it with an amber banner in the affected container's DetailPanel. The idea is to help you adopt good practices without having to remember them.
|
ContainerFlow no solo monitorea: detecta configuración sub-óptima de Docker y la marca con un banner ámbar en el DetailPanel del container afectado. La idea es ayudarte a adoptar buenas prácticas sin tener que recordarlas tú.
|
||||||
|
|
||||||
### Active recommendations (automatic warnings)
|
### Recomendaciones activas (warnings automáticos)
|
||||||
|
|
||||||
| Detection | Why it matters | How it shows in ContainerFlow |
|
| Detección | Por qué importa | Cómo se ve en ContainerFlow |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **No `memory_limit`** | A container without a RAM cap can hog all host memory and take down everything else (including the daemon). The kernel does random OOM kills under pressure. | Banner: "No memory limit configured in Docker" |
|
| **Sin `memory_limit`** | Un container sin tope de RAM puede acaparar toda la memoria del host y tumbar a los demás (incluido el daemon). El kernel hace OOM kill aleatorio bajo presión. | Banner: "Sin límite de memoria configurado en Docker" |
|
||||||
| **No `cpu_quota`** | Similar to memory — a container can saturate all cores. Critical in multi-tenant, degrades host responsiveness in single-tenant. | Banner: "No CPU limit configured in Docker" |
|
| **Sin `cpu_quota`** | Similar al de memoria — un container puede saturar todos los núcleos. En multi-tenant esto es crítico, en single-tenant degrada la responsividad del host. | Banner: "Sin límite de CPU configurado en Docker" |
|
||||||
| **`restart: no` or empty** | If the process dies, the container stays dead. In production you almost always want `unless-stopped` (restarts on crash, does **NOT** if you stopped it manually). | Banner: "Restart policy: none — the container will not restart automatically if stopped" |
|
| **`restart: no` o vacío** | Si el proceso muere, el container queda muerto. En producción casi siempre quieres `unless-stopped` (reinicia si crashea, **NO** si lo paraste manualmente). | Banner: "Restart policy: none — el contenedor no se reiniciará automáticamente si se detiene" |
|
||||||
|
|
||||||
### Recommended config (template)
|
### Configuración recomendada (template)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml — best practices
|
# docker-compose.yml — buenas prácticas
|
||||||
services:
|
services:
|
||||||
my-app:
|
mi-app:
|
||||||
image: my-app:latest
|
image: mi-app:latest
|
||||||
restart: unless-stopped # ← restarts on crash, respects manual stops
|
restart: unless-stopped # ← reinicia tras crashes, respeta stops manuales
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpus: "0.5" # ← maximum half a core
|
cpus: "0.5" # ← máximo medio núcleo
|
||||||
memory: 256M # ← absolute cap, prevents host OOM
|
memory: 256M # ← tope absoluto, evita OOM del host
|
||||||
healthcheck: # ← detects "alive but broken" apps
|
healthcheck: # ← detecta apps "vivas pero rotas"
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
@@ -174,118 +168,117 @@ services:
|
|||||||
start_period: 30s
|
start_period: 30s
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why ContainerFlow does this
|
### Por qué ContainerFlow hace esto
|
||||||
|
|
||||||
Most Docker tutorials don't mention these settings because "it works without them". But in production they're the difference between:
|
La mayoría de tutoriales de Docker no mencionan estas configuraciones porque "funciona sin ellas". Pero en producción son la diferencia entre:
|
||||||
|
|
||||||
- **No limits**: a memory leak in one service takes down the ENTIRE server
|
- **Sin límites**: un memory leak en un servicio tumba a TODO el servidor
|
||||||
- **With limits**: the container kills itself, the rest stays alive, restart policies revive it
|
- **Con límites**: el container se mata a sí mismo, el resto sigue vivo, las restart policies lo reviven
|
||||||
|
|
||||||
ContainerFlow reminds you visually each time you open the DetailPanel — not spam, just educational context where it applies.
|
ContainerFlow te lo recuerda visualmente cada vez que abres el DetailPanel — no es spam, es contexto educativo solo donde aplica.
|
||||||
|
|
||||||
### On the roadmap
|
### En roadmap
|
||||||
|
|
||||||
- **Healthcheck recommendations**: detect containers without `HEALTHCHECK` and suggest a contextual one based on the image (postgres → `pg_isready`, redis → `redis-cli ping`, http app → `curl /health`, etc.)
|
- **Healthcheck recommendations**: detectar containers sin `HEALTHCHECK` y sugerir uno contextual según la imagen (postgres → `pg_isready`, redis → `redis-cli ping`, http app → `curl /health`, etc.)
|
||||||
- **Non-persistent mounts**: warning when a DB uses `tmpfs` or binds to an ephemeral directory
|
- **Mounts no persistentes**: warning cuando una DB usa `tmpfs` o bind a directorio efímero
|
||||||
- **`:latest` tag**: warning when a container uses `image:latest` (not reproducible)
|
- **Versión latest**: warning cuando un container usa `image:latest` (no reproducible)
|
||||||
|
|
||||||
## Monitoring and history
|
## Monitoreo e historial
|
||||||
|
|
||||||
ContainerFlow keeps a metrics history and notifies important events to Discord.
|
ContainerFlow guarda un historial de métricas y notifica eventos importantes a Discord.
|
||||||
|
|
||||||
### Metrics history
|
### Historial de métricas
|
||||||
|
|
||||||
- **Persistence** — CPU and memory stats stored in SQLite (`.dockerflow-stats.db`) on every Docker polling cycle (~3s)
|
- **Persistencia** — stats de CPU y memoria se almacenan en SQLite (`.dockerflow-stats.db`) cada vez que se hace polling de Docker (~3s)
|
||||||
- **Ranges** — `1h`, `6h`, `24h`, `7d` with aggregated buckets (30s / 60s / 5min / 30min) for performance
|
- **Rangos** — `1h`, `6h`, `24h`, `7d` con buckets agregados (30s / 60s / 5min / 30min) para rendimiento
|
||||||
- **Retention** — hourly auto-cleanup drops data older than 7 days and compacts the database with `VACUUM`
|
- **Retención** — auto-limpieza horaria descarta datos con más de 7 días y compacta la base con `VACUUM`
|
||||||
- **API** —
|
- **API** —
|
||||||
- `GET /api/stats/history?range=1h` — history of all services
|
- `GET /api/stats/history?range=1h` — historial de todos los servicios
|
||||||
- `GET /api/stats/history/:uid?range=1h` — history of a specific service
|
- `GET /api/stats/history/:uid?range=1h` — historial de un servicio específico
|
||||||
- **UI** — the monitoring page (`MonitoringPage.tsx`) shows a card per service with CPU and MEM sparklines, current value, average and threshold line. Each card can be expanded for a larger chart, and filtered by project and/or service (filters are cumulative).
|
- **UI** — la página de monitoring (`MonitoringPage.tsx`) muestra una tarjeta por servicio con sparkline de CPU y MEM, valor actual, promedio y línea de umbral. Cada tarjeta puede expandirse para ver una gráfica más grande, y se filtra por proyecto y/o servicio (los filtros son acumulativos).
|
||||||
|
|
||||||
### Discord notifications
|
### Notificaciones Discord
|
||||||
|
|
||||||
Configured from **Settings → Discord Notifications**. Requires a webhook URL starting with `https://discord.com/api/webhooks/`.
|
Configurables desde **Settings → Discord Notifications**. Requiere un webhook URL que empiece por `https://discord.com/api/webhooks/`.
|
||||||
|
|
||||||
Supported events (each can be toggled on/off):
|
Eventos soportados (cada uno se puede activar/desactivar):
|
||||||
|
|
||||||
| Event | When it fires |
|
| Evento | Cuándo dispara |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Container State Changes** | `start`, `stop`, `die` (crash), `restart`, `health_status`. `stop`/`die` events are debounced 15s to detect restarts and send a single "Container Restarted" message instead of separate stop+start |
|
| **Container State Changes** | `start`, `stop`, `die` (crash), `restart`, `health_status`. Los eventos `stop`/`die` se debouncean 15s para detectar reinicios y enviar un solo mensaje "Container Restarted" en lugar de stop+start separados |
|
||||||
| **Resource Alerts** | A container's CPU or memory exceeds the threshold (global or per-container) |
|
| **Resource Alerts** | CPU o memoria de un contenedor supera el umbral (global o por-container) |
|
||||||
| **UI Actions** | Manual action triggered from the panel: start/stop/restart/rebuild/remove |
|
| **UI Actions** | Acción manual disparada desde el panel: start/stop/restart/rebuild/remove |
|
||||||
| **Action Errors** | An action executed from the UI failed (includes the error message) |
|
| **Action Errors** | Falló una acción ejecutada desde la UI (incluye el mensaje de error) |
|
||||||
|
|
||||||
Anti-spam mechanisms:
|
Mecanismos anti-spam:
|
||||||
|
|
||||||
- **Global cooldown** — minimum minutes between alerts of the same type+service (default `5 min`, configurable `1-60`)
|
- **Cooldown global** — minutos mínimos entre alertas del mismo tipo+servicio (default `5 min`, configurable `1-60`)
|
||||||
- **Down reminder** — if a container stays down, resends a "Container Still Down" reminder every N minutes (default `5 min`)
|
- **Down reminder** — si un contenedor sigue caído, reenvía un recordatorio "Container Still Down" cada N minutos (default `5 min`)
|
||||||
- **Queue with rate limit** — 500ms minimum between webhooks; if Discord responds `429`, respects `Retry-After` and retries
|
- **Cola con rate limit** — 500ms mínimo entre webhooks; si Discord responde `429`, respeta el `Retry-After` y reintenta
|
||||||
- **Stop/die debounce** — 15s buffer to collapse restart/redeploy into a single notification
|
- **Debounce de stop/die** — buffer de 15s para colapsar restart/redeploy en una sola notificación
|
||||||
|
|
||||||
Thresholds:
|
Umbrales:
|
||||||
|
|
||||||
- **Globals** — CPU% and MEM% in Settings (default 50% / 60%)
|
- **Globales** — CPU% y MEM% en Settings (default 50% / 60%)
|
||||||
- **Per container** — from the monitoring page, click the ⚙️ icon on a service to open the inline panel. Allows:
|
- **Por contenedor** — desde la página de monitoring, click en el ícono ⚙️ de un servicio para abrir el panel inline. Permite:
|
||||||
- Enable/disable notifications for that container
|
- Activar/desactivar notificaciones para ese contenedor
|
||||||
- CPU threshold override (slider)
|
- Override del umbral de CPU (drag del slider)
|
||||||
- Memory threshold override
|
- Override del umbral de memoria
|
||||||
- Reset to global value (X)
|
- Reset al valor global (X)
|
||||||
- Overrides persist in `.dockerflow-container-settings.json` and auto-save with 400ms debounce
|
- Los overrides se persisten en `.dockerflow-container-settings.json` y se auto-guardan con debounce de 400ms
|
||||||
|
|
||||||
A **Test** button in Settings sends a test embed to the webhook to verify it works before enabling.
|
Botón **Test** en Settings envía un embed de prueba al webhook para verificar que funciona antes de habilitarlo.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
The project uses [Vitest](https://vitest.dev/) for unit tests.
|
El proyecto usa [Vitest](https://vitest.dev/) para tests unitarios.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Correr todos los tests
|
||||||
bun run test
|
bun run test
|
||||||
|
|
||||||
# Watch mode (re-runs on save)
|
# Correr en modo watch (re-ejecuta al guardar)
|
||||||
bun run test:watch
|
bun run test:watch
|
||||||
|
|
||||||
# Type-check TypeScript
|
# Verificar tipos TypeScript
|
||||||
bun run typecheck
|
bun run typecheck
|
||||||
```
|
```
|
||||||
|
|
||||||
Tests cover:
|
Los tests cubren:
|
||||||
|
|
||||||
- **Processing logic** (`src/client/hooks/processing.test.ts`) — state sync when actions are executed on containers (start/stop/restart), including crashed/dead states, timeouts and minDuration handling
|
- **Logica de processing** (`src/client/hooks/processing.test.ts`) — sincronizacion de estados cuando se ejecutan acciones sobre containers (start/stop/restart), incluyendo manejo de estados crashed/dead, timeouts y minDuration
|
||||||
- **Connection detection** (`src/server/docker.test.ts`) — discovery of service relationships via shared networks, service classification (infra, proxy, worker) and deduplication
|
- **Deteccion de conexiones** (`src/server/docker.test.ts`) — descubrimiento de relaciones entre servicios por red compartida, clasificacion de servicios (infra, proxy, worker) y deduplicacion
|
||||||
- **Memory breakdown calc** (`src/server/watcher.test.ts`) — `computeMemoryBreakdown()` covering cgroup v1 and v2, fallback paths and edge cases
|
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
GitHub Actions runs automatically on every push/PR to `main`:
|
GitHub Actions ejecuta automaticamente en cada push/PR a `main`:
|
||||||
|
|
||||||
1. Typecheck (type errors)
|
1. Typecheck (errores de tipos)
|
||||||
2. Tests (Vitest)
|
2. Tests (Vitest)
|
||||||
3. Build (production)
|
3. Build (produccion)
|
||||||
|
|
||||||
See `.github/workflows/ci.yml`.
|
Ver `.github/workflows/ci.yml`.
|
||||||
|
|
||||||
## Security
|
## Seguridad
|
||||||
|
|
||||||
### Network and authentication
|
### Red y autenticación
|
||||||
|
|
||||||
- **HTTPS required in production** — the auth token travels in HTTP headers. Without HTTPS, it's plaintext visible on the network. Use a TLS-terminating reverse proxy (nginx, Caddy, Cloudflare Tunnel) in front of ContainerFlow.
|
- **HTTPS obligatorio en produccion** — el token de autenticacion viaja en headers HTTP. Sin HTTPS, es texto plano visible en la red. Usa un reverse proxy con TLS (nginx, Caddy, Cloudflare Tunnel) delante de ContainerFlow.
|
||||||
- **Rate limiting** — included by default: 5 failed attempts per minute per IP. After the limit, returns `429 Too Many Requests`. Applies to both REST API and WebSocket authentication.
|
- **Rate limiting** — incluido por defecto: 5 intentos fallidos por minuto por IP. Despues del limite, retorna `429 Too Many Requests`. Aplica tanto a la API REST como a la autenticacion WebSocket.
|
||||||
- **Local access by default** — without `AUTH_TOKEN`, the server only listens on `127.0.0.1`. With `AUTH_TOKEN`, it listens on `0.0.0.0` for remote access.
|
- **Acceso local por defecto** — sin `AUTH_TOKEN`, el servidor solo escucha en `127.0.0.1`. Con `AUTH_TOKEN`, escucha en `0.0.0.0` para acceso remoto.
|
||||||
|
|
||||||
### Container privileges
|
### Privilegios del container
|
||||||
|
|
||||||
ContainerFlow is a privileged tool by design:
|
ContainerFlow es una herramienta privilegiada por diseño:
|
||||||
|
|
||||||
- **Docker socket** (`/var/run/docker.sock`) — full daemon access. Equivalent to root on the host: can create privileged containers, mount any path, read/write the entire filesystem. If ContainerFlow is compromised, the host is compromised.
|
- **Docker socket** (`/var/run/docker.sock`) — acceso completo al daemon Docker. Equivalente a root en el host: puede crear containers privilegiados, montar cualquier path, leer/escribir el filesystem completo. Si ContainerFlow se compromete, el host está comprometido.
|
||||||
- **Read-only host mounts** — `docker-compose.yml` mounts `/home`, `/opt`, `/srv` and `/root` as `:ro` so the `rebuild` and `exec` actions can read compose files. Allows **read access** to files in those directories (including SSH keys, git credentials, etc. of any user on the system).
|
- **Mounts read-only del host** — el `docker-compose.yml` monta `/home`, `/opt`, `/srv` y `/root` como `:ro` para que las acciones `rebuild` y `exec` puedan leer compose files. Permite **lectura** de archivos en esos directorios (incluyendo SSH keys, git credentials, etc. de cualquier usuario en el sistema).
|
||||||
|
|
||||||
**Implications on a multi-user server:** if multiple users (`/home/jorge`, `/home/israel`, `/home/pedro`) have projects on the same host, ContainerFlow can read all their files. Docker socket access makes this relatively moot (anyone with the socket already has full host access), but worth being aware of.
|
**Implicaciones en servidor multi-usuario:** si varios usuarios (`/home/jorge`, `/home/israel`, `/home/pedro`) tienen sus proyectos en el mismo host, ContainerFlow puede leer los archivos de todos ellos. El acceso al socket Docker hace que esto sea ruido relativo (cualquiera con el socket ya tiene acceso total al host), pero conviene estar consciente.
|
||||||
|
|
||||||
### Recommended setup for single-user
|
### Setup recomendado para single-user
|
||||||
|
|
||||||
Current defaults — convenient and sufficient:
|
Defaults actuales — convenientes y suficientes:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
volumes:
|
volumes:
|
||||||
@@ -297,141 +290,137 @@ volumes:
|
|||||||
- /root:/root:ro
|
- /root:/root:ro
|
||||||
```
|
```
|
||||||
|
|
||||||
### Recommended setup for multi-user / production
|
### Setup recomendado para multi-user / producción
|
||||||
|
|
||||||
Limit mounts to specific directories where you have projects:
|
Limita los mounts a directorios específicos donde tienes proyectos:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- containerflow-data:/app/data
|
- containerflow-data:/app/data
|
||||||
# Instead of all of /home, only your projects
|
# En vez de /home completo, solo tus proyectos
|
||||||
- /home/jorge/git:/home/jorge/git:ro
|
- /home/jorge/git:/home/jorge/git:ro
|
||||||
- /srv/apps:/srv/apps:ro
|
- /srv/apps:/srv/apps:ro
|
||||||
```
|
```
|
||||||
|
|
||||||
This reduces blast radius if there's a bug that leaks paths.
|
Esto reduce el blast radius si hay un bug que filtre paths.
|
||||||
|
|
||||||
### Recommended setup for shared deploys: `ALLOWED_PATHS`
|
### Setup recomendado para deploys compartidos: `ALLOWED_PATHS`
|
||||||
|
|
||||||
If multiple admins share a server and each should only interact with their own containers, configure the `ALLOWED_PATHS` env var in `.env`:
|
Si varios admins comparten un servidor y cada uno solo debe interactuar con sus propios containers, configura la variable `ALLOWED_PATHS` en `.env`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# .env
|
# .env
|
||||||
ALLOWED_PATHS=/home/jorge:/srv/myapp # paths separated by ":"
|
ALLOWED_PATHS=/home/jorge:/srv/myapp # rutas separadas por ":"
|
||||||
ALLOW_NON_COMPOSE=false # optional, default false
|
ALLOW_NON_COMPOSE=false # opcional, default false
|
||||||
```
|
```
|
||||||
|
|
||||||
**Behavior:**
|
**Comportamiento:**
|
||||||
|
|
||||||
- `ALLOWED_PATHS` empty (default) → permissive mode: all actions available for all containers
|
- `ALLOWED_PATHS` vacío (default) → modo permisivo: todas las acciones disponibles para todos los containers
|
||||||
- `ALLOWED_PATHS` with values → strict mode:
|
- `ALLOWED_PATHS` con valores → modo estricto:
|
||||||
- **Visualization, stats and logs:** always available for all containers (visibility comes from the Docker socket)
|
- **Visualización, stats y logs:** siempre disponibles para todos los containers (la visibilidad viene del Docker socket)
|
||||||
- **Actions** (start/stop/restart/rebuild/remove/exec): only allowed if the container's compose file is under an allowed path
|
- **Acciones** (start/stop/restart/rebuild/remove/exec): solo permitidas si el compose file del container está bajo una ruta permitida
|
||||||
- Containers outside the paths appear with a **lock icon 🔒** and all their actions are disabled
|
- Los containers fuera de las rutas aparecen con un **ícono de candado 🔒** y todas sus acciones quedan deshabilitadas
|
||||||
- The context menu and detail panel show a "View-only" badge
|
- El menú contextual y el panel de detalle muestran un badge "View-only"
|
||||||
|
|
||||||
**`ALLOW_NON_COMPOSE`** controls what happens with manually-run containers (`docker run` without compose labels):
|
**`ALLOW_NON_COMPOSE`** controla qué pasa con containers corridos manualmente (`docker run` sin labels de compose):
|
||||||
|
|
||||||
- `false` (default): blocks actions — view-only for non-compose containers
|
- `false` (default): bloquea acciones — view-only para containers no-compose
|
||||||
- `true`: allows actions on non-compose containers (useful if you have utility containers like Portainer agent, Watchtower, etc.)
|
- `true`: permite acciones sobre containers no-compose (útil si tienes containers utilitarios como Portainer agent, Watchtower, etc.)
|
||||||
|
|
||||||
**Multi-user example:**
|
**Ejemplo multi-usuario:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Shared server with jorge, israel, pedro, nayeli
|
# Servidor compartido con jorge, israel, pedro, nayeli
|
||||||
# Each runs their own ContainerFlow instance on a different port
|
# Cada uno corre su propia instancia de ContainerFlow en puerto distinto
|
||||||
# jorge's:
|
# El de jorge:
|
||||||
ALLOWED_PATHS=/home/jorge
|
ALLOWED_PATHS=/home/jorge
|
||||||
|
|
||||||
# israel's:
|
# El de israel:
|
||||||
ALLOWED_PATHS=/home/israel
|
ALLOWED_PATHS=/home/israel
|
||||||
```
|
```
|
||||||
|
|
||||||
Each one sees **all** the server's containers, but can only rebuild/restart/exec their own.
|
Cada uno ve **todos** los containers del servidor, pero solo puede hacer rebuild/restart/exec sobre los suyos.
|
||||||
|
|
||||||
**Relevant endpoint:** `GET /api/config` returns the active config (consumed by the frontend to disable buttons).
|
**Endpoint relevante:** `GET /api/config` devuelve la config activa (consumido por el frontend para deshabilitar botones).
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
| Component | Technology |
|
| Componente | Tecnologia |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Runtime | Bun |
|
| Runtime | Bun |
|
||||||
| Server | Hono |
|
| Server | Hono |
|
||||||
| Frontend | React 19 + Vite 6 |
|
| Frontend | React 19 + Vite 6 |
|
||||||
| Graph | @xyflow/react 12 |
|
| Grafos | @xyflow/react 12 |
|
||||||
| Styles | Tailwind CSS 4 |
|
| Estilos | Tailwind CSS 4 |
|
||||||
| Icons | Lucide React |
|
| Iconos | Lucide React |
|
||||||
| Docker API | dockerode |
|
| Docker API | dockerode |
|
||||||
| Communication | Native WebSocket |
|
| Comunicacion | WebSocket nativo |
|
||||||
| Tests | Vitest |
|
| Tests | Vitest |
|
||||||
|
|
||||||
## Structure
|
## Estructura
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
server/
|
server/
|
||||||
index.ts — Hono server + WebSocket + CLI args + REST API
|
index.ts — servidor Hono + WebSocket + CLI args + REST API
|
||||||
docker.ts — service and connection discovery
|
docker.ts — descubrimiento de servicios y conexiones
|
||||||
watcher.ts — stats polling + Docker events stream (computeMemoryBreakdown)
|
watcher.ts — polling de stats + stream de eventos Docker
|
||||||
stats-db.ts — SQLite stats history (insert, query by range, 7d cleanup)
|
stats-db.ts — SQLite de historial de stats (insert, query por rango, cleanup 7d)
|
||||||
events-db.ts — SQLite events_log + notifications_log
|
discord.ts — webhooks Discord (state changes, resource alerts, cooldown, debounce, queue)
|
||||||
discord.ts — Discord webhooks (state changes, resource alerts, cooldown, debounce, queue)
|
container-settings.ts — overrides por contenedor (umbrales y toggle de notificaciones)
|
||||||
container-settings.ts — per-container overrides (thresholds and notification toggle)
|
|
||||||
client/
|
client/
|
||||||
App.tsx — main dashboard + login screen
|
App.tsx — dashboard principal + login screen
|
||||||
main.tsx — React entry point
|
main.tsx — entry point React
|
||||||
index.css — Tailwind + custom animations
|
index.css — Tailwind + animaciones custom
|
||||||
i18n.tsx — translations EN + ES, useT() hook
|
|
||||||
nodes/
|
nodes/
|
||||||
ServiceNode.tsx — visual node per container
|
ServiceNode.tsx — nodo visual por container
|
||||||
GroupNode.tsx — group header (project/compose)
|
GroupNode.tsx — header de grupo (proyecto/compose)
|
||||||
hooks/
|
hooks/
|
||||||
useDocker.ts — WebSocket hook for real-time data + action error toasts
|
useDocker.ts — hook WebSocket para datos en tiempo real + toast de errores de accion
|
||||||
useServerConfig.ts — fetch /api/config + canInteract() helper for ALLOWED_PATHS
|
useServerConfig.ts — fetch /api/config + helper canInteract() para ALLOWED_PATHS
|
||||||
useStatsHistory.ts — fetch stats history by range (1h/6h/24h/7d)
|
useStatsHistory.ts — fetch del historial de stats por rango (1h/6h/24h/7d)
|
||||||
useStatsStore.ts — in-memory store for live stats
|
useStatsStore.ts — store en memoria para stats live
|
||||||
processing.ts — pure processing state logic
|
processing.ts — logica pura de estados processing
|
||||||
engine/
|
engine/
|
||||||
layout.ts — group layout + grid + edges
|
layout.ts — layout de grupos + grid + edges
|
||||||
components/
|
components/
|
||||||
HeaderBar.tsx — top navigation bar with notification bell
|
HeaderBar.tsx — barra superior con navegacion
|
||||||
EdgeLegend.tsx — connection type legend
|
EdgeLegend.tsx — leyenda de tipos de conexion
|
||||||
LoginScreen.tsx — authentication screen
|
LoginScreen.tsx — pantalla de autenticacion
|
||||||
NodeContextMenu.tsx — node context menu (disabled when locked)
|
NodeContextMenu.tsx — menu contextual de nodos (con disable cuando locked)
|
||||||
OffsetEdge.tsx — custom edge with offset to avoid overlap
|
OffsetEdge.tsx — edge custom con offset para evitar superposicion
|
||||||
Sparkline.tsx — lightweight line chart for stats history
|
Sparkline.tsx — gráfica de línea ligera para historial de stats
|
||||||
StatsCard.tsx — metric card with sparkline, hover, average and threshold
|
StatsCard.tsx — tarjeta de métrica con sparkline, hover, promedio y umbral
|
||||||
ThresholdBar.tsx — per-container threshold slider with override/reset
|
ThresholdBar.tsx — slider de umbral por contenedor con override/reset
|
||||||
ActionErrorToast.tsx — top-right toast stack for action errors
|
ActionErrorToast.tsx — stack de toasts top-right para errores de acciones
|
||||||
Tooltip.tsx — info tooltip with portal + smart placement
|
|
||||||
panels/
|
panels/
|
||||||
DetailPanel.tsx — side panel with info, stats, env, config and logs
|
DetailPanel.tsx — panel lateral con info, stats, env, config y logs
|
||||||
LogPanel.tsx — log panel per container
|
LogPanel.tsx — panel de logs por container
|
||||||
pages/
|
pages/
|
||||||
MonitoringPage.tsx — CPU/RAM history, Docker events and per-container thresholds
|
MonitoringPage.tsx — historial de CPU/RAM, eventos Docker y umbrales por contenedor
|
||||||
SettingsPage.tsx — configuration (auth, Discord webhook, events, global thresholds)
|
SettingsPage.tsx — configuracion (auth, Discord webhook, eventos, umbrales globales)
|
||||||
shared/
|
shared/
|
||||||
types.ts — shared server/client types
|
types.ts — tipos compartidos server/client
|
||||||
```
|
```
|
||||||
|
|
||||||
## Community and contributions
|
## Comunidad y contribuciones
|
||||||
|
|
||||||
ContainerFlow is in active development (`v0.x`).
|
ContainerFlow está en desarrollo activo (`v0.x`).
|
||||||
|
|
||||||
- 🐛 **Bug?** Open an [issue](https://github.com/RGJorge/containerflow/issues/new?template=bug_report.md)
|
- 🐛 **Bug?** Abre un [issue](https://github.com/RGJorge/containerflow/issues/new?template=bug_report.md)
|
||||||
- 💡 **Idea?** Open a [feature request](https://github.com/RGJorge/containerflow/issues/new?template=feature_request.md)
|
- 💡 **Idea?** Abre un [feature request](https://github.com/RGJorge/containerflow/issues/new?template=feature_request.md)
|
||||||
- 💬 **Discussion / question?** Open a [discussion](https://github.com/RGJorge/containerflow/discussions)
|
- 🔒 **Vulnerabilidad de seguridad?** Reporta privadamente — ver [SECURITY.md](SECURITY.md)
|
||||||
- 🔒 **Security vulnerability?** Report privately — see [SECURITY.md](SECURITY.md)
|
- 📜 **Code of Conduct** — ver [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
||||||
- 📜 **Code of Conduct** — see [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
- 🛠 **Quiero contribuir código** — ver [CONTRIBUTING.md](CONTRIBUTING.md). Actualmente solo aceptamos issues; PRs se abrirán cuando el proyecto madure.
|
||||||
- 🛠 **Want to contribute code?** — see [CONTRIBUTING.md](CONTRIBUTING.md). Currently we only accept issues; PRs will open as the project matures and patterns stabilize.
|
|
||||||
|
|
||||||
If ContainerFlow is useful to you, a ⭐ on GitHub helps project visibility.
|
Si ContainerFlow te resulta útil, una ⭐ en GitHub ayuda a la visibilidad del proyecto.
|
||||||
|
|
||||||
## License
|
## Licencia
|
||||||
|
|
||||||
Copyright (C) 2026 Jorge Gonzalez D. (RGJorge)
|
Copyright (C) 2026 Jorge Gonzalez D. (RGJorge)
|
||||||
|
|
||||||
This project is licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0). See the [LICENSE](LICENSE) file for full terms.
|
Este proyecto esta licenciado bajo **GNU Affero General Public License v3.0** (AGPL-3.0). Ver el archivo [LICENSE](LICENSE) para los terminos completos.
|
||||||
|
|
||||||
For commercial use with closed source, contact for a commercial license: alteonx.servicios@gmail.com
|
Para uso comercial con codigo cerrado, contactar para una licencia comercial: alteonx.servicios@gmail.com
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "containerflow",
|
"name": "containerflow",
|
||||||
"version": "0.1.3",
|
"version": "0.1.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"author": "Jorge Gonzalez D. (RGJorge)",
|
"author": "Jorge Gonzalez D. (RGJorge)",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+4
-5
@@ -671,7 +671,7 @@ function Dashboard({ token }: { token: string }) {
|
|||||||
<ChevronDown size={14} className={`text-slate-500 transition-transform ${filterOpen ? "rotate-180" : ""}`} />
|
<ChevronDown size={14} className={`text-slate-500 transition-transform ${filterOpen ? "rotate-180" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
{filterOpen && (
|
{filterOpen && (
|
||||||
<div className="absolute top-full right-0 mt-1.5 bg-slate-800 border border-slate-700 rounded-lg shadow-xl shadow-black/40 py-1.5 min-w-[220px] max-h-[280px] overflow-y-auto">
|
<div className="absolute top-full right-0 mt-1.5 bg-slate-800 border border-slate-700 rounded-lg shadow-xl shadow-black/40 py-1.5 min-w-[220px]">
|
||||||
{/* Select/Deselect all */}
|
{/* Select/Deselect all */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -706,16 +706,15 @@ function Dashboard({ token }: { token: string }) {
|
|||||||
<button
|
<button
|
||||||
key={p}
|
key={p}
|
||||||
onClick={() => toggleProject(p)}
|
onClick={() => toggleProject(p)}
|
||||||
title={p}
|
|
||||||
className="flex items-center gap-2.5 w-full px-3.5 py-2 text-sm hover:bg-slate-700/60 transition-colors"
|
className="flex items-center gap-2.5 w-full px-3.5 py-2 text-sm hover:bg-slate-700/60 transition-colors"
|
||||||
>
|
>
|
||||||
<div className={`w-4 h-4 rounded border flex items-center justify-center shrink-0 ${
|
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
|
||||||
active ? "bg-cyan-500 border-cyan-500" : "border-slate-600"
|
active ? "bg-cyan-500 border-cyan-500" : "border-slate-600"
|
||||||
}`}>
|
}`}>
|
||||||
{active && <Check size={12} className="text-white" />}
|
{active && <Check size={12} className="text-white" />}
|
||||||
</div>
|
</div>
|
||||||
<span className={`flex-1 min-w-0 truncate text-left ${active ? "text-slate-200" : "text-slate-500"}`}>{p}</span>
|
<span className={active ? "text-slate-200" : "text-slate-500"}>{p}</span>
|
||||||
<span className="ml-auto flex items-center gap-1.5 text-xs shrink-0">
|
<span className="ml-auto flex items-center gap-1.5 text-xs">
|
||||||
<span className="text-emerald-500/70">{running}</span>
|
<span className="text-emerald-500/70">{running}</span>
|
||||||
<span className="text-slate-600">/</span>
|
<span className="text-slate-600">/</span>
|
||||||
<span className="text-slate-400">{projectServices.length}</span>
|
<span className="text-slate-400">{projectServices.length}</span>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const ServiceNode = memo(function ServiceNode({ data, id }: NodeProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative rounded-xl border ${s.border} ${s.bg} backdrop-blur-sm
|
className={`relative rounded-xl border ${s.border} ${s.bg} backdrop-blur-sm
|
||||||
shadow-lg shadow-black/30 p-4 min-w-[220px] max-w-[240px] ring-2 ${s.ring}
|
shadow-lg shadow-black/30 p-4 min-w-[220px] ring-2 ${s.ring}
|
||||||
transition-[opacity,box-shadow] duration-300 ${flashClass} ${d.locked ? "opacity-70" : ""}`}
|
transition-[opacity,box-shadow] duration-300 ${flashClass} ${d.locked ? "opacity-70" : ""}`}
|
||||||
>
|
>
|
||||||
{d.locked && (
|
{d.locked && (
|
||||||
@@ -181,7 +181,7 @@ export const ServiceNode = memo(function ServiceNode({ data, id }: NodeProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-bold text-white text-sm truncate" title={d.label}>{d.label}</span>
|
<span className="font-bold text-white text-sm truncate">{d.label}</span>
|
||||||
{d.state === "processing" ? (
|
{d.state === "processing" ? (
|
||||||
<div className="flex items-center gap-0.5 shrink-0">
|
<div className="flex items-center gap-0.5 shrink-0">
|
||||||
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse" />
|
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse" />
|
||||||
@@ -193,7 +193,7 @@ export const ServiceNode = memo(function ServiceNode({ data, id }: NodeProps) {
|
|||||||
<div className={`w-2 h-2 rounded-full shrink-0 ${s.dot}`} />
|
<div className={`w-2 h-2 rounded-full shrink-0 ${s.dot}`} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-500 truncate mt-0.5" title={d.image}>
|
<div className="text-xs text-slate-500 truncate mt-0.5">
|
||||||
{d.image.startsWith("sha256:") ? `${t("node.noTag")} (${d.image.slice(7, 19)})` : d.image}
|
{d.image.startsWith("sha256:") ? `${t("node.noTag")} (${d.image.slice(7, 19)})` : d.image}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -394,13 +394,12 @@ export function DetailPanel({ service, stats, logLines, token, closing, locked,
|
|||||||
className={`absolute top-0 left-0 bottom-0 w-[900px] bg-slate-900/95 backdrop-blur-sm border-r border-slate-700/60 flex flex-col z-50 rounded-l-xl transition-transform duration-[400ms] ease-out ${visible ? "translate-x-0" : "-translate-x-full"}`}
|
className={`absolute top-0 left-0 bottom-0 w-[900px] bg-slate-900/95 backdrop-blur-sm border-r border-slate-700/60 flex flex-col z-50 rounded-l-xl transition-transform duration-[400ms] ease-out ${visible ? "translate-x-0" : "-translate-x-full"}`}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-800 shrink-0 gap-3">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-800 shrink-0">
|
||||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
<div className="flex items-center gap-2.5">
|
||||||
<span className={`w-2 h-2 rounded-full shrink-0 ${stateDot}`} />
|
<span className={`w-2 h-2 rounded-full ${stateDot}`} />
|
||||||
<span className="text-sm font-semibold text-white truncate min-w-0 max-w-[240px]">{service.name}</span>
|
<span className="text-sm font-semibold text-white truncate">{service.name}</span>
|
||||||
<Tooltip text={service.name} placement="bottom" width="w-72" size={12} />
|
|
||||||
{locked && (
|
{locked && (
|
||||||
<span className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium text-slate-400 bg-slate-700/60 border border-slate-600/50 rounded shrink-0" title={t("access.viewOnly")}>
|
<span className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium text-slate-400 bg-slate-700/60 border border-slate-600/50 rounded" title={t("access.viewOnly")}>
|
||||||
<Lock size={10} />
|
<Lock size={10} />
|
||||||
{t("access.viewOnly")}
|
{t("access.viewOnly")}
|
||||||
</span>
|
</span>
|
||||||
@@ -410,20 +409,20 @@ export function DetailPanel({ service, stats, logLines, token, closing, locked,
|
|||||||
href={`http://${window.location.hostname}:${service.ports[0].host}`}
|
href={`http://${window.location.hostname}:${service.ports[0].host}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 text-slate-500 hover:text-cyan-400 transition-colors shrink-0"
|
className="flex items-center gap-1 text-slate-500 hover:text-cyan-400 transition-colors"
|
||||||
title={`Open http://${window.location.hostname}:${service.ports[0].host}`}
|
title={`Open http://${window.location.hostname}:${service.ports[0].host}`}
|
||||||
>
|
>
|
||||||
<ExternalLink size={12} />
|
<ExternalLink size={12} />
|
||||||
<span className="text-[11px] font-mono">:{service.ports[0].host}</span>
|
<span className="text-[11px] font-mono">:{service.ports[0].host}</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
<span className={`text-xs font-mono ${stateColor} flex items-center gap-1 shrink-0`}>
|
<span className={`text-xs font-mono ${stateColor} flex items-center gap-1`}>
|
||||||
{isProcessing ? `processing... ${elapsed}s` :
|
{isProcessing ? `processing... ${elapsed}s` :
|
||||||
isCrashed ? <><AlertTriangle size={11} />crashed (exit {service.exit_code}{service.oom_killed ? ", OOM" : ""})</> :
|
isCrashed ? <><AlertTriangle size={11} />crashed (exit {service.exit_code}{service.oom_killed ? ", OOM" : ""})</> :
|
||||||
service.state}
|
service.state}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
<div className="flex items-center gap-1.5">
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<div className="flex items-center gap-1.5 px-2 py-1 text-[11px] font-medium text-yellow-400">
|
<div className="flex items-center gap-1.5 px-2 py-1 text-[11px] font-medium text-yellow-400">
|
||||||
@@ -1266,17 +1265,16 @@ export function DetailPanel({ service, stats, logLines, token, closing, locked,
|
|||||||
{/* Logs fullscreen modal */}
|
{/* Logs fullscreen modal */}
|
||||||
{logsModal && (
|
{logsModal && (
|
||||||
<div className="fixed inset-0 z-[100] bg-slate-900 flex flex-col">
|
<div className="fixed inset-0 z-[100] bg-slate-900 flex flex-col">
|
||||||
<div className="flex items-center justify-between px-6 py-3 border-b border-slate-800 shrink-0 gap-3">
|
<div className="flex items-center justify-between px-6 py-3 border-b border-slate-800 shrink-0">
|
||||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
<div className="flex items-center gap-2.5">
|
||||||
<Terminal size={16} className="text-cyan-400 shrink-0" />
|
<Terminal size={16} className="text-cyan-400" />
|
||||||
<span className="text-sm font-semibold text-white truncate min-w-0 max-w-[240px]">{service.name}</span>
|
<span className="text-sm font-semibold text-white">{service.name}</span>
|
||||||
<Tooltip text={service.name} placement="bottom" width="w-72" size={12} />
|
<span className="text-xs text-slate-500 font-mono">logs</span>
|
||||||
<span className="text-xs text-slate-500 font-mono shrink-0">logs</span>
|
|
||||||
{service.state === "running" && subscribedRef.current && (
|
{service.state === "running" && subscribedRef.current && (
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse shrink-0" />
|
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const text = allLines.map((l) => `${l.timestamp ? formatTimestamp(l.timestamp) + " " : ""}${l.line}`).join("\n");
|
const text = allLines.map((l) => `${l.timestamp ? formatTimestamp(l.timestamp) + " " : ""}${l.line}`).join("\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user