Compare commits
10
Commits
0b71597ced
...
0db6fdf16a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0db6fdf16a | ||
|
|
8020f416dc | ||
|
|
bb95f050a1 | ||
|
|
bbcd097ccf | ||
|
|
5884eea4d8 | ||
|
|
d013a7e12a | ||
|
|
8d947a5ae1 | ||
|
|
148516688c | ||
|
|
5c4836564c | ||
|
|
733a40fec0 |
No files matched your search
@@ -0,0 +1,10 @@
|
|||||||
|
proxmox:
|
||||||
|
hosts:
|
||||||
|
pve-goose:
|
||||||
|
ansible_host: 192.168.7.240
|
||||||
|
pve-duck:
|
||||||
|
ansible_host: 192.168.7.241
|
||||||
|
|
||||||
|
vars:
|
||||||
|
ansible_user: "ansible"
|
||||||
|
ansible_ssh_private_key_file: "/home/nick/.ssh/infra_ansible_key"
|
||||||
@@ -24,21 +24,28 @@
|
|||||||
|
|
||||||
- name: Deploy Domain Watchdog
|
- name: Deploy Domain Watchdog
|
||||||
vars:
|
vars:
|
||||||
|
# Keep this path: compose prefixes named volumes (database_data etc.)
|
||||||
|
# with the directory name, so moving it orphans the existing data.
|
||||||
domain_watchdog_dir: /opt/domain-watchdog
|
domain_watchdog_dir: /opt/domain-watchdog
|
||||||
|
# Branch of AtlasHorizon/compose to deploy from; override with
|
||||||
|
# `-e compose_branch=<branch>` to test unmerged changes.
|
||||||
|
compose_branch: main
|
||||||
block:
|
block:
|
||||||
- name: Clone domain-watchdog repo
|
|
||||||
ansible.builtin.git:
|
|
||||||
repo: https://github.com/maelgangloff/domain-watchdog.git
|
|
||||||
dest: "{{ domain_watchdog_dir }}"
|
|
||||||
version: master
|
|
||||||
update: yes
|
|
||||||
|
|
||||||
- name: Create public/content directory
|
- name: Create public/content directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ domain_watchdog_dir }}/public/content"
|
path: "{{ domain_watchdog_dir }}/public/content"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
|
# force: true so changes in the compose repo are picked up on reruns;
|
||||||
|
# it only reports changed when the content differs.
|
||||||
|
- name: Download docker-compose.yml
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "https://git.atlashorizon.net/AtlasHorizon/compose/raw/branch/{{ compose_branch }}/domain-watchdog/docker-compose.yml"
|
||||||
|
dest: "{{ domain_watchdog_dir }}/docker-compose.yml"
|
||||||
|
mode: "0644"
|
||||||
|
force: true
|
||||||
|
|
||||||
# Pulled from Infisical, not generated/persisted to disk. Run this
|
# Pulled from Infisical, not generated/persisted to disk. Run this
|
||||||
# playbook as `infisical run -- ansible-playbook -i inventory ...`
|
# playbook as `infisical run -- ansible-playbook -i inventory ...`
|
||||||
# with DOMAIN_WATCHDOG_POSTGRES_PASSWORD, DOMAIN_WATCHDOG_APP_SECRET,
|
# with DOMAIN_WATCHDOG_POSTGRES_PASSWORD, DOMAIN_WATCHDOG_APP_SECRET,
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
grist_dir: /opt/grist
|
grist_dir: /opt/grist
|
||||||
grist_hostname: grist.atlashorizon.net
|
grist_hostname: grist.atlashorizon.net
|
||||||
grist_oidc_issuer_host: auth.atlashorizon.net
|
grist_oidc_issuer_host: auth.atlashorizon.net
|
||||||
|
# Branch of AtlasHorizon/compose to deploy from; override with
|
||||||
|
# `-e compose_branch=<branch>` to test unmerged changes.
|
||||||
|
compose_branch: main
|
||||||
block:
|
block:
|
||||||
- name: Create persist directory
|
- name: Create persist directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@@ -34,11 +37,14 @@
|
|||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
- name: Deploy docker-compose.yml
|
# force: true so changes in the compose repo are picked up on reruns;
|
||||||
ansible.builtin.copy:
|
# it only reports changed when the content differs.
|
||||||
src: templates/grist-docker-compose.yml
|
- name: Download docker-compose.yml
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "https://git.atlashorizon.net/AtlasHorizon/compose/raw/branch/{{ compose_branch }}/grist/docker-compose.yml"
|
||||||
dest: "{{ grist_dir }}/docker-compose.yml"
|
dest: "{{ grist_dir }}/docker-compose.yml"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
force: true
|
||||||
|
|
||||||
# Pulled from Infisical, not generated/persisted to disk. Run this
|
# Pulled from Infisical, not generated/persisted to disk. Run this
|
||||||
# playbook as `infisical run -- ansible-playbook -i inventory ...`
|
# playbook as `infisical run -- ansible-playbook -i inventory ...`
|
||||||
|
|||||||
@@ -24,21 +24,28 @@
|
|||||||
|
|
||||||
- name: Deploy Nametag
|
- name: Deploy Nametag
|
||||||
vars:
|
vars:
|
||||||
|
# Keep this path: compose prefixes named volumes (postgres_data etc.)
|
||||||
|
# with the directory name, so moving it orphans the existing data.
|
||||||
nametag_dir: /opt/nametag
|
nametag_dir: /opt/nametag
|
||||||
|
# Branch of AtlasHorizon/compose to deploy from; override with
|
||||||
|
# `-e compose_branch=<branch>` to test unmerged changes.
|
||||||
|
compose_branch: main
|
||||||
block:
|
block:
|
||||||
- name: Clone nametag repo
|
|
||||||
ansible.builtin.git:
|
|
||||||
repo: https://github.com/mattogodoy/nametag.git
|
|
||||||
dest: "{{ nametag_dir }}"
|
|
||||||
version: master
|
|
||||||
update: yes
|
|
||||||
|
|
||||||
- name: Create backups directory
|
- name: Create backups directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ nametag_dir }}/backups"
|
path: "{{ nametag_dir }}/backups"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
|
# force: true so changes in the compose repo are picked up on reruns;
|
||||||
|
# it only reports changed when the content differs.
|
||||||
|
- name: Download docker-compose.yml
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "https://git.atlashorizon.net/AtlasHorizon/compose/raw/branch/{{ compose_branch }}/nametag/docker-compose.yml"
|
||||||
|
dest: "{{ nametag_dir }}/docker-compose.yml"
|
||||||
|
mode: "0644"
|
||||||
|
force: true
|
||||||
|
|
||||||
# Pulled from Infisical, not generated/persisted to disk. Run this
|
# Pulled from Infisical, not generated/persisted to disk. Run this
|
||||||
# playbook as `infisical run -- ansible-playbook -i inventory ...`
|
# playbook as `infisical run -- ansible-playbook -i inventory ...`
|
||||||
# with NAMETAG_DB_PASSWORD, NAMETAG_NEXTAUTH_SECRET,
|
# with NAMETAG_DB_PASSWORD, NAMETAG_NEXTAUTH_SECRET,
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Set up Supernote Private Cloud
|
|
||||||
hosts: supernote
|
|
||||||
become: yes
|
|
||||||
tasks:
|
|
||||||
- name: default_packages
|
|
||||||
ansible.builtin.include_tasks: ../../templates/default_packages.yml
|
|
||||||
|
|
||||||
- name: create_user
|
|
||||||
ansible.builtin.include_tasks: ../../templates/create_user.yml
|
|
||||||
vars:
|
|
||||||
username: nick
|
|
||||||
github_user: Chewbaccalakis
|
|
||||||
|
|
||||||
- name: Install Docker
|
|
||||||
ansible.builtin.include_tasks: ../../templates/install_docker.yml
|
|
||||||
|
|
||||||
- name: Add nick to docker group
|
|
||||||
ansible.builtin.user:
|
|
||||||
name: nick
|
|
||||||
groups:
|
|
||||||
- docker
|
|
||||||
append: true
|
|
||||||
|
|
||||||
- name: Deploy Supernote Private Cloud
|
|
||||||
vars:
|
|
||||||
supernote_dir: /opt/supernote
|
|
||||||
block:
|
|
||||||
- name: Create data persistence directories
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ supernote_dir }}/{{ item }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
loop:
|
|
||||||
- sndata/db_data
|
|
||||||
- sndata/config/mysql/conf.d
|
|
||||||
- sndata/redis_data
|
|
||||||
- sndata/recycle
|
|
||||||
- sndata/logs/cloud
|
|
||||||
- sndata/logs/app
|
|
||||||
- sndata/logs/web
|
|
||||||
- sndata/convert
|
|
||||||
- supernote_data
|
|
||||||
|
|
||||||
# Downloaded fresh each run so upstream schema changes apply on
|
|
||||||
# reruns, per Supernote's own upgrade instructions. docker-compose.yml
|
|
||||||
# is vendored instead (see templates/supernote-docker-compose.yml) -
|
|
||||||
# the upstream file is missing the initdb mounts this playbook needs.
|
|
||||||
- name: Download Supernote database init script
|
|
||||||
ansible.builtin.get_url:
|
|
||||||
url: https://supernote-private-cloud.supernote.com/cloud/supernotedb.sql
|
|
||||||
dest: "{{ supernote_dir }}/supernotedb.sql"
|
|
||||||
mode: "0644"
|
|
||||||
force: yes
|
|
||||||
|
|
||||||
- name: Deploy docker-compose.yml
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: templates/supernote-docker-compose.yml
|
|
||||||
dest: "{{ supernote_dir }}/docker-compose.yml"
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
# Pulled from Infisical, not generated/persisted to disk. Run this
|
|
||||||
# playbook as `infisical run -- ansible-playbook -i inventory ...`
|
|
||||||
# with SUPERNOTE_MYSQL_ROOT_PASSWORD, SUPERNOTE_MYSQL_PASSWORD,
|
|
||||||
# SUPERNOTE_REDIS_PASSWORD, SUPERNOTE_SMTP_SERVER, SUPERNOTE_SMTP_USER,
|
|
||||||
# and SUPERNOTE_SMTP_PASSWORD secrets set in the workspace.
|
|
||||||
# SUPERNOTE_SMTP_PORT (default 465) and SUPERNOTE_SMTP_ENCRYPTION
|
|
||||||
# (default ssl) are optional.
|
|
||||||
- name: Read secrets from environment (populated by `infisical run`)
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
supernote_mysql_root_password: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_MYSQL_ROOT_PASSWORD') }}"
|
|
||||||
supernote_mysql_password: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_MYSQL_PASSWORD') }}"
|
|
||||||
supernote_redis_password: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_REDIS_PASSWORD') }}"
|
|
||||||
supernote_smtp_server: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_SERVER') }}"
|
|
||||||
supernote_smtp_port: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_PORT') }}"
|
|
||||||
supernote_smtp_user: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_USER') }}"
|
|
||||||
supernote_smtp_password: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_PASSWORD') }}"
|
|
||||||
supernote_smtp_encryption: "{{ lookup('ansible.builtin.env', 'SUPERNOTE_SMTP_ENCRYPTION') }}"
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Verify required secrets were provided
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- supernote_mysql_root_password | length > 0
|
|
||||||
- supernote_mysql_password | length > 0
|
|
||||||
- supernote_redis_password | length > 0
|
|
||||||
- supernote_smtp_server | length > 0
|
|
||||||
- supernote_smtp_user | length > 0
|
|
||||||
- supernote_smtp_password | length > 0
|
|
||||||
fail_msg: >-
|
|
||||||
SUPERNOTE_MYSQL_ROOT_PASSWORD, SUPERNOTE_MYSQL_PASSWORD,
|
|
||||||
SUPERNOTE_REDIS_PASSWORD, SUPERNOTE_SMTP_SERVER,
|
|
||||||
SUPERNOTE_SMTP_USER, and SUPERNOTE_SMTP_PASSWORD must be set.
|
|
||||||
Run this playbook via `infisical run -- ansible-playbook -i
|
|
||||||
inventory ...` with those secrets defined in the workspace.
|
|
||||||
|
|
||||||
- name: Deploy .env file (docker compose variable substitution)
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: templates/supernote.env.j2
|
|
||||||
dest: "{{ supernote_dir }}/.env"
|
|
||||||
mode: "0600"
|
|
||||||
|
|
||||||
- name: Deploy SMTP config init script
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: templates/supernote-email-init.sql.j2
|
|
||||||
dest: "{{ supernote_dir }}/supernote-email-init.sql"
|
|
||||||
mode: "0600"
|
|
||||||
|
|
||||||
- name: Pull prebuilt Supernote images
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: docker compose pull
|
|
||||||
chdir: "{{ supernote_dir }}"
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Start Supernote Private Cloud via docker compose
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: docker compose up -d
|
|
||||||
chdir: "{{ supernote_dir }}"
|
|
||||||
register: supernote_compose_up
|
|
||||||
changed_when: "'Started' in supernote_compose_up.stderr or 'Created' in supernote_compose_up.stderr"
|
|
||||||
|
|
||||||
# docker-entrypoint-initdb.d scripts only run once, against an empty
|
|
||||||
# database, so reruns wouldn't otherwise pick up SMTP changes made in
|
|
||||||
# Infisical. Re-applying here keeps Infisical the source of truth.
|
|
||||||
- name: Apply SMTP configuration to the database
|
|
||||||
ansible.builtin.shell:
|
|
||||||
cmd: >-
|
|
||||||
docker compose exec -T -e MYSQL_PWD="{{ supernote_mysql_root_password }}"
|
|
||||||
mariadb mysql -u root supernotedb < {{ supernote_dir }}/supernote-email-init.sql
|
|
||||||
chdir: "{{ supernote_dir }}"
|
|
||||||
changed_when: false
|
|
||||||
no_log: true
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Managed by Ansible (grist.yml). Vendored here since grist-core has no
|
|
||||||
# docker-compose.yml at its repo root (see docker-compose-examples/ upstream).
|
|
||||||
services:
|
|
||||||
grist:
|
|
||||||
image: gristlabs/grist:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8484:8484"
|
|
||||||
volumes:
|
|
||||||
- ./persist:/persist
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# Managed by Ansible (omnivore.yml) - generated secrets persist in
|
|
||||||
# ansible/plays/machine_setup/credentials/omnivore/ on the control node.
|
|
||||||
|
|
||||||
# Postgres & Migrate
|
|
||||||
|
|
||||||
POSTGRES_USER=postgres
|
|
||||||
POSTGRES_PASSWORD={{ omnivore_postgres_password }}
|
|
||||||
POSTGRES_DB=omnivore
|
|
||||||
PGPASSWORD={{ omnivore_postgres_password }}
|
|
||||||
PG_REPLICATION=false
|
|
||||||
|
|
||||||
PG_HOST=postgres
|
|
||||||
PG_PASSWORD={{ omnivore_pg_password }}
|
|
||||||
PG_DB=omnivore
|
|
||||||
PG_USER=app_user
|
|
||||||
PG_PORT=5432
|
|
||||||
PG_POOL_MAX=20
|
|
||||||
|
|
||||||
# API
|
|
||||||
|
|
||||||
API_ENV=local
|
|
||||||
IMAGE_PROXY_SECRET={{ omnivore_image_proxy_secret }}
|
|
||||||
JWT_SECRET={{ omnivore_jwt_secret }}
|
|
||||||
SSO_JWT_SECRET={{ omnivore_sso_jwt_secret }}
|
|
||||||
GATEWAY_URL=http://api:8080/api
|
|
||||||
CONTENT_FETCH_URL=http://content-fetch:8080/?token={{ omnivore_verification_token }}
|
|
||||||
GCS_USE_LOCAL_HOST=true
|
|
||||||
GCS_UPLOAD_BUCKET=omnivore
|
|
||||||
AUTO_VERIFY=true
|
|
||||||
# Minio credentials are hardcoded in docker-compose.yml for the minio and
|
|
||||||
# createbuckets services, so these must match those literal values.
|
|
||||||
AWS_ACCESS_KEY_ID=minio
|
|
||||||
AWS_SECRET_ACCESS_KEY=miniominio
|
|
||||||
AWS_REGION=us-east-1
|
|
||||||
CONTENT_FETCH_QUEUE_ENABLED=true
|
|
||||||
|
|
||||||
IMAGE_PROXY_URL=http://{{ ansible_host }}:7070
|
|
||||||
CLIENT_URL=http://{{ ansible_host }}:3000
|
|
||||||
LOCAL_MINIO_URL=http://{{ ansible_host }}:1010
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
REDIS_URL=redis://redis:6379/0
|
|
||||||
|
|
||||||
# Mail
|
|
||||||
WATCHER_API_KEY={{ omnivore_mail_watcher_key }}
|
|
||||||
LOCAL_EMAIL_DOMAIN=domain.tld
|
|
||||||
# SNS_ARN=arn_of_sns #for if you use SES and SNS for Email.
|
|
||||||
|
|
||||||
# Web
|
|
||||||
APP_ENV=prod
|
|
||||||
BASE_URL=http://{{ ansible_host }}:3000
|
|
||||||
SERVER_BASE_URL=http://{{ ansible_host }}:4000
|
|
||||||
HIGHLIGHTS_BASE_URL=http://{{ ansible_host }}:3000
|
|
||||||
# USE_DISCOVER_AI=true
|
|
||||||
# OPENAI_API_KEY=<OPENAI_API_KEY>
|
|
||||||
|
|
||||||
# Content Fetch
|
|
||||||
VERIFICATION_TOKEN={{ omnivore_verification_token }}
|
|
||||||
REST_BACKEND_ENDPOINT=http://api:8080/api
|
|
||||||
SKIP_UPLOAD_ORIGINAL=true
|
|
||||||
|
|
||||||
# Minio
|
|
||||||
MINIO_ACCESS_KEY=minio
|
|
||||||
MINIO_SECRET_KEY=miniominio
|
|
||||||
AWS_S3_ENDPOINT_URL=http://minio:9000
|
|
||||||
|
|
||||||
# Export
|
|
||||||
EXPORT_QUEUE_NAME=omnivore-backend-queue
|
|
||||||
|
|
||||||
# Youtube
|
|
||||||
YOUTUBE_TRANSCRIPT_PROMPT=Reformat the supplied transcript data adding formatting, puncuation, and paragraphs, but do not summarize or change the content. Format the output as markdown.
|
|
||||||
YOUTUBE_MAXIMUM_VIDEO_DURATION_TRANSCRIPT=1801
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
# Vendored from https://supernote-private-cloud.supernote.com/cloud/docker-compose.yml,
|
|
||||||
# with two fixes applied:
|
|
||||||
# - the mariadb service is missing docker-entrypoint-initdb.d mounts in the
|
|
||||||
# upstream file, so supernotedb.sql would never actually load into a
|
|
||||||
# fresh database; added below.
|
|
||||||
# - a second init script (supernote-email-init.sql, rendered by Ansible
|
|
||||||
# from Infisical-sourced secrets) pre-seeds SMTP settings into
|
|
||||||
# u_email_config so registration/password-reset emails work without a
|
|
||||||
# manual trip through the web UI's "Email Settings" dialog.
|
|
||||||
# supernote-service's DB_HOSTNAME/MYSQL_*/REDIS_* env vars are also inlined
|
|
||||||
# directly here instead of upstream's separate .dbenv/env_file, since they're
|
|
||||||
# just ${VAR} substitutions from the same .env used by mariadb/redis.
|
|
||||||
services:
|
|
||||||
mariadb:
|
|
||||||
image: mariadb:10.6.24
|
|
||||||
container_name: mariadb
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
|
||||||
MYSQL_DATABASE: supernotedb
|
|
||||||
MYSQL_USER: ${MYSQL_USER}
|
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
|
||||||
TZ: ${TZ:-UTC}
|
|
||||||
command: --skip-name-resolve --bind-address=0.0.0.0
|
|
||||||
volumes:
|
|
||||||
- ./sndata/db_data:/var/lib/mysql
|
|
||||||
- ./sndata/config/mysql/conf.d:/etc/mysql/conf.d:ro
|
|
||||||
- ./supernotedb.sql:/docker-entrypoint-initdb.d/00-supernotedb.sql:ro
|
|
||||||
- ./supernote-email-init.sql:/docker-entrypoint-initdb.d/01-supernote-email-init.sql:ro
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD || exit 1"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
start_period: 30s
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7.4.7
|
|
||||||
container_name: redis
|
|
||||||
environment:
|
|
||||||
TZ: ${TZ:-UTC}
|
|
||||||
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
|
|
||||||
volumes:
|
|
||||||
- ./sndata/redis_data:/data
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
notelib:
|
|
||||||
image: docker.io/supernote/notelib:6.9.3
|
|
||||||
container_name: notelib
|
|
||||||
environment:
|
|
||||||
TZ: ${TZ:-UTC}
|
|
||||||
volumes:
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
supernote-service:
|
|
||||||
image: docker.io/supernote/supernote-service:26.06.04
|
|
||||||
container_name: supernote-service
|
|
||||||
depends_on:
|
|
||||||
mariadb:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "19071:19071"
|
|
||||||
- "18072:18072"
|
|
||||||
- "${FRONTEND_PORT:-19072}:8080"
|
|
||||||
- "${DOMAIN_PORT:-8199}:8080"
|
|
||||||
environment:
|
|
||||||
EXTERNAL_PORT_BACKEND: "19071"
|
|
||||||
EXTERNAL_PORT_FRONTEND: ${FRONTEND_PORT:-19072}
|
|
||||||
EXTERNAL_PORT_DOMAIN: ${DOMAIN_PORT:-8199}
|
|
||||||
TZ: ${TZ:-UTC}
|
|
||||||
DB_HOSTNAME: mariadb
|
|
||||||
MYSQL_DATABASE: supernotedb
|
|
||||||
MYSQL_USER: ${MYSQL_USER}
|
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
|
||||||
REDIS_HOST: redis
|
|
||||||
REDIS_PORT: 6379
|
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
|
||||||
volumes:
|
|
||||||
- ./sndata/recycle:/home/supernote/recycle
|
|
||||||
- ./supernote_data:/home/supernote/data
|
|
||||||
- ./sndata/logs/cloud:/home/supernote/cloud/logs
|
|
||||||
- ./sndata/logs/app:/home/supernote/logs
|
|
||||||
- ./sndata/logs/web:/var/log/nginx
|
|
||||||
- ./sndata/convert:/home/supernote/convert
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
restart: unless-stopped
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
-- Managed by Ansible (supernote.yml) - SMTP secrets sourced from Infisical
|
|
||||||
-- via `infisical run -- ansible-playbook ...`.
|
|
||||||
--
|
|
||||||
-- Mounted into the mariadb container's docker-entrypoint-initdb.d/ so it
|
|
||||||
-- seeds u_email_config on first boot, and re-applied by the playbook via
|
|
||||||
-- `docker compose exec` on every run so Infisical stays the source of truth.
|
|
||||||
--
|
|
||||||
-- u_email_config's schema/upsert key come from a third-party reverse
|
|
||||||
-- engineering of Supernote's supernotedb.sql (not officially documented) -
|
|
||||||
-- see https://github.com/liketheduck/supernote-private-cloud-docker. Verify
|
|
||||||
-- against Email Settings in the web UI if Supernote changes this schema.
|
|
||||||
|
|
||||||
USE supernotedb;
|
|
||||||
|
|
||||||
INSERT INTO u_email_config (smtp_server, port, user_name, password, encryption, flag)
|
|
||||||
VALUES (
|
|
||||||
'{{ supernote_smtp_server }}',
|
|
||||||
'{{ supernote_smtp_port | default(465) }}',
|
|
||||||
'{{ supernote_smtp_user }}',
|
|
||||||
'{{ supernote_smtp_password }}',
|
|
||||||
'{{ supernote_smtp_encryption | default("ssl") }}',
|
|
||||||
'Y'
|
|
||||||
)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
smtp_server='{{ supernote_smtp_server }}',
|
|
||||||
port='{{ supernote_smtp_port | default(465) }}',
|
|
||||||
user_name='{{ supernote_smtp_user }}',
|
|
||||||
password='{{ supernote_smtp_password }}',
|
|
||||||
encryption='{{ supernote_smtp_encryption | default("ssl") }}',
|
|
||||||
flag='Y';
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Managed by Ansible (supernote.yml) - secrets sourced from Infisical via
|
|
||||||
# `infisical run -- ansible-playbook ...`.
|
|
||||||
#
|
|
||||||
# Consumed by docker compose for ${VAR} substitution in docker-compose.yml.
|
|
||||||
|
|
||||||
MYSQL_ROOT_PASSWORD={{ supernote_mysql_root_password }}
|
|
||||||
MYSQL_USER=supernote
|
|
||||||
MYSQL_PASSWORD={{ supernote_mysql_password }}
|
|
||||||
|
|
||||||
REDIS_PASSWORD={{ supernote_redis_password }}
|
|
||||||
+2
-2
@@ -8,9 +8,9 @@ terraform {
|
|||||||
backend "s3" {
|
backend "s3" {
|
||||||
bucket = "terraform"
|
bucket = "terraform"
|
||||||
key = "infra/terraform.tfstate"
|
key = "infra/terraform.tfstate"
|
||||||
region = "garage"
|
region = "us-east-1"
|
||||||
|
|
||||||
endpoint = "http://s3.lan.oswaldtheotter.net:30188"
|
endpoint = "https://goldie.lan.atlashorizon.net:30292"
|
||||||
use_path_style = true
|
use_path_style = true
|
||||||
|
|
||||||
skip_credentials_validation = true
|
skip_credentials_validation = true
|
||||||
|
|||||||
+361
-87
@@ -8,9 +8,12 @@ systemd:
|
|||||||
contents: |
|
contents: |
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Gitea MCP Server
|
Description=Gitea MCP Server
|
||||||
After=docker.service
|
After=docker.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
[Service]
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
ExecStartPre=-/usr/bin/docker rm --force gitea-mcp
|
ExecStartPre=-/usr/bin/docker rm --force gitea-mcp
|
||||||
# The image ships a broken HEALTHCHECK: it is CMD-SHELL form but the
|
# The image ships a broken HEALTHCHECK: it is CMD-SHELL form but the
|
||||||
@@ -21,7 +24,7 @@ systemd:
|
|||||||
ExecStart=/usr/bin/docker run --name gitea-mcp --no-healthcheck \
|
ExecStart=/usr/bin/docker run --name gitea-mcp --no-healthcheck \
|
||||||
-e GITEA_HOST=https://git.atlashorizon.net \
|
-e GITEA_HOST=https://git.atlashorizon.net \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
docker.gitea.com/gitea-mcp-server \
|
$${GITEA_MCP_IMAGE} \
|
||||||
/app/gitea-mcp -t http --port 8080
|
/app/gitea-mcp -t http --port 8080
|
||||||
ExecStop=/usr/bin/docker stop gitea-mcp
|
ExecStop=/usr/bin/docker stop gitea-mcp
|
||||||
Restart=always
|
Restart=always
|
||||||
@@ -33,9 +36,12 @@ systemd:
|
|||||||
contents: |
|
contents: |
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=LiteLLM proxy
|
Description=LiteLLM proxy
|
||||||
After=docker.service
|
After=docker.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
[Service]
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
ExecStartPre=-/usr/bin/docker rm --force litellm
|
ExecStartPre=-/usr/bin/docker rm --force litellm
|
||||||
ExecStart=/usr/bin/docker run --name litellm \
|
ExecStart=/usr/bin/docker run --name litellm \
|
||||||
@@ -45,112 +51,380 @@ systemd:
|
|||||||
-e LITELLM_SALT_KEY=${litellm_salt_key} \
|
-e LITELLM_SALT_KEY=${litellm_salt_key} \
|
||||||
-e LITELLM_LOG=ERROR \
|
-e LITELLM_LOG=ERROR \
|
||||||
-e JSON_LOGS=True \
|
-e JSON_LOGS=True \
|
||||||
|
-v /etc/litellm/config.yaml:/app/config.yaml:ro \
|
||||||
-p 4000:4000 \
|
-p 4000:4000 \
|
||||||
docker.litellm.ai/berriai/litellm:main-stable
|
$${LITELLM_IMAGE} \
|
||||||
|
--config /app/config.yaml --port 4000
|
||||||
ExecStop=/usr/bin/docker stop litellm
|
ExecStop=/usr/bin/docker stop litellm
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
# ── Open WebUI (disabled) ──────────────────────────────────────────────
|
# Open WebUI, with Qdrant alongside it for document embeddings.
|
||||||
# Commented out until it is ready to launch. To re-enable:
|
|
||||||
# 1. strip the leading "# " from every line of the block below (this
|
|
||||||
# restores the original indentation exactly)
|
|
||||||
# 2. un-double the dollar signs in each interpolation: $$ back to $
|
|
||||||
# 3. re-add the seven OPENWEBUI_* keys to secrets.tmpl (listed below;
|
|
||||||
# they are removed while this is off so the generated JSON, which
|
|
||||||
# data.external parses, stays valid)
|
|
||||||
#
|
#
|
||||||
# OPENWEBUI_DATABASE_URL -> openwebui_database_url
|
# Prerequisites, all of which must exist before the VM is rebuilt:
|
||||||
# OPENWEBUI_SECRET_KEY -> openwebui_secret_key
|
# - Postgres database (plain; no pgvector extension needed -- embeddings
|
||||||
# OPENWEBUI_S3_ACCESS_KEY -> openwebui_s3_access_key
|
# go to Qdrant, and Postgres only holds users, chats and settings)
|
||||||
# OPENWEBUI_S3_SECRET_KEY -> openwebui_s3_secret_key
|
# - openwebui bucket in Garage, plus a key with r/w on it
|
||||||
# OPENWEBUI_OAUTH_CLIENT_ID -> openwebui_oauth_client_id
|
# - an embedding model in LiteLLM matching RAG_EMBEDDING_MODEL below
|
||||||
# OPENWEBUI_OAUTH_CLIENT_SECRET -> openwebui_oauth_client_secret
|
# - an OIDC client at the provider, with the callback URI registered as
|
||||||
# OPENWEBUI_OIDC_PROVIDER_URL -> openwebui_oidc_provider_url
|
# https://llm.atlashorizon.net/oauth/oidc/callback
|
||||||
|
# - llm.atlashorizon.net terminating TLS and proxying to this host's
|
||||||
|
# :3000 (not managed here -- see WEBUI_URL below)
|
||||||
|
# - the OPENWEBUI_* secrets in Infisical (see secrets.tmpl)
|
||||||
#
|
#
|
||||||
# The doubling in step 2 is not cosmetic: templatefile() substitutes on
|
# After a taint: the vector store is empty and Open WebUI does not say so
|
||||||
# raw text before anything parses it as YAML, so an interpolation left
|
# -- knowledge bases still appear, retrieval just returns nothing. Re-embed
|
||||||
# intact here would still be resolved -- and would still fail the plan on
|
# with POST /api/v1/knowledge/reindex (admin). Note that this covers
|
||||||
# a missing key -- even though it sits inside a comment.
|
# knowledge bases only: files attached directly to a single chat are not
|
||||||
# # Open WebUI.
|
# re-indexed, so anything worth keeping belongs in a knowledge base.
|
||||||
# #
|
- name: qdrant.service
|
||||||
# # Prerequisites, all of which must exist before the VM is rebuilt:
|
enabled: true
|
||||||
# # - Postgres database, with "CREATE EXTENSION vector;" run inside it
|
contents: |
|
||||||
# # - openwebui bucket in Garage, plus a key with r/w on it
|
[Unit]
|
||||||
# # - an embedding model in LiteLLM matching RAG_EMBEDDING_MODEL below
|
Description=Qdrant vector database
|
||||||
# # - an OIDC client at the provider, with the callback URI registered as
|
After=docker.service versions-fetch.service
|
||||||
# # https://llm.atlashorizon.net/oauth/oidc/callback
|
Wants=versions-fetch.service
|
||||||
# # - llm.atlashorizon.net terminating TLS and proxying to this host's
|
Requires=docker.service
|
||||||
# # :3000 (not managed here -- see WEBUI_URL below)
|
[Service]
|
||||||
# # - the OPENWEBUI_* secrets in Infisical (see secrets.tmpl)
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
# - name: open-webui.service
|
EnvironmentFile=/etc/versions.env
|
||||||
# enabled: true
|
TimeoutStartSec=0
|
||||||
# contents: |
|
# Both containers sit on a private docker network and Qdrant publishes
|
||||||
# [Unit]
|
# no ports: it ships with no authentication, and nothing outside this
|
||||||
# Description=Open WebUI
|
# host needs to reach it. Creation is idempotent, hence the "-".
|
||||||
# After=docker.service litellm.service
|
ExecStartPre=-/usr/bin/docker network create openwebui
|
||||||
# Requires=docker.service
|
ExecStartPre=/usr/bin/docker pull $${QDRANT_IMAGE}
|
||||||
# [Service]
|
ExecStartPre=-/usr/bin/docker rm --force qdrant
|
||||||
# TimeoutStartSec=0
|
# Vectors live on the VM disk, so a taint destroys them. That is
|
||||||
# # Large image, so it is pulled up front: a slow first pull is then not
|
# deliberate -- they are derived data, rebuildable from the source
|
||||||
# # mistaken for a hung unit.
|
# files in Garage (see the reindex note above). The bind mount is what
|
||||||
# ExecStartPre=/usr/bin/docker pull ghcr.io/open-webui/open-webui:v0.11.4
|
# keeps them across container restarts and image pulls, which the
|
||||||
# ExecStartPre=-/usr/bin/docker rm --force open-webui
|
# docker rm above would otherwise wipe every time.
|
||||||
# # No volume is mounted. Every piece of durable state lives off-box --
|
ExecStart=/usr/bin/docker run --name qdrant \
|
||||||
# # Postgres for app data, pgvector for embeddings, Garage for uploads --
|
--network openwebui \
|
||||||
# # so /app/backend/data holds nothing worth keeping and the container
|
-v /var/lib/qdrant:/qdrant/storage \
|
||||||
# # (and the whole VM) can be rebuilt at will. What stays local is cache
|
$${QDRANT_IMAGE}
|
||||||
# # only, re-fetched on the next start.
|
ExecStop=/usr/bin/docker stop qdrant
|
||||||
# #
|
Restart=always
|
||||||
# # gitea-mcp already owns host :8080, so the UI is published on :3000.
|
RestartSec=5s
|
||||||
# ExecStart=/usr/bin/docker run --name open-webui \
|
[Install]
|
||||||
# -e DATABASE_URL=$${openwebui_database_url} \
|
WantedBy=multi-user.target
|
||||||
# -e VECTOR_DB=pgvector \
|
# Open Terminal: the sandbox Open WebUI's models get a shell in, for
|
||||||
# -e PGVECTOR_DB_URL=$${openwebui_database_url} \
|
# running code, installing packages and working on files.
|
||||||
# -e STORAGE_PROVIDER=s3 \
|
#
|
||||||
# -e S3_ENDPOINT_URL=http://s3.lan.oswaldtheotter.net:30188 \
|
# Anything a model does lands inside this container, not on the host: no
|
||||||
# -e S3_REGION_NAME=garage \
|
# docker socket, no host paths, no published port. It is reachable only
|
||||||
# -e S3_BUCKET_NAME=openwebui \
|
# over the private "openwebui" network, so the API key below is defence in
|
||||||
# -e S3_ACCESS_KEY_ID=$${openwebui_s3_access_key} \
|
# depth rather than the only thing standing in the way.
|
||||||
# -e S3_SECRET_ACCESS_KEY=$${openwebui_s3_secret_key} \
|
#
|
||||||
# -e WEBUI_SECRET_KEY=$${openwebui_secret_key} \
|
# Note that it does have outbound network access -- it needs it to install
|
||||||
# -e ENABLE_OLLAMA_API=false \
|
# packages -- so a model can reach the rest of the LAN from in here. That
|
||||||
# -e OPENAI_API_BASE_URL=http://192.168.7.61:4000/v1 \
|
# is the part worth thinking about before pointing an untrusted model at
|
||||||
# -e OPENAI_API_KEY=$${litellm_master_key} \
|
# it; docker's --internal would close it off, but would also break package
|
||||||
# -e RAG_EMBEDDING_ENGINE=openai \
|
# installation, which is most of the point.
|
||||||
# -e RAG_OPENAI_API_BASE_URL=http://192.168.7.61:4000/v1 \
|
- name: open-terminal.service
|
||||||
# -e RAG_OPENAI_API_KEY=$${litellm_master_key} \
|
enabled: true
|
||||||
# -e RAG_EMBEDDING_MODEL=text-embedding-3-small \
|
contents: |
|
||||||
# -e OAUTH_CLIENT_ID=$${openwebui_oauth_client_id} \
|
[Unit]
|
||||||
# -e OAUTH_CLIENT_SECRET=$${openwebui_oauth_client_secret} \
|
Description=Open Terminal sandbox
|
||||||
# -e OPENID_PROVIDER_URL=$${openwebui_oidc_provider_url} \
|
After=docker.service versions-fetch.service
|
||||||
# -e OAUTH_PROVIDER_NAME=SSO \
|
Wants=versions-fetch.service
|
||||||
# -e ENABLE_OAUTH_SIGNUP=true \
|
Requires=docker.service
|
||||||
# -e WEBUI_URL=https://llm.atlashorizon.net \
|
[Service]
|
||||||
# -p 3000:8080 \
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
# ghcr.io/open-webui/open-webui:v0.11.4
|
EnvironmentFile=/etc/versions.env
|
||||||
# ExecStop=/usr/bin/docker stop open-webui
|
TimeoutStartSec=0
|
||||||
# Restart=always
|
ExecStartPre=-/usr/bin/docker network create openwebui
|
||||||
# RestartSec=5s
|
ExecStartPre=/usr/bin/docker pull $${OPEN_TERMINAL_IMAGE}
|
||||||
# [Install]
|
ExecStartPre=-/usr/bin/docker rm --force open-terminal
|
||||||
# WantedBy=multi-user.target
|
# A named volume, not a bind mount: /home/user belongs to the image's
|
||||||
|
# unprivileged user, and a named volume inherits that ownership from
|
||||||
|
# the image where a root-owned bind mount would not. It lives under
|
||||||
|
# /var/lib/docker on the VM disk, so a taint discards the workspace --
|
||||||
|
# the right lifetime for scratch space.
|
||||||
|
ExecStart=/usr/bin/docker run --name open-terminal \
|
||||||
|
--network openwebui \
|
||||||
|
-v open-terminal:/home/user \
|
||||||
|
-e OPEN_TERMINAL_API_KEY=${openwebui_terminal_api_key} \
|
||||||
|
$${OPEN_TERMINAL_IMAGE}
|
||||||
|
ExecStop=/usr/bin/docker stop open-terminal
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
- name: open-webui.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=Open WebUI
|
||||||
|
# After= orders the start only, it does not wait for Qdrant to be
|
||||||
|
# serving. Restart=always covers the gap if Qdrant is still coming up.
|
||||||
|
After=docker.service litellm.service qdrant.service open-terminal.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
|
Requires=docker.service
|
||||||
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
|
TimeoutStartSec=0
|
||||||
|
# Large image, so it is pulled up front: a slow first pull is then not
|
||||||
|
# mistaken for a hung unit.
|
||||||
|
ExecStartPre=/usr/bin/docker pull $${OPEN_WEBUI_IMAGE}
|
||||||
|
ExecStartPre=-/usr/bin/docker network create openwebui
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force open-webui
|
||||||
|
# No volume is mounted. Every piece of durable state lives off-box --
|
||||||
|
# Postgres for app data, Garage for uploads -- so /app/backend/data
|
||||||
|
# holds nothing worth keeping and the container can be recreated at
|
||||||
|
# will. Qdrant is the one exception, and it is expendable by design.
|
||||||
|
#
|
||||||
|
# gitea-mcp already owns host :8080, so the UI is published on :3000.
|
||||||
|
ExecStart=/usr/bin/docker run --name open-webui \
|
||||||
|
--network openwebui \
|
||||||
|
-e DATABASE_URL=${openwebui_database_url} \
|
||||||
|
-e VECTOR_DB=qdrant \
|
||||||
|
-e QDRANT_URI=http://qdrant:6333 \
|
||||||
|
-e QDRANT_ON_DISK=true \
|
||||||
|
-e STORAGE_PROVIDER=s3 \
|
||||||
|
-e S3_ENDPOINT_URL=http://s3.lan.oswaldtheotter.net:30188 \
|
||||||
|
-e S3_REGION_NAME=garage \
|
||||||
|
-e S3_BUCKET_NAME=openwebui \
|
||||||
|
-e S3_ACCESS_KEY_ID=${openwebui_s3_access_key} \
|
||||||
|
-e S3_SECRET_ACCESS_KEY=${openwebui_s3_secret_key} \
|
||||||
|
-e WEBUI_SECRET_KEY=${openwebui_secret_key} \
|
||||||
|
-e ENABLE_OLLAMA_API=false \
|
||||||
|
-e OPENAI_API_BASE_URL=http://192.168.7.61:4000/v1 \
|
||||||
|
-e OPENAI_API_KEY=${litellm_master_key} \
|
||||||
|
-e RAG_EMBEDDING_ENGINE=openai \
|
||||||
|
-e RAG_OPENAI_API_BASE_URL=http://192.168.7.61:4000/v1 \
|
||||||
|
-e RAG_OPENAI_API_KEY=${litellm_master_key} \
|
||||||
|
-e RAG_EMBEDDING_MODEL=text-embedding-3-small \
|
||||||
|
-e OAUTH_CLIENT_ID=${openwebui_oauth_client_id} \
|
||||||
|
-e OAUTH_CLIENT_SECRET=${openwebui_oauth_client_secret} \
|
||||||
|
-e OPENID_PROVIDER_URL=${openwebui_oidc_provider_url} \
|
||||||
|
-e OAUTH_PROVIDER_NAME=AtlasHorizon \
|
||||||
|
-e ENABLE_OAUTH_SIGNUP=true \
|
||||||
|
-e OAUTH_UPDATE_PICTURE_ON_LOGIN=true \
|
||||||
|
-e ENABLE_OAUTH_GROUP_MANAGEMENT=true \
|
||||||
|
-e OAUTH_PICTURE_CLAIM=avatar \
|
||||||
|
-e "OAUTH_SCOPES=openid email profile groups avatar" \
|
||||||
|
-e OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true \
|
||||||
|
-e CORS_ALLOW_ORIGIN=https://llm.atlashorizon.net \
|
||||||
|
-e WEBUI_URL=https://llm.atlashorizon.net \
|
||||||
|
-e 'TERMINAL_SERVER_CONNECTIONS=[{"id":"local","name":"Open Terminal","url":"http://open-terminal:8000","key":"${openwebui_terminal_api_key}","auth_type":"bearer","enabled":true}]' \
|
||||||
|
-p 3000:8080 \
|
||||||
|
$${OPEN_WEBUI_IMAGE}
|
||||||
|
ExecStop=/usr/bin/docker stop open-webui
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
# OpenCode, run headless as a coding agent that n8n drives over HTTP.
|
||||||
|
#
|
||||||
|
# Prerequisites, all of which must exist before the VM is rebuilt:
|
||||||
|
# - OPENCODE_SERVER_PASSWORD in Infisical (see secrets.tmpl). Every
|
||||||
|
# Butane file is rendered on every plan, so until the secret exists
|
||||||
|
# no plan succeeds -- not just this VM's
|
||||||
|
# - each model listed in /etc/opencode/opencode.json (below) defined
|
||||||
|
# in LiteLLM under the same model_name
|
||||||
|
#
|
||||||
|
# Unlike Open Terminal, this publishes a port: n8n runs on oswald-runner,
|
||||||
|
# so the private "openwebui" network is out of its reach. That leaves HTTP
|
||||||
|
# Basic Auth (user "opencode") as the only thing between the LAN and an
|
||||||
|
# agent with a shell, and over plain HTTP. The agent runs as root inside
|
||||||
|
# the container, with the same outbound network access as Open Terminal.
|
||||||
|
#
|
||||||
|
# From n8n: http://192.168.7.61:4096, with the n8n-nodes-opencode-ai
|
||||||
|
# community node or HTTP Request nodes (the OpenAPI spec is at /doc).
|
||||||
|
- name: opencode.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=OpenCode server
|
||||||
|
After=docker.service litellm.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
|
Requires=docker.service
|
||||||
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
|
TimeoutStartSec=0
|
||||||
|
ExecStartPre=/usr/bin/docker pull $${OPENCODE_IMAGE}
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force opencode
|
||||||
|
# Both directories are on the VM disk, so they survive container
|
||||||
|
# restarts but not a taint. workspace is what the agent edits; data
|
||||||
|
# holds its sessions, which n8n refers to by id across executions.
|
||||||
|
#
|
||||||
|
# The LiteLLM key is passed in the environment and read by the config
|
||||||
|
# file as {env:LITELLM_API_KEY}, which keeps the file free of secrets.
|
||||||
|
ExecStart=/usr/bin/docker run --name opencode \
|
||||||
|
-e OPENCODE_SERVER_USERNAME=opencode \
|
||||||
|
-e OPENCODE_SERVER_PASSWORD=${opencode_server_password} \
|
||||||
|
-e LITELLM_API_KEY=${litellm_master_key} \
|
||||||
|
-e OPENCODE_CONFIG=/etc/opencode/opencode.json \
|
||||||
|
-v /etc/opencode/opencode.json:/etc/opencode/opencode.json:ro \
|
||||||
|
-v /var/lib/opencode/workspace:/workspace \
|
||||||
|
-v /var/lib/opencode/data:/root/.local/share/opencode \
|
||||||
|
-w /workspace \
|
||||||
|
-p 4096:4096 \
|
||||||
|
$${OPENCODE_IMAGE} \
|
||||||
|
serve --hostname 0.0.0.0 --port 4096
|
||||||
|
ExecStop=/usr/bin/docker stop opencode
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
# Rebuilds the vector store after a taint. Runs on every boot, but only
|
||||||
|
# acts when Qdrant holds no Open WebUI collections -- that is, when the
|
||||||
|
# local volume went with the VM.
|
||||||
|
#
|
||||||
|
# The admin API key it authenticates with lives in Postgres, so it
|
||||||
|
# survives the rebuild that wipes Qdrant. That is what makes this work.
|
||||||
|
- name: openwebui-reindex.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=Rebuild Open WebUI vectors when Qdrant is empty
|
||||||
|
After=open-webui.service qdrant.service
|
||||||
|
Requires=open-webui.service qdrant.service
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
TimeoutStartSec=30min
|
||||||
|
ExecStart=/opt/bin/openwebui-reindex.sh
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
- name: dozzle-agent.service
|
- name: dozzle-agent.service
|
||||||
enabled: true
|
enabled: true
|
||||||
contents: |
|
contents: |
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Dozzle Agent
|
Description=Dozzle Agent
|
||||||
After=docker.service
|
After=docker.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
[Service]
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
ExecStartPre=-/usr/bin/docker rm --force dozzle-agent
|
ExecStartPre=-/usr/bin/docker rm --force dozzle-agent
|
||||||
ExecStart=/usr/bin/docker run --name dozzle-agent \
|
ExecStart=/usr/bin/docker run --name dozzle-agent \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-p 7007:7007 \
|
-p 7007:7007 \
|
||||||
amir20/dozzle:v11.1.1 agent
|
$${DOZZLE_IMAGE} agent
|
||||||
ExecStop=/usr/bin/docker stop dozzle-agent
|
ExecStop=/usr/bin/docker stop dozzle-agent
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
storage:
|
||||||
|
directories:
|
||||||
|
# Qdrant's vector store. On the VM disk on purpose: a taint is meant to
|
||||||
|
# discard it. The qdrant image runs as root (ARG USER_ID=0), so a
|
||||||
|
# root-owned 0700 directory is what it expects.
|
||||||
|
- path: /var/lib/qdrant
|
||||||
|
mode: 0700
|
||||||
|
# OpenCode's workspace and session store; see opencode.service. The
|
||||||
|
# image runs as root.
|
||||||
|
- path: /var/lib/opencode/workspace
|
||||||
|
mode: 0755
|
||||||
|
- path: /var/lib/opencode/data
|
||||||
|
mode: 0700
|
||||||
|
files:
|
||||||
|
# Companion to openwebui-reindex.service above.
|
||||||
|
#
|
||||||
|
# Note on quoting: templatefile() substitutes on raw text, so shell
|
||||||
|
# variables are written as $VAR without braces. A braced shell variable
|
||||||
|
# anywhere in this script would be read as a template reference and fail
|
||||||
|
# the plan; only the interpolation below is meant to be substituted.
|
||||||
|
- path: /opt/bin/openwebui-reindex.sh
|
||||||
|
mode: 0700
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
#!/usr/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
OWUI=http://127.0.0.1:3000
|
||||||
|
OWUI_KEY='${openwebui_admin_api_key}'
|
||||||
|
|
||||||
|
# Qdrant publishes no ports -- it is reachable only on the private
|
||||||
|
# "openwebui" docker network -- so it is probed from inside the
|
||||||
|
# open-webui container, which is on that network and ships curl.
|
||||||
|
qdrant_curl() {
|
||||||
|
docker exec open-webui curl -fsS "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait up to 10 minutes for both services to answer.
|
||||||
|
ready=0
|
||||||
|
for i in $(seq 1 120); do
|
||||||
|
if curl -fsS "$OWUI/health" >/dev/null 2>&1 \
|
||||||
|
&& qdrant_curl http://qdrant:6333/readyz >/dev/null 2>&1; then
|
||||||
|
ready=1; break
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
[ "$ready" = 1 ] || { echo "services never became ready"; exit 1; }
|
||||||
|
|
||||||
|
# Collections are named "<QDRANT_COLLECTION_PREFIX>_<id>", and the
|
||||||
|
# prefix defaults to open-webui. Any hit means the volume survived.
|
||||||
|
if qdrant_curl http://qdrant:6333/collections \
|
||||||
|
| grep -q '"name":"open-webui'; then
|
||||||
|
echo "Qdrant already populated; skipping reindex"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Qdrant empty; reindexing knowledge bases"
|
||||||
|
curl -fsS -X POST -H "Authorization: Bearer $OWUI_KEY" \
|
||||||
|
"$OWUI/api/v1/knowledge/reindex"
|
||||||
|
# LiteLLM proxy settings. Models stay in the database
|
||||||
|
# (STORE_MODEL_IN_DB), so this file carries general_settings only --
|
||||||
|
# those have no environment equivalent and are readable only from a
|
||||||
|
# config file.
|
||||||
|
#
|
||||||
|
# Requests reach :4000 through a Cloudflare tunnel whose connector runs
|
||||||
|
# on 192.168.0.0/24, so every request arrives from that connector's
|
||||||
|
# address. Without use_x_forwarded_for LiteLLM does MCP access control
|
||||||
|
# against that peer, which sits inside the default internal ranges, and
|
||||||
|
# so treats every caller on the public internet as internal -- exposing
|
||||||
|
# MCP servers marked "available_on_public_internet: false".
|
||||||
|
#
|
||||||
|
# mcp_trusted_proxy_ranges is what keeps that safe: X-Forwarded-For is
|
||||||
|
# honoured only when the direct peer is the connector, so a crafted
|
||||||
|
# header from anywhere else cannot forge a client IP. Widen it if the
|
||||||
|
# tunnel ever moves off that subnet, or the header is ignored again.
|
||||||
|
- path: /etc/litellm/config.yaml
|
||||||
|
mode: 0644
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
general_settings:
|
||||||
|
use_x_forwarded_for: true
|
||||||
|
mcp_trusted_proxy_ranges:
|
||||||
|
- 192.168.0.0/24
|
||||||
|
# OpenCode's provider config. LiteLLM speaks the OpenAI API, so it is
|
||||||
|
# wired in through @ai-sdk/openai-compatible.
|
||||||
|
#
|
||||||
|
# Model keys must match LiteLLM model_name values exactly. Those live in
|
||||||
|
# the LiteLLM database, not in a file here, so check GET :4000/v1/models
|
||||||
|
# when changing this list. "model" is the default for new sessions.
|
||||||
|
- path: /etc/opencode/opencode.json
|
||||||
|
mode: 0644
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"model": "litellm/claude-sonnet-5",
|
||||||
|
"provider": {
|
||||||
|
"litellm": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "LiteLLM",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "http://192.168.7.61:4000/v1",
|
||||||
|
"apiKey": "{env:LITELLM_API_KEY}"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"claude-sonnet-5": {
|
||||||
|
"name": "Claude Sonnet 5"
|
||||||
|
},
|
||||||
|
"qwen3:4b-instruct": {
|
||||||
|
"name": "Qwen3 4B Instruct"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
variant: flatcar
|
|
||||||
version: 1.0.0
|
|
||||||
|
|
||||||
# CISA KEV RSS feed. Turns the CISA Known Exploited Vulnerabilities catalog
|
|
||||||
# into an RSS 2.0 feed (https://github.com/xsaveopt/cisa-kev-rss).
|
|
||||||
#
|
|
||||||
# Feed: http://192.168.7.64:3000/rss
|
|
||||||
# Health: http://192.168.7.64:3000/health
|
|
||||||
#
|
|
||||||
# Stateless: the container refreshes from CISA every ten minutes and keeps
|
|
||||||
# nothing on disk, so it is safe to rebuild at any time.
|
|
||||||
|
|
||||||
systemd:
|
|
||||||
units:
|
|
||||||
- name: cisa-kev-rss.service
|
|
||||||
enabled: true
|
|
||||||
contents: |
|
|
||||||
[Unit]
|
|
||||||
Description=CISA KEV RSS feed
|
|
||||||
After=docker.service
|
|
||||||
Requires=docker.service
|
|
||||||
[Service]
|
|
||||||
TimeoutStartSec=0
|
|
||||||
ExecStartPre=/usr/bin/docker pull ghcr.io/xsaveopt/cisa-kev-rss:latest
|
|
||||||
ExecStartPre=-/usr/bin/docker rm --force cisa-kev-rss
|
|
||||||
ExecStart=/usr/bin/docker run --rm --name cisa-kev-rss \
|
|
||||||
-p 3000:3000 \
|
|
||||||
ghcr.io/xsaveopt/cisa-kev-rss:latest
|
|
||||||
ExecStop=/usr/bin/docker stop cisa-kev-rss
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5s
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
- name: dozzle-agent.service
|
|
||||||
enabled: true
|
|
||||||
contents: |
|
|
||||||
[Unit]
|
|
||||||
Description=Dozzle Agent
|
|
||||||
After=docker.service
|
|
||||||
Requires=docker.service
|
|
||||||
[Service]
|
|
||||||
TimeoutStartSec=0
|
|
||||||
ExecStartPre=-/usr/bin/docker rm --force dozzle-agent
|
|
||||||
ExecStart=/usr/bin/docker run --name dozzle-agent \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
-p 7007:7007 \
|
|
||||||
amir20/dozzle:v11.1.1 agent
|
|
||||||
ExecStop=/usr/bin/docker stop dozzle-agent
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5s
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
+128
-9
@@ -13,6 +13,8 @@ version: 1.0.0
|
|||||||
# VMs (see the dozzle-agent units in the other butane configs) and, via the
|
# VMs (see the dozzle-agent units in the other butane configs) and, via the
|
||||||
# local socket, from this host's own containers. Reachable on :8080.
|
# local socket, from this host's own containers. Reachable on :8080.
|
||||||
#
|
#
|
||||||
|
# Also runs Alerta on :8082; see alerta.service below.
|
||||||
|
#
|
||||||
# No state is kept on disk: /data is a tmpfs, so the container is fully
|
# No state is kept on disk: /data is a tmpfs, so the container is fully
|
||||||
# disposable and there is nothing to back up. That is also why simple auth
|
# disposable and there is nothing to back up. That is also why simple auth
|
||||||
# (users.yml) is not used -- it would need a persistent /data.
|
# (users.yml) is not used -- it would need a persistent /data.
|
||||||
@@ -20,10 +22,11 @@ version: 1.0.0
|
|||||||
# Hub <-> agent traffic is TLS, using the certificate pair baked into the
|
# Hub <-> agent traffic is TLS, using the certificate pair baked into the
|
||||||
# dozzle image. Every hub and agent on the same image version shares it, so
|
# dozzle image. Every hub and agent on the same image version shares it, so
|
||||||
# nothing needs mounting here -- which is also why the agents must stay pinned
|
# nothing needs mounting here -- which is also why the agents must stay pinned
|
||||||
# to the same tag as the hub. To move to a custom pair later, generate one
|
# to the same tag as the hub (DOZZLE_IMAGE in versions.env). To move to a
|
||||||
# with (writes into $PWD):
|
# custom pair later, generate one with (writes into $PWD, using the tag from
|
||||||
|
# DOZZLE_IMAGE):
|
||||||
#
|
#
|
||||||
# docker run --rm -v "$PWD":/out amir20/dozzle:v11.1.1 generate-certs \
|
# docker run --rm -v "$PWD":/out amir20/dozzle:<tag> generate-certs \
|
||||||
# --cert-out /out/dozzle_cert.pem --key-out /out/dozzle_key.pem
|
# --cert-out /out/dozzle_cert.pem --key-out /out/dozzle_key.pem
|
||||||
#
|
#
|
||||||
# ...then mount it at /dozzle_cert.pem and /dozzle_key.pem (read-only) on the
|
# ...then mount it at /dozzle_cert.pem and /dozzle_key.pem (read-only) on the
|
||||||
@@ -37,16 +40,20 @@ systemd:
|
|||||||
contents: |
|
contents: |
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Dozzle log viewer hub
|
Description=Dozzle log viewer hub
|
||||||
After=docker.service
|
After=docker.service versions-fetch.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
[Service]
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
# Kept as unit environment rather than inline -e values so that
|
# Kept as unit environment rather than inline -e values so that
|
||||||
# wait-for-agents.sh can read the agent list from the same place the
|
# wait-for-agents.sh can read the agent list from the same place the
|
||||||
# container does. docker run forwards them by bare name below.
|
# container does. docker run forwards them by bare name below.
|
||||||
Environment="DOZZLE_HOSTNAME=Oswald Logs"
|
Environment="DOZZLE_HOSTNAME=Oswald Logs"
|
||||||
Environment="DOZZLE_ENABLE_MCP=true"
|
# The agent list lives in a file because dozzle-mcp.service needs the
|
||||||
Environment="DOZZLE_REMOTE_AGENT=192.168.7.60:7007|oswald-runner|Goose,192.168.7.61:7007|ai|Goose,192.168.7.62:7007|oswl-connector|Goose,192.168.7.64:7007|cisa-kev-rss|Goose"
|
# identical list; keeping it inline in both units would drift.
|
||||||
|
EnvironmentFile=/etc/dozzle/agents.env
|
||||||
# OIDC via Authelia. Dozzle builds its redirect from the incoming
|
# OIDC via Authelia. Dozzle builds its redirect from the incoming
|
||||||
# request, so the proxy in front must pass the real Host and
|
# request, so the proxy in front must pass the real Host and
|
||||||
# X-Forwarded-Proto; the URI to register in Authelia is
|
# X-Forwarded-Proto; the URI to register in Authelia is
|
||||||
@@ -62,7 +69,7 @@ systemd:
|
|||||||
Environment="DOZZLE_AUTH_OIDC_SCOPES=dozzle_roles,picture"
|
Environment="DOZZLE_AUTH_OIDC_SCOPES=dozzle_roles,picture"
|
||||||
Environment="DOZZLE_AUTH_OIDC_ROLES_CLAIM=dozzle_roles"
|
Environment="DOZZLE_AUTH_OIDC_ROLES_CLAIM=dozzle_roles"
|
||||||
# Pulled up front so a slow first pull is not mistaken for a hung unit.
|
# Pulled up front so a slow first pull is not mistaken for a hung unit.
|
||||||
ExecStartPre=/usr/bin/docker pull amir20/dozzle:v11.1.1
|
ExecStartPre=/usr/bin/docker pull $${DOZZLE_IMAGE}
|
||||||
# Leading "-" so a missing container is not a failure.
|
# Leading "-" so a missing container is not a failure.
|
||||||
ExecStartPre=-/usr/bin/docker rm --force dozzle
|
ExecStartPre=-/usr/bin/docker rm --force dozzle
|
||||||
# If the hub starts before the agents are listening, the failed initial
|
# If the hub starts before the agents are listening, the failed initial
|
||||||
@@ -76,7 +83,6 @@ systemd:
|
|||||||
--tmpfs /data \
|
--tmpfs /data \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-e DOZZLE_HOSTNAME \
|
-e DOZZLE_HOSTNAME \
|
||||||
-e DOZZLE_ENABLE_MCP \
|
|
||||||
-e DOZZLE_REMOTE_AGENT \
|
-e DOZZLE_REMOTE_AGENT \
|
||||||
-e DOZZLE_AUTH_PROVIDER \
|
-e DOZZLE_AUTH_PROVIDER \
|
||||||
-e DOZZLE_AUTH_OIDC_ISSUER \
|
-e DOZZLE_AUTH_OIDC_ISSUER \
|
||||||
@@ -85,15 +91,128 @@ systemd:
|
|||||||
-e DOZZLE_AUTH_OIDC_NAME \
|
-e DOZZLE_AUTH_OIDC_NAME \
|
||||||
-e DOZZLE_AUTH_OIDC_SCOPES \
|
-e DOZZLE_AUTH_OIDC_SCOPES \
|
||||||
-e DOZZLE_AUTH_OIDC_ROLES_CLAIM \
|
-e DOZZLE_AUTH_OIDC_ROLES_CLAIM \
|
||||||
amir20/dozzle:v11.1.1
|
$${DOZZLE_IMAGE}
|
||||||
ExecStop=/usr/bin/docker stop dozzle
|
ExecStop=/usr/bin/docker stop dozzle
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
# Second Dozzle, dedicated to MCP on :8081.
|
||||||
|
#
|
||||||
|
# It exists because the two requirements cannot be met by one instance:
|
||||||
|
# the oidc provider is the only one that lets Authelia own the user list,
|
||||||
|
# but it offers no non-interactive login, so an MCP client can never
|
||||||
|
# authenticate against it. This instance therefore runs with no auth at
|
||||||
|
# all (DOZZLE_AUTH_PROVIDER unset) and is reached only through the
|
||||||
|
# Cloudflare tunnel, which is where access control has to live.
|
||||||
|
#
|
||||||
|
# ANYTHING that can open :8081 gets unauthenticated read access to every
|
||||||
|
# log on every agent. Do not publish this port beyond the tunnel, and put
|
||||||
|
# a Cloudflare Access policy (service token) on its hostname.
|
||||||
|
- name: dozzle-mcp.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=Dozzle MCP endpoint
|
||||||
|
After=docker.service versions-fetch.service
|
||||||
|
Requires=docker.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
|
TimeoutStartSec=0
|
||||||
|
# Same agents as the hub, same file, so the two cannot drift.
|
||||||
|
EnvironmentFile=/etc/dozzle/agents.env
|
||||||
|
Environment="DOZZLE_HOSTNAME=Oswald Logs (MCP)"
|
||||||
|
Environment="DOZZLE_ENABLE_MCP=true"
|
||||||
|
ExecStartPre=/usr/bin/docker pull $${DOZZLE_IMAGE}
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force dozzle-mcp
|
||||||
|
ExecStartPre=-/usr/bin/timeout 180 /opt/dozzle/wait-for-agents.sh
|
||||||
|
ExecStart=/usr/bin/docker run --rm --name dozzle-mcp \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--tmpfs /data \
|
||||||
|
-p 8081:8080 \
|
||||||
|
-e DOZZLE_HOSTNAME \
|
||||||
|
-e DOZZLE_REMOTE_AGENT \
|
||||||
|
-e DOZZLE_ENABLE_MCP \
|
||||||
|
$${DOZZLE_IMAGE}
|
||||||
|
ExecStop=/usr/bin/docker stop dozzle-mcp
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
# Alerta alert console (API + web UI) on :8082.
|
||||||
|
#
|
||||||
|
# Prerequisites, all of which must exist before the VM is rebuilt:
|
||||||
|
# - an "alerta" Postgres database and role; the schema is created on
|
||||||
|
# first start
|
||||||
|
# - an OIDC client for Alerta in Authelia, with the Alerta URL as a
|
||||||
|
# redirect URI and token_endpoint_auth_method client_secret_basic
|
||||||
|
# - ALERTA_DATABASE_URL, ALERTA_SECRET_KEY, ALERTA_ADMIN_PASSWORD,
|
||||||
|
# ALERTA_OIDC_CLIENT_ID and ALERTA_OIDC_CLIENT_SECRET in
|
||||||
|
# Infisical (see secrets.tmpl). Every Butane file is rendered on every
|
||||||
|
# plan, so until they exist no plan succeeds -- not just this VM's.
|
||||||
|
# Generate the key with `openssl rand -hex 32`, and keep "%" out of
|
||||||
|
# the database URL: systemd reads it as a specifier
|
||||||
|
#
|
||||||
|
# Alerts, users and API keys all live in Postgres, so a taint loses
|
||||||
|
# nothing. SECRET_KEY signs login tokens; without a fixed one the image
|
||||||
|
# generates a new one per start and every session is dropped.
|
||||||
|
#
|
||||||
|
# ADMIN_PASSWORD is used only when the admin user does not exist yet.
|
||||||
|
# Changing it in Infisical afterwards has no effect: change it in the UI.
|
||||||
|
# Web logins go through Authelia; the basic-auth admin user is kept for
|
||||||
|
# API access. Self sign-up is off, so further users are added by the admin.
|
||||||
|
#
|
||||||
|
# --log-driver none keeps this container out of Dozzle. The image's
|
||||||
|
# entrypoint dumps its whole environment and prints the admin API key at
|
||||||
|
# every start, and dozzle-mcp above serves logs without auth. Output
|
||||||
|
# still reaches the journal (journalctl -u alerta) through the attached
|
||||||
|
# docker run.
|
||||||
|
- name: alerta.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=Alerta alert console
|
||||||
|
After=docker.service versions-fetch.service
|
||||||
|
Requires=docker.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
|
TimeoutStartSec=0
|
||||||
|
ExecStartPre=/usr/bin/docker pull $${ALERTA_IMAGE}
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force alerta
|
||||||
|
ExecStart=/usr/bin/docker run --rm --name alerta \
|
||||||
|
--log-driver none \
|
||||||
|
-e DATABASE_URL=${alerta_database_url} \
|
||||||
|
-e SECRET_KEY=${alerta_secret_key} \
|
||||||
|
-e AUTH_REQUIRED=True \
|
||||||
|
-e AUTH_PROVIDER=openid \
|
||||||
|
-e OIDC_ISSUER_URL=https://auth.atlashorizon.net \
|
||||||
|
-e OAUTH2_CLIENT_ID=${alerta_oidc_client_id} \
|
||||||
|
-e OAUTH2_CLIENT_SECRET=${alerta_oidc_client_secret} \
|
||||||
|
-e SIGNUP_ENABLED=True \
|
||||||
|
-e ADMIN_USERS=admin \
|
||||||
|
-e ADMIN_PASSWORD=${alerta_admin_password} \
|
||||||
|
-p 8082:8080 \
|
||||||
|
$${ALERTA_IMAGE}
|
||||||
|
ExecStop=/usr/bin/docker stop alerta
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
files:
|
files:
|
||||||
|
# Single source of truth for the agent list, shared by dozzle.service and
|
||||||
|
# dozzle-mcp.service. systemd EnvironmentFile format: no quoting, so the
|
||||||
|
# "|" and "," separators are taken literally.
|
||||||
|
- path: /etc/dozzle/agents.env
|
||||||
|
mode: 0644
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
DOZZLE_REMOTE_AGENT=192.168.7.60:7007|oswald-runner|Goose,192.168.7.61:7007|ai|Goose,192.168.7.62:7007|oswl-connector|Goose,192.168.20.74:7007|Argo|Truenas,192.168.20.4:7007|Atalantia|Truenas,192.168.7.4:7007|Goldie|Truenas,192.168.20.217:7007|Laika|CallieCorner
|
||||||
# Blocks until every agent named in DOZZLE_REMOTE_AGENT accepts a TCP
|
# Blocks until every agent named in DOZZLE_REMOTE_AGENT accepts a TCP
|
||||||
# connection. The list is read from the environment, so adding or removing
|
# connection. The list is read from the environment, so adding or removing
|
||||||
# an agent above needs no change here.
|
# an agent above needs no change here.
|
||||||
|
|||||||
@@ -25,9 +25,12 @@ systemd:
|
|||||||
contents: |
|
contents: |
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Gitea Actions runner
|
Description=Gitea Actions runner
|
||||||
After=docker.service
|
After=docker.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
[Service]
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
ExecStartPre=-/usr/bin/docker rm --force oswald_runner
|
ExecStartPre=-/usr/bin/docker rm --force oswald_runner
|
||||||
ExecStart=/usr/bin/docker run --name oswald_runner \
|
ExecStart=/usr/bin/docker run --name oswald_runner \
|
||||||
@@ -36,26 +39,257 @@ systemd:
|
|||||||
-e GITEA_RUNNER_NAME=oswald_runner \
|
-e GITEA_RUNNER_NAME=oswald_runner \
|
||||||
-v /var/lib/oswald_runner:/data \
|
-v /var/lib/oswald_runner:/data \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
docker.io/gitea/runner:3
|
$${GITEA_RUNNER_IMAGE}
|
||||||
ExecStop=/usr/bin/docker stop oswald_runner
|
ExecStop=/usr/bin/docker stop oswald_runner
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
# n8n workflow automation, served publicly at n8n.atlashorizon.net.
|
||||||
|
#
|
||||||
|
# Prerequisites, all of which must exist before the VM is rebuilt:
|
||||||
|
# - an "n8n" Postgres database and role on argo.lan.atlashorizon.net
|
||||||
|
# - N8N_DB_PASSWORD and N8N_ENCRYPTION_KEY in Infisical (see
|
||||||
|
# secrets.tmpl); generate the key with `openssl rand -hex 32`
|
||||||
|
# - n8n.atlashorizon.net terminating TLS and proxying to this host's
|
||||||
|
# :5678 (not managed here)
|
||||||
|
#
|
||||||
|
# Workflows, credentials and execution history all live in Postgres, so
|
||||||
|
# a taint loses nothing. The encryption key is what makes that true:
|
||||||
|
# without a fixed one n8n generates its own under /home/node/.n8n, and
|
||||||
|
# the credentials in Postgres become undecryptable the moment the VM
|
||||||
|
# disk goes. Never rotate it without re-entering every credential.
|
||||||
|
- name: n8n.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=n8n workflow automation
|
||||||
|
After=docker.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
|
Requires=docker.service
|
||||||
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
|
TimeoutStartSec=0
|
||||||
|
ExecStartPre=/usr/bin/docker pull $${N8N_IMAGE}
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force n8n
|
||||||
|
# /var/lib/n8n holds only what is left in /home/node/.n8n once the
|
||||||
|
# database and key are external: installed community nodes and
|
||||||
|
# n8n's own config file. Losing it to a taint means reinstalling
|
||||||
|
# community nodes, nothing more.
|
||||||
|
#
|
||||||
|
# N8N_PROXY_HOPS=1: requests arrive through the reverse proxy, so
|
||||||
|
# n8n takes the client IP from X-Forwarded-For (rate limiting and
|
||||||
|
# audit logs would otherwise see only the proxy).
|
||||||
|
ExecStart=/usr/bin/docker run --name n8n \
|
||||||
|
-e DB_TYPE=postgresdb \
|
||||||
|
-e DB_POSTGRESDB_HOST=argo.lan.atlashorizon.net \
|
||||||
|
-e DB_POSTGRESDB_PORT=5432 \
|
||||||
|
-e DB_POSTGRESDB_DATABASE=n8n \
|
||||||
|
-e DB_POSTGRESDB_USER=n8n \
|
||||||
|
-e DB_POSTGRESDB_PASSWORD=${n8n_db_password} \
|
||||||
|
-e N8N_ENCRYPTION_KEY=${n8n_encryption_key} \
|
||||||
|
-e N8N_HOST=n8n.atlashorizon.net \
|
||||||
|
-e N8N_PROTOCOL=https \
|
||||||
|
-e WEBHOOK_URL=https://n8n.atlashorizon.net/ \
|
||||||
|
-e N8N_PROXY_HOPS=1 \
|
||||||
|
-e N8N_DIAGNOSTICS_ENABLED=false \
|
||||||
|
-v /var/lib/n8n:/home/node/.n8n \
|
||||||
|
-p 5678:5678 \
|
||||||
|
$${N8N_IMAGE}
|
||||||
|
ExecStop=/usr/bin/docker stop n8n
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
# CISA KEV RSS feed. Turns the CISA Known Exploited Vulnerabilities
|
||||||
|
# catalog into an RSS 2.0 feed (https://github.com/xsaveopt/cisa-kev-rss).
|
||||||
|
#
|
||||||
|
# Feed: http://192.168.7.60:3000/rss
|
||||||
|
# Health: http://192.168.7.60:3000/health
|
||||||
|
#
|
||||||
|
# Stateless: the container refreshes from CISA every ten minutes and keeps
|
||||||
|
# nothing on disk. Moved here from its own VM (cisa-kev-rss, 192.168.7.64).
|
||||||
|
- name: cisa-kev-rss.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=CISA KEV RSS feed
|
||||||
|
After=docker.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
|
Requires=docker.service
|
||||||
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
|
TimeoutStartSec=0
|
||||||
|
ExecStartPre=/usr/bin/docker pull $${CISA_KEV_RSS_IMAGE}
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force cisa-kev-rss
|
||||||
|
ExecStart=/usr/bin/docker run --rm --name cisa-kev-rss \
|
||||||
|
-p 3000:3000 \
|
||||||
|
$${CISA_KEV_RSS_IMAGE}
|
||||||
|
ExecStop=/usr/bin/docker stop cisa-kev-rss
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
# Domain Watchdog: RDAP-based domain expiry/change monitoring
|
||||||
|
# (https://github.com/maelgangloff/domain-watchdog), served at
|
||||||
|
# domainwatchdog.atlashorizon.net. Three containers on a private docker
|
||||||
|
# network: the web app on :8083, a messenger worker, and Valkey as the
|
||||||
|
# job queue and lock store.
|
||||||
|
#
|
||||||
|
# Prerequisites, all of which must exist before the VM is rebuilt:
|
||||||
|
# - a "domainwatchdog" Postgres database and role on
|
||||||
|
# argo.lan.atlashorizon.net; the schema is migrated on every start
|
||||||
|
# - an OIDC client in Authelia with redirect URI
|
||||||
|
# https://domainwatchdog.atlashorizon.net/login/oauth/check, scopes
|
||||||
|
# openid/email/profile, and token_endpoint_auth_method
|
||||||
|
# client_secret_post (the app sends the secret in the POST body)
|
||||||
|
# - DOMAIN_WATCHDOG_DB_PASSWORD, DOMAIN_WATCHDOG_APP_SECRET,
|
||||||
|
# DOMAIN_WATCHDOG_OIDC_CLIENT_ID and DOMAIN_WATCHDOG_OIDC_CLIENT_SECRET
|
||||||
|
# in Infisical (see secrets.tmpl). Every Butane file is rendered on
|
||||||
|
# every plan, so until they exist no plan succeeds. Generate the app
|
||||||
|
# secret with `openssl rand -hex 32`; keep the DB password URL-safe
|
||||||
|
# and free of "%" (it is spliced into DATABASE_URL, and systemd reads
|
||||||
|
# "%" as a specifier)
|
||||||
|
# - domainwatchdog.atlashorizon.net terminating TLS and proxying to this
|
||||||
|
# host's :8083 with X-Forwarded-Proto/Host (not managed here)
|
||||||
|
#
|
||||||
|
# Users, watchlists and domain history all live in Postgres, so a taint
|
||||||
|
# loses nothing that matters. What does not survive it:
|
||||||
|
# - queued RDAP jobs in Valkey; the scheduler re-queues them
|
||||||
|
# - the JWT signing keys in /var/lib/domain-watchdog/jwt, which the
|
||||||
|
# entrypoint regenerates when missing, so everyone logs in again.
|
||||||
|
# They are on disk rather than in the container only so that a
|
||||||
|
# plain restart does not do the same.
|
||||||
|
#
|
||||||
|
# Login is Authelia-only: registration is off, and the first OIDC login
|
||||||
|
# for an email creates that user, so Authelia's access rules for the
|
||||||
|
# client decide who gets in.
|
||||||
|
- name: domain-watchdog-valkey.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=Domain Watchdog Valkey
|
||||||
|
After=docker.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
|
Requires=docker.service
|
||||||
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
|
TimeoutStartSec=0
|
||||||
|
ExecStartPre=/usr/bin/docker pull $${DOMAIN_WATCHDOG_VALKEY_IMAGE}
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force domain-watchdog-valkey
|
||||||
|
# Every Domain Watchdog unit tries to create the network, so start
|
||||||
|
# order does not matter; "-" because all but the first will fail.
|
||||||
|
ExecStartPre=-/usr/bin/docker network create domain-watchdog
|
||||||
|
# No volume and no port: the queue is disposable and only the other
|
||||||
|
# two containers reach it, by name, over the domain-watchdog network.
|
||||||
|
ExecStart=/usr/bin/docker run --rm --name domain-watchdog-valkey \
|
||||||
|
--network domain-watchdog \
|
||||||
|
--network-alias valkey \
|
||||||
|
$${DOMAIN_WATCHDOG_VALKEY_IMAGE}
|
||||||
|
ExecStop=/usr/bin/docker stop domain-watchdog-valkey
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
- name: domain-watchdog.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=Domain Watchdog web app
|
||||||
|
After=docker.service domain-watchdog-valkey.service versions-fetch.service
|
||||||
|
Requires=docker.service
|
||||||
|
Wants=domain-watchdog-valkey.service versions-fetch.service
|
||||||
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
|
TimeoutStartSec=0
|
||||||
|
# Secrets are unit environment rather than lines in app.env, which
|
||||||
|
# is world-readable; docker run forwards them by bare name below.
|
||||||
|
Environment="APP_SECRET=${domain_watchdog_app_secret}"
|
||||||
|
Environment="DATABASE_URL=postgresql://domainwatchdog:${domain_watchdog_db_password}@argo.lan.atlashorizon.net:5432/domainwatchdog?serverVersion=16&charset=utf8"
|
||||||
|
Environment="OAUTH_CLIENT_ID=${domain_watchdog_oidc_client_id}"
|
||||||
|
Environment="OAUTH_CLIENT_SECRET=${domain_watchdog_oidc_client_secret}"
|
||||||
|
ExecStartPre=/usr/bin/docker pull $${DOMAIN_WATCHDOG_IMAGE}
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force domain-watchdog
|
||||||
|
ExecStartPre=-/usr/bin/docker network create domain-watchdog
|
||||||
|
# The entrypoint runs DB migrations and generates the JWT keypair
|
||||||
|
# into /app/config/jwt if it is missing, then starts FrankenPHP on
|
||||||
|
# :80 (plain HTTP: SERVER_NAME=:80 in app.env, TLS is the proxy's).
|
||||||
|
ExecStart=/usr/bin/docker run --rm --name domain-watchdog \
|
||||||
|
--network domain-watchdog \
|
||||||
|
--env-file /etc/domain-watchdog/app.env \
|
||||||
|
-e APP_SECRET \
|
||||||
|
-e DATABASE_URL \
|
||||||
|
-e OAUTH_CLIENT_ID \
|
||||||
|
-e OAUTH_CLIENT_SECRET \
|
||||||
|
-v /var/lib/domain-watchdog/jwt:/app/config/jwt \
|
||||||
|
-v /etc/domain-watchdog/content:/app/public/content:ro \
|
||||||
|
-p 8083:80 \
|
||||||
|
$${DOMAIN_WATCHDOG_IMAGE}
|
||||||
|
ExecStop=/usr/bin/docker stop domain-watchdog
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
# Runs the queued RDAP lookups and notifications. Same image, so the
|
||||||
|
# entrypoint also runs migrations here; ordering after the web app keeps
|
||||||
|
# the two from racing on a fresh database. It exits every hour
|
||||||
|
# (--time-limit, as upstream recommends against PHP memory creep) and
|
||||||
|
# Restart=always brings it straight back.
|
||||||
|
- name: domain-watchdog-worker.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=Domain Watchdog worker
|
||||||
|
After=docker.service domain-watchdog-valkey.service domain-watchdog.service versions-fetch.service
|
||||||
|
Requires=docker.service
|
||||||
|
Wants=domain-watchdog-valkey.service versions-fetch.service
|
||||||
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
|
TimeoutStartSec=0
|
||||||
|
Environment="APP_SECRET=${domain_watchdog_app_secret}"
|
||||||
|
Environment="DATABASE_URL=postgresql://domainwatchdog:${domain_watchdog_db_password}@argo.lan.atlashorizon.net:5432/domainwatchdog?serverVersion=16&charset=utf8"
|
||||||
|
ExecStartPre=/usr/bin/docker pull $${DOMAIN_WATCHDOG_IMAGE}
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force domain-watchdog-worker
|
||||||
|
ExecStartPre=-/usr/bin/docker network create domain-watchdog
|
||||||
|
# --no-healthcheck: the image's HEALTHCHECK probes FrankenPHP's
|
||||||
|
# metrics on :2019, which only the web app runs, so the worker would
|
||||||
|
# always show unhealthy (upstream's compose disables it the same way).
|
||||||
|
ExecStart=/usr/bin/docker run --rm --name domain-watchdog-worker \
|
||||||
|
--no-healthcheck \
|
||||||
|
--network domain-watchdog \
|
||||||
|
--env-file /etc/domain-watchdog/app.env \
|
||||||
|
-e APP_SECRET \
|
||||||
|
-e DATABASE_URL \
|
||||||
|
-e MESSENGER_CONSUMER_NAME=worker \
|
||||||
|
$${DOMAIN_WATCHDOG_IMAGE} \
|
||||||
|
php /app/bin/console messenger:consume --all --time-limit=3600 -vv
|
||||||
|
ExecStop=/usr/bin/docker stop domain-watchdog-worker
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
- name: dozzle-agent.service
|
- name: dozzle-agent.service
|
||||||
enabled: true
|
enabled: true
|
||||||
contents: |
|
contents: |
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Dozzle Agent
|
Description=Dozzle Agent
|
||||||
After=docker.service
|
After=docker.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
[Service]
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
ExecStartPre=-/usr/bin/docker rm --force dozzle-agent
|
ExecStartPre=-/usr/bin/docker rm --force dozzle-agent
|
||||||
ExecStart=/usr/bin/docker run --name dozzle-agent \
|
ExecStart=/usr/bin/docker run --name dozzle-agent \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-p 7007:7007 \
|
-p 7007:7007 \
|
||||||
amir20/dozzle:v11.1.1 agent
|
$${DOZZLE_IMAGE} agent
|
||||||
ExecStop=/usr/bin/docker stop dozzle-agent
|
ExecStop=/usr/bin/docker stop dozzle-agent
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
@@ -66,3 +300,85 @@ storage:
|
|||||||
directories:
|
directories:
|
||||||
- path: /var/lib/oswald_runner
|
- path: /var/lib/oswald_runner
|
||||||
mode: 0755
|
mode: 0755
|
||||||
|
# The n8n image runs as the "node" user (uid 1000), which needs to own
|
||||||
|
# its data directory.
|
||||||
|
- path: /var/lib/n8n
|
||||||
|
mode: 0700
|
||||||
|
user:
|
||||||
|
id: 1000
|
||||||
|
group:
|
||||||
|
id: 1000
|
||||||
|
# Domain Watchdog's JWT keypair; the container runs as root.
|
||||||
|
- path: /var/lib/domain-watchdog/jwt
|
||||||
|
mode: 0700
|
||||||
|
files:
|
||||||
|
# Non-secret Domain Watchdog settings, shared by the web app and worker
|
||||||
|
# units (docker --env-file format: no quoting, values taken literally).
|
||||||
|
- path: /etc/domain-watchdog/app.env
|
||||||
|
mode: 0644
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
APP_ENV=prod
|
||||||
|
SERVER_NAME=:80
|
||||||
|
HTTP_SECURE_COOKIE=true
|
||||||
|
SYMFONY_TRUSTED_PROXIES=private_ranges
|
||||||
|
SYMFONY_TRUSTED_HEADERS=x-forwarded-for,x-forwarded-host,x-forwarded-proto,x-forwarded-port
|
||||||
|
LOCK_DSN=redis://valkey:6379
|
||||||
|
MESSENGER_ASYNC_TRANSPORT_DSN=redis://valkey:6379/messages
|
||||||
|
MESSENGER_RDAP_LOW_TRANSPORT_DSN=redis://valkey:6379/messages-rdap-low
|
||||||
|
MESSENGER_RDAP_HIGH_TRANSPORT_DSN=redis://valkey:6379/messages-rdap-high
|
||||||
|
REGISTRATION_ENABLED=false
|
||||||
|
REGISTRATION_VERIFY_EMAIL=false
|
||||||
|
OAUTH_AUTHORIZATION_URL=https://auth.atlashorizon.net/api/oidc/authorization
|
||||||
|
OAUTH_TOKEN_URL=https://auth.atlashorizon.net/api/oidc/token
|
||||||
|
OAUTH_USERINFO_URL=https://auth.atlashorizon.net/api/oidc/userinfo
|
||||||
|
OAUTH_SCOPE=openid email profile
|
||||||
|
SSO_AUTO_REDIRECT=true
|
||||||
|
MAILER_DSN=null://null
|
||||||
|
# Markdown pages the frontend fetches from /content/<name>; a missing one
|
||||||
|
# renders as "Please create the /public/content/<name> file." Mounted
|
||||||
|
# read-only into the web app. ads.md is shown on the login page and is
|
||||||
|
# left empty on purpose (an empty file renders as nothing).
|
||||||
|
- path: /etc/domain-watchdog/content/home.md
|
||||||
|
mode: 0644
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
# Domain Watchdog
|
||||||
|
|
||||||
|
Tracks our domains through RDAP and alerts on expiry, transfers and
|
||||||
|
other registry changes.
|
||||||
|
|
||||||
|
Add domains under **Tracking > Watchlists**, and attach a
|
||||||
|
notification connector to a watchlist to be alerted.
|
||||||
|
- path: /etc/domain-watchdog/content/faq.md
|
||||||
|
mode: 0644
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
# FAQ
|
||||||
|
|
||||||
|
**Who can log in?** Anyone allowed through Authelia for this app.
|
||||||
|
Your account is created on first login.
|
||||||
|
|
||||||
|
**How often are domains checked?** The worker refreshes watched
|
||||||
|
domains from RDAP on a schedule; domains close to expiry are
|
||||||
|
checked more often.
|
||||||
|
- path: /etc/domain-watchdog/content/tos.md
|
||||||
|
mode: 0644
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
# Terms of use
|
||||||
|
|
||||||
|
Private instance for Atlas Horizon use only.
|
||||||
|
- path: /etc/domain-watchdog/content/privacy.md
|
||||||
|
mode: 0644
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
# Privacy
|
||||||
|
|
||||||
|
Stores your email address (from Authelia) and your watchlists.
|
||||||
|
Nothing is shared with third parties; RDAP lookups go directly to
|
||||||
|
the registries.
|
||||||
|
- path: /etc/domain-watchdog/content/ads.md
|
||||||
|
mode: 0644
|
||||||
|
contents:
|
||||||
|
inline: ""
|
||||||
@@ -20,10 +20,12 @@ systemd:
|
|||||||
contents: |
|
contents: |
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Tailscale node (headscale subnet router + exit node)
|
Description=Tailscale node (headscale subnet router + exit node)
|
||||||
After=docker.service network-online.target
|
After=docker.service network-online.target versions-fetch.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
Wants=network-online.target
|
Wants=network-online.target versions-fetch.service
|
||||||
[Service]
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
# None of these are loaded by default on a stock Flatcar boot, and the
|
# None of these are loaded by default on a stock Flatcar boot, and the
|
||||||
# container cannot load them itself: it has NET_ADMIN (enough to write
|
# container cannot load them itself: it has NET_ADMIN (enough to write
|
||||||
@@ -49,7 +51,7 @@ systemd:
|
|||||||
-e TS_ACCEPT_DNS=false \
|
-e TS_ACCEPT_DNS=false \
|
||||||
-e "TS_EXTRA_ARGS=--login-server=https://hs.atlashorizon.net --advertise-exit-node" \
|
-e "TS_EXTRA_ARGS=--login-server=https://hs.atlashorizon.net --advertise-exit-node" \
|
||||||
-e TS_USERSPACE=false \
|
-e TS_USERSPACE=false \
|
||||||
docker.io/tailscale/tailscale:stable
|
$${TAILSCALE_IMAGE}
|
||||||
ExecStop=/usr/bin/docker stop tailscaled
|
ExecStop=/usr/bin/docker stop tailscaled
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
@@ -60,15 +62,18 @@ systemd:
|
|||||||
contents: |
|
contents: |
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Dozzle Agent
|
Description=Dozzle Agent
|
||||||
After=docker.service
|
After=docker.service versions-fetch.service
|
||||||
|
Wants=versions-fetch.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
[Service]
|
[Service]
|
||||||
|
# Image tag comes from versions.env; see butane/versions-sync.yaml.
|
||||||
|
EnvironmentFile=/etc/versions.env
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
ExecStartPre=-/usr/bin/docker rm --force dozzle-agent
|
ExecStartPre=-/usr/bin/docker rm --force dozzle-agent
|
||||||
ExecStart=/usr/bin/docker run --name dozzle-agent \
|
ExecStart=/usr/bin/docker run --name dozzle-agent \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-p 7007:7007 \
|
-p 7007:7007 \
|
||||||
amir20/dozzle:v11.1.1 agent
|
$${DOZZLE_IMAGE} agent
|
||||||
ExecStop=/usr/bin/docker stop dozzle-agent
|
ExecStop=/usr/bin/docker stop dozzle-agent
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
variant: flatcar
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
# Shared by every Flatcar VM: main.tf passes this file as each VM's inline
|
||||||
|
# butane snippet. Loaded with file(), not templatefile(), so it is not a
|
||||||
|
# template -- ${...} below reaches the VM as written.
|
||||||
|
#
|
||||||
|
# Keeps /etc/versions.env in sync with versions.env at the root of
|
||||||
|
# AtlasHorizon/compose, which holds the image tag of every container on the
|
||||||
|
# Flatcar VMs. A version bump merged to the branch below reaches each VM
|
||||||
|
# within ~10 minutes with no plan or rebuild; see fetch-versions.sh for how
|
||||||
|
# the affected units are found and restarted.
|
||||||
|
#
|
||||||
|
# To opt a container unit in, in its butane file:
|
||||||
|
# [Unit] After=... versions-fetch.service
|
||||||
|
# Wants=versions-fetch.service
|
||||||
|
# [Service] EnvironmentFile=/etc/versions.env
|
||||||
|
# and use the variable, in braces, wherever the image goes (the docker pull
|
||||||
|
# and docker run arguments). In a .tftpl file the braces need a doubled
|
||||||
|
# dollar sign so templatefile() leaves them alone.
|
||||||
|
|
||||||
|
systemd:
|
||||||
|
units:
|
||||||
|
# Not enabled itself: the container units pull it in at boot (Wants= and
|
||||||
|
# After=), so they start on the freshest copy, and the timer re-runs it.
|
||||||
|
# A failed fetch keeps the last good copy, so a Gitea outage only delays
|
||||||
|
# updates -- except on a brand-new VM, whose units cannot start until the
|
||||||
|
# first fetch succeeds. Restart= keeps retrying that first fetch.
|
||||||
|
- name: versions-fetch.service
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=Fetch image versions from AtlasHorizon/compose
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
# Override with a drop-in to test unreleased versions on one VM.
|
||||||
|
Environment=VERSIONS_BRANCH=main
|
||||||
|
ExecStart=/opt/versions/fetch-versions.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30s
|
||||||
|
- name: versions-fetch.timer
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=Periodically re-fetch image versions
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=10min
|
||||||
|
OnUnitActiveSec=10min
|
||||||
|
RandomizedDelaySec=1min
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
||||||
|
storage:
|
||||||
|
files:
|
||||||
|
- path: /opt/versions/fetch-versions.sh
|
||||||
|
mode: 0755
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
#!/usr/bin/bash
|
||||||
|
# Fetches versions.env and restarts the units that use a variable
|
||||||
|
# whose value changed. A unit "uses" a variable when its unit file
|
||||||
|
# contains it in braces (the docker pull/run image arguments), so
|
||||||
|
# adding a container needs no change here.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
url="https://git.atlashorizon.net/AtlasHorizon/compose/raw/branch/$VERSIONS_BRANCH/versions.env"
|
||||||
|
dest=/etc/versions.env
|
||||||
|
|
||||||
|
tmp=$(mktemp /etc/.versions.env.XXXXXX)
|
||||||
|
trap 'rm -f "$tmp"' EXIT
|
||||||
|
|
||||||
|
curl -fsS --max-time 30 -o "$tmp" "$url"
|
||||||
|
|
||||||
|
# Only comments, blank lines and VAR=value lines are accepted, and
|
||||||
|
# at least one *_IMAGE line must be present, so an error page or a
|
||||||
|
# truncated download never replaces the last good copy.
|
||||||
|
if grep -qvE '^(#.*|[A-Z0-9_]+=[^[:space:]]+|)$' "$tmp" ||
|
||||||
|
! grep -qE '^[A-Z0-9_]+_IMAGE=' "$tmp"; then
|
||||||
|
echo "refusing malformed versions.env from $url" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$dest" ] && cmp -s "$tmp" "$dest"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# With no previous copy this is a brand-new VM: its units are waiting
|
||||||
|
# on (or already failed without) this file, so start them outright.
|
||||||
|
# Otherwise only units that are running get restarted, so a unit
|
||||||
|
# stopped by hand stays stopped.
|
||||||
|
if [ -f "$dest" ]; then
|
||||||
|
action=try-restart
|
||||||
|
changed=$(diff <(grep -E '^[A-Z0-9_]+=' "$dest" | sort) \
|
||||||
|
<(grep -E '^[A-Z0-9_]+=' "$tmp" | sort) |
|
||||||
|
sed -nE 's/^[<>] ([A-Z0-9_]+)=.*/\1/p' | sort -u || true)
|
||||||
|
else
|
||||||
|
action=restart
|
||||||
|
changed=$(sed -nE 's/^([A-Z0-9_]+)=.*/\1/p' "$tmp")
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod 0644 "$tmp"
|
||||||
|
mv -f "$tmp" "$dest"
|
||||||
|
|
||||||
|
for var in $changed; do
|
||||||
|
echo "$var changed"
|
||||||
|
pattern=$(printf '${%s}' "$var")
|
||||||
|
# --no-block: the units are ordered after this one, so waiting on
|
||||||
|
# their jobs from inside it would deadlock at boot.
|
||||||
|
for unit in $(grep -lF "$pattern" /etc/systemd/system/*.service || true); do
|
||||||
|
systemctl --no-block "$action" "$(basename "$unit")"
|
||||||
|
done
|
||||||
|
done
|
||||||
@@ -36,6 +36,20 @@ locals {
|
|||||||
# provider alias in providers.tf and a module block here. Everything else
|
# provider alias in providers.tf and a module block here. Everything else
|
||||||
# (node contents, common config) lives in the parent repo's terraform.tfvars.
|
# (node contents, common config) lives in the parent repo's terraform.tfvars.
|
||||||
|
|
||||||
|
# butane/versions-sync.yaml goes onto every Flatcar VM through its inline
|
||||||
|
# butane field (the module merges it with butane_file). Inline snippets are
|
||||||
|
# not templated, so the file is read as-is. No VM sets butane in tfvars; one
|
||||||
|
# that did would replace this snippet, not add to it, and lose version sync.
|
||||||
|
locals {
|
||||||
|
flatcar_common_butane = file("${path.module}/butane/versions-sync.yaml")
|
||||||
|
flatcar_with_common = {
|
||||||
|
for hv in ["proxmox", "goose"] : hv => {
|
||||||
|
for name, vm in try(var.nodes[hv].flatcar, {}) :
|
||||||
|
name => merge(vm, { butane = coalesce(vm.butane, local.flatcar_common_butane) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module "proxmox" {
|
module "proxmox" {
|
||||||
source = "./opentofu/modules/proxmox_node"
|
source = "./opentofu/modules/proxmox_node"
|
||||||
|
|
||||||
@@ -43,7 +57,7 @@ module "proxmox" {
|
|||||||
storage = var.hypervisors["proxmox"].storage
|
storage = var.hypervisors["proxmox"].storage
|
||||||
lxc = try(var.nodes["proxmox"].lxc, {})
|
lxc = try(var.nodes["proxmox"].lxc, {})
|
||||||
machines = try(var.nodes["proxmox"].machines, {})
|
machines = try(var.nodes["proxmox"].machines, {})
|
||||||
flatcar = try(var.nodes["proxmox"].flatcar, {})
|
flatcar = local.flatcar_with_common["proxmox"]
|
||||||
search_domain = local.node_common.search_domain
|
search_domain = local.node_common.search_domain
|
||||||
dns_nameservers = local.node_common.dns_nameservers
|
dns_nameservers = local.node_common.dns_nameservers
|
||||||
ssh_key = local.node_common.ssh_key
|
ssh_key = local.node_common.ssh_key
|
||||||
@@ -68,7 +82,7 @@ module "goose" {
|
|||||||
storage = var.hypervisors["goose"].storage
|
storage = var.hypervisors["goose"].storage
|
||||||
lxc = try(var.nodes["goose"].lxc, {})
|
lxc = try(var.nodes["goose"].lxc, {})
|
||||||
machines = try(var.nodes["goose"].machines, {})
|
machines = try(var.nodes["goose"].machines, {})
|
||||||
flatcar = try(var.nodes["goose"].flatcar, {})
|
flatcar = local.flatcar_with_common["goose"]
|
||||||
search_domain = local.node_common.search_domain
|
search_domain = local.node_common.search_domain
|
||||||
dns_nameservers = local.node_common.dns_nameservers
|
dns_nameservers = local.node_common.dns_nameservers
|
||||||
ssh_key = local.node_common.ssh_key
|
ssh_key = local.node_common.ssh_key
|
||||||
|
|||||||
@@ -14,9 +14,30 @@
|
|||||||
{{- if eq $secret.Key "LITELLM_DATABASE_URL" }}"litellm_database_url": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "LITELLM_DATABASE_URL" }}"litellm_database_url": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "LITELLM_MASTER_KEY" }}"litellm_master_key": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "LITELLM_MASTER_KEY" }}"litellm_master_key": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "LITELLM_SALT_KEY" }}"litellm_salt_key": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "LITELLM_SALT_KEY" }}"litellm_salt_key": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENCODE_SERVER_PASSWORD" }}"opencode_server_password": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "HEADSCALE_AUTHKEY" }}"headscale_authkey": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "HEADSCALE_AUTHKEY" }}"headscale_authkey": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENWEBUI_DATABASE_URL" }}"openwebui_database_url": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENWEBUI_SECRET_KEY" }}"openwebui_secret_key": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENWEBUI_S3_ACCESS_KEY" }}"openwebui_s3_access_key": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENWEBUI_S3_SECRET_KEY" }}"openwebui_s3_secret_key": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENWEBUI_OAUTH_CLIENT_ID" }}"openwebui_oauth_client_id": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENWEBUI_OAUTH_CLIENT_SECRET" }}"openwebui_oauth_client_secret": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENWEBUI_OIDC_PROVIDER_URL" }}"openwebui_oidc_provider_url": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENWEBUI_TERMINAL_API_KEY" }}"openwebui_terminal_api_key": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENWEBUI_ADMIN_API_KEY" }}"openwebui_admin_api_key": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "N8N_DB_PASSWORD" }}"n8n_db_password": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "N8N_ENCRYPTION_KEY" }}"n8n_encryption_key": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "DOZZLE_OIDC_CLIENT_ID" }}"dozzle_oidc_client_id": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "DOZZLE_OIDC_CLIENT_ID" }}"dozzle_oidc_client_id": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "DOZZLE_OIDC_CLIENT_SECRET" }}"dozzle_oidc_client_secret": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "DOZZLE_OIDC_CLIENT_SECRET" }}"dozzle_oidc_client_secret": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "ALERTA_DATABASE_URL" }}"alerta_database_url": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "ALERTA_SECRET_KEY" }}"alerta_secret_key": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "ALERTA_ADMIN_PASSWORD" }}"alerta_admin_password": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "ALERTA_OIDC_CLIENT_ID" }}"alerta_oidc_client_id": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "ALERTA_OIDC_CLIENT_SECRET" }}"alerta_oidc_client_secret": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "DOMAIN_WATCHDOG_DB_PASSWORD" }}"domain_watchdog_db_password": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "DOMAIN_WATCHDOG_APP_SECRET" }}"domain_watchdog_app_secret": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "DOMAIN_WATCHDOG_OIDC_CLIENT_ID" }}"domain_watchdog_oidc_client_id": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "DOMAIN_WATCHDOG_OIDC_CLIENT_SECRET" }}"domain_watchdog_oidc_client_secret": "{{$secret.Value}}",{{ end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
"_sentinel": "1"
|
"_sentinel": "1"
|
||||||
}
|
}
|
||||||
+5
-27
@@ -203,13 +203,10 @@ nodes = {
|
|||||||
vmid = 210
|
vmid = 210
|
||||||
ip = "192.168.7.60"
|
ip = "192.168.7.60"
|
||||||
vlan = 7
|
vlan = 7
|
||||||
memory = 4096
|
memory = 6144
|
||||||
cores = 4
|
cores = 4
|
||||||
disk_size = 32
|
disk_size = 32
|
||||||
qemu_guest_agent = false
|
qemu_guest_agent = false
|
||||||
# Rendered with templatefile(): ${gitea_runner_token} comes from
|
|
||||||
# Infisical (GITEA_RUNNER_TOKEN in secrets.tmpl), so the token is
|
|
||||||
# never committed.
|
|
||||||
butane_file = "butane/oswald_runner.yaml.tftpl"
|
butane_file = "butane/oswald_runner.yaml.tftpl"
|
||||||
}
|
}
|
||||||
"ai" = {
|
"ai" = {
|
||||||
@@ -217,15 +214,12 @@ nodes = {
|
|||||||
vmid = 211
|
vmid = 211
|
||||||
ip = "192.168.7.61"
|
ip = "192.168.7.61"
|
||||||
vlan = 7
|
vlan = 7
|
||||||
memory = 4096
|
memory = 8192
|
||||||
cores = 2
|
cores = 4
|
||||||
disk_size = 32
|
disk_size = 32
|
||||||
qemu_guest_agent = false
|
qemu_guest_agent = false
|
||||||
butane_file = "butane/ai.yaml.tftpl"
|
butane_file = "butane/ai.yaml.tftpl"
|
||||||
}
|
}
|
||||||
# Headscale subnet router / exit node. Registers as an ephemeral node,
|
|
||||||
# so the preauth key must be --ephemeral --reusable; the routes and the
|
|
||||||
# exit node still need approving on the headscale side once it appears.
|
|
||||||
"oswl-connector" = {
|
"oswl-connector" = {
|
||||||
hostname = "oswl-connector"
|
hostname = "oswl-connector"
|
||||||
vmid = 212
|
vmid = 212
|
||||||
@@ -237,33 +231,17 @@ nodes = {
|
|||||||
qemu_guest_agent = false
|
qemu_guest_agent = false
|
||||||
butane_file = "butane/oswl_connector.yaml.tftpl"
|
butane_file = "butane/oswl_connector.yaml.tftpl"
|
||||||
}
|
}
|
||||||
# Dozzle log viewer hub. Pulls container logs from the dozzle agents on
|
|
||||||
# the other Flatcar VMs (port 7007) plus its own local docker socket.
|
|
||||||
# Keeps no state on disk, so it is safe to rebuild at any time.
|
|
||||||
"monitor" = {
|
"monitor" = {
|
||||||
hostname = "monitor"
|
hostname = "monitor"
|
||||||
vmid = 213
|
vmid = 213
|
||||||
ip = "192.168.7.63"
|
ip = "192.168.7.63"
|
||||||
vlan = 7
|
vlan = 7
|
||||||
memory = 2048
|
memory = 4096
|
||||||
cores = 2
|
cores = 2
|
||||||
disk_size = 32
|
disk_size = 32
|
||||||
qemu_guest_agent = false
|
qemu_guest_agent = false
|
||||||
butane_file = "butane/monitor.yaml.tftpl"
|
butane_file = "butane/monitor.yaml.tftpl"
|
||||||
}
|
}
|
||||||
# CISA Known Exploited Vulnerabilities catalog as an RSS feed.
|
|
||||||
# Stateless container; feed at :3000/rss.
|
|
||||||
"cisa-kev-rss" = {
|
|
||||||
hostname = "cisa-kev-rss"
|
|
||||||
vmid = 214
|
|
||||||
ip = "192.168.7.64"
|
|
||||||
vlan = 7
|
|
||||||
memory = 1024
|
|
||||||
cores = 1
|
|
||||||
disk_size = 16
|
|
||||||
qemu_guest_agent = false
|
|
||||||
butane_file = "butane/cisa_kev_rss.yaml"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
machines = {
|
machines = {
|
||||||
"grist" = {
|
"grist" = {
|
||||||
@@ -278,7 +256,7 @@ nodes = {
|
|||||||
agent = 1
|
agent = 1
|
||||||
memory = 4096
|
memory = 4096
|
||||||
disk_size = "32"
|
disk_size = "32"
|
||||||
balloon = 1
|
balloon = 1024
|
||||||
cpu_type = "kvm64"
|
cpu_type = "kvm64"
|
||||||
cores = 4
|
cores = 4
|
||||||
sockets = 1
|
sockets = 1
|
||||||
|
|||||||
Reference in new issue
Block a user