Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/release-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ jobs:
umask 077
cat > .env <<EOF
IMAGE_TAG='$IMAGE_TAG'
COMPOSE_PROFILES=observability
ENVIRONMENT=production
SECRET_KEY='$SECRET_KEY'
MONGO_USER='$MONGO_USER'
Expand Down
3 changes: 1 addition & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Platform for running scripts online in isolated Kubernetes pods. Users write Pyt
| Backend | FastAPI, Dishka DI, Beanie ODM (MongoDB), Python 3.14 (backend/.python-version), async throughout, httpx2 for HTTP |
| Events | Kafka (KRaft, no ZooKeeper) + FastStream (Pydantic JSON) |
| Worker | one process (`app/worker.py`): execution scheduler, k8s worker, pod monitor, result processor, DLQ retries |
| Infra | MongoDB 9 with a daily mongodump (`mongo-backup`), Redis 8; optional Grafana, Jaeger, Victoria Metrics, Loki, OpenSearch, OTel Collector plus its host-metrics collector (compose profile `observability`) and Kafdrop (profile `debug`) |
| Infra | MongoDB 9 with a daily mongodump (`mongo-backup`), Redis 8; Grafana, Jaeger, Victoria Metrics, Loki, OpenSearch, OTel Collector plus its host-metrics collector; Kafdrop under the compose profile `debug` |
| Runtimes | Python 3.11 to 3.14, Node 22/24/26, Ruby 3.3/3.4/4.0, Bash 5.3, Go 1.26/1.27 (app/runtime_registry.py) |

