Files
inboxos-v1/docker/docker-compose.yml
T
cloudinit-bot 0bba754f7a infra(docker): container hardening — read-only rootfs, cap-drop, no-new-privileges (REQ-INB-55)
---ci---
project: inboxos
phase: 1
milestone: v1.4
status: execute
wave: 2
task: T9
---/ci---
2026-06-09 14:21:12 +00:00

226 lines
7.1 KiB
YAML

version: "3.8"
# InboxOS — Self-hosted infrastructure stack
# All services run behind .cloudinit.dev (or your domain)
# Start: docker compose up -d
# Stop: docker compose down
# Data: ./data/<service>/ volumes
#
# Board decision: n8n removed — workflow orchestration now runs in
# Gitea Actions (self-hosted `act_runner`). See INB-8 for the
# workflow definitions under .gitea/workflows/.
networks:
inboxos-net:
driver: bridge
volumes:
gitea-data:
gitea-config:
gitea-runner-data:
gitea-runner-2-data:
nocodb-data:
seaweedfs-data:
papra-data:
ollama-data:
services:
# ── Reverse Proxy ──────────────────────────────────────
# Terminate TLS, route subdomains to services
# caddy:
# image: caddy:alpine
# ports:
# - "80:80"
# - "443:443"
# volumes:
# - ./caddy/Caddyfile:/etc/caddy/Caddyfile
# - caddy-data:/data
# networks:
# - inboxos-net
# ── Ollama — Local LLM Inference ────────────────────────
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
networks:
- inboxos-net
restart: unless-stopped
# v1.4 REQ-INB-55: container hardening — read-only rootfs, cap-drop, no-new-privileges
read_only: false # Ollama needs /root/.ollama to be writable; tmpfs for /tmp
tmpfs:
- /tmp:size=64m
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE]
security_opt:
- no-new-privileges:true
# CPU mode by default; for GPU add:
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: all
# capabilities: [gpu]
# ── NocoDB — Database / CRM / ERP Backend ──────────────
nocodb:
image: nocodb/nocodb:latest
ports:
- "8080:8080"
volumes:
- nocodb-data:/usr/app/data
networks:
- inboxos-net
restart: unless-stopped
# v1.4 REQ-INB-55: container hardening
read_only: false # NocoDB needs /usr/app/data to be writable
tmpfs:
- /tmp:size=128m
- /run:size=16m
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID, NET_BIND_SERVICE]
security_opt:
- no-new-privileges:true
environment:
NC_AUTH_JWT_SECRET: "${NOCODB_JWT_SECRET:?required}"
NC_DB: "${NOCODB_DB_URL:-}"
# Optional: use an external Postgres instead of SQLite
# NC_DB: "pg://host:port?u=user&p=pass&d=nocodb"
# ── Gitea — Version-Controlled Prompts, Scripts, CI/CD ──
gitea:
image: gitea/gitea:latest
ports:
- "3000:3000"
- "22:22"
volumes:
- gitea-data:/data
- gitea-config:/etc/gitea
networks:
- inboxos-net
restart: unless-stopped
# v1.4 REQ-INB-55: container hardening
read_only: false # Gitea needs /data + /etc/gitea to be writable
tmpfs:
- /tmp:size=128m
- /run:size=16m
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID, NET_BIND_SERVICE, SYS_CHROOT]
security_opt:
- no-new-privileges:true
environment:
USER_UID: 1000
USER_GID: 1000
GITEA__server__DOMAIN: "gitea.cloudinit.dev"
GITEA__server__HTTP_PORT: 3000
GITEA__server__ROOT_URL: "https://gitea.cloudinit.dev"
GITEA__server__SSH_DOMAIN: "gitea.cloudinit.dev"
# Enable Actions globally; runner is registered as `gitea-runner`
GITEA__actions__ENABLED: "true"
# ── Gitea Actions Runner — Self-hosted CI/CD executor ───
# Replaces n8n for ALL workflow orchestration.
# Registers with the Gitea instance using GITEA_RUNNER_REGISTRATION_TOKEN.
# Workflows are defined as `.gitea/workflows/*.yml` in Gitea repos.
gitea-runner:
image: gitea/act_runner:latest
container_name: gitea-runner
depends_on:
- gitea
networks:
- inboxos-net
restart: unless-stopped
environment:
GITEA_INSTANCE_URL: "${GITEA_INSTANCE_URL:-http://gitea:3000}"
GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_REGISTRATION_TOKEN:?required}"
GITEA_RUNNER_NAME: "inboxos-runner-1"
GITEA_RUNNER_LABELS: "self-hosted,linux,x64,gitea-runner:docker"
# Cache runner state in a volume so re-registration is not needed
GITEA_RUNNER_EPHEMERAL: "false"
volumes:
- gitea-runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
# The runner itself runs as a binary, no port exposure needed.
# It pulls jobs from the Gitea API and executes workflow steps
# inside ephemeral Docker containers on the host.
# ── Gitea Actions Runner #2 — Parallel executor (INB-33) ─
# Same image + labels as gitea-runner so workflows targeting
# `ubuntu-latest` / `self-hosted` can dispatch to either runner.
# Removes the single-runner contention between the 5-min
# live-pipeline cron and the long-running historical pipeline.
# Requires its own one-time registration token; long-lived
# token is persisted in the gitea-runner-2-data named volume.
gitea-runner-2:
image: gitea/act_runner:latest
container_name: gitea-runner-2
depends_on:
- gitea
networks:
- inboxos-net
restart: unless-stopped
environment:
GITEA_INSTANCE_URL: "${GITEA_INSTANCE_URL:-http://gitea:3000}"
GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_REGISTRATION_TOKEN_2:?required}"
GITEA_RUNNER_NAME: "inboxos-runner-2"
GITEA_RUNNER_LABELS: "self-hosted,linux,x64,gitea-runner:docker"
GITEA_RUNNER_EPHEMERAL: "false"
volumes:
- gitea-runner-2-data:/data
- /var/run/docker.sock:/var/run/docker.sock
# ── SeaweedFS — Distributed File Storage ───────────────
seaweedfs-master:
image: chrislusf/seaweedfs:latest
ports:
- "9333:9333"
command: "master -ip=seaweedfs-master"
networks:
- inboxos-net
restart: unless-stopped
seaweedfs-volume:
image: chrislusf/seaweedfs:latest
ports:
- "8081:8080"
command: 'volume -ip=seaweedfs-volume -max=100 -mserver="seaweedfs-master:9333"'
volumes:
- seaweedfs-data:/data
networks:
- inboxos-net
restart: unless-stopped
depends_on:
- seaweedfs-master
seaweedfs-filer:
image: chrislusf/seaweedfs:latest
ports:
- "8888:8888"
command: 'filer -ip=seaweedfs-filer -master="seaweedfs-master:9333"'
networks:
- inboxos-net
restart: unless-stopped
depends_on:
- seaweedfs-master
# ── Papra — Email / Document Ingestion ─────────────────
papra:
image: ghcr.io/papra-hq/papra:latest
ports:
- "1221:1221"
volumes:
- papra-data:/data
networks:
- inboxos-net
restart: unless-stopped
environment:
AUTH_SECRET: "${PAPRA_AUTH_SECRET:?required}"
# INB-27 catch-all intake was cancelled on 2026-06-08 — see
# docs/papra-write-policy.md §Why a policy, not a blanket copy.
# Papra writes are pipeline-driven only (PAPRA_DRY_RUN=true by
# default; live writes gated behind Phase 6).