Compare commits
1 commit
master
...
feat/vps-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
862c04a612 |
33
hosts/vps/runtime/ai-cluster/docker-compose.override.yml
Normal file
33
hosts/vps/runtime/ai-cluster/docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# AI cluster memory limits — HARD caps, containers are OOM-killed and auto-restarted
|
||||||
|
# by Docker rather than consuming host memory. ai-cluster is the primary OOM suspect
|
||||||
|
# (unbounded Python workers, no limits since deployment).
|
||||||
|
#
|
||||||
|
# Architectural note: compute workloads here should migrate to SOLARIA (GPU node).
|
||||||
|
# Until migration: contain the blast radius with per-container limits.
|
||||||
|
#
|
||||||
|
# Pre-cutover: service-ops-worker still mounts compose/env from old paths.
|
||||||
|
# After cutover and git pull, these overrides are removed and base compose paths are used.
|
||||||
|
|
||||||
|
services:
|
||||||
|
codex-worker:
|
||||||
|
mem_limit: 64m
|
||||||
|
|
||||||
|
openclaw:
|
||||||
|
mem_limit: 128m
|
||||||
|
|
||||||
|
planner-worker:
|
||||||
|
mem_limit: 64m
|
||||||
|
|
||||||
|
service-ops-worker:
|
||||||
|
mem_limit: 64m
|
||||||
|
# Pre-cutover: override bind mounts to keep pointing at old dockeruser paths
|
||||||
|
volumes:
|
||||||
|
- /home/dockeruser/docker/ai-cluster/docker-compose.yml:/app/docker-compose.yml:ro
|
||||||
|
- /home/dockeruser/docker/ai-cluster/.env:/app/.env:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:rw
|
||||||
|
|
||||||
|
redis:
|
||||||
|
mem_limit: 32m
|
||||||
|
|
||||||
|
mosquitto:
|
||||||
|
mem_limit: 32m
|
||||||
6
hosts/vps/runtime/joplin/docker-compose.override.yml
Normal file
6
hosts/vps/runtime/joplin/docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
mem_limit: 224m
|
||||||
|
|
||||||
|
db:
|
||||||
|
mem_limit: 128m
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
services:
|
||||||
|
node_exporter:
|
||||||
|
mem_limit: 32m
|
||||||
6
hosts/vps/runtime/npm/docker-compose.override.yml
Normal file
6
hosts/vps/runtime/npm/docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
services:
|
||||||
|
npm:
|
||||||
|
mem_limit: 160m
|
||||||
|
# Public ingress — elevated OOM protection so TLS termination + proxy host
|
||||||
|
# config survive memory pressure. Host OOM-killer will not target this container.
|
||||||
|
oom_score_adj: -800
|
||||||
9
hosts/vps/runtime/outline/docker-compose.override.yml
Normal file
9
hosts/vps/runtime/outline/docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
services:
|
||||||
|
outline:
|
||||||
|
mem_limit: 512m
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
mem_limit: 96m
|
||||||
|
|
||||||
|
redis:
|
||||||
|
mem_limit: 32m
|
||||||
|
|
@ -41,3 +41,81 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
local: []
|
local: []
|
||||||
external: []
|
external: []
|
||||||
|
|
||||||
|
npm:
|
||||||
|
role: reverse-proxy-ingress
|
||||||
|
deployment_model: docker-compose
|
||||||
|
exposure: public
|
||||||
|
offline_required: false
|
||||||
|
depends_on:
|
||||||
|
local: []
|
||||||
|
external: []
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
container_port: 80
|
||||||
|
protocol: tcp
|
||||||
|
- name: https
|
||||||
|
container_port: 443
|
||||||
|
protocol: tcp
|
||||||
|
- name: admin
|
||||||
|
container_port: 81
|
||||||
|
protocol: tcp
|
||||||
|
runtime:
|
||||||
|
data_path: /home/dockeruser/docker/npm/data
|
||||||
|
config_path: /opt/homelab/config/npm
|
||||||
|
|
||||||
|
outline:
|
||||||
|
role: team-wiki
|
||||||
|
deployment_model: docker-compose
|
||||||
|
exposure: public
|
||||||
|
offline_required: false
|
||||||
|
depends_on:
|
||||||
|
local:
|
||||||
|
- npm
|
||||||
|
external: []
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
container_port: 3000
|
||||||
|
protocol: tcp
|
||||||
|
runtime:
|
||||||
|
config_path: /opt/homelab/config/outline
|
||||||
|
|
||||||
|
joplin:
|
||||||
|
role: note-sync-server
|
||||||
|
deployment_model: docker-compose
|
||||||
|
exposure: tailscale-internal
|
||||||
|
offline_required: false
|
||||||
|
depends_on:
|
||||||
|
local:
|
||||||
|
- npm
|
||||||
|
external: []
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
container_port: 22300
|
||||||
|
bind: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
runtime:
|
||||||
|
config_path: /opt/homelab/config/joplin
|
||||||
|
|
||||||
|
ai-cluster:
|
||||||
|
role: ai-worker-cluster
|
||||||
|
deployment_model: docker-compose
|
||||||
|
exposure: tailscale-internal
|
||||||
|
offline_required: false
|
||||||
|
depends_on:
|
||||||
|
local: []
|
||||||
|
external:
|
||||||
|
- piha:gateway
|
||||||
|
ports:
|
||||||
|
- name: openclaw-api
|
||||||
|
container_port: 8000
|
||||||
|
protocol: tcp
|
||||||
|
- name: mqtt
|
||||||
|
container_port: 1883
|
||||||
|
protocol: tcp
|
||||||
|
bind: tailscale
|
||||||
|
runtime:
|
||||||
|
config_path: /opt/homelab/config/ai-cluster
|
||||||
|
notes:
|
||||||
|
- "Local images must be built on VPS — not pulled from registry"
|
||||||
|
- "Compute workloads belong on SOLARIA; migrate when possible"
|
||||||
|
|
|
||||||
110
services/ai-cluster/docker-compose.yml
Normal file
110
services/ai-cluster/docker-compose.yml
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
services:
|
||||||
|
codex-worker:
|
||||||
|
image: ai-cluster-codex-worker
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- AGENT_ID=vps-dev-1
|
||||||
|
- ROLE=dev
|
||||||
|
- MQTT_HOST=mosquitto
|
||||||
|
- MQTT_PORT=1883
|
||||||
|
- MQTT_USERNAME=${MQTT_USERNAME:-codex}
|
||||||
|
- MQTT_PASSWORD=${MQTT_PASSWORD}
|
||||||
|
- GATEWAY_BASE_URL=${GATEWAY_BASE_URL:-http://piha:8080}
|
||||||
|
- REQUEST_TIMEOUT_SECONDS=30
|
||||||
|
command: ["python", "worker.py"]
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
openclaw:
|
||||||
|
image: ai-cluster-openclaw
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- MQTT_HOST=mosquitto
|
||||||
|
- MQTT_PORT=1883
|
||||||
|
- MQTT_USERNAME=${MQTT_USERNAME:-codex}
|
||||||
|
- MQTT_PASSWORD=${MQTT_PASSWORD}
|
||||||
|
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
- npm_default
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
planner-worker:
|
||||||
|
image: ai-cluster-planner-worker
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- AGENT_ID=vps-planner-1
|
||||||
|
- ROLE=planner
|
||||||
|
- MQTT_HOST=mosquitto
|
||||||
|
- MQTT_PORT=1883
|
||||||
|
- MQTT_USERNAME=${MQTT_USERNAME:-codex}
|
||||||
|
- MQTT_PASSWORD=${MQTT_PASSWORD}
|
||||||
|
command: ["python", "planner_worker.py"]
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
service-ops-worker:
|
||||||
|
image: ai-cluster-service-ops-worker
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- AGENT_ID=vps-service-ops-1
|
||||||
|
- ROLE=service-ops
|
||||||
|
- MQTT_HOST=mosquitto
|
||||||
|
- MQTT_PORT=1883
|
||||||
|
- MQTT_USERNAME=${MQTT_USERNAME:-codex}
|
||||||
|
- MQTT_PASSWORD=${MQTT_PASSWORD}
|
||||||
|
- COMPOSE_PROJECT_NAME=ai-cluster
|
||||||
|
command: ["python", "service_ops_worker.py"]
|
||||||
|
volumes:
|
||||||
|
# Post-migration: compose definition and env are in the repo/runtime paths.
|
||||||
|
# Pre-cutover these are overridden to old paths via docker-compose.override.yml.
|
||||||
|
- /home/oskar/homelab-codex-ws/services/ai-cluster/docker-compose.yml:/app/docker-compose.yml:ro
|
||||||
|
- /opt/homelab/config/ai-cluster/.env:/app/.env:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:rw
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["redis-server"]
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
mosquitto:
|
||||||
|
image: eclipse-mosquitto:2
|
||||||
|
container_name: mosquitto
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["/usr/sbin/mosquitto", "-c", "/mosquitto/config/mosquitto.conf"]
|
||||||
|
ports:
|
||||||
|
# Tailscale IP binding — matches running container
|
||||||
|
- "100.95.58.48:1883:1883"
|
||||||
|
volumes:
|
||||||
|
# Config: kept at old path until mosquitto config migration is complete
|
||||||
|
- /home/dockeruser/docker/ai-cluster/mosquitto:/mosquitto/config:ro
|
||||||
|
- mosquitto_data:/mosquitto/data
|
||||||
|
- mosquitto_log:/mosquitto/log
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data:
|
||||||
|
mosquitto_data:
|
||||||
|
mosquitto_log:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
driver: bridge
|
||||||
|
name: ai-cluster_ai-cluster
|
||||||
|
npm_default:
|
||||||
|
external: true
|
||||||
|
name: npm_default
|
||||||
14
services/ai-cluster/env.example
Normal file
14
services/ai-cluster/env.example
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# AI Cluster — /opt/homelab/config/ai-cluster/.env
|
||||||
|
# Read by all worker containers and mounted into service-ops-worker as /app/.env
|
||||||
|
|
||||||
|
# MQTT broker credentials
|
||||||
|
MQTT_HOST=mosquitto
|
||||||
|
MQTT_PORT=1883
|
||||||
|
MQTT_USERNAME=codex
|
||||||
|
MQTT_PASSWORD=
|
||||||
|
|
||||||
|
# API gateway (piha)
|
||||||
|
GATEWAY_BASE_URL=http://piha:8080
|
||||||
|
|
||||||
|
# Compose project name (required for service-ops-worker docker-compose operations)
|
||||||
|
COMPOSE_PROJECT_NAME=ai-cluster
|
||||||
15
services/ai-cluster/healthcheck.sh
Normal file
15
services/ai-cluster/healthcheck.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Healthcheck for AI cluster (checks openclaw API gateway is responding)
|
||||||
|
|
||||||
|
if ! docker ps --filter "name=ai-cluster-openclaw-1" --filter "status=running" | grep -q "openclaw"; then
|
||||||
|
echo "[FAIL] openclaw container is not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! curl -sf http://localhost:8000/health > /dev/null; then
|
||||||
|
echo "[FAIL] openclaw HTTP health endpoint not responding"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[OK] ai-cluster is healthy"
|
||||||
|
exit 0
|
||||||
37
services/ai-cluster/service.yaml
Normal file
37
services/ai-cluster/service.yaml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
service:
|
||||||
|
name: ai-cluster
|
||||||
|
owner_node: vps
|
||||||
|
exposure: tailscale-internal
|
||||||
|
dependencies:
|
||||||
|
- mosquitto
|
||||||
|
- redis
|
||||||
|
ports:
|
||||||
|
- container: 8000
|
||||||
|
host: 8000
|
||||||
|
protocol: tcp
|
||||||
|
service: openclaw
|
||||||
|
- container: 1883
|
||||||
|
host: 1883
|
||||||
|
protocol: tcp
|
||||||
|
bind: 100.95.58.48 # Tailscale only
|
||||||
|
service: mosquitto
|
||||||
|
healthcheck:
|
||||||
|
type: http
|
||||||
|
endpoint: http://localhost:8000/health
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
restart_policy: unless-stopped
|
||||||
|
persistence:
|
||||||
|
paths:
|
||||||
|
- volume:mosquitto_config_bind # /home/dockeruser/docker/ai-cluster/mosquitto (bind, not volume)
|
||||||
|
runtime:
|
||||||
|
env_file: /opt/homelab/config/ai-cluster/.env
|
||||||
|
env_vars:
|
||||||
|
- MQTT_PASSWORD
|
||||||
|
- MQTT_USERNAME
|
||||||
|
- GATEWAY_BASE_URL
|
||||||
|
notes:
|
||||||
|
- "Local images (ai-cluster-*) must be built on VPS before deployment"
|
||||||
|
- "service-ops-worker mounts docker.sock and the compose file — needs post-migration path update"
|
||||||
|
- "Recommendation: move ai-cluster compute workloads to SOLARIA (GPU/compute node)"
|
||||||
44
services/joplin/docker-compose.yml
Normal file
44
services/joplin/docker-compose.yml
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: joplin/server:latest
|
||||||
|
container_name: joplin-server
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- /opt/homelab/config/joplin/.env
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:22300:22300"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- joplin_net
|
||||||
|
- npm_default
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:18
|
||||||
|
container_name: joplin-db
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- /opt/homelab/config/joplin/.env
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql
|
||||||
|
networks:
|
||||||
|
- joplin_net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U joplin -d joplin"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
external: true
|
||||||
|
name: joplin_postgres_data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
joplin_net:
|
||||||
|
driver: bridge
|
||||||
|
name: joplin-net
|
||||||
|
npm_default:
|
||||||
|
external: true
|
||||||
|
name: npm_default
|
||||||
20
services/joplin/env.example
Normal file
20
services/joplin/env.example
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Joplin Server — /opt/homelab/config/joplin/.env
|
||||||
|
# Both the `app` (joplin-server) and `db` (postgres) containers read this file.
|
||||||
|
|
||||||
|
# Application
|
||||||
|
APP_BASE_URL=https://joplin.example.com
|
||||||
|
APP_PORT=22300
|
||||||
|
TRUST_PROXY=1
|
||||||
|
RUNNING_IN_DOCKER=1
|
||||||
|
|
||||||
|
# Database connection (joplin-server reads these)
|
||||||
|
DB_CLIENT=pg
|
||||||
|
POSTGRES_HOST=db
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=joplin
|
||||||
|
POSTGRES_DB=joplin
|
||||||
|
POSTGRES_DATABASE=joplin
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
PM2_HOME=/opt/pm2
|
||||||
15
services/joplin/healthcheck.sh
Normal file
15
services/joplin/healthcheck.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Healthcheck for Joplin Server
|
||||||
|
|
||||||
|
if ! docker ps --filter "name=joplin-server" --filter "status=running" | grep -q "joplin-server"; then
|
||||||
|
echo "[FAIL] joplin-server container is not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! curl -sf http://localhost:22300/api/ping > /dev/null; then
|
||||||
|
echo "[FAIL] Joplin Server HTTP endpoint not responding"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[OK] Joplin Server is healthy"
|
||||||
|
exit 0
|
||||||
31
services/joplin/service.yaml
Normal file
31
services/joplin/service.yaml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
service:
|
||||||
|
name: joplin
|
||||||
|
owner_node: vps
|
||||||
|
exposure: tailscale-internal
|
||||||
|
dependencies:
|
||||||
|
- db
|
||||||
|
ports:
|
||||||
|
- container: 22300
|
||||||
|
host: 22300
|
||||||
|
protocol: tcp
|
||||||
|
bind: 127.0.0.1
|
||||||
|
healthcheck:
|
||||||
|
type: http
|
||||||
|
endpoint: http://localhost:22300/api/ping
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
restart_policy: unless-stopped
|
||||||
|
persistence:
|
||||||
|
paths:
|
||||||
|
- volume:joplin_postgres_data # Joplin notes DB
|
||||||
|
runtime:
|
||||||
|
env_file: /opt/homelab/config/joplin/.env
|
||||||
|
env_vars:
|
||||||
|
- APP_BASE_URL
|
||||||
|
- APP_PORT
|
||||||
|
- DB_CLIENT
|
||||||
|
- POSTGRES_HOST
|
||||||
|
- POSTGRES_USER
|
||||||
|
- POSTGRES_PASSWORD
|
||||||
|
- POSTGRES_DB
|
||||||
|
|
@ -8,5 +8,7 @@ services:
|
||||||
- '81:81'
|
- '81:81'
|
||||||
- '443:443'
|
- '443:443'
|
||||||
volumes:
|
volumes:
|
||||||
- /opt/homelab/data/npm/data:/data
|
# Data lives at dockeruser's path — do NOT move these without a migration plan.
|
||||||
- /opt/homelab/data/npm/letsencrypt:/etc/letsencrypt
|
# Proxy hosts, SSL certs, and DB are stored here.
|
||||||
|
- /home/dockeruser/docker/npm/data:/data
|
||||||
|
- /home/dockeruser/docker/npm/letsencrypt:/etc/letsencrypt
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,6 @@ service:
|
||||||
restart_policy: unless-stopped
|
restart_policy: unless-stopped
|
||||||
persistence:
|
persistence:
|
||||||
paths:
|
paths:
|
||||||
- /opt/homelab/data/npm/data
|
- /home/dockeruser/docker/npm/data
|
||||||
- /opt/homelab/data/npm/letsencrypt
|
- /home/dockeruser/docker/npm/letsencrypt
|
||||||
runtime:
|
|
||||||
directories:
|
|
||||||
- /opt/homelab/data/npm/data
|
|
||||||
- /opt/homelab/data/npm/letsencrypt
|
|
||||||
env_vars: []
|
env_vars: []
|
||||||
|
|
|
||||||
68
services/outline/docker-compose.yml
Normal file
68
services/outline/docker-compose.yml
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
services:
|
||||||
|
outline:
|
||||||
|
image: outlinewiki/outline:1.6.1
|
||||||
|
container_name: outline-outline-1
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- /opt/homelab/config/outline/.env
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- outline_storage:/var/lib/outline/data
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- outline_internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:3000/_health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: outline-postgres-1
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- /opt/homelab/config/outline/.env
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- outline_internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U outline -d outline"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: outline-redis-1
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- outline_internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
outline_storage:
|
||||||
|
external: true
|
||||||
|
name: outline_outline_storage
|
||||||
|
postgres_data:
|
||||||
|
external: true
|
||||||
|
name: outline_postgres_data
|
||||||
|
redis_data:
|
||||||
|
external: true
|
||||||
|
name: outline_redis_data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
outline_internal:
|
||||||
|
driver: bridge
|
||||||
|
name: outline_outline_internal
|
||||||
40
services/outline/env.example
Normal file
40
services/outline/env.example
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Outline Wiki — /opt/homelab/config/outline/.env
|
||||||
|
# Both the `outline` and `postgres` containers read this file.
|
||||||
|
|
||||||
|
# Application
|
||||||
|
URL=https://outline.example.com
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
FILE_STORAGE=local
|
||||||
|
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
|
||||||
|
FORCE_HTTPS=true
|
||||||
|
|
||||||
|
# Secrets — generate with: openssl rand -hex 32
|
||||||
|
SECRET_KEY=
|
||||||
|
UTILS_SECRET=
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgres://outline:<password>@postgres:5432/outline
|
||||||
|
PGSSLMODE=disable
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
|
# Postgres sidecar vars (read by the postgres container)
|
||||||
|
POSTGRES_USER=outline
|
||||||
|
POSTGRES_DB=outline
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# Google OAuth (optional)
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# SMTP
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USERNAME=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_FROM_EMAIL=outline@example.com
|
||||||
|
SMTP_REPLY_EMAIL=outline@example.com
|
||||||
|
SMTP_SECURE=false
|
||||||
|
ALLOWED_DOMAINS=
|
||||||
15
services/outline/healthcheck.sh
Normal file
15
services/outline/healthcheck.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Healthcheck for Outline Wiki stack
|
||||||
|
|
||||||
|
if ! docker ps --filter "name=outline-outline-1" --filter "status=running" | grep -q "outline-outline-1"; then
|
||||||
|
echo "[FAIL] outline container is not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! curl -sf http://localhost:3000/_health > /dev/null; then
|
||||||
|
echo "[FAIL] Outline HTTP health endpoint not responding"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[OK] Outline is healthy"
|
||||||
|
exit 0
|
||||||
36
services/outline/service.yaml
Normal file
36
services/outline/service.yaml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
service:
|
||||||
|
name: outline
|
||||||
|
owner_node: vps
|
||||||
|
exposure: public
|
||||||
|
dependencies:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
ports:
|
||||||
|
- container: 3000
|
||||||
|
host: 3000
|
||||||
|
protocol: tcp
|
||||||
|
healthcheck:
|
||||||
|
type: http
|
||||||
|
endpoint: http://localhost:3000/_health
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
restart_policy: unless-stopped
|
||||||
|
persistence:
|
||||||
|
paths:
|
||||||
|
# Docker named volumes — data stays at Docker volume paths
|
||||||
|
- volume:outline_outline_storage # /var/lib/outline/data inside container
|
||||||
|
- volume:outline_postgres_data # Postgres data directory
|
||||||
|
- volume:outline_redis_data # Redis persistence
|
||||||
|
runtime:
|
||||||
|
env_file: /opt/homelab/config/outline/.env
|
||||||
|
env_vars:
|
||||||
|
- URL
|
||||||
|
- DATABASE_URL
|
||||||
|
- REDIS_URL
|
||||||
|
- SECRET_KEY
|
||||||
|
- UTILS_SECRET
|
||||||
|
- FILE_STORAGE
|
||||||
|
- POSTGRES_USER
|
||||||
|
- POSTGRES_PASSWORD
|
||||||
|
- POSTGRES_DB
|
||||||
Loading…
Reference in a new issue