## Key Directory Map
Expand Down Expand Up @@ -91,7 +91,6 @@ cp .env.example .env # once: the local secrets
docker compose up -d # start
docker compose up -d --build # rebuild and start
docker compose up -d --wait # start and wait for healthy
docker compose --profile observability up -d # plus Grafana, Jaeger, Victoria Metrics, Loki, OpenSearch, OTel Collector, host-metrics
docker compose --profile debug up -d # plus Kafdrop
docker compose down # stop

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ curl -k https://localhost/api/v1/health/live
|--------------------|--------------------------------------------------|---------------------------------------------------------|
| Frontend | [https://localhost:5001](https://localhost:5001) | `user` / `user123` or `admin` / `admin123` |
| Backend API | [https://localhost:443](https://localhost:443) | |
| Grafana | [http://localhost:3000](http://localhost:3000) | needs the `observability` profile; `admin` / `admin123` |
| Jaeger (Tracing) | [http://localhost:16686](http://localhost:16686) | needs the `observability` profile |
| Grafana | [http://localhost:3000](http://localhost:3000) | `admin` / `admin123` |
| Jaeger (Tracing) | [http://localhost:16686](http://localhost:16686) | |
| Kafdrop (Kafka UI) | [http://localhost:9000](http://localhost:9000) | needs the `debug` profile |

`docker compose --profile observability up -d` adds Grafana, Jaeger, Victoria Metrics, Loki, OpenSearch and the OTel Collector, and `--profile debug` adds Kafdrop.
The stack includes Grafana, Jaeger, Victoria Metrics, Loki, OpenSearch and the OTel Collector, and `docker compose --profile debug up -d` adds Kafdrop.

nginx and the backend each make a self-signed certificate when they first start, so accept the browser warning. nginx verifies the backend's certificate on every proxied request. A certificate mounted at `/etc/nginx/certs` in the frontend container is used as is.

Expand Down
10 changes: 1 addition & 9 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
x-logging: &logging
driver: fluentd
options:
# async: a container still starts, and buffers its lines, while the collector is down or not in the profile
# async: a container still starts, and buffers its lines, while the collector is down
fluentd-address: localhost:24224
fluentd-async: "true"
tag: "{{.Name}}"
Expand Down Expand Up @@ -175,7 +175,6 @@ services:
grafana:
logging: *logging
container_name: grafana
profiles: ["observability"]
image: grafana/grafana:13.2.4
user: "472"
# Alert rules load after dashboards create the integr8scode folder; loaded first, they'd make one with a random UID
Expand Down Expand Up @@ -350,7 +349,6 @@ services:
# 2.21+ dropped the search API Grafana's Jaeger datasource calls (grafana/grafana#112336)
image: jaegertracing/jaeger:2.20.0
container_name: jaeger
profiles: ["observability"]
mem_limit: 256m
# The built-in all-in-one config keeps 100 000 traces in memory, about 260 MiB at ~2.3 KB each; 50 000 is about 4 days
command: ["--set=extensions.jaeger_storage.backends.some_storage.memory.max_traces=50000"]
Expand All @@ -370,7 +368,6 @@ services:
loki:
logging: *logging
container_name: loki
profiles: ["observability"]
image: grafana/loki:3.7.8
ports:
- "127.0.0.1:3100:3100"
Expand All @@ -388,7 +385,6 @@ services:
opensearch:
logging: *logging
container_name: opensearch
profiles: ["observability"]
image: opensearchproject/opensearch:3.9.0
entrypoint: ["bash", "/etc/opensearch-setup/start.sh"]
environment:
Expand Down Expand Up @@ -422,7 +418,6 @@ services:
logging: *logging
image: victoriametrics/victoria-metrics:v1.154.0
container_name: victoria-metrics
profiles: ["observability"]
mem_limit: 256m
ports:
- "127.0.0.1:8428:8428"
Expand Down Expand Up @@ -454,7 +449,6 @@ services:
max-file: "3"
image: otel/opentelemetry-collector-contrib:0.163.1
container_name: otel-collector
profiles: ["observability"]
mem_limit: 192m
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
Expand Down Expand Up @@ -490,7 +484,6 @@ services:
logging: *logging
image: tecnativa/docker-socket-proxy:v0.5.0
container_name: docker-socket-proxy
profiles: ["observability"]
mem_limit: 32m
environment:
- CONTAINERS=1
Expand All @@ -504,7 +497,6 @@ services:
logging: *logging
image: otel/opentelemetry-collector-contrib:0.163.1
container_name: host-metrics
profiles: ["observability"]
# In the host's network namespace the network scraper reads the host's interfaces, and 127.0.0.1:4317 reaches otel-collector
network_mode: host
mem_limit: 128m
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The browser loads the SPA from the frontend container, an nginx that serves HTTP

The worker is one more container from the same backend image, running `python -m app.worker`. Inside it, the execution scheduler starts executions from a Redis-backed queue, the k8s worker creates their pods through the Kubernetes API, the pod monitor watches the pods, and the result processor writes results to MongoDB. A DLQ job retries failed events every 10 seconds.

The observability profile adds an OpenTelemetry Collector that ships traces to Jaeger, metrics to VictoriaMetrics and logs to Loki, with Grafana on top of all three. Kafdrop is under the debug profile. No container runs once and exits. nginx and the backend each make their own TLS certificate when they first start, and the backend seeds the users before it serves. The worker reads k3s's own kubeconfig and creates the `integr8scode` namespace with its NetworkPolicy and ResourceQuota.
An OpenTelemetry Collector ships traces to Jaeger, metrics to VictoriaMetrics and logs to Loki, with Grafana on top of all three. Kafdrop is under the debug profile. No container runs once and exits. nginx and the backend each make their own TLS certificate when they first start, and the backend seeds the users before it serves. The worker reads k3s's own kubeconfig and creates the `integr8scode` namespace with its NetworkPolicy and ResourceQuota.

## Backend wiring

Expand Down
6 changes: 3 additions & 3 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ Limits are system-wide, not per execution. An execution request carries only the

## Look around

Kafdrop, Grafana and Jaeger aren't part of the default stack. `docker compose --profile observability up -d` adds Grafana, Jaeger, Victoria Metrics, Loki, OpenSearch and the OTel collector, and `--profile debug` adds Kafdrop. All of them listen on localhost only.
The stack also runs Grafana, Jaeger, Victoria Metrics, Loki, OpenSearch and the OTel collector, and `docker compose --profile debug up -d` adds Kafdrop. All of them listen on localhost only.

| Service | URL | Needs |
|---------|-----|-------|
| Grafana | [http://localhost:3000](http://localhost:3000) (admin / admin123) | `--profile observability` |
| Jaeger | [http://localhost:16686](http://localhost:16686) | `--profile observability` |
| Grafana | [http://localhost:3000](http://localhost:3000) (admin / admin123) | |
| Jaeger | [http://localhost:16686](http://localhost:16686) | |
| Kafdrop | [http://localhost:9000](http://localhost:9000) | `--profile debug` |

## Troubleshooting
Expand Down
17 changes: 8 additions & 9 deletions docs/operations/deployment.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
# Deployment

Everything runs from one docker-compose.yaml: the backend, the frontend, the worker, MongoDB, Redis, Kafka and the optional observability stack. The worker reuses the backend image with a different `command:`, so there is one application image to build. Kubernetes only runs the executor pods. The worker reads k3s's own kubeconfig from `/etc/rancher/k3s`, reaches the API at `host.docker.internal:6443` and on start creates the `integr8scode` namespace with its Pod Security labels, NetworkPolicy and ResourceQuota.
Everything runs from one docker-compose.yaml: the backend, the frontend, the worker, MongoDB, Redis, Kafka and the observability stack. The worker reuses the backend image with a different `command:`, so there is one application image to build. Kubernetes only runs the executor pods. The worker reads k3s's own kubeconfig from `/etc/rancher/k3s`, reaches the API at `host.docker.internal:6443` and on start creates the `integr8scode` namespace with its Pod Security labels, NetworkPolicy and ResourceQuota.

`backend/Dockerfile` builds on `python:3.14-slim` in three stages. The builder stage runs `uv sync --locked --no-dev --no-install-project --no-build`, the `base` stage copies only the virtualenv plus a few runtime libraries, and the last stage adds the application code. On first start the container makes a self-signed certificate and keeps it in the `backend_tls_key` and `backend_tls_cert` volumes, then it seeds the default users and starts gunicorn with uvicorn workers on port 443 over TLS. nginx mounts the certificate volume and verifies the backend against it. The frontend image builds the Svelte app with `node:26-alpine` and serves it from `nginx:1.31-alpine`, see [Nginx configuration](nginx-configuration.md).

## Local stack

```bash
cp .env.example .env # once
docker compose up -d # core stack
docker compose --profile observability up -d # plus Grafana, Jaeger, Loki, OpenSearch, Victoria Metrics, OTel Collector, host-metrics
docker compose up -d # the whole stack, Grafana, Loki, OpenSearch and the rest included
docker compose --profile debug up -d # plus Kafdrop
```

`--build` rebuilds the images first and `--wait` returns once every service is running, and healthy where it has a healthcheck. `docker compose logs -f <service>` follows one service and `docker compose ps` lists them.

The core stack is MongoDB with its backup, Redis, Kafka (KRaft, no ZooKeeper), the backend, the frontend and the worker. No container runs once and exits. Secrets come from `.env`, your copy of `.env.example`; compose has no defaults for them and refuses to start while one is empty. Kafka creates topics on first use (`KAFKA_AUTO_CREATE_TOPICS_ENABLE`). Topic names are the event types themselves, such as `execution_requested`, with no prefix.
The stack is MongoDB with its backup, Redis, Kafka (KRaft, no ZooKeeper), the backend, the frontend, the worker and the observability services, the same locally and in production. No container runs once and exits. Secrets come from `.env`, your copy of `.env.example`; compose has no defaults for them and refuses to start while one is empty. Kafka creates topics on first use (`KAFKA_AUTO_CREATE_TOPICS_ENABLE`). Topic names are the event types themselves, such as `execution_requested`, with no prefix.

| Service | URL | Needs |
|--------------------|-------------------------|-------------------------------|
Expand All @@ -24,10 +23,10 @@ The core stack is MongoDB with its backup, Redis, Kafka (KRaft, no ZooKeeper), t
| Kafka (host) | localhost:9092 | |
| MongoDB | 127.0.0.1:27017 | |
| Redis | 127.0.0.1:6379 | |
| Grafana | http://localhost:3000 | `--profile observability` |
| Jaeger UI | http://localhost:16686 | `--profile observability` |
| Victoria Metrics | http://localhost:8428 | `--profile observability` |
| Loki | http://localhost:3100 | `--profile observability` |
| Grafana | http://localhost:3000 | |
| Jaeger UI | http://localhost:16686 | |
| Victoria Metrics | http://localhost:8428 | |
| Loki | http://localhost:3100 | |
| Kafdrop | http://localhost:9000 | `--profile debug` |

Inside the compose network Kafka is `kafka:29092`. From the host use `localhost:9092`, since that's the listener advertised for host clients.
Expand Down Expand Up @@ -174,7 +173,7 @@ IMAGE_TAG=sha-abc1234 docker compose up -d --no-build

## Production

Production gets one release a day. Every merge to `main` goes through CI and Docker Scan & Promote, which moves `latest`; at 23:00 Europe/Berlin [Release & Deploy](cicd.md#release-deploy) releases whatever `latest` is and deploys it, and skips the night when nothing new passed. The deploy job SSHes into the server, removes images no container uses, checks out the release's git tag in `/opt/Integr8sCode` (so the compose file and config match the images) and writes `/opt/Integr8sCode/.env` (mode 600) from the repo's secrets (`SECRET_KEY`, `MONGO_USER`, `MONGO_PASSWORD`, `MONGO_MONITOR_PASSWORD`, `REDIS_PASSWORD`, `ADMIN_USER_PASSWORD`, `DEFAULT_USER_PASSWORD`, the Grafana and Mailjet values) together with `IMAGE_TAG`, `COMPOSE_PROFILES=observability` and `GRAFANA_ROOT_URL=https://grafana.integr8scode.cc/`. Compose reads that file for every command, so the backup cron and commands run by hand on the server use the same values, and a secret that came through empty stops compose before anything starts. The values sit in single quotes, so a secret can't contain one. Then it runs:
Production gets one release a day. Every merge to `main` goes through CI and Docker Scan & Promote, which moves `latest`; at 23:00 Europe/Berlin [Release & Deploy](cicd.md#release-deploy) releases whatever `latest` is and deploys it, and skips the night when nothing new passed. The deploy job SSHes into the server, removes images no container uses, checks out the release's git tag in `/opt/Integr8sCode` (so the compose file and config match the images) and writes `/opt/Integr8sCode/.env` (mode 600) from the repo's secrets (`SECRET_KEY`, `MONGO_USER`, `MONGO_PASSWORD`, `MONGO_MONITOR_PASSWORD`, `REDIS_PASSWORD`, `ADMIN_USER_PASSWORD`, `DEFAULT_USER_PASSWORD`, the Grafana and Mailjet values) together with `IMAGE_TAG` and `GRAFANA_ROOT_URL=https://grafana.integr8scode.cc/`. Compose reads that file for every command, so the backup cron and commands run by hand on the server use the same values, and a secret that came through empty stops compose before anything starts. The values sit in single quotes, so a secret can't contain one. Then it runs:

```bash
docker compose pull
Expand Down
2 changes: 1 addition & 1 deletion docs/operations/grafana-integration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Grafana integration

Grafana runs under the `observability` compose profile. Everything it shows is provisioned from [`grafana/`](https://github.com/HardMax71/Integr8sCode/tree/main/grafana): three datasources, thirteen dashboards, thirteen alert rules, an email contact point and a heartbeat webhook. Locally it's at `http://localhost:3000`; in production nginx serves it on `grafana.integr8scode.cc` (see [Nginx configuration](nginx-configuration.md#grafana-server)). Anonymous access is off. The admin login comes from `GRAFANA_ADMIN_USER` and `GRAFANA_ADMIN_PASSWORD` (default `admin` / `admin123`), and the entrypoint resets the password to that value on every start.
Everything Grafana shows is provisioned from [`grafana/`](https://github.com/HardMax71/Integr8sCode/tree/main/grafana): three datasources, thirteen dashboards, thirteen alert rules, an email contact point and a heartbeat webhook. Locally it's at `http://localhost:3000`; in production nginx serves it on `grafana.integr8scode.cc` (see [Nginx configuration](nginx-configuration.md#grafana-server)). Anonymous access is off. The admin login comes from `GRAFANA_ADMIN_USER` and `GRAFANA_ADMIN_PASSWORD` (default `admin` / `admin123`), and the entrypoint resets the password to that value on every start.

## Datasources

Expand Down
Loading
Loading