Compare commits
2
Commits
a664cf68f5
...
f4fca1797f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4fca1797f | ||
|
|
913a57f8a6 |
No files matched your search
@@ -183,8 +183,8 @@ currently parked (dir present, unit not active) — imported anyway, noted as
|
|||||||
parked in the catalog.
|
parked in the catalog.
|
||||||
|
|
||||||
**Excluded:** Watchtower (retired — replaced by the update timer after it took
|
**Excluded:** Watchtower (retired — replaced by the update timer after it took
|
||||||
gitea down; its dir stays on junior until cleanup), Completeuphoria,
|
gitea down; its dir stays on junior until cleanup), the app source
|
||||||
sierraandtyler, Bang (source checkouts deployed by CI from their own repos —
|
checkouts such as sierraandtyler and Bang (deployed by CI from their own repos —
|
||||||
Panama owning a second copy of their compose would drift), `ci/`, `logs/`,
|
Panama owning a second copy of their compose would drift), `ci/`, `logs/`,
|
||||||
and junior's loose scripts other than `update-containers.sh` (§4).
|
and junior's loose scripts other than `update-containers.sh` (§4).
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -3,11 +3,13 @@
|
|||||||
/config/bash/.env.lock
|
/config/bash/.env.lock
|
||||||
# Server secrets and state never enter the repo: live .env files belong in
|
# Server secrets and state never enter the repo: live .env files belong in
|
||||||
# ~/Server/<Name>/ (only compose.yml is symlinked back here), and data/ is
|
# ~/Server/<Name>/ (only compose.yml is symlinked back here), and data/ is
|
||||||
# the bind-mount convention. Ignored anyway as a seatbelt -- the repo is
|
# the bind-mount convention (the home server's older services use volume/ and
|
||||||
# public, and tests/server/compose-secrets-contract is the check that this
|
# volumes/). Ignored anyway as a seatbelt -- the repo is public, and
|
||||||
# line still exists.
|
# tests/server/compose-secrets-contract is the check that these lines still exist.
|
||||||
/server/containers/**/.env
|
/server/**/.env
|
||||||
/server/containers/**/data/
|
/server/**/data/
|
||||||
|
/server/**/volume/
|
||||||
|
/server/**/volumes/
|
||||||
# Personal espanso triggers (name, email), seeded per-machine by setup-identity.
|
# Personal espanso triggers (name, email), seeded per-machine by setup-identity.
|
||||||
/config/dot/espanso/match/identity.yml
|
/config/dot/espanso/match/identity.yml
|
||||||
# Ignore backups of old config files
|
# Ignore backups of old config files
|
||||||
|
|||||||
+3
-3
@@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# panama-server – the compose services a server machine runs.
|
# panama-server – the compose services a server machine runs.
|
||||||
#
|
#
|
||||||
# The repository carries the catalog: server/containers/<Name>/ holds a
|
# The repository carries the catalog: server/vps/<Name>/ holds a
|
||||||
# compose.yml, a podman-<name>.service user unit, and an .env.example naming
|
# compose.yml, a podman-<name>.service user unit, and an .env.example naming
|
||||||
# what the service needs told. The machine carries the rest: ~/Server/<Name>/
|
# what the service needs told. The machine carries the rest: ~/Server/<Name>/
|
||||||
# is a real directory owning the .env (secrets never sit inside the checkout,
|
# is a real directory owning the .env (secrets never sit inside the checkout,
|
||||||
@@ -27,7 +27,7 @@ PANAMA_DIR=$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)
|
|||||||
|
|
||||||
# Overridable so the contract can point this at a fixture catalog and a
|
# Overridable so the contract can point this at a fixture catalog and a
|
||||||
# throwaway HOME rather than the machine's real services.
|
# throwaway HOME rather than the machine's real services.
|
||||||
SERVER_DIR="${PANAMA_SERVER_DIR:-$PANAMA_DIR/server/containers}"
|
SERVER_DIR="${PANAMA_SERVER_DIR:-$PANAMA_DIR/server/vps}"
|
||||||
TARGET_DIR="${PANAMA_SERVER_TARGET:-$HOME/Server}"
|
TARGET_DIR="${PANAMA_SERVER_TARGET:-$HOME/Server}"
|
||||||
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||||
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/panama/server-definitions"
|
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/panama/server-definitions"
|
||||||
@@ -46,7 +46,7 @@ err() { printf '%s✗%s %s\n' "${RED}${BOLD}" "$RESET" "$*" >&2; }
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
${BOLD}$PROGRAM${RESET} – manage the compose services in server/containers/
|
${BOLD}$PROGRAM${RESET} – manage the compose services in server/vps/
|
||||||
|
|
||||||
${BOLD}Usage:${RESET}
|
${BOLD}Usage:${RESET}
|
||||||
$PROGRAM list Every service in the catalog, and its state here
|
$PROGRAM list Every service in the catalog, and its state here
|
||||||
|
|||||||
+26
-5
@@ -6,13 +6,34 @@ screen, plus rootless podman compose services managed as systemd user units.
|
|||||||
|
|
||||||
```
|
```
|
||||||
server/
|
server/
|
||||||
containers/ One directory per service: compose.yml, the
|
vps/ The catalog panama-server installs from, one directory
|
||||||
podman-<name>.service user unit, and an .env.example
|
per service: compose.yml, the podman-<name>.service user
|
||||||
naming what the service needs told
|
unit, and an .env.example naming what the service needs told
|
||||||
|
home/ The home server's services in the same shape (see below)
|
||||||
scripts/ update-containers, the nightly image updater
|
scripts/ update-containers, the nightly image updater
|
||||||
systemd/ The units behind it, linked by setup-server
|
systemd/ The units behind it, linked by setup-server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## The home server
|
||||||
|
|
||||||
|
`server/home/` mirrors `~/Server` on the home server, where the services still
|
||||||
|
run under Docker. Each directory has the same three files as the VPS catalog,
|
||||||
|
and the unit is drafted for the eventual move to rootless podman. It has never
|
||||||
|
run, and the composes still use Docker-only features (host networking, the
|
||||||
|
GPU, the Docker socket) that need converting first. Nothing here is
|
||||||
|
installable: panama-server reads `server/vps/` only, so a service moves into
|
||||||
|
the catalog when it actually moves to podman.
|
||||||
|
|
||||||
|
Paths keep their nesting (`server/home/Agents/Gitea` is `~/Server/Agents/Gitea`).
|
||||||
|
Three services are checkouts of upstream repositories. For those, only the
|
||||||
|
local files are kept, and an `UPSTREAM` file names the repository and the exact
|
||||||
|
commit the checkout sits at: `PostHog`, `Sentry` and `Plausible`.
|
||||||
|
|
||||||
|
Both trees are snapshots of the live machines with every secret replaced by a
|
||||||
|
`${VAR}` named in `.env.example`, or by `CHANGE_ME` in files that are not
|
||||||
|
compose files. The live servers still carry some secrets inline, so a live
|
||||||
|
compose and its copy here can differ on those lines alone.
|
||||||
|
|
||||||
## The contract with ~/Server
|
## The contract with ~/Server
|
||||||
|
|
||||||
The repository carries the definitions; the machine carries the state.
|
The repository carries the definitions; the machine carries the state.
|
||||||
@@ -35,7 +56,7 @@ is what `panama server enable` refuses to start on.
|
|||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- Service directories are TitleCase, matching `~/Server` as it has always
|
- Service directories are TitleCase, matching `~/Server` as it has always
|
||||||
looked: `server/containers/Gitea` becomes `~/Server/Gitea`.
|
looked: `server/vps/Gitea` becomes `~/Server/Gitea`.
|
||||||
- Bind mounts live under `./data/` in new services. Imported services keep
|
- Bind mounts live under `./data/` in new services. Imported services keep
|
||||||
the volume path they were running with until their cutover, because the
|
the volume path they were running with until their cutover, because the
|
||||||
repo copy must render identically to the live one — renames happen with
|
repo copy must render identically to the live one — renames happen with
|
||||||
@@ -73,7 +94,7 @@ ones — and verify it came back up. One service at a time, quiet hours.
|
|||||||
|
|
||||||
## Adding a service
|
## Adding a service
|
||||||
|
|
||||||
Add `server/containers/<Name>/` with the three files, modelled on any
|
Add `server/vps/<Name>/` with the three files, modelled on any
|
||||||
existing service. The unit is the standard oneshot `podman compose up -d`
|
existing service. The unit is the standard oneshot `podman compose up -d`
|
||||||
shape (`WorkingDirectory=%h/Server/<Name>`); the shape contract pins the
|
shape (`WorkingDirectory=%h/Server/<Name>`); the shape contract pins the
|
||||||
details. Secrets go in `.env.example` as `KEY=CHANGE_ME` lines.
|
details. Secrets go in `.env.example` as `KEY=CHANGE_ME` lines.
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
networks:
|
|
||||||
nginx-bridge:
|
|
||||||
external: true
|
|
||||||
services:
|
|
||||||
gitea:
|
|
||||||
image: docker.io/gitea/gitea:latest
|
|
||||||
container_name: gitea
|
|
||||||
hostname: gitea
|
|
||||||
domainname: git.gbrown.org
|
|
||||||
networks:
|
|
||||||
- nginx-bridge
|
|
||||||
ports:
|
|
||||||
- '2222:22'
|
|
||||||
environment:
|
|
||||||
- TZ=America/New_York
|
|
||||||
- USER_UID=1001
|
|
||||||
- USER_GID=1003
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: 'true'
|
|
||||||
volumes:
|
|
||||||
- ./volume:/data:Z
|
|
||||||
- /home/gib/Media/gitea-packages:/data/gitea/packages:Z
|
|
||||||
- ./ssh:/data/git/.ssh:Z
|
|
||||||
tty: true
|
|
||||||
restart: unless-stopped
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
PENPOT_SECRET_KEY=CHANGE_ME
|
|
||||||
PENPOT_DATABASE_PASSWORD=CHANGE_ME
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# Penpot — VPS (ROOTLESS PODMAN). Ported from the home server 2026-08-12.
|
|
||||||
#
|
|
||||||
# ⚠️ NOT STARTED. No systemd unit is enabled. It has never run on either machine, so
|
|
||||||
# there is no data to migrate. Bring it up with:
|
|
||||||
# systemctl --user enable --now podman-penpot.service
|
|
||||||
#
|
|
||||||
# BEFORE FIRST START, create its role and database on the SHARED postgres (this stack
|
|
||||||
# no longer ships its own):
|
|
||||||
# podman exec -i postgresql psql -U npm <<'SQL'
|
|
||||||
# CREATE ROLE penpot LOGIN PASSWORD '<see this service .env on the machine>';
|
|
||||||
# CREATE DATABASE penpot OWNER penpot;
|
|
||||||
# SQL
|
|
||||||
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql.
|
|
||||||
#
|
|
||||||
# NOTE: the home version used postgres:15-alpine with --data-checksums. The shared
|
|
||||||
# instance is postgres 17 without checksums. Penpot does not care, and since there is
|
|
||||||
# no existing data this is not a migration — but if you ever DO need checksums, they
|
|
||||||
# can only be set at initdb time for the whole cluster.
|
|
||||||
#
|
|
||||||
# Redis stays local to this stack: Penpot uses it for transient session and
|
|
||||||
# notification state only, so it is not worth centralising.
|
|
||||||
#
|
|
||||||
# Rootless adaptations from the home version:
|
|
||||||
# - Dropped its private postgres:15-alpine; uses the shared instance (AGENTS.md §2).
|
|
||||||
# - PENPOT_DATABASE_URI repointed from penpot-db to the shared postgresql.
|
|
||||||
# - :Z on the assets bind mount. It is shared between frontend and backend, so it
|
|
||||||
# uses LOWERCASE :z -- uppercase would give each container a private MCS category
|
|
||||||
# and the second to start would lock the first out (AGENTS.md §6).
|
|
||||||
# - /etc/localtime mounts removed in favour of TZ (AGENTS.md §8).
|
|
||||||
#
|
|
||||||
# Registration is disabled by default (PENPOT_FLAGS disable-registration), so create
|
|
||||||
# the first account from the backend container before you can log in.
|
|
||||||
#
|
|
||||||
# NPM: proxy penpot.gbrown.org -> http://penpot-frontend:8080
|
|
||||||
|
|
||||||
networks:
|
|
||||||
nginx-bridge:
|
|
||||||
external: true
|
|
||||||
|
|
||||||
services:
|
|
||||||
penpot-frontend:
|
|
||||||
image: docker.io/penpotapp/frontend:latest
|
|
||||||
container_name: penpot-frontend
|
|
||||||
hostname: penpot-frontend
|
|
||||||
domainname: penpot.gbrown.org
|
|
||||||
networks: ['nginx-bridge']
|
|
||||||
environment:
|
|
||||||
- TZ=America/New_York
|
|
||||||
- PENPOT_FLAGS=disable-registration enable-login-with-password disable-smtp
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: "true"
|
|
||||||
volumes:
|
|
||||||
- ./volumes/assets:/opt/data/assets:z
|
|
||||||
depends_on:
|
|
||||||
- penpot-backend
|
|
||||||
- penpot-exporter
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
penpot-backend:
|
|
||||||
image: docker.io/penpotapp/backend:latest
|
|
||||||
container_name: penpot-backend
|
|
||||||
hostname: penpot-backend
|
|
||||||
domainname: penpot.gbrown.org
|
|
||||||
networks: ['nginx-bridge']
|
|
||||||
environment:
|
|
||||||
- TZ=America/New_York
|
|
||||||
- PENPOT_FLAGS=disable-registration enable-login-with-password disable-smtp enable-prepl-server
|
|
||||||
- PENPOT_PUBLIC_URI=https://penpot.gbrown.org
|
|
||||||
- PENPOT_SECRET_KEY=${PENPOT_SECRET_KEY}
|
|
||||||
# Shared postgres, not a private container
|
|
||||||
- PENPOT_DATABASE_URI=postgresql://postgresql/penpot
|
|
||||||
- PENPOT_DATABASE_USERNAME=penpot
|
|
||||||
- PENPOT_DATABASE_PASSWORD=${PENPOT_DATABASE_PASSWORD}
|
|
||||||
- PENPOT_REDIS_URI=redis://penpot-redis/0
|
|
||||||
- PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
|
|
||||||
- PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets
|
|
||||||
- PENPOT_TELEMETRY_ENABLED=false
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: "true"
|
|
||||||
volumes:
|
|
||||||
- ./volumes/assets:/opt/data/assets:z
|
|
||||||
depends_on:
|
|
||||||
- penpot-redis
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
penpot-exporter:
|
|
||||||
image: docker.io/penpotapp/exporter:latest
|
|
||||||
container_name: penpot-exporter
|
|
||||||
hostname: penpot-exporter
|
|
||||||
domainname: penpot.gbrown.org
|
|
||||||
networks: ['nginx-bridge']
|
|
||||||
environment:
|
|
||||||
- TZ=America/New_York
|
|
||||||
- PENPOT_PUBLIC_URI=http://penpot-frontend:8080
|
|
||||||
- PENPOT_REDIS_URI=redis://penpot-redis/0
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: "true"
|
|
||||||
depends_on:
|
|
||||||
- penpot-redis
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
penpot-redis:
|
|
||||||
image: docker.io/library/redis:7-alpine
|
|
||||||
container_name: penpot-redis
|
|
||||||
hostname: penpot-redis
|
|
||||||
domainname: penpot.gbrown.org
|
|
||||||
networks: ['nginx-bridge']
|
|
||||||
environment:
|
|
||||||
- TZ=America/New_York
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: "true"
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
restart: unless-stopped
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
RUSTDESK_API_RUSTDESK_JWT_KEY=CHANGE_ME
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
name: usesend
|
|
||||||
networks:
|
|
||||||
nginx-bridge:
|
|
||||||
external: true
|
|
||||||
services:
|
|
||||||
redis:
|
|
||||||
image: redis:7
|
|
||||||
container_name: usesend-redis
|
|
||||||
hostname: usesend-redis
|
|
||||||
networks:
|
|
||||||
- nginx-bridge
|
|
||||||
env_file: .env
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: 'true'
|
|
||||||
volumes:
|
|
||||||
- ./volumes/redis:/data:Z
|
|
||||||
command:
|
|
||||||
- redis-server
|
|
||||||
- --maxmemory-policy
|
|
||||||
- noeviction
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
restart: unless-stopped
|
|
||||||
minio:
|
|
||||||
image: minio/minio
|
|
||||||
container_name: usesend-storage
|
|
||||||
hostname: usesend-storage
|
|
||||||
networks:
|
|
||||||
- nginx-bridge
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
|
||||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: 'true'
|
|
||||||
volumes:
|
|
||||||
- ./volumes/minio:/data:Z
|
|
||||||
entrypoint: sh
|
|
||||||
command: -c 'mkdir -p /data/unsend && minio server /data --console-address ':9001' --address ':9002''
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
restart: unless-stopped
|
|
||||||
smtp-server:
|
|
||||||
image: usesend/smtp-proxy:v1.6.6
|
|
||||||
container_name: usesend-smtp-server
|
|
||||||
hostname: usesend-smtp
|
|
||||||
networks:
|
|
||||||
- nginx-bridge
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
- SMTP_AUTH_USERNAME=${SMTP_USER:?err}
|
|
||||||
- USESEND_BASE_URL=${USESEND_URL:?err}
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: 'true'
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
restart: unless-stopped
|
|
||||||
usesend:
|
|
||||||
image: usesend/usesend:v1.6.6
|
|
||||||
container_name: usesend
|
|
||||||
hostname: usesend
|
|
||||||
domainname: ${USESEND_URL:?err}
|
|
||||||
networks:
|
|
||||||
- nginx-bridge
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
- PORT=${USESEND_PORT:-3000}
|
|
||||||
- DATABASE_URL=${DATABASE_URL:?err}
|
|
||||||
- NEXTAUTH_URL=${USESEND_URL:?err}
|
|
||||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
|
|
||||||
- AWS_ACCESS_KEY=${AWS_ACCESS_KEY:?err}
|
|
||||||
- AWS_SECRET_KEY=${AWS_SECRET_KEY:?err}
|
|
||||||
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:?err}
|
|
||||||
- GITHUB_ID=${GITHUB_ID:?err}
|
|
||||||
- GITHUB_SECRET=${GITHUB_SECRET:?err}
|
|
||||||
- REDIS_URL=${REDIS_URL:?err}
|
|
||||||
- NEXT_PUBLIC_IS_CLOUD=${NEXT_PUBLIC_IS_CLOUD:-false}
|
|
||||||
- API_RATE_LIMIT=${API_RATE_LIMIT:-1}
|
|
||||||
- SMTP_HOST=${SMTP_HOST:-smtp.usesend.gbrown.org}
|
|
||||||
- SMTP_USER=${SMTP_USER:-usesend}
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: 'true'
|
|
||||||
depends_on:
|
|
||||||
redis:
|
|
||||||
condition: service_started
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
restart: unless-stopped
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ADMIN_TOKEN=CHANGE_ME
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Vaultwarden — VPS (ROOTLESS PODMAN) port of the home server's ~/Server/vaultwarden,
|
|
||||||
# staged 2026-08-12.
|
|
||||||
#
|
|
||||||
# ⚠️ STAGED, NOT AUTHORITATIVE. The home instance is still live and still serving
|
|
||||||
# vault.gbrown.org. This copy holds a point-in-time snapshot of the SQLite database.
|
|
||||||
# DO NOT log into this one and add credentials before cutover -- the two would diverge
|
|
||||||
# and one side's changes would be lost.
|
|
||||||
#
|
|
||||||
# Cutover procedure (do it in this order, it matters):
|
|
||||||
# 1. stop vaultwarden on the home server (clean SQLite, no torn WAL)
|
|
||||||
# 2. rsync -a server.gib:~/Server/vaultwarden/volume/ ~/Server/Vaultwarden/volume/
|
|
||||||
# 3. systemctl --user restart podman-vaultwarden
|
|
||||||
# 4. point vault.gbrown.org at the VPS, add the NPM proxy host
|
|
||||||
# 5. verify a login + an item decrypts, THEN leave home stopped
|
|
||||||
#
|
|
||||||
# The SQLite copy taken while home was running is crash-consistent only. Step 1 is what
|
|
||||||
# makes it clean -- don't skip it.
|
|
||||||
#
|
|
||||||
# Rootless adaptations:
|
|
||||||
# - :Z on the data mount (SELinux Enforcing).
|
|
||||||
# - /etc/localtime mount dropped in favour of TZ (AGENTS.md §8).
|
|
||||||
# - No published port; NPM proxies to http://vaultwarden:80 over nginx-bridge.
|
|
||||||
# - Image starts as root and drops privileges internally, so no userns_mode needed.
|
|
||||||
#
|
|
||||||
# NOTE: ADMIN_TOKEN is inline here, matching the home server's file verbatim. It grants
|
|
||||||
# access to /admin. Worth moving to a mode-600 .env at some point -- compose files get
|
|
||||||
# rsynced offsite by ~/Server/backup.
|
|
||||||
|
|
||||||
networks:
|
|
||||||
nginx-bridge:
|
|
||||||
external: true
|
|
||||||
|
|
||||||
services:
|
|
||||||
vaultwarden:
|
|
||||||
image: docker.io/vaultwarden/server:latest
|
|
||||||
container_name: vaultwarden
|
|
||||||
hostname: vaultwarden
|
|
||||||
domainname: vault.gbrown.org
|
|
||||||
networks:
|
|
||||||
- nginx-bridge
|
|
||||||
environment:
|
|
||||||
- TZ=America/New_York
|
|
||||||
- DOMAIN=https://vault.gbrown.org
|
|
||||||
- LOGIN_RATELIMIT_MAX_BURST=10
|
|
||||||
- LOGIN_RATELIMIT_SECONDS=60
|
|
||||||
- WEB_VAULT_ENABLED=true
|
|
||||||
- SIGNUPS_ALLOWED=false
|
|
||||||
- WEBSOCKET_ENABLED=true
|
|
||||||
- ADMIN_TOKEN=${ADMIN_TOKEN}
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: "true"
|
|
||||||
volumes:
|
|
||||||
- ./volume:/data:Z
|
|
||||||
tty: true
|
|
||||||
restart: unless-stopped
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Beszel AGENT ONLY — the hub now lives on the VPS.
|
||||||
|
#
|
||||||
|
# The hub (web UI + database) was moved to the VPS on 2026-08-11 and is reachable at
|
||||||
|
# https://beszel.gibbyb.com. This host keeps only the agent, which collects metrics and
|
||||||
|
# serves them to that hub over the WireGuard link (VPS 192.168.2.2 -> here 192.168.1.2:45876).
|
||||||
|
#
|
||||||
|
# KEY below is the VPS hub's public key. If the VPS hub is ever rebuilt its keypair
|
||||||
|
# changes, and this value must be updated to match:
|
||||||
|
# ssh-keygen -y -f ~/Server/Beszel/volumes/hub/id_ed25519 (run on the VPS)
|
||||||
|
#
|
||||||
|
# Previous config, including the local hub service, is in compose.yml.bak-2026-08-11.
|
||||||
|
#
|
||||||
|
# The agent uses network_mode: host — that is the documented setup and is what lets it
|
||||||
|
# read real host network interfaces rather than a container's veth. Docker forbids setting
|
||||||
|
# hostname/domainname together with host networking, so those keys are absent.
|
||||||
|
|
||||||
|
services:
|
||||||
|
beszel-agent:
|
||||||
|
image: henrygd/beszel-agent-nvidia:latest
|
||||||
|
container_name: beszel-agent
|
||||||
|
network_mode: host
|
||||||
|
# Give the agent read access to NVIDIA's management interface so Beszel can
|
||||||
|
# report GPU utilization, temperature, power draw, and VRAM usage.
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [utility]
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
- PORT=45876
|
||||||
|
- KEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDBWNqxKGRltqXJZg+wlDOtICeLo19ftQ6P3P8+uJg8y
|
||||||
|
# /boot is a separate 974M partition (nvme1n1p1), not part of the LUKS
|
||||||
|
# root the agent finds on its own. It filling up is what broke a 1032
|
||||||
|
# package dnf transaction on 2026-08-09, so it is worth alerting on.
|
||||||
|
- EXTRA_FILESYSTEMS=/boot
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/agent:/var/lib/beszel-agent
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- /boot:/boot:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Agents/Beszel
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Agents/Beszel
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
GITEA_RUNNER_REGISTRATION_TOKEN=CHANGE_ME
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# Gitea Actions runners — 3 parallel runners, each with its own dind sidecar.
|
||||||
|
#
|
||||||
|
# Each runner gets an isolated Docker daemon rather than the host socket, so a
|
||||||
|
# CI job cannot reach the other ~180 containers on this box. Cost: each dind
|
||||||
|
# keeps its own image cache under ./volumes/runner-N/dind.
|
||||||
|
#
|
||||||
|
# The pre-existing host runner (/home/gib/.local/bin/act_runner, name
|
||||||
|
# "server-gib") is intentionally left running.
|
||||||
|
#
|
||||||
|
# Registration token generated with:
|
||||||
|
# docker exec -u git gitea gitea actions generate-runner-token
|
||||||
|
# It is reusable — all three runners register with the same one.
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
x-runner-env: &runner-env
|
||||||
|
TZ: America/New_York
|
||||||
|
GITEA_INSTANCE_URL: https://git.gbrown.org
|
||||||
|
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
|
||||||
|
GITEA_RUNNER_LABELS: "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04,ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||||
|
DOCKER_TLS_VERIFY: "1"
|
||||||
|
DOCKER_CERT_PATH: /certs/client
|
||||||
|
CONFIG_FILE: /data/config.yml
|
||||||
|
|
||||||
|
x-dind-env: &dind-env
|
||||||
|
TZ: America/New_York
|
||||||
|
DOCKER_TLS_CERTDIR: /certs
|
||||||
|
|
||||||
|
services:
|
||||||
|
gitea-runner-1:
|
||||||
|
image: gitea/act_runner:latest
|
||||||
|
container_name: gitea-runner-1
|
||||||
|
hostname: gitea-runner-1
|
||||||
|
domainname: git.gbrown.org
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
environment:
|
||||||
|
<<: *runner-env
|
||||||
|
GITEA_RUNNER_NAME: gitea-runner-1
|
||||||
|
DOCKER_HOST: tcp://gitea-runner-1-dind:2376
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/runner-1/data:/data
|
||||||
|
- ./volumes/runner-1/certs:/certs
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
depends_on:
|
||||||
|
- gitea-runner-1-dind
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gitea-runner-1-dind:
|
||||||
|
image: docker:dind
|
||||||
|
container_name: gitea-runner-1-dind
|
||||||
|
hostname: gitea-runner-1-dind
|
||||||
|
domainname: git.gbrown.org
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
privileged: true
|
||||||
|
environment:
|
||||||
|
<<: *dind-env
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/runner-1/certs:/certs
|
||||||
|
- ./volumes/runner-1/dind:/var/lib/docker
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gitea-runner-2:
|
||||||
|
image: gitea/act_runner:latest
|
||||||
|
container_name: gitea-runner-2
|
||||||
|
hostname: gitea-runner-2
|
||||||
|
domainname: git.gbrown.org
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
environment:
|
||||||
|
<<: *runner-env
|
||||||
|
GITEA_RUNNER_NAME: gitea-runner-2
|
||||||
|
DOCKER_HOST: tcp://gitea-runner-2-dind:2376
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/runner-2/data:/data
|
||||||
|
- ./volumes/runner-2/certs:/certs
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
depends_on:
|
||||||
|
- gitea-runner-2-dind
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gitea-runner-2-dind:
|
||||||
|
image: docker:dind
|
||||||
|
container_name: gitea-runner-2-dind
|
||||||
|
hostname: gitea-runner-2-dind
|
||||||
|
domainname: git.gbrown.org
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
privileged: true
|
||||||
|
environment:
|
||||||
|
<<: *dind-env
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/runner-2/certs:/certs
|
||||||
|
- ./volumes/runner-2/dind:/var/lib/docker
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gitea-runner-3:
|
||||||
|
image: gitea/act_runner:latest
|
||||||
|
container_name: gitea-runner-3
|
||||||
|
hostname: gitea-runner-3
|
||||||
|
domainname: git.gbrown.org
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
environment:
|
||||||
|
<<: *runner-env
|
||||||
|
GITEA_RUNNER_NAME: gitea-runner-3
|
||||||
|
DOCKER_HOST: tcp://gitea-runner-3-dind:2376
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/runner-3/data:/data
|
||||||
|
- ./volumes/runner-3/certs:/certs
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
depends_on:
|
||||||
|
- gitea-runner-3-dind
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gitea-runner-3-dind:
|
||||||
|
image: docker:dind
|
||||||
|
container_name: gitea-runner-3-dind
|
||||||
|
hostname: gitea-runner-3-dind
|
||||||
|
domainname: git.gbrown.org
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
privileged: true
|
||||||
|
environment:
|
||||||
|
<<: *dind-env
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/runner-3/certs:/certs
|
||||||
|
- ./volumes/runner-3/dind:/var/lib/docker
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Agents/Gitea
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Agents/Gitea
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
services:
|
||||||
|
# portainer:
|
||||||
|
# image: portainer/portainer-ee:latest
|
||||||
|
# container_name: portainer
|
||||||
|
# hostname: portainer
|
||||||
|
# domainname: port.gbrown.org
|
||||||
|
# networks:
|
||||||
|
# - nginx-bridge
|
||||||
|
# labels:
|
||||||
|
# com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
# ports:
|
||||||
|
# - 9443:9443
|
||||||
|
# volumes:
|
||||||
|
# - ./volumes/data:/data
|
||||||
|
# - /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
# tty: true
|
||||||
|
# stdin_open: true
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
|
portainer-agent:
|
||||||
|
image: portainer/agent:2.33.2
|
||||||
|
container_name: portainer-agent
|
||||||
|
hostname: portainer-agent
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
ports: ["9001:9001"]
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- /var/lib/docker/volumes:/var/lib/docker/volumes
|
||||||
|
- /:/host
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Agents/Portainer
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Agents/Portainer
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
RTSP_PASSWORD=CHANGE_ME
|
||||||
|
PLUS_API_KEY=CHANGE_ME
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
services:
|
||||||
|
frigate:
|
||||||
|
image: ghcr.io/blakeblackshear/frigate:0.17.1-tensorrt # For Nvidia GPU
|
||||||
|
container_name: frigate
|
||||||
|
hostname: frigate
|
||||||
|
domainname: frigate.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
ports:
|
||||||
|
#- "8971:8971"
|
||||||
|
- "5000:5000"
|
||||||
|
#- "8554:8554"
|
||||||
|
#- "8555:8555"
|
||||||
|
# Check host system's actual render/video/plugdev group IDs with 'getent group render', 'getent group video', and 'getent group plugdev'
|
||||||
|
# Must add these exact IDs in container's group_add section or OpenVINO GPU acceleration will fail
|
||||||
|
group_add:
|
||||||
|
- "105" # render
|
||||||
|
- "110" # render
|
||||||
|
- "39" # video
|
||||||
|
- "46" # plugdev
|
||||||
|
shm_size: "256mb"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
- PGID=1000
|
||||||
|
- PUID=1000
|
||||||
|
- FRIGATE_RTSP_PASSWORD=${RTSP_PASSWORD}
|
||||||
|
#- HSA_OVERRIDE_GFX_VERSION=11.0.0
|
||||||
|
- PLUS_API_KEY=${PLUS_API_KEY}
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/frigate/config:/config:rw
|
||||||
|
- ./volumes/frigate/data:/usr/local/nginx/conf:rw
|
||||||
|
- /home/gib/Public/Server_Backup/frigate:/media/frigate
|
||||||
|
- type: tmpfs
|
||||||
|
target: /tmp/cache
|
||||||
|
tmpfs:
|
||||||
|
size: 1000000000
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
# - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB
|
||||||
|
devices:
|
||||||
|
- /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
|
||||||
|
#- /dev/kfd:/dev/kfd
|
||||||
|
stop_grace_period: 30s
|
||||||
|
restart: unless-stopped
|
||||||
|
tty: true
|
||||||
|
security_opt: ['seccomp:unconfined','label:disable','systempaths=unconfined']
|
||||||
|
#privileged: true
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
image: eclipse-mosquitto:2
|
||||||
|
container_name: mqtt
|
||||||
|
hostname: mqtt
|
||||||
|
domainname: mqtt.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
ports: ["1883:1883"]
|
||||||
|
#env_file: [.env]
|
||||||
|
environment: [TZ=America/New_York]
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/mosquitto/config:/mosquitto/config:rw
|
||||||
|
- ./volumes/mosquitto/data:/mosquitto/data:rw
|
||||||
|
- ./volumes/mosquitto/log:/mosquitto/log:rw
|
||||||
|
tty: true
|
||||||
|
security_opt: ['seccomp:unconfined','label:disable','systempaths=unconfined']
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Frigate
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Frigate
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
AGENT_INGEST_SECRET=CHANGE_ME
|
||||||
|
INSTANCE_NAME=CHANGE_ME
|
||||||
|
INSTANCE_SECRET=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_CONVEX_URL=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_SRC=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_POSTHOG_HOST=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_POSTHOG_KEY=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_SENTRY_ORG=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_SENTRY_PROJECT_NAME=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_SENTRY_URL=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_SITE_URL=CHANGE_ME
|
||||||
|
SENTRY_AUTH_TOKEN=CHANGE_ME
|
||||||
|
UNIFI_API_KEY=CHANGE_ME
|
||||||
|
UNIFI_CA=CHANGE_ME
|
||||||
|
UNIFI_URL=CHANGE_ME
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# Production stack on the Home Server (server.gib, rootful Docker), deployed
|
||||||
|
# as ~/Server/Gbrown/compose.yml. This file is the versioned source: edit it
|
||||||
|
# here and copy it over, never the other way round.
|
||||||
|
#
|
||||||
|
# ~/Server/Gbrown holds only this file, .env and data/. Every bind mount lives
|
||||||
|
# under data/ (data/convex, data/gibtv/channel, data/gibtv/nginx.conf, copied
|
||||||
|
# from apps/gibtv/nginx.conf). No source or build context lives on the server:
|
||||||
|
# app images come from the Gitea registry. CI redeploys at once through the
|
||||||
|
# forced-command key behind ~/Server/ci-deploy.sh (`docker compose pull && up -d`).
|
||||||
|
#
|
||||||
|
# Secrets come from ~/Server/Gbrown/.env (mode 600), written from Infisical
|
||||||
|
# `prod` by scripts/write-prod-env. Never commit or print it.
|
||||||
|
#
|
||||||
|
# NPM reaches everything over nginx-bridge, so nothing publishes a host port:
|
||||||
|
# api.gbrown.org -> http://gbrown-backend:3210
|
||||||
|
# convex.gbrown.org -> http://gbrown-backend:3211
|
||||||
|
# dashboard.gbrown.org -> http://gbrown-dashboard:6791
|
||||||
|
# gbrown.org (host 119) -> http://gbrown-next:3000 (at cutover)
|
||||||
|
# tv.gbrown.org -> http://gbrown-gibtv-web:80 (phase 8)
|
||||||
|
#
|
||||||
|
# Watchtower (label-enabled, 04:00) updates the app images. The Convex backend,
|
||||||
|
# its dashboard and the socket proxy are pinned and opt out: a backend upgrade
|
||||||
|
# migrates its data, so it happens on purpose, not overnight.
|
||||||
|
#
|
||||||
|
# The `tools` profile keeps the one-off normalize pass out of a plain `up -d`
|
||||||
|
# and `pull`. Run it with `docker compose run --rm gbrown-gibtv-normalize`.
|
||||||
|
name: gbrown
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
# The agent and its socket proxy only. No route out.
|
||||||
|
agent-socket:
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# The live HLS window is about 10 MB and rewritten every 6 s. Keep it in RAM.
|
||||||
|
gibtv-hls:
|
||||||
|
driver_opts:
|
||||||
|
type: tmpfs
|
||||||
|
device: tmpfs
|
||||||
|
o: size=64m,uid=1000,gid=1000
|
||||||
|
|
||||||
|
services:
|
||||||
|
gbrown-backend:
|
||||||
|
# Pinned to the build the VPS Convex stacks already run. Bump on purpose.
|
||||||
|
image: ghcr.io/get-convex/convex-backend@sha256:d715e9ec088784407ca4ba2d3db592702cd328d02c76cdca3852c0018f2a76b4
|
||||||
|
container_name: gbrown-backend
|
||||||
|
hostname: gbrown-backend
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
volumes: ['./data/convex:/convex/data']
|
||||||
|
# No TZ: the backend panics unless it runs in UTC.
|
||||||
|
environment:
|
||||||
|
- INSTANCE_NAME=${INSTANCE_NAME:?}
|
||||||
|
- INSTANCE_SECRET=${INSTANCE_SECRET:?}
|
||||||
|
- CONVEX_CLOUD_ORIGIN=https://api.gbrown.org
|
||||||
|
- CONVEX_SITE_ORIGIN=https://convex.gbrown.org
|
||||||
|
- DISABLE_BEACON=true
|
||||||
|
- REDACT_LOGS_TO_CLIENT=true
|
||||||
|
labels: ['com.centurylinklabs.watchtower.enable=false']
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'curl', '-f', 'http://localhost:3210/version']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
stop_grace_period: 10s
|
||||||
|
stop_signal: SIGINT
|
||||||
|
|
||||||
|
gbrown-dashboard:
|
||||||
|
# Released alongside the backend; pinned with it.
|
||||||
|
image: ghcr.io/get-convex/convex-dashboard@sha256:f85cf0d0448b9c835ae3df5c7c1c6f0145dd4c8f0f92eb0245daf304efdd9f1a
|
||||||
|
container_name: gbrown-dashboard
|
||||||
|
hostname: gbrown-dashboard
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
environment:
|
||||||
|
# The browser talks to the backend directly, so this is the public URL.
|
||||||
|
- NEXT_PUBLIC_DEPLOYMENT_URL=https://api.gbrown.org
|
||||||
|
depends_on:
|
||||||
|
gbrown-backend:
|
||||||
|
condition: service_healthy
|
||||||
|
labels: ['com.centurylinklabs.watchtower.enable=false']
|
||||||
|
restart: unless-stopped
|
||||||
|
stop_grace_period: 10s
|
||||||
|
stop_signal: SIGINT
|
||||||
|
|
||||||
|
gbrown-next:
|
||||||
|
image: git.gbrown.org/gib/gbrown-next:latest
|
||||||
|
container_name: gbrown-next
|
||||||
|
hostname: gbrown-next
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
- NODE_ENV=production
|
||||||
|
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
||||||
|
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
|
||||||
|
- NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL}
|
||||||
|
- NEXT_PUBLIC_PLAUSIBLE_SRC=${NEXT_PUBLIC_PLAUSIBLE_SRC:-}
|
||||||
|
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY:-}
|
||||||
|
- NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST:-}
|
||||||
|
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||||
|
- NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
|
||||||
|
- NEXT_PUBLIC_SENTRY_ORG=${NEXT_PUBLIC_SENTRY_ORG}
|
||||||
|
- NEXT_PUBLIC_SENTRY_PROJECT_NAME=${NEXT_PUBLIC_SENTRY_PROJECT_NAME}
|
||||||
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:3000/']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
# The agent never sees the Docker socket. The proxy answers only
|
||||||
|
# GET /containers/json, /events, /_ping and /version (with or without an API
|
||||||
|
# version prefix), refuses everything else, and only accepts connections from
|
||||||
|
# the gbrown-agent hostname on the internal agent-socket network.
|
||||||
|
# Docker here applies no SELinux labels to containers, so mounts need no :z.
|
||||||
|
gbrown-agent-socket-proxy:
|
||||||
|
image: docker.io/wollomatic/socket-proxy:1.13.1
|
||||||
|
container_name: gbrown-agent-socket-proxy
|
||||||
|
hostname: gbrown-agent-socket-proxy
|
||||||
|
networks: ['agent-socket']
|
||||||
|
command:
|
||||||
|
- '-loglevel=info'
|
||||||
|
- '-listenip=0.0.0.0'
|
||||||
|
- '-allowfrom=gbrown-agent'
|
||||||
|
- '-allowGET=/(v1\.[0-9]+/)?(containers/json|events|_ping|version)'
|
||||||
|
- '-allowhealthcheck'
|
||||||
|
# Exit if the socket breaks (a Docker upgrade) so Docker restarts it.
|
||||||
|
- '-watchdoginterval=3600'
|
||||||
|
- '-stoponwatchdog'
|
||||||
|
- '-shutdowngracetime=5'
|
||||||
|
# nobody, plus gid 963: the host's docker group, which owns the socket.
|
||||||
|
user: '65534:963'
|
||||||
|
volumes: ['/var/run/docker.sock:/var/run/docker.sock:ro']
|
||||||
|
read_only: true
|
||||||
|
mem_limit: 64M
|
||||||
|
cap_drop: [ALL]
|
||||||
|
security_opt: ['no-new-privileges:true']
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', './healthcheck']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 2
|
||||||
|
# Pinned on purpose. Watchtower must not swap the gatekeeper on its own.
|
||||||
|
labels: ['com.centurylinklabs.watchtower.enable=false']
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gbrown-agent:
|
||||||
|
image: git.gbrown.org/gib/gbrown-agent:latest
|
||||||
|
container_name: gbrown-agent
|
||||||
|
hostname: gbrown-agent
|
||||||
|
# nginx-bridge to reach gbrown-backend, agent-socket to reach the proxy.
|
||||||
|
networks: ['nginx-bridge', 'agent-socket']
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
- HOST_ID=home
|
||||||
|
- DOCKER_HOST=tcp://gbrown-agent-socket-proxy:2375
|
||||||
|
# Straight to the backend's HTTP actions port, no proxy in between.
|
||||||
|
- INGEST_URL=http://gbrown-backend:3211/ingest
|
||||||
|
- AGENT_INGEST_SECRET=${AGENT_INGEST_SECRET:?}
|
||||||
|
# The UniFi gateway on the LAN. UNIFI_CA is its own certificate, the only
|
||||||
|
# one the agent trusts there. Leave all three unset to skip the network.
|
||||||
|
- UNIFI_URL=${UNIFI_URL:-}
|
||||||
|
- UNIFI_API_KEY=${UNIFI_API_KEY:-}
|
||||||
|
- UNIFI_CA=${UNIFI_CA:-}
|
||||||
|
volumes: ['/home/gib/Server/Nginx/volumes/data/logs:/npm-logs:ro']
|
||||||
|
depends_on:
|
||||||
|
gbrown-agent-socket-proxy:
|
||||||
|
condition: service_healthy
|
||||||
|
read_only: true
|
||||||
|
cap_drop: [ALL]
|
||||||
|
security_opt: ['no-new-privileges:true']
|
||||||
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||||
|
restart: unless-stopped
|
||||||
|
stop_grace_period: 10s
|
||||||
|
|
||||||
|
# gib.tv (apps/gibtv). Nothing here touches the media library except the
|
||||||
|
# normalize pass, which mounts it read-only. The streamer stream-copies, so
|
||||||
|
# CPU is near zero; each viewer pulls about 1 Mbps of home upload.
|
||||||
|
gbrown-gibtv:
|
||||||
|
image: git.gbrown.org/gib/gbrown-gibtv:latest
|
||||||
|
container_name: gbrown-gibtv
|
||||||
|
hostname: gbrown-gibtv
|
||||||
|
# No network needed: it only writes the HLS window to the shared volume.
|
||||||
|
network_mode: none
|
||||||
|
environment: ['TZ=America/New_York']
|
||||||
|
volumes:
|
||||||
|
- ./data/gibtv/channel:/media/channel:ro
|
||||||
|
- gibtv-hls:/srv/hls
|
||||||
|
cpus: 0.5
|
||||||
|
mem_limit: 256m
|
||||||
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gbrown-gibtv-web:
|
||||||
|
image: docker.io/library/nginx:alpine
|
||||||
|
container_name: gbrown-gibtv-web
|
||||||
|
hostname: gbrown-gibtv-web
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
depends_on: [gbrown-gibtv]
|
||||||
|
volumes:
|
||||||
|
- gibtv-hls:/usr/share/nginx/html:ro
|
||||||
|
- ./data/gibtv/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# One-off: `docker compose run --rm gbrown-gibtv-normalize`. Writes one
|
||||||
|
# 640x480 h264 file per episode, skips finished files, safe to resume.
|
||||||
|
gbrown-gibtv-normalize:
|
||||||
|
image: git.gbrown.org/gib/gbrown-gibtv:latest
|
||||||
|
profiles: ['tools']
|
||||||
|
command: ['nice', '-n', '19', 'bun', 'src/cli/normalize.ts']
|
||||||
|
network_mode: none
|
||||||
|
volumes:
|
||||||
|
- '/home/gib/Public/TV/Dragnet (1951):/media/source:ro'
|
||||||
|
- ./data/gibtv/channel:/media/channel
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Gbrown
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Gbrown
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
services:
|
||||||
|
homeassistant:
|
||||||
|
image: homeassistant/home-assistant:latest
|
||||||
|
container_name: home
|
||||||
|
hostname: home
|
||||||
|
domainname: home.gbrown.org
|
||||||
|
network_mode: host
|
||||||
|
cap_add: ['NET_ADMIN','NET_RAW']
|
||||||
|
#networks:
|
||||||
|
#- nginx-bridge
|
||||||
|
#ports:
|
||||||
|
#- "8123:8123"
|
||||||
|
volumes:
|
||||||
|
- ./volume:/config
|
||||||
|
- /run/dbus:/run/dbus:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: 'true'
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
#networks:
|
||||||
|
#nginx-bridge:
|
||||||
|
#external: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Home
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Home
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
UPLOAD_LOCATION=CHANGE_ME
|
||||||
|
DB_DATA_LOCATION=CHANGE_ME
|
||||||
|
IMMICH_VERSION=CHANGE_ME
|
||||||
|
DB_PASSWORD=CHANGE_ME
|
||||||
|
DB_USERNAME=CHANGE_ME
|
||||||
|
DB_DATABASE_NAME=CHANGE_ME
|
||||||
|
TZ=CHANGE_ME
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
name: immich
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||||
|
extends:
|
||||||
|
file: hwaccel.transcoding.yml
|
||||||
|
service: nvenc
|
||||||
|
container_name: immich-server
|
||||||
|
hostname: immich-server
|
||||||
|
domainname: immich.gbrown.org
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
#ports: ['2283:2283']
|
||||||
|
env_file: [.env]
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ${UPLOAD_LOCATION}:/data
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- database
|
||||||
|
healthcheck:
|
||||||
|
disable: false
|
||||||
|
privileged: true
|
||||||
|
|
||||||
|
immich-machine-learning:
|
||||||
|
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-cuda
|
||||||
|
extends:
|
||||||
|
file: hwaccel.transcoding.yml
|
||||||
|
service: nvenc
|
||||||
|
container_name: immich-machine-learning
|
||||||
|
hostname: immich-machine-learning
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
#ports: ['3003:3003']
|
||||||
|
env_file: [.env]
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes: ['./volumes/model-cache:/cache:z']
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
disable: false
|
||||||
|
privileged: true
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||||
|
container_name: immich-redis
|
||||||
|
hostname: immich-redis
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
env_file: [.env]
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
database:
|
||||||
|
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
|
||||||
|
container_name: immich-postgres
|
||||||
|
hostname: immich-postgres
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
#ports: ['5432:5432']
|
||||||
|
env_file: [.env]
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
|
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 128mb
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Immich
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Immich
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
PLEX_CLAIM=CHANGE_ME
|
||||||
|
PLEX_TOKEN=CHANGE_ME
|
||||||
|
JELLYFIN_TOKEN=CHANGE_ME
|
||||||
|
OPENVPN_PASSWORD=CHANGE_ME
|
||||||
|
QBIT_PASS=CHANGE_ME
|
||||||
|
SONARR_API_KEY=CHANGE_ME
|
||||||
|
RADARR_API_KEY=CHANGE_ME
|
||||||
|
UN_SONARR_0_API_KEY=CHANGE_ME
|
||||||
|
UN_RADARR_0_API_KEY=CHANGE_ME
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
plex:
|
||||||
|
image: plexinc/pms-docker:latest
|
||||||
|
container_name: plex
|
||||||
|
hostname: plex
|
||||||
|
domainname: plex.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
group_add: [105, 39]
|
||||||
|
ports:
|
||||||
|
- 32400:32400/tcp # REQUIRED
|
||||||
|
#- 5353:5353/udp # For older Bonjour/Avahi network discovery
|
||||||
|
#- 8324:8324/tcp # For Plex Roku App
|
||||||
|
- 32410:32410/udp # For network discovery
|
||||||
|
- 32412:32412/udp # For network discovery
|
||||||
|
- 32413:32413/udp # For network discovery
|
||||||
|
- 32414:32414/udp # For network discovery
|
||||||
|
#- 1900:1900/udp # For DLNA
|
||||||
|
- 32469:32469/tcp # For DLNA
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
- PLEX_UID=1000
|
||||||
|
- PLEX_GID=1000
|
||||||
|
- PLEX_CLAIM=${PLEX_CLAIM}
|
||||||
|
- ADVERTISE_IP=http://192.168.0.43
|
||||||
|
- NVIDIA_VISIBLE_DEVICES=all
|
||||||
|
- NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
|
||||||
|
labels: ["com.centurylinklabs.watchtower.enable=true"]
|
||||||
|
volumes:
|
||||||
|
- /run:/run
|
||||||
|
- ./volumes/plex/config:/config
|
||||||
|
- ./volumes/plex/transcode:/transcode
|
||||||
|
- /home/gib/Public/Movies:/data/movies
|
||||||
|
- /home/gib/Public/TV:/data/tvshows
|
||||||
|
- /home/gib/Public/Music:/data/music
|
||||||
|
- /home/gib/Public/Audiobooks:/data/audiobooks
|
||||||
|
- /home/gib/Public/Podcasts:/data/podcasts
|
||||||
|
devices: [/dev/dri:/dev/dri]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt: ["seccomp:unconfined"]
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
jellyfin:
|
||||||
|
image: jellyfin/jellyfin
|
||||||
|
container_name: jellyfin
|
||||||
|
hostname: jellyfin
|
||||||
|
domainname: jellyfin.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
group_add: [105, 39]
|
||||||
|
ports: [7359:7359/udp]
|
||||||
|
user: 1000:1000
|
||||||
|
environment:
|
||||||
|
- JELLYFIN_PublishedServerUrl=https://jellyfin.gbrown.org
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/jellyfin/config:/config
|
||||||
|
- ./volumes/jellyfin/cache:/cache
|
||||||
|
- /home/gib/Public/Movies:/data/movies
|
||||||
|
- /home/gib/Public/TV:/data/tvshows
|
||||||
|
- /home/gib/Public/Music:/data/music
|
||||||
|
- /home/gib/Public/Audiobooks:/data/audiobooks
|
||||||
|
- /home/gib/Public/Podcasts:/data/podcasts
|
||||||
|
- /home/gib/.local/share/fonts:/usr/local/share/fonts/custom:ro
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
devices: [/dev/dri:/dev/dri]
|
||||||
|
security_opt: [seccomp:unconfined]
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
audiobookshelf:
|
||||||
|
image: ghcr.io/advplyr/audiobookshelf:latest
|
||||||
|
container_name: audiobookshelf
|
||||||
|
hostname: audiobookshelf
|
||||||
|
domainname: audio.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [80:80]
|
||||||
|
environment: [TZ=America/New_York, PUID=1000, PGID=1000]
|
||||||
|
labels: ["com.centurylinklabs.watchtower.enable=true"]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/audiobookshelf/config:/config
|
||||||
|
- ./volumes/audiobookshelf/metadata:/metadata
|
||||||
|
- /home/gib/Public/Audiobooks:/audiobooks
|
||||||
|
- /home/gib/Public/Podcasts:/podcasts
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
jellyplex-watched:
|
||||||
|
image: luigi311/jellyplex-watched:latest
|
||||||
|
container_name: jellyplex-watched
|
||||||
|
hostname: jellyplex-watched
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
environment:
|
||||||
|
DRYRUN: False
|
||||||
|
DEBUG_LEVEL: INFO # Or debug for verbose logging
|
||||||
|
RUN_ONLY_ONCE: False
|
||||||
|
SLEEP_DURATION: 3600
|
||||||
|
REQUEST_TIMEOUT: 300
|
||||||
|
MAX_THREADS: 2
|
||||||
|
GENERATE_GUIDS: False
|
||||||
|
GENERATE_LOCATIONS: True
|
||||||
|
USER_MAPPING: '{ "gib": "gibby_b", "adrian": "Adrian"}'
|
||||||
|
LIBRARY_MAPPING: '{ "TV Shows": "Shows"}'
|
||||||
|
PLEX_BASEURL: http://plex:32400
|
||||||
|
PLEX_TOKEN: ${PLEX_TOKEN}
|
||||||
|
SSL_BYPASS: False
|
||||||
|
JELLYFIN_BASEURL: http://jellyfin:8096
|
||||||
|
JELLYFIN_TOKEN: ${JELLYFIN_TOKEN}
|
||||||
|
SYNC_FROM_PLEX_TO_JELLYFIN: True
|
||||||
|
SYNC_FROM_JELLYFIN_TO_PLEX: True
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gluetun:
|
||||||
|
image: qmcgaw/gluetun:latest
|
||||||
|
container_name: gluetun
|
||||||
|
hostname: gluetun
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
cap_add: [NET_ADMIN]
|
||||||
|
devices: [/dev/net/tun:/dev/net/tun]
|
||||||
|
#ports:
|
||||||
|
#- 8000:8000 # HTTP control server
|
||||||
|
#- 8080:8080 # For SABnzbd
|
||||||
|
#- 6969:6969 # For qBittorrent
|
||||||
|
#- 6881:6881 # For torrent connectivity
|
||||||
|
#- 6881:6881/udp # For torrent connectivity
|
||||||
|
environment:
|
||||||
|
- VPN_SERVICE_PROVIDER=private internet access
|
||||||
|
- VPN_TYPE=openvpn
|
||||||
|
- SERVER_REGIONS=US East
|
||||||
|
- OPENVPN_USER=p7671057
|
||||||
|
- OPENVPN_PASSWORD=${OPENVPN_PASSWORD}
|
||||||
|
- TZ=America/New_York
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=100
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
restart: unless-stopped
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
|
||||||
|
sabnzbd:
|
||||||
|
image: lscr.io/linuxserver/sabnzbd:latest
|
||||||
|
container_name: sabnzbd
|
||||||
|
domainname: sabnzbd.gbrown.org
|
||||||
|
network_mode: service:gluetun
|
||||||
|
environment: [PUID=1000, PGID=1000, TZ=America/New_York]
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/sabnzbd:/config
|
||||||
|
- /home/gib/Public/Downloads:/downloads
|
||||||
|
- /home/gib/Public/Downloads/incomplete:/incomplete-downloads
|
||||||
|
depends_on: [gluetun]
|
||||||
|
restart: unless-stopped
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
|
||||||
|
qbittorrent:
|
||||||
|
image: linuxserver/qbittorrent:latest
|
||||||
|
container_name: qbittorrent
|
||||||
|
network_mode: service:gluetun
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- TZ=America/New_York
|
||||||
|
- WEBUI_PORT=6969
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/qbittorrent:/config
|
||||||
|
- /home/gib/Public/Downloads:/downloads
|
||||||
|
depends_on: [gluetun]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
qbit-guard:
|
||||||
|
image: ghcr.io/gengines/qbit-guard:latest
|
||||||
|
container_name: qbitguard
|
||||||
|
hostname: qbitguard
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
environment:
|
||||||
|
- QBIT_HOST=http://gluetun:6969
|
||||||
|
- QBIT_USER=gib
|
||||||
|
- QBIT_PASS=${QBIT_PASS}
|
||||||
|
- QBIT_ALLOWED_CATEGORIES=tv-sonarr,radarr
|
||||||
|
- ENABLE_PREAIR_CHECK=1
|
||||||
|
- SONARR_URL=http://sonarr:8989
|
||||||
|
- SONARR_API_KEY=${SONARR_API_KEY}
|
||||||
|
- SONARR_CATEGORIES=tv-sonarr
|
||||||
|
- RADARR_URL=http://radarr:7878
|
||||||
|
- RADARR_API_KEY=${RADARR_API_KEY}
|
||||||
|
- RADARR_CATEGORIES=radarr
|
||||||
|
- ENABLE_ISO_CHECK=1
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
seerr:
|
||||||
|
image: ghcr.io/seerr-team/seerr:latest
|
||||||
|
container_name: seerr
|
||||||
|
init: true
|
||||||
|
hostname: seerr
|
||||||
|
domainname: seerr.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [5055:5055]
|
||||||
|
environment: [LOG_LEVEL=debug, TZ=America/New_York]
|
||||||
|
volumes: [./volumes/seerr:/app/config]
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
depends_on: [radarr, sonarr]
|
||||||
|
healthcheck:
|
||||||
|
test: wget --no-verbose --tries=1 --spider http://localhost:5055/api/v1/status || exit 1
|
||||||
|
start_period: 20s
|
||||||
|
timeout: 3s
|
||||||
|
interval: 15s
|
||||||
|
retries: 3
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
prowlarr:
|
||||||
|
image: lscr.io/linuxserver/prowlarr:latest
|
||||||
|
container_name: prowlarr
|
||||||
|
hostname: prowlarr
|
||||||
|
domainname: prowlarr.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [9696:9696]
|
||||||
|
environment: [PUID=1000, PGID=1000, TZ=America/New_York]
|
||||||
|
volumes: [./volumes/prowlarr:/config]
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
depends_on: [qbittorrent, sabnzbd]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
lidarr:
|
||||||
|
image: lscr.io/linuxserver/lidarr:latest
|
||||||
|
container_name: lidarr
|
||||||
|
hostname: lidarr
|
||||||
|
domainname: lidarr.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [8686:8686]
|
||||||
|
environment: [PUID=1000, PGID=1000, TZ=America/New_York]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/lidarr:/config
|
||||||
|
- /home/gib/Public/Music:/music
|
||||||
|
- /home/gib/Public/Downloads:/downloads
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
depends_on: [prowlarr, qbittorrent, sabnzbd]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
radarr:
|
||||||
|
image: ghcr.io/linuxserver/radarr:latest
|
||||||
|
container_name: radarr
|
||||||
|
hostname: radarr
|
||||||
|
domainname: radarr.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [7878:7878]
|
||||||
|
environment: [PUID=1000, PGID=1000, TZ=America/New_York]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/radarr:/config
|
||||||
|
- /home/gib/Public/Movies:/movies
|
||||||
|
- /home/gib/Public/Downloads:/downloads
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
depends_on: [prowlarr, qbittorrent, sabnzbd]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
sonarr:
|
||||||
|
image: ghcr.io/linuxserver/sonarr:latest
|
||||||
|
container_name: sonarr
|
||||||
|
hostname: sonarr
|
||||||
|
domainname: sonarr.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [8989:8989]
|
||||||
|
environment: [PUID=1000, PGID=1000, TZ=America/New_York]
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/sonarr:/config
|
||||||
|
- /home/gib/Public/TV:/tv
|
||||||
|
- /home/gib/Public/Downloads:/downloads
|
||||||
|
depends_on: [prowlarr, qbittorrent, sabnzbd]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
bazarr:
|
||||||
|
image: lscr.io/linuxserver/bazarr:latest
|
||||||
|
container_name: bazarr
|
||||||
|
hostname: bazarr
|
||||||
|
domainname: bazarr.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [6767:6767]
|
||||||
|
environment: [PUID=1000, PGID=1000, TZ=America/New_York]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/bazarr:/config
|
||||||
|
- /home/gib/Public/Movies:/movies
|
||||||
|
- /home/gib/Public/TV:/tv
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
whisperasr:
|
||||||
|
image: onerahmet/openai-whisper-asr-webservice:latest-gpu
|
||||||
|
container_name: whisperasr
|
||||||
|
hostname: whisperasr
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [9000:9000]
|
||||||
|
environment: [TZ=America/New_York, PUID=1000, PGID=1000, ASR_MODEL=small]
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
devices: [/dev/dri:/dev/dri]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt: ["seccomp:unconfined"]
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
unpackerr:
|
||||||
|
image: golift/unpackerr:latest
|
||||||
|
container_name: unpackerr
|
||||||
|
hostname: unpackerr
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
volumes:
|
||||||
|
- /home/gib/Public/Downloads:/downloads
|
||||||
|
- /home/gib/Server/Media/volumes/unpackerr:/config
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
# log lives in its own volume, NOT in /downloads where the *arr apps scan
|
||||||
|
- UN_LOG_FILE=/config/unpackerr.log
|
||||||
|
- UN_LOG_FILES=4
|
||||||
|
- UN_LOG_FILE_MB=5
|
||||||
|
- UN_SONARR_0_URL=http://sonarr:8989
|
||||||
|
- UN_SONARR_0_API_KEY=${UN_SONARR_0_API_KEY}
|
||||||
|
- UN_RADARR_0_URL=http://radarr:7878
|
||||||
|
- UN_RADARR_0_API_KEY=${UN_RADARR_0_API_KEY}
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
metube:
|
||||||
|
image: ghcr.io/alexta69/metube:latest
|
||||||
|
container_name: metube
|
||||||
|
hostname: metube
|
||||||
|
domainname: metube.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [8081:8081]
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- YTDL_OPTIONS={"cookiefile":"/cookies/cookies.txt"}
|
||||||
|
volumes:
|
||||||
|
- ./volumes/metube:/cookies:rw
|
||||||
|
- /home/gib/Public/Downloads:/downloads:rw
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
pinchflat:
|
||||||
|
image: ghcr.io/kieraneglin/pinchflat:latest
|
||||||
|
container_name: pinchflat
|
||||||
|
hostname: pinchflat
|
||||||
|
domainname: pinchflat.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports: [8945:8945]
|
||||||
|
environment: [TZ=America/New_York, PUID=1000, PGID=1000]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/pinchflat:/config:rw
|
||||||
|
- /home/gib/Public/Downloads:/downloads:rw
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
configarr:
|
||||||
|
image: ghcr.io/raydak-labs/configarr:latest
|
||||||
|
container_name: configarr
|
||||||
|
hostname: configarr
|
||||||
|
domainname: configarr.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
environment: [TZ=America/New_York, PUID=1000, PGID=1000]
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
volumes:
|
||||||
|
- ./volumes/configarr/config:/app/config
|
||||||
|
- ./volumes/configarr/repos:/app/repos
|
||||||
|
- ./volumes/configarr/cfs:/app/cfs
|
||||||
|
- ./volumes/configarr/templates:/app/templates
|
||||||
|
depends_on: [radarr, sonarr]
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
# Run-to-completion: configarr syncs TRaSH guides into Sonarr/Radarr,
|
||||||
|
# prints an execution summary, and exits 0. It has no built-in scheduler
|
||||||
|
# (upstream points at Ofelia or cron for that), so `unless-stopped` here
|
||||||
|
# meant a completed sync was immediately restarted -- a hot loop of ~9
|
||||||
|
# full syncs every 3 minutes. `no` lets it run once per `up` and stop.
|
||||||
|
restart: "no"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Media
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Media
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
bedrock-minecraft:
|
||||||
|
image: itzg/minecraft-bedrock-server
|
||||||
|
container_name: mc-bedrock-adrian
|
||||||
|
network_mode: host
|
||||||
|
#hostname: mc-bedrock-adrian
|
||||||
|
#domainname: mc-bedrock-adrian.gbrown.org
|
||||||
|
#networks: ['nginx-bridge']
|
||||||
|
#ports:
|
||||||
|
#- 19132:19132
|
||||||
|
#- 19132:19132/udp
|
||||||
|
environment:
|
||||||
|
- EULA=TRUE
|
||||||
|
#- UID=1000
|
||||||
|
#- GID=1000
|
||||||
|
- TZ=America/New_York
|
||||||
|
#user: 1000:1000
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- './data:/data'
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Minecraft/Bedrock/Adrian
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Minecraft/Bedrock/Adrian
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Java Edition Minecraft behind mc-router. Live since 2026-10-06 at mc.gbrown.org.
|
||||||
|
#
|
||||||
|
# mc-router owns the one public port (25565) and routes each player by the hostname
|
||||||
|
# they typed: Java clients send it in their first packet, so several servers can
|
||||||
|
# share one port and one IP. Bedrock clients do not send it, which is why the
|
||||||
|
# Bedrock server in ../Bedrock stays separate on its own UDP port.
|
||||||
|
#
|
||||||
|
# Start: docker compose up -d (from this directory)
|
||||||
|
#
|
||||||
|
# Each hostname needs a DNS-only (grey cloud) A record in Cloudflare pointing at the
|
||||||
|
# home IP. The proxied *.gbrown.org wildcard can't carry Minecraft traffic, and a
|
||||||
|
# specific record overrides it. UniFi forwards 25565/tcp to this host. Home
|
||||||
|
# Assistant's Cloudflare integration keeps these records on the current home IP;
|
||||||
|
# select each new record there too.
|
||||||
|
#
|
||||||
|
# Add a server:
|
||||||
|
# 1. mkdir servers/<name>, then copy the "gib" service below and point it there
|
||||||
|
# 2. add "<hostname>=<service>:25565" to MAPPING
|
||||||
|
# 3. add the DNS-only record for <hostname>
|
||||||
|
# Servers publish no ports of their own; only mc-router is reachable from outside.
|
||||||
|
|
||||||
|
services:
|
||||||
|
mc-router:
|
||||||
|
image: itzg/mc-router:latest
|
||||||
|
container_name: mc-router
|
||||||
|
environment:
|
||||||
|
# <hostname players type>=<service name>:<port inside the network>
|
||||||
|
MAPPING: |
|
||||||
|
mc.gbrown.org=gib:25565
|
||||||
|
ports: ["25565:25565"]
|
||||||
|
networks: [minecraft]
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
gib:
|
||||||
|
image: itzg/minecraft-server:latest
|
||||||
|
container_name: mc-gib
|
||||||
|
environment:
|
||||||
|
# 26.1.2 matches the newest OptiFine (SEUS shaders) on the desktop. The 1.20.4
|
||||||
|
# world is in backups/; upgrades rewrite the world and cannot be undone.
|
||||||
|
- VERSION=26.1.2
|
||||||
|
- DIFFICULTY=normal
|
||||||
|
- MAX_PLAYERS=10
|
||||||
|
- TYPE=VANILLA
|
||||||
|
- MEMORY=12G
|
||||||
|
- SIMULATION_DISTANCE=16
|
||||||
|
- VIEW_DISTANCE=16
|
||||||
|
- ALLOW_FLIGHT=true
|
||||||
|
- ENABLE_COMMAND_BLOCK=true
|
||||||
|
- ENABLE_WHITELIST=true
|
||||||
|
- UID=1000
|
||||||
|
- GID=1000
|
||||||
|
- EULA=true
|
||||||
|
- TZ=America/New_York
|
||||||
|
- ENABLE_ROLLING_LOGS=true
|
||||||
|
- LANG=en_US.UTF-8
|
||||||
|
- LANGUAGE=en_US:en
|
||||||
|
- LC_ALL=en_US.UTF-8
|
||||||
|
- SERVER_NAME=Gib's Minecraft Server
|
||||||
|
- SNOOPER_ENABLED=false
|
||||||
|
- WHITELIST_FILE=/data/whitelist.json
|
||||||
|
#- ENABLE_AUTOPAUSE=true
|
||||||
|
# Don't change MOTD it was really hard to make it look good.
|
||||||
|
- MOTD= \u00A7aGib's Minecraft Server\u00A7r \u00A74Welcome to HECK
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./servers/gib:/data
|
||||||
|
networks: [minecraft]
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
minecraft: {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Minecraft/Java
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Minecraft/Java
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DB_POSTGRES_PASSWORD=CHANGE_ME
|
||||||
|
POSTGRES_PASSWORD=CHANGE_ME
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: 'jc21/nginx-proxy-manager:latest'
|
||||||
|
container_name: nginx-proxy-manager
|
||||||
|
hostname: nginx-proxy-manager
|
||||||
|
domainname: nginx.gbrown.org
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
- '443:443'
|
||||||
|
- '81:81' # Admin Web Port
|
||||||
|
#- '21:21' # FTP
|
||||||
|
#- '22:22' # SSH
|
||||||
|
#- '25565:25565' # Minecraft
|
||||||
|
environment:
|
||||||
|
PUID: 1000
|
||||||
|
PGID: 1000
|
||||||
|
DB_POSTGRES_HOST: 'db'
|
||||||
|
DB_POSTGRES_PORT: '5432'
|
||||||
|
DB_POSTGRES_USER: 'npm'
|
||||||
|
DB_POSTGRES_PASSWORD: ${DB_POSTGRES_PASSWORD}
|
||||||
|
DB_POSTGRES_NAME: 'npm'
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: 'true'
|
||||||
|
volumes:
|
||||||
|
- ./volumes/data:/data
|
||||||
|
- ./volumes/letsencrypt:/etc/letsencrypt
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:17
|
||||||
|
container_name: nginx-proxy-manager-db
|
||||||
|
hostname: nginx-proxy-manager-db
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: 'npm'
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: 'npm'
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: 'true'
|
||||||
|
volumes:
|
||||||
|
- ./volumes/postgres:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.25.0.0/24
|
||||||
|
name: nginx-bridge
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Nginx
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Nginx
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
BW_SESSION=CHANGE_ME
|
||||||
|
OPENAI_API_KEY=CHANGE_ME
|
||||||
|
OAUTH_CLIENT_ID=CHANGE_ME
|
||||||
|
OAUTH_CLIENT_SECRET=CHANGE_ME
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
openwebui:
|
||||||
|
image: ghcr.io/open-webui/open-webui:main
|
||||||
|
#image: ghcr.io/open-webui/open-webui:ollama
|
||||||
|
container_name: openwebui
|
||||||
|
hostname: openwebui
|
||||||
|
domainname: chat.gbrown.org
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
#ports:
|
||||||
|
#- 8080:8080
|
||||||
|
environment:
|
||||||
|
- WEBUI_NAME=Gib's Chat
|
||||||
|
- WEBUI_URL=https://chat.gbrown.org
|
||||||
|
- OLLAMA_BASE_URL=http://ollama:11434
|
||||||
|
- [email protected]
|
||||||
|
- DEFAULT_MODELS=llama3.1
|
||||||
|
- TASK_MODEL=llama3.1
|
||||||
|
- ENABLE_AUTOCOMPLETE_GENERATION=false
|
||||||
|
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||||
|
- AUTOMATIC1111_BASE_URL=http://192.168.0.43:7860
|
||||||
|
- COMFYUI_BASE_URL=http://comfyui:8188
|
||||||
|
- WHISPER_MODEL_AUTO_UPDATE=True
|
||||||
|
- PDF_EXTRACT_IMAGES=True
|
||||||
|
- ENABLE_RAG_WEB_SEARCH=True
|
||||||
|
- RAG_WEB_SEARCH_ENGINE=searxng
|
||||||
|
- SEARXNG_QUERY_URL=https://search.gbrown.org/search?q=<query>
|
||||||
|
- ENABLE_IMAGE_GENERATION=True
|
||||||
|
- IMAGE_GENERATION_ENGINE=ComfyUI
|
||||||
|
- IMAGE_SIZE=1216x832
|
||||||
|
#- USE_OLLAMA_DOCKER=True
|
||||||
|
#- USE_CUDA_DOCKER=True
|
||||||
|
- DO_NOT_TRACK=True
|
||||||
|
- ENABLE_OAUTH_SIGNUP=true
|
||||||
|
- OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true
|
||||||
|
#- ENABLE_OAUTH_ROLE_MANAGEMENT=true
|
||||||
|
#- OAUTH_ADMIN_ROLES=admin,internal,superuser
|
||||||
|
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
|
||||||
|
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
|
||||||
|
- OPENID_PROVIDER_URL=https://auth.gbrown.org/application/o/openwebui/.well-known/openid-configuration
|
||||||
|
- OPENID_REDIRECT_URI=https://chat.gbrown.org/oauth/oidc/callback
|
||||||
|
- OAUTH_PROVIDER_NAME=Gib's Auth
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/openwebui:/app/backend/data
|
||||||
|
#- /home/gib/Public/Server_Backup/Ollama_Models:/root/.ollama
|
||||||
|
#devices:
|
||||||
|
#- /dev/dri:/dev/dri
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
|
#security_opt: ['seccomp:unconfined','label:disable']
|
||||||
|
#deploy:
|
||||||
|
#resources:
|
||||||
|
#reservations:
|
||||||
|
#devices:
|
||||||
|
#- driver: nvidia
|
||||||
|
#count: 1
|
||||||
|
#capabilities: [gpu]
|
||||||
|
|
||||||
|
ollama:
|
||||||
|
#image: ollama/ollama:rocm
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
container_name: ollama
|
||||||
|
hostname: ollama
|
||||||
|
domainname: ollama.gbrown.org
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
ports:
|
||||||
|
- 11434:11434
|
||||||
|
volumes:
|
||||||
|
- /home/gib/Public/Server_Backup/Ollama_Models:/root/.ollama
|
||||||
|
devices:
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt: ["seccomp:unconfined", "label:disable"]
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
comfyui:
|
||||||
|
#image: yanwk/comfyui-boot:rocm
|
||||||
|
image: yanwk/comfyui-boot:cu128-megapak
|
||||||
|
init: true
|
||||||
|
container_name: comfyui
|
||||||
|
hostname: comfyui
|
||||||
|
domainname: comfyui.gbrown.org
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
#ports: [8188:8188]
|
||||||
|
volumes:
|
||||||
|
- /home/gib/Public/Server_Backup/ComfyUI/:/root
|
||||||
|
#environment:
|
||||||
|
#- HSA_OVERRIDE_GFX_VERSION=11.0.0
|
||||||
|
#- CLI_ARGS=''
|
||||||
|
devices:
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
#- /dev/kfd:/dev/kfd
|
||||||
|
tty: true
|
||||||
|
security_opt: ["seccomp:unconfined", "label:disable"]
|
||||||
|
restart: unless-stopped
|
||||||
|
group_add: [video]
|
||||||
|
#cap_add: [SYS_PTRACE]
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
mcpo:
|
||||||
|
image: ghcr.io/open-webui/mcpo:main
|
||||||
|
container_name: mcpo
|
||||||
|
hostname: mcpo.gbrown.org
|
||||||
|
domainname: mcpo.gbrown.org
|
||||||
|
command: 'mcpo --port 8010 --config /app/config.json --api-key "Tragedy8-Unfazed-Craftily-Hedging-Gem-Velcro"'
|
||||||
|
restart: unless-stopped
|
||||||
|
tty: true
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
ports:
|
||||||
|
- 8010:8010
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./volumes/mcpo/openapi.json:/app/config.json
|
||||||
|
- ./volumes/mcpo/bin/gitea-mcp:/usr/local/bin/gitea-mcp
|
||||||
|
- ./volumes/mcpo/bin/bw:/usr/local/bin/bw
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: OpenWebUI
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/OpenWebUI
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
BASE_URL=CHANGE_ME
|
||||||
|
SECRET_KEY_BASE=CHANGE_ME
|
||||||
|
HTTP_PORT=CHANGE_ME
|
||||||
|
HTTPS_PORT=CHANGE_ME
|
||||||
|
SMTP_USER_NAME=CHANGE_ME
|
||||||
|
SMTP_USER_PWD=CHANGE_ME
|
||||||
|
POSTGRES_PASSWORD=CHANGE_ME
|
||||||
|
TOTP_VAULT_KEY=CHANGE_ME
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
checkout: ~/Server/Plausible/
|
||||||
|
repo: https://github.com/plausible/community-edition
|
||||||
|
ref: tag v3.0.1
|
||||||
|
commit: 20385b05aa8e5645c88fc1a526360608e0fba324
|
||||||
|
|
||||||
|
compose.yml here is the local, modified copy of the upstream file.
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
plausible_db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: plausible_db
|
||||||
|
hostname: plausible_db
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
start_period: 1m
|
||||||
|
restart: always
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
plausible_events_db:
|
||||||
|
image: clickhouse/clickhouse-server:24.12-alpine
|
||||||
|
container_name: plausible_events_db
|
||||||
|
hostname: plausible_events_db
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- event-data:/var/lib/clickhouse
|
||||||
|
- event-logs:/var/log/clickhouse-server
|
||||||
|
- ./clickhouse/logs.xml:/etc/clickhouse-server/config.d/logs.xml:ro
|
||||||
|
# This makes ClickHouse bind to IPv4 only, since Docker doesn't enable IPv6 in bridge networks by default.
|
||||||
|
# Fixes "Listen [::]:9000 failed: Address family for hostname not supported" warnings.
|
||||||
|
- ./clickhouse/ipv4-only.xml:/etc/clickhouse-server/config.d/ipv4-only.xml:ro
|
||||||
|
# This makes ClickHouse consume less resources, which is useful for small setups.
|
||||||
|
# https://clickhouse.com/docs/en/operations/tips#using-less-than-16gb-of-ram
|
||||||
|
#- ./clickhouse/low-resources.xml:/etc/clickhouse-server/config.d/low-resources.xml:ro
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 262144
|
||||||
|
hard: 262144
|
||||||
|
environment:
|
||||||
|
- CLICKHOUSE_SKIP_USER_SETUP=1
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"wget --no-verbose --tries=1 -O - http://127.0.0.1:8123/ping || exit 1",
|
||||||
|
]
|
||||||
|
start_period: 1m
|
||||||
|
restart: always
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
plausible:
|
||||||
|
image: ghcr.io/plausible/community-edition:v3.2.1
|
||||||
|
container_name: plausible
|
||||||
|
hostname: plausible
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
#ports:
|
||||||
|
#- ${HTTP_PORT}:8000
|
||||||
|
#- ${HTTPS_PORT}:443
|
||||||
|
command: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
|
||||||
|
depends_on:
|
||||||
|
plausible_db:
|
||||||
|
condition: service_healthy
|
||||||
|
plausible_events_db:
|
||||||
|
condition: service_healthy
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- plausible-data:/var/lib/plausible
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 65535
|
||||||
|
hard: 65535
|
||||||
|
extra_hosts:
|
||||||
|
# Pin the relay to the WireGuard address. Docker containers do NOT read the host
|
||||||
|
# /etc/hosts, and the public *.gbrown.org wildcard still returns Cloudflare AAAA
|
||||||
|
# records alongside the UniFi A record -- so without this, delivery depends on
|
||||||
|
# resolver address-ordering. The name (not the IP) is used so the wildcard cert
|
||||||
|
# still validates by hostname.
|
||||||
|
- "smtp.usesend.gbrown.org:192.168.2.2"
|
||||||
|
environment:
|
||||||
|
- TMPDIR=/var/lib/plausible/tmp
|
||||||
|
# required: https://github.com/plausible/community-edition/wiki/configuration#required
|
||||||
|
- BASE_URL=${BASE_URL}
|
||||||
|
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
|
||||||
|
# optional: https://github.com/plausible/community-edition/wiki/configuration#optional
|
||||||
|
# registration: https://github.com/plausible/community-edition/wiki/configuration#registration
|
||||||
|
- TOTP_VAULT_KEY=${TOTP_VAULT_KEY}
|
||||||
|
- DISABLE_REGISTRATION=invite_only
|
||||||
|
- ENABLE_EMAIL_VERIFICATION=true
|
||||||
|
# web: https://github.com/plausible/community-edition/wiki/configuration#web
|
||||||
|
- HTTP_PORT=${HTTP_PORT}
|
||||||
|
- HTTPS_PORT=${HTTPS_PORT}
|
||||||
|
# databases: https://github.com/plausible/community-edition/wiki/configuration#database
|
||||||
|
- DATABASE_URL
|
||||||
|
- CLICKHOUSE_DATABASE_URL
|
||||||
|
# Google: https://github.com/plausible/community-edition/wiki/configuration#google
|
||||||
|
- GOOGLE_CLIENT_ID
|
||||||
|
- GOOGLE_CLIENT_SECRET
|
||||||
|
# geolocation: https://github.com/plausible/community-edition/wiki/configuration#ip-geolocation
|
||||||
|
- IP_GEOLOCATION_DB
|
||||||
|
- GEONAMES_SOURCE_FILE
|
||||||
|
- MAXMIND_LICENSE_KEY
|
||||||
|
- MAXMIND_EDITION
|
||||||
|
# email: https://github.com/plausible/community-edition/wiki/configuration#email
|
||||||
|
- MAILER_ADAPTER
|
||||||
|
- [email protected]
|
||||||
|
- MAILER_NAME=Gib's Plausible Analytics
|
||||||
|
- SMTP_HOST_ADDR=smtp.usesend.gbrown.org
|
||||||
|
- SMTP_HOST_PORT=587
|
||||||
|
- SMTP_USER_NAME=${SMTP_USER_NAME}
|
||||||
|
- SMTP_USER_PWD=${SMTP_USER_PWD}
|
||||||
|
- SMTP_HOST_SSL_ENABLED=false
|
||||||
|
- POSTMARK_API_KEY
|
||||||
|
- MAILGUN_API_KEY
|
||||||
|
- MAILGUN_DOMAIN
|
||||||
|
- MAILGUN_BASE_URI
|
||||||
|
- MANDRILL_API_KEY
|
||||||
|
- SENDGRID_API_KEY
|
||||||
|
restart: always
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
|
event-data:
|
||||||
|
event-logs:
|
||||||
|
plausible-data:
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Plausible
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Plausible
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
POSTHOG_SECRET=CHANGE_ME
|
||||||
|
ENCRYPTION_SALT_KEYS=CHANGE_ME
|
||||||
|
BROWSERLESS_SECRET=CHANGE_ME
|
||||||
|
DOMAIN=CHANGE_ME
|
||||||
|
REGISTRY_URL=CHANGE_ME
|
||||||
|
POSTHOG_APP_TAG=CHANGE_ME
|
||||||
|
TLS_BLOCK=CHANGE_ME
|
||||||
|
CADDY_TLS_BLOCK=CHANGE_ME
|
||||||
|
CADDY_HOST=CHANGE_ME
|
||||||
|
POSTHOG_HTTP_PORT=CHANGE_ME
|
||||||
|
ANTHROPIC_API_KEY=CHANGE_ME
|
||||||
|
OPENAI_API_KEY=CHANGE_ME
|
||||||
|
USESEND_API_KEY=CHANGE_ME
|
||||||
|
POSTHOG_GITLAB_LOGIN_KEY=CHANGE_ME
|
||||||
|
POSTHOG_GITLAB_LOGIN_SECRET=CHANGE_ME
|
||||||
|
SECRET_KEY=CHANGE_ME
|
||||||
|
OBJECT_STORAGE_ACCESS_KEY_ID=CHANGE_ME
|
||||||
|
OBJECT_STORAGE_SECRET_ACCESS_KEY=CHANGE_ME
|
||||||
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID=CHANGE_ME
|
||||||
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY=CHANGE_ME
|
||||||
|
INTERNAL_API_SECRET=CHANGE_ME
|
||||||
|
CSP_REPORT_ENDPOINT=CHANGE_ME
|
||||||
|
DOCKER_REGISTRY_PREFIX=CHANGE_ME
|
||||||
|
ELAPSED=CHANGE_ME
|
||||||
|
HEATMAP_BROWSERLESS_SCREENSHOT_COOKIES_ENABLED=CHANGE_ME
|
||||||
|
OPT_OUT_CAPTURE=CHANGE_ME
|
||||||
|
POSTHOG_NODE_TAG=CHANGE_ME
|
||||||
|
SEAWEEDFS_DOCKER_NAME=CHANGE_ME
|
||||||
|
TIMEOUT=CHANGE_ME
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
# PostHog (self-hosted) on Fedora
|
||||||
|
|
||||||
|
PostHog's official installer (`bin/deploy-hobby`) assumes Ubuntu/Debian and a
|
||||||
|
bare host. This directory is that install done by hand, adapted to this server.
|
||||||
|
|
||||||
|
## Why the official installer doesn't work here
|
||||||
|
|
||||||
|
| What `deploy-hobby` does | Why it fails / what we did instead |
|
||||||
|
|---|---|
|
||||||
|
| `apt update`, `apt install git brotli curl` | Not Debian. All three are already installed via dnf. |
|
||||||
|
| Installs Docker from `download.docker.com/linux/ubuntu` | Docker 29.8 is already installed. |
|
||||||
|
| Downloads the v1 `docker-compose` binary to `/usr/local/bin` | We use the Compose v5 plugin (`docker compose`). |
|
||||||
|
| Runs Caddy on host ports `80:80` / `443:443` and gets its own Let's Encrypt certs | nginx-proxy-manager owns 80/443. Caddy now serves plain HTTP on `127.0.0.1:8002` and NPM terminates TLS in front of it. |
|
||||||
|
| Publishes Temporal on `0.0.0.0:7233` and its UI on `0.0.0.0:8081` | Rebound to `127.0.0.1`. |
|
||||||
|
| Phones home to PostHog Cloud with your domain | Skipped. |
|
||||||
|
| Generates `compose/start` calling `./compose/wait` | Upstream bug, fixed here. WORKDIR is `/code` but the mount is at `/compose`, so the relative path never resolves. Harmless (`bin/migrate` retries 10x) but it meant the readiness check never ran. Ours calls `/compose/wait`. |
|
||||||
|
|
||||||
|
Note: Docker here runs *without* SELinux labelling (`docker info` lists only
|
||||||
|
seccomp and cgroupns), so the bind mounts need no `:z` / `:Z` relabelling even
|
||||||
|
though the host is SELinux-enforcing.
|
||||||
|
|
||||||
|
Caddy is kept rather than replaced by NPM because it does path-based routing
|
||||||
|
that NPM can't express in one proxy host: `/e`, `/i/v0`, `/batch`, `/capture`
|
||||||
|
go to `capture`; `/s` to `replay-capture`; `/i/v1/logs` to `capture-logs`;
|
||||||
|
`/livestream` to `livestream`; `/posthog/*` to object storage; everything else
|
||||||
|
to `web`. NPM just forwards the whole hostname to Caddy.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
compose.yml docker-compose.hobby.yml + local patches
|
||||||
|
compose.override.yml additive local settings, auto-merged by compose
|
||||||
|
docker-compose.base.yml verbatim from the repo (compose.yml `extends` it)
|
||||||
|
.env secrets + domain (chmod 600, never regenerate)
|
||||||
|
.env.services verbatim from the repo
|
||||||
|
posthog/ upstream checkout, only used for config bind mounts
|
||||||
|
share/ GeoLite2-City.mmdb
|
||||||
|
compose/ entrypoint scripts the web/worker containers run,
|
||||||
|
plus local-patches.py (see "Multiple projects" below)
|
||||||
|
bin/upgrade Fedora-native replacement for bin/upgrade-hobby
|
||||||
|
bin/apply-local-patches.py replays our compose.yml deltas after an upgrade
|
||||||
|
|
||||||
|
## nginx-proxy-manager
|
||||||
|
|
||||||
|
Add a proxy host:
|
||||||
|
|
||||||
|
- Domain: `posthog.gbrown.org`
|
||||||
|
- Scheme: `http`, Forward host: `posthog`, Forward port: `80`
|
||||||
|
- **Websockets: ON** (required — the `/livestream` endpoint)
|
||||||
|
- Request an SSL cert, force SSL
|
||||||
|
|
||||||
|
`posthog` is a network alias on `nginx-bridge` for the Caddy container.
|
||||||
|
|
||||||
|
Also worth raising NPM's `client_max_body_size` for this host (Advanced tab:
|
||||||
|
`client_max_body_size 50m;`) — session-recording batches and source-map
|
||||||
|
uploads exceed the 1 MB default.
|
||||||
|
|
||||||
|
## Two config files, two reasons
|
||||||
|
|
||||||
|
`compose.yml` holds changes that *replace* upstream values -- the proxy's ports
|
||||||
|
and networks. These have to be patched into the file directly, because Compose
|
||||||
|
merges list-valued keys like `ports` by appending: an override file saying
|
||||||
|
`8002:80` would leave you with 80, 443 *and* 8002. `bin/apply-local-patches.py`
|
||||||
|
replays them after an upgrade overwrites the file.
|
||||||
|
|
||||||
|
`compose.override.yml` holds purely *additive* settings (currently
|
||||||
|
`TRUST_ALL_PROXIES` for the three Django services). Compose merges it
|
||||||
|
automatically and no upgrade ever touches it, so nothing needs replaying.
|
||||||
|
|
||||||
|
Rule of thumb: if you're adding an env var, put it in the override file. If
|
||||||
|
you're changing a port or a network, patch compose.yml and add it to
|
||||||
|
`bin/apply-local-patches.py`. `command` is the exception: Compose replaces it
|
||||||
|
on merge rather than appending, so the worker's command lives in the override.
|
||||||
|
|
||||||
|
## Operating it
|
||||||
|
|
||||||
|
docker compose up -d # start
|
||||||
|
docker compose stop # stop (keeps volumes)
|
||||||
|
docker compose logs -f web # watch migrations / app
|
||||||
|
docker compose ps # status
|
||||||
|
bin/upgrade # upgrade (runs async migration checks first)
|
||||||
|
|
||||||
|
First boot runs Django + ClickHouse migrations and takes 5-15 minutes before
|
||||||
|
`/_health` returns 200. The first person to visit the site creates the admin
|
||||||
|
account, so log in promptly.
|
||||||
|
|
||||||
|
## Multiple projects (source patches)
|
||||||
|
|
||||||
|
Without a license, upstream PostHog allows one project per org and hides the
|
||||||
|
"New project" button. PostHog no longer sells self-hosted licenses, so
|
||||||
|
`compose/local-patches.py` patches two files in `/code` when `web`, `worker`
|
||||||
|
and `temporal-django-worker` start:
|
||||||
|
|
||||||
|
- `posthog/utils.py`: `can_create_org` follows `MULTI_ORG_ENABLED` alone, which
|
||||||
|
shows the button.
|
||||||
|
- `posthog/models/organization.py`: the org keeps the `organizations_projects`
|
||||||
|
feature, which the project API checks. Without this, the hourly
|
||||||
|
`sync_all_organization_available_product_features` task resets the org's
|
||||||
|
features to `[]` and the API refuses a second project.
|
||||||
|
|
||||||
|
Both are gated on `MULTI_ORG_ENABLED` in `compose.override.yml`. Because they run
|
||||||
|
at container start, recreating a container no longer loses them. An earlier
|
||||||
|
version patched running containers by hand and was wiped when the containers
|
||||||
|
were recreated on 2026-09-22.
|
||||||
|
|
||||||
|
If upstream changes the code, the script logs `PATTERN NOT FOUND` and the app
|
||||||
|
starts unpatched. Check after upgrades (`bin/upgrade` prints it):
|
||||||
|
|
||||||
|
docker compose logs web worker | grep LOCAL-PATCH
|
||||||
|
|
||||||
|
## Rebooting the host
|
||||||
|
|
||||||
|
A host boot is not `docker compose up`. Dockerd starts containers from each one's
|
||||||
|
restart policy alone, in no order, and never reads `depends_on`, so the dependency
|
||||||
|
ordering the stack is built around does not exist at boot time.
|
||||||
|
|
||||||
|
Upstream ships `restart: no` or `on-failure` on nearly every service, and
|
||||||
|
`on-failure` does not retry a clean exit. Several services exit *zero* on purpose:
|
||||||
|
`hypercache-server` when it loses the race to `redis7` on a cold boot, and
|
||||||
|
`capture` when it loses Kafka (on 2026-09-27 a host memory spike stalled Kafka,
|
||||||
|
capture shut down gracefully, and it stayed down for three days). So
|
||||||
|
`compose.override.yml` sets `restart: unless-stopped` on every long-running
|
||||||
|
service. A manual `docker compose stop` is still respected.
|
||||||
|
|
||||||
|
Expect `hypercache-server` to flap a few times early in boot until `redis7`
|
||||||
|
answers. That is the retry working, not a fault.
|
||||||
|
|
||||||
|
`kafka-init` sitting at `Exited (0)` is correct. It is a one-shot init job and
|
||||||
|
must keep `restart: no`.
|
||||||
|
|
||||||
|
The web UI serves fine while these four are missing, so check
|
||||||
|
`docker ps -a --filter name=posthog` for anything not `Up` before assuming a
|
||||||
|
reboot went cleanly. To bring back anything stopped, in dependency order, without
|
||||||
|
recreating running containers:
|
||||||
|
|
||||||
|
docker compose up -d --no-recreate
|
||||||
|
|
||||||
|
## Watchtower is deliberately not enabled
|
||||||
|
|
||||||
|
Every other stack here carries `com.centurylinklabs.watchtower.enable=true`.
|
||||||
|
PostHog does not, on purpose: its upgrades run schema migrations that must be
|
||||||
|
checked with `asyncmigrationscheck` before the app restarts, and a silent image
|
||||||
|
swap underneath a running ClickHouse/Kafka pipeline can corrupt data. Use
|
||||||
|
`bin/upgrade` instead.
|
||||||
|
|
||||||
|
## Resource notes
|
||||||
|
|
||||||
|
This stack is 37 containers (ClickHouse, Kafka, Zookeeper, Postgres, Redis,
|
||||||
|
Valkey, Temporal, Elasticsearch, SeaweedFS, Browserless, and ~12 Rust/Node
|
||||||
|
consumers). PostHog asks for 8 GB minimum; with Sentry already running here,
|
||||||
|
keep an eye on `free -h`.
|
||||||
|
|
||||||
|
If memory gets tight, these can be commented out of `compose.yml` with little
|
||||||
|
loss on a personal instance:
|
||||||
|
|
||||||
|
- `elasticsearch` — Temporal is configured with `ENABLE_ES=false`, so it is
|
||||||
|
only there for Temporal's advanced visibility. Saves ~1 GB.
|
||||||
|
- `temporal-ui` — debugging UI for Temporal workflows only.
|
||||||
|
- `cymbal` / `cymbal-resolution` — error-tracking stack-trace symbolication.
|
||||||
|
Skip if you're keeping errors in Sentry rather than PostHog.
|
||||||
|
|
||||||
|
## Ad blockers and the hostname
|
||||||
|
|
||||||
|
The host is `analytics.gbrown.org`. It was originally `posthog.gbrown.org`, which
|
||||||
|
uBlock Origin blocked, leaving the dashboard a blank white page.
|
||||||
|
|
||||||
|
The culprit is EasyPrivacy line 2964 (EasyPrivacy is enabled by default in uBO):
|
||||||
|
|
||||||
|
://posthog.$script,domain=~posthog.com
|
||||||
|
|
||||||
|
That is an *unanchored substring* pattern, not a domain rule, so any URL
|
||||||
|
containing `://posthog.` matches -- including `https://posthog.gbrown.org/...`.
|
||||||
|
The `$script` option is why the symptom was a white page rather than a failed
|
||||||
|
connection: the HTML document is a `document` request and loaded fine, but every
|
||||||
|
`/static/*.js` was blocked, so React never mounted.
|
||||||
|
|
||||||
|
Renaming fixes it. Checked against all five lists uBO enables by default
|
||||||
|
(EasyPrivacy, EasyList, uBO filters/privacy/badware, ~160k rules), with controls
|
||||||
|
validating in both directions:
|
||||||
|
|
||||||
|
posthog.gbrown.org BLOCKED (rule above)
|
||||||
|
analytics.gbrown.org clean
|
||||||
|
stats/insights/metrics clean
|
||||||
|
bills.gbrown.org clean (control)
|
||||||
|
|
||||||
|
Also clean on the new host: `/static/array.js`, `/static/recorder.js`,
|
||||||
|
`/static/recorder-v2.js`, `/e/`, `/i/v0/e/`, `/batch/`, `/capture/`, `/decide/`.
|
||||||
|
|
||||||
|
Generic words like "analytics" survive because the lists anchor to specific
|
||||||
|
vendor domains (`||google-analytics.com`) rather than bare words -- blocking the
|
||||||
|
word outright would break the web. The `://posthog.` rule is the unusual one.
|
||||||
|
|
||||||
|
### To change the hostname again
|
||||||
|
|
||||||
|
1. Edit `DOMAIN` in `.env`.
|
||||||
|
2. Add the proxy host in nginx-proxy-manager: http -> `posthog` -> 80,
|
||||||
|
websockets on, `client_max_body_size 50m;`.
|
||||||
|
3. `docker compose up -d`.
|
||||||
|
|
||||||
|
Caddy listens on `:80` regardless of hostname, so nothing inside the stack needs
|
||||||
|
touching -- that is the payoff of terminating TLS at NPM instead of in Caddy.
|
||||||
|
Note that `SITE_URL` feeds `CSRF_TRUSTED_ORIGINS`, so existing browser sessions
|
||||||
|
may need a fresh login after a rename. Capture/API traffic is unaffected.
|
||||||
|
|
||||||
|
Still worth proxying capture through the app's own origin (a Next.js `/ingest`
|
||||||
|
rewrite): it is same-origin, so no CORS and no preflight, and it survives future
|
||||||
|
filter-list changes regardless of what this host is called.
|
||||||
|
|
||||||
|
## Capture settings for the bill-tracker app (decided 2026-09-17)
|
||||||
|
|
||||||
|
The bill tracker (Next + Expo + Convex) sends to this instance. Gib asked for
|
||||||
|
everything on, so both clients ship with session replay, autocapture, heatmaps,
|
||||||
|
console log capture and exception capture ENABLED, on posthog-js defaults:
|
||||||
|
|
||||||
|
maskAllInputs: true (default — form inputs are masked)
|
||||||
|
rendered text: NOT masked (default)
|
||||||
|
|
||||||
|
The tradeoff, recorded deliberately rather than discovered later: this is a
|
||||||
|
finance app, so bill amounts, balances, payee names and due dates are rendered
|
||||||
|
*text*, not inputs, and therefore land in session replays verbatim. Autocapture
|
||||||
|
also stores the text of clicked elements, so a button reading "Pay $1,247.33 to
|
||||||
|
Chase" becomes an event property. Console log capture ships whatever the app
|
||||||
|
logs, which in dev is often a whole transaction object.
|
||||||
|
|
||||||
|
This is considered acceptable because the instance is self-hosted on Gib's own
|
||||||
|
hardware and serves his own household — the data never leaves the house.
|
||||||
|
|
||||||
|
To tighten later:
|
||||||
|
|
||||||
|
maskTextSelector: '*' // or scope to the money elements
|
||||||
|
class="ph-no-capture" // on balance / amount / payee nodes
|
||||||
|
|
||||||
|
One asymmetry worth remembering: tightening does NOT retroactively clean stored
|
||||||
|
replays. Anything recorded before the change stays recorded, so the cheap moment
|
||||||
|
to decide is before real household data flows, not after.
|
||||||
|
|
||||||
|
Client config lives in the bill-tracker repo, not here. The host is read from
|
||||||
|
NEXT_PUBLIC_POSTHOG_HOST / EXPO_PUBLIC_POSTHOG_HOST (Infisical), and web routes
|
||||||
|
capture through a same-origin /ingest rewrite rather than hitting this host
|
||||||
|
directly.
|
||||||
|
|
||||||
|
## Blocked asset filenames — why the client requests ext-a.js / ext-b.js
|
||||||
|
|
||||||
|
Renaming the host to `analytics.gbrown.org` fixed the dashboard, but two of
|
||||||
|
PostHog's own script filenames are blocked by rules that match the FILENAME, not
|
||||||
|
the host, so a reverse proxy cannot dodge them:
|
||||||
|
|
||||||
|
easyprivacy.txt line 1906: /posthog-recorder.js
|
||||||
|
easyprivacy.txt line 809: /dead-clicks-autocapture.js
|
||||||
|
|
||||||
|
Both are unanchored substrings with no type or domain options, so they match on
|
||||||
|
any host, any request type, first-party included. Confirmed blocked both direct
|
||||||
|
and through the app's /ingest proxy. Symptom: session replay and dead-click
|
||||||
|
capture silently never start for anyone running uBlock.
|
||||||
|
|
||||||
|
The bill-tracker app works around this with posthog-js's
|
||||||
|
`prepare_external_dependency_script` hook, serving them under inert names:
|
||||||
|
|
||||||
|
posthog-recorder.js -> ext-a.js
|
||||||
|
dead-clicks-autocapture.js -> ext-b.js
|
||||||
|
|
||||||
|
(`strict_script_versioning` off, so only the plain `/static/<name>.js?v=` form is
|
||||||
|
requested.) The Next rewrite maps the disguised names back. Verified: replay
|
||||||
|
blobs land in `posthog.session_replay_events`, which was stuck at 0 before.
|
||||||
|
|
||||||
|
Everything else PostHog serves is clean on both paths — `array.js`,
|
||||||
|
`array.full.js`, `recorder.js`, `recorder-v2.js`, `surveys.js`,
|
||||||
|
`exception-autocapture.js`, `web-vitals-with-attribution.js`,
|
||||||
|
`tracing-headers.js`, `toolbar.js`, `external-scripts-loader.js`, `/e/`,
|
||||||
|
`/flags/`. Note the legacy `recorder.js` and `recorder-v2.js` names are NOT
|
||||||
|
listed; only the newer `posthog-recorder.js` is.
|
||||||
|
|
||||||
|
One more, shape-dependent: `/s/array/<token>/config` is blocked by
|
||||||
|
ubo-privacy.txt line 3159 (`$script,xhr,1p` — first-party too), but
|
||||||
|
`/array/<token>/config.js` is clean. posthog-js 1.433.10 requests the latter, so
|
||||||
|
it does not bite today. Worth re-checking after a major posthog-js upgrade.
|
||||||
|
|
||||||
|
### Expected absences (not bugs)
|
||||||
|
|
||||||
|
- `$pageleave` does NOT fire on client-side SPA navigation. posthog-js hangs it
|
||||||
|
off the real unload/visibilitychange lifecycle, so it tracks actual page exits
|
||||||
|
only. Verified against every pageleave in the instance.
|
||||||
|
- `$web_vitals` flushes on page hide; `$dead_click` needs a click that resolves
|
||||||
|
to nothing. Both stay absent until those conditions occur.
|
||||||
|
|
||||||
|
## nginx-proxy-manager buffer sizes (required, or replay breaks)
|
||||||
|
|
||||||
|
Symptom: the Session Replay list page shows "Error while trying to load
|
||||||
|
recordings". `GET /api/environments/<id>/session_recordings` returns 502 from
|
||||||
|
openresty in ~130ms, while a SINGLE recording
|
||||||
|
(`.../session_recordings/<session_id>`) returns 200 and `/events` returns 200.
|
||||||
|
|
||||||
|
Cause is nginx, not PostHog. From `/data/logs/proxy-host-121_error.log`:
|
||||||
|
|
||||||
|
upstream sent too big header while reading response header from upstream
|
||||||
|
|
||||||
|
nginx's default `proxy_buffer_size` is 4-8KB. PostHog's session_recordings LIST
|
||||||
|
endpoint sends response headers larger than that, so nginx aborts before reading
|
||||||
|
the body and returns 502. The request never reaches Django, which is why nothing
|
||||||
|
appears in the web container logs and Caddy logs no 5xx at all. The single
|
||||||
|
recording endpoint sends normal-sized headers, hence the asymmetry.
|
||||||
|
|
||||||
|
Required in the NPM Advanced tab for this proxy host:
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 4 256k;
|
||||||
|
|
||||||
|
The Sentry proxy host (1.conf) already carried the same three buffer directives —
|
||||||
|
same nginx default, same fix, just never applied to PostHog's host.
|
||||||
|
|
||||||
|
Verified in `/data/logs/proxy-host-121_access.log`: 502 at 22:46:06 before the
|
||||||
|
change, 200 at 22:46:36 after, no further "too big header" entries.
|
||||||
|
|
||||||
|
Do NOT edit the generated `.conf` files directly — NPM regenerates them from its
|
||||||
|
own database, so a direct edit reverts on the next save.
|
||||||
|
|
||||||
|
### Verifying replay from the server side
|
||||||
|
|
||||||
|
ClickHouse `session_replay_events` proves events ARRIVED; it does not prove they
|
||||||
|
can be PLAYED BACK. Those are different paths (ingestion -> Kafka -> object
|
||||||
|
storage vs. API -> proxy -> browser). To check the storage side:
|
||||||
|
|
||||||
|
docker compose exec seaweedfs sh -c \
|
||||||
|
"echo 'fs.ls -l /buckets/posthog/session_recordings/5y' | weed shell -master=localhost:9333"
|
||||||
|
|
||||||
|
Blobs live under `session_recordings/<retention>/`, named `<epoch_ms>-<hash>`.
|
||||||
|
|
||||||
|
## "No recordings found" with replay working fine
|
||||||
|
|
||||||
|
The Session Replay list ships a DEFAULT filter of `> 5 active seconds`. Any
|
||||||
|
session with 5 or fewer *active* seconds is hidden, even though it recorded
|
||||||
|
perfectly and plays back fine.
|
||||||
|
|
||||||
|
Active time is not wall-clock time. The first verified session here ran 458s
|
||||||
|
(7.6 min) but logged only 643ms of activity — one click and a little mouse
|
||||||
|
movement in an otherwise idle tab. PostHog counts interaction, not duration,
|
||||||
|
deliberately, to suppress bounces and accidental loads.
|
||||||
|
|
||||||
|
That default makes quick dev checks vanish and looks identical to "replay is
|
||||||
|
broken". To see them: open the duration chip on /replay/home and set the
|
||||||
|
condition to `> 0 active seconds`, or save a filter preset. Date range and the
|
||||||
|
"filter out internal and test users" toggle are usually NOT the culprit.
|
||||||
|
|
||||||
|
Check the real numbers server-side before assuming anything is missing:
|
||||||
|
|
||||||
|
docker compose exec clickhouse clickhouse-client -q \
|
||||||
|
"select session_id, sum(active_milliseconds) active_ms,
|
||||||
|
dateDiff('second', min(min_first_timestamp), max(max_last_timestamp)) duration_s,
|
||||||
|
sum(click_count) clicks, sum(event_count) evts
|
||||||
|
from posthog.session_replay_events group by session_id"
|
||||||
|
|
||||||
|
Verified 2026-09-17: session 01a0b177-d35c-7292-ac59-021af608d29a recorded,
|
||||||
|
listed once the threshold was lowered, and played back in the browser.
|
||||||
|
|
||||||
|
## Real client IPs through NPM -> Caddy
|
||||||
|
|
||||||
|
By default Django logged `x_forwarded_for=172.25.0.8` — nginx-proxy-manager's own
|
||||||
|
container IP — so every event's `$ip` resolved to the proxy and GeoIP failed on a
|
||||||
|
private address.
|
||||||
|
|
||||||
|
NPM was not at fault; it sends `X-Forwarded-For $proxy_add_x_forwarded_for` and
|
||||||
|
`X-Real-IP $remote_addr` from its global proxy include. The loss was in Caddy:
|
||||||
|
its global options block was empty, so `trusted_proxies` was unset, and Caddy's
|
||||||
|
default is to distrust an incoming X-Forwarded-For and OVERWRITE it with the
|
||||||
|
direct peer. Proven by sending `X-Forwarded-For: 9.9.9.9` to Caddy and watching
|
||||||
|
Django log `172.25.0.1`.
|
||||||
|
|
||||||
|
Fixed in `.env` — no compose patch, because `CADDY_TLS_BLOCK` is substituted
|
||||||
|
directly into Caddy's global options block:
|
||||||
|
|
||||||
|
CADDY_TLS_BLOCK="servers {
|
||||||
|
trusted_proxies static 172.25.0.0/24
|
||||||
|
}"
|
||||||
|
|
||||||
|
Multi-line values work in `.env`; `docker compose config` resolves it correctly
|
||||||
|
and `caddy validate` accepts it. Apply with `docker compose up -d proxy`.
|
||||||
|
|
||||||
|
Verified after: a spoofed `X-Forwarded-For: 9.9.9.9` arrives as `ip=9.9.9.9`, and
|
||||||
|
a real request through NPM arrives as the true client address.
|
||||||
|
|
||||||
|
TRADEOFF: `/24` trusts every container on nginx-bridge (~42 of them), any of
|
||||||
|
which could forge an X-Forwarded-For that Caddy now believes — compounded by
|
||||||
|
`TRUST_ALL_PROXIES=true` on Django (see compose.override.yml). Acceptable for a
|
||||||
|
LAN-only household instance; tighten to NPM's address if this is ever exposed to
|
||||||
|
the internet. Pinning `172.25.0.8/32` is narrower but breaks silently whenever
|
||||||
|
Docker reassigns NPM's IP.
|
||||||
|
|
||||||
|
Note this does NOT produce geolocation: LAN clients are 192.168.x.x, which is not
|
||||||
|
in the MaxMind database, so GeoIP still errors. What it buys is the ability to
|
||||||
|
tell devices apart.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
checkout: ~/Server/PostHog/posthog/
|
||||||
|
repo: https://github.com/PostHog/posthog
|
||||||
|
ref: master at this commit
|
||||||
|
commit: ddfaf8843fca8e796bf1f795976842d2d1adb380
|
||||||
|
|
||||||
|
docker-compose.base.yml is the upstream file at that commit, unchanged.
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Re-apply this server's deltas to a freshly-copied docker-compose.hobby.yml.
|
||||||
|
|
||||||
|
Upstream's bin/upgrade-hobby overwrites compose.yml from the repo on every
|
||||||
|
upgrade, so these three changes have to be replayed afterwards. Idempotent:
|
||||||
|
running it on an already-patched file is a no-op.
|
||||||
|
|
||||||
|
Usage: bin/apply-local-patches.py [compose.yml]
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = sys.argv[1] if len(sys.argv) > 1 else 'compose.yml'
|
||||||
|
s = open(path).read()
|
||||||
|
changed = []
|
||||||
|
|
||||||
|
# 1) nginx-proxy-manager owns host 80/443. Publish Caddy on loopback instead and
|
||||||
|
# put it on nginx-bridge so NPM can reach it by name.
|
||||||
|
old = """ ports:
|
||||||
|
- '80:80'
|
||||||
|
- '443:443'
|
||||||
|
"""
|
||||||
|
new = """ ports:
|
||||||
|
# nginx-proxy-manager owns host 80/443. Loopback only; NPM reaches
|
||||||
|
# this container over nginx-bridge as "posthog".
|
||||||
|
- '127.0.0.1:${POSTHOG_HTTP_PORT:-8002}:80'
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
nginx-bridge:
|
||||||
|
aliases:
|
||||||
|
- posthog
|
||||||
|
"""
|
||||||
|
if old in s:
|
||||||
|
s = s.replace(old, new)
|
||||||
|
changed.append('proxy ports + nginx-bridge')
|
||||||
|
|
||||||
|
# 2) TLS terminates at NPM, so Caddy must not try to get its own certs. Take the
|
||||||
|
# site address from .env (CADDY_HOST=:80) instead of the hardcoded domain.
|
||||||
|
old = " CADDY_HOST: '$DOMAIN, http://, https://'\n"
|
||||||
|
if old in s:
|
||||||
|
s = s.replace(old, " CADDY_HOST: $CADDY_HOST\n")
|
||||||
|
changed.append('CADDY_HOST from .env')
|
||||||
|
|
||||||
|
# 3) Temporal gRPC + UI on loopback, not every interface.
|
||||||
|
for a, b in ((" - 7233:7233\n", " - 127.0.0.1:7233:7233\n"),
|
||||||
|
(" - 8081:8080\n", " - 127.0.0.1:8081:8080\n")):
|
||||||
|
if a in s:
|
||||||
|
s = s.replace(a, b)
|
||||||
|
changed.append(a.strip())
|
||||||
|
|
||||||
|
# 4) nginx-bridge is created and owned by ~/Server/Nginx.
|
||||||
|
old = "networks:\n otel_network:\n"
|
||||||
|
if old in s and 'nginx-bridge:\n external: true' not in s:
|
||||||
|
s = s.replace(old, "networks:\n otel_network:\n nginx-bridge:\n external: true\n")
|
||||||
|
changed.append('nginx-bridge external')
|
||||||
|
|
||||||
|
open(path, 'w').write(s)
|
||||||
|
print(f"{path}: " + (", ".join(changed) if changed else "already patched, no changes"))
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Fedora-native replacement for posthog/bin/upgrade-hobby.
|
||||||
|
#
|
||||||
|
# Upstream's script is unusable here: it calls apt-get, re-downloads the v1
|
||||||
|
# `docker-compose` binary, and overwrites compose.yml from the repo (throwing
|
||||||
|
# away this server's nginx-proxy-manager and port changes). This does the same
|
||||||
|
# work using dnf-era tooling and replays the local patches afterwards.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "==> Refreshing the PostHog checkout"
|
||||||
|
git -C posthog fetch origin
|
||||||
|
git -C posthog reset --hard origin/master
|
||||||
|
echo " now on $(git -C posthog rev-parse --short HEAD)"
|
||||||
|
|
||||||
|
echo "==> Refreshing compose files and re-applying local patches"
|
||||||
|
cp posthog/docker-compose.base.yml docker-compose.base.yml
|
||||||
|
cp posthog/docker-compose.hobby.yml compose.yml
|
||||||
|
cp posthog/.env.services .env.services
|
||||||
|
bin/apply-local-patches.py compose.yml
|
||||||
|
docker compose config >/dev/null && echo " compose config OK"
|
||||||
|
|
||||||
|
echo "==> Refreshing the GeoIP database"
|
||||||
|
curl -sL 'https://mmdbcdn.posthog.net/' --http1.1 | brotli --decompress > share/GeoLite2-City.mmdb
|
||||||
|
echo "{\"date\": \"$(date +%F)\"}" > share/GeoLite2-City.json
|
||||||
|
chmod 644 share/GeoLite2-City.mmdb share/GeoLite2-City.json
|
||||||
|
|
||||||
|
echo "==> Pulling images"
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
echo "==> Checking async migrations against the new version"
|
||||||
|
docker compose up -d db redis7 clickhouse
|
||||||
|
docker compose run --rm asyncmigrationscheck
|
||||||
|
|
||||||
|
echo "==> Restarting the stack"
|
||||||
|
docker compose up -d --remove-orphans
|
||||||
|
|
||||||
|
# web, worker and temporal-django-worker apply compose/local-patches.py on start.
|
||||||
|
# Surface the result so an upstream change that breaks a patch is not missed.
|
||||||
|
echo "==> Local source patches"
|
||||||
|
sleep 15
|
||||||
|
docker compose logs --since 5m web worker temporal-django-worker 2>&1 | grep LOCAL-PATCH || \
|
||||||
|
echo " no LOCAL-PATCH lines yet, check: docker compose logs web worker | grep LOCAL-PATCH"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done. Watch the migrations with: docker compose logs -f web"
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
services:
|
||||||
|
web: &id001
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TRUST_ALL_PROXIES: 'true'
|
||||||
|
MULTI_ORG_ENABLED: 'true'
|
||||||
|
# SSO: the "GitLab" login button is really Authentik. NPM on the VPS maps GitLab's
|
||||||
|
# /oauth/* and /api/v4/user paths on auth.gbrown.org to Authentik, the same shim
|
||||||
|
# Infisical uses. GitLab login needs no license here; only Google is gated.
|
||||||
|
SOCIAL_AUTH_GITLAB_API_URL: https://auth.gbrown.org
|
||||||
|
SOCIAL_AUTH_GITLAB_KEY: ${POSTHOG_GITLAB_LOGIN_KEY:?set POSTHOG_GITLAB_LOGIN_KEY in .env}
|
||||||
|
SOCIAL_AUTH_GITLAB_SECRET: ${POSTHOG_GITLAB_LOGIN_SECRET:?set POSTHOG_GITLAB_LOGIN_SECRET in .env}
|
||||||
|
PERSONHOG_ADDR: personhog-router:50052
|
||||||
|
PERSISTED_FEATURE_FLAGS: sql-editor-vim-mode,cdp-hog-sources,customer-analytics-roadmap,marketing-analytics,web-experiments,dashboard-widgets,twig,tracing,metrics
|
||||||
|
EMAIL_ENABLED: 'true'
|
||||||
|
EMAIL_HOST: smtp.usesend.gbrown.org
|
||||||
|
EMAIL_PORT: '587'
|
||||||
|
EMAIL_HOST_USER: usesend
|
||||||
|
EMAIL_HOST_PASSWORD: ${USESEND_API_KEY:?set USESEND_API_KEY in .env}
|
||||||
|
EMAIL_USE_TLS: 'true'
|
||||||
|
EMAIL_USE_SSL: 'false'
|
||||||
|
EMAIL_DEFAULT_FROM: PostHog <[email protected]>
|
||||||
|
EMAIL_REPLY_TO: [email protected]
|
||||||
|
worker:
|
||||||
|
<<: *id001
|
||||||
|
# compose/worker applies compose/local-patches.py, then runs upstream's
|
||||||
|
# command. `command` is replaced on merge (not appended), so it is safe here.
|
||||||
|
command: /compose/worker
|
||||||
|
volumes:
|
||||||
|
- ./compose:/compose
|
||||||
|
temporal-django-worker: *id001
|
||||||
|
# Every long-running service restarts regardless of exit code. Upstream ships
|
||||||
|
# `on-failure` (or `no`), which never retries a clean exit, and these services
|
||||||
|
# exit 0 on purpose: capture shuts down gracefully when it loses Kafka
|
||||||
|
# (2026-09-27, down 3 days), hypercache-server exits 0 when it beats redis7 at
|
||||||
|
# boot. web/worker/temporal-django-worker get this via the anchor above.
|
||||||
|
# kafka-init and asyncmigrationscheck are one-shots and must stay `no`.
|
||||||
|
browserless:
|
||||||
|
restart: unless-stopped
|
||||||
|
capture:
|
||||||
|
restart: unless-stopped
|
||||||
|
capture-logs:
|
||||||
|
restart: unless-stopped
|
||||||
|
clickhouse:
|
||||||
|
restart: unless-stopped
|
||||||
|
cymbal:
|
||||||
|
restart: unless-stopped
|
||||||
|
cymbal-resolution:
|
||||||
|
restart: unless-stopped
|
||||||
|
db:
|
||||||
|
restart: unless-stopped
|
||||||
|
elasticsearch:
|
||||||
|
restart: unless-stopped
|
||||||
|
feature-flags:
|
||||||
|
restart: unless-stopped
|
||||||
|
hypercache-server:
|
||||||
|
restart: unless-stopped
|
||||||
|
ingestion-error-tracking:
|
||||||
|
restart: unless-stopped
|
||||||
|
ingestion-general:
|
||||||
|
restart: unless-stopped
|
||||||
|
ingestion-logs:
|
||||||
|
restart: unless-stopped
|
||||||
|
ingestion-sessionreplay:
|
||||||
|
restart: unless-stopped
|
||||||
|
ingestion-traces:
|
||||||
|
restart: unless-stopped
|
||||||
|
kafka:
|
||||||
|
restart: unless-stopped
|
||||||
|
livestream:
|
||||||
|
restart: unless-stopped
|
||||||
|
objectstorage:
|
||||||
|
restart: unless-stopped
|
||||||
|
personhog-replica:
|
||||||
|
restart: unless-stopped
|
||||||
|
personhog-router:
|
||||||
|
restart: unless-stopped
|
||||||
|
plugins:
|
||||||
|
restart: unless-stopped
|
||||||
|
property-defs-rs:
|
||||||
|
restart: unless-stopped
|
||||||
|
recording-api:
|
||||||
|
restart: unless-stopped
|
||||||
|
redis7:
|
||||||
|
restart: unless-stopped
|
||||||
|
replay-capture:
|
||||||
|
restart: unless-stopped
|
||||||
|
seaweedfs:
|
||||||
|
restart: unless-stopped
|
||||||
|
temporal:
|
||||||
|
restart: unless-stopped
|
||||||
|
temporal-admin-tools:
|
||||||
|
restart: unless-stopped
|
||||||
|
temporal-ui:
|
||||||
|
restart: unless-stopped
|
||||||
|
valkey:
|
||||||
|
restart: unless-stopped
|
||||||
|
zookeeper:
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,767 @@
|
|||||||
|
#
|
||||||
|
# `docker-compose` file used ONLY for hobby deployments.
|
||||||
|
#
|
||||||
|
# Please take a look at https://posthog.com/docs/self-host/deploy/hobby
|
||||||
|
# for more info.
|
||||||
|
#
|
||||||
|
# PostHog has sunset support for self-hosted K8s deployments.
|
||||||
|
# See: https://posthog.com/blog/sunsetting-helm-support-posthog
|
||||||
|
#
|
||||||
|
|
||||||
|
x-logging: &default-logging
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: '50m'
|
||||||
|
max-file: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: db
|
||||||
|
image: ${DOCKER_REGISTRY_PREFIX:-}postgres:15.12-alpine
|
||||||
|
logging: *default-logging
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
redis7:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: redis7
|
||||||
|
logging: *default-logging
|
||||||
|
volumes:
|
||||||
|
- redis7-data:/data
|
||||||
|
|
||||||
|
valkey:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: valkey
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
clickhouse:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: clickhouse
|
||||||
|
restart: on-failure
|
||||||
|
logging: *default-logging
|
||||||
|
depends_on:
|
||||||
|
- kafka
|
||||||
|
- zookeeper
|
||||||
|
volumes:
|
||||||
|
- ./posthog/posthog/idl:/idl
|
||||||
|
- ./posthog/docker/clickhouse/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
|
||||||
|
- ./posthog/docker/clickhouse/config.xml:/etc/clickhouse-server/config.xml
|
||||||
|
- ./posthog/docker/clickhouse/config.d/default.xml:/etc/clickhouse-server/config.d/default.xml
|
||||||
|
- ./posthog/docker/clickhouse/users.xml:/etc/clickhouse-server/users.xml
|
||||||
|
- ./posthog/docker/clickhouse/user_defined_function.xml:/etc/clickhouse-server/user_defined_function.xml
|
||||||
|
- ./posthog/posthog/user_scripts:/var/lib/clickhouse/user_scripts
|
||||||
|
- clickhouse-data:/var/lib/clickhouse
|
||||||
|
|
||||||
|
zookeeper:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: zookeeper
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
ZOO_AUTOPURGE_PURGEINTERVAL: 1
|
||||||
|
ZOO_AUTOPURGE_SNAPRETAINCOUNT: 3
|
||||||
|
volumes:
|
||||||
|
- zookeeper-datalog:/datalog
|
||||||
|
- zookeeper-data:/data
|
||||||
|
- zookeeper-logs:/logs
|
||||||
|
kafka:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: kafka
|
||||||
|
logging: *default-logging
|
||||||
|
depends_on:
|
||||||
|
- zookeeper
|
||||||
|
environment:
|
||||||
|
KAFKA_LOG_RETENTION_MS: 3600000
|
||||||
|
KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS: 300000
|
||||||
|
KAFKA_LOG_RETENTION_HOURS: 1
|
||||||
|
volumes:
|
||||||
|
- kafka-data:/bitnami/kafka
|
||||||
|
|
||||||
|
worker:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: worker
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
SITE_URL: https://$DOMAIN
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
||||||
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
||||||
|
OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000
|
||||||
|
OBJECT_STORAGE_PUBLIC_ENDPOINT: https://$DOMAIN
|
||||||
|
SESSION_RECORDING_V2_S3_ENDPOINT: http://seaweedfs:8333
|
||||||
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: ${SESSION_RECORDING_V2_S3_ACCESS_KEY_ID}
|
||||||
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: ${SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY}
|
||||||
|
OBJECT_STORAGE_ENABLED: true
|
||||||
|
RECORDING_API_URL: http://recording-api:6738
|
||||||
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:-$POSTHOG_SECRET}
|
||||||
|
ENCRYPTION_SALT_KEYS: ${ENCRYPTION_SALT_KEYS}
|
||||||
|
POSTHOG_SKIP_MIGRATION_CHECKS: '1'
|
||||||
|
PERSONHOG_ADDR: 'personhog-router:50052'
|
||||||
|
PERSONHOG_ENABLED: 'true'
|
||||||
|
BROWSERLESS_CDP_URL: 'ws://browserless:3000'
|
||||||
|
BROWSERLESS_TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
||||||
|
HEATMAP_BROWSERLESS_URL: 'http://browserless:3000'
|
||||||
|
HEATMAP_BROWSERLESS_TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
||||||
|
HEATMAP_BROWSERLESS_SCREENSHOT_COOKIES_ENABLED: ${HEATMAP_BROWSERLESS_SCREENSHOT_COOKIES_ENABLED:-false}
|
||||||
|
# Point Django at the Rust feature-flags container; the setting
|
||||||
|
# otherwise defaults to localhost:3001.
|
||||||
|
FEATURE_FLAGS_SERVICE_URL: 'http://feature-flags:3001'
|
||||||
|
# The logs product uses a dedicated ClickHouse cluster config (separate
|
||||||
|
# from CLICKHOUSE_HOST) whose host defaults to localhost and whose SECURE
|
||||||
|
# defaults to true outside DEBUG. Point it at the stack's ClickHouse over
|
||||||
|
# plaintext; the other logs-cluster settings already default correctly
|
||||||
|
# (native port 9000, user "default", empty password).
|
||||||
|
CLICKHOUSE_LOGS_CLUSTER_HOST: 'clickhouse'
|
||||||
|
CLICKHOUSE_LOGS_CLUSTER_SECURE: 'false'
|
||||||
|
# AI provider keys for PostHog's AI features. Blank unless set in .env.
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||||
|
image: $REGISTRY_URL:$POSTHOG_APP_TAG
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis7:
|
||||||
|
condition: service_healthy
|
||||||
|
clickhouse:
|
||||||
|
condition: service_started
|
||||||
|
kafka:
|
||||||
|
condition: service_healthy
|
||||||
|
web:
|
||||||
|
condition: service_started
|
||||||
|
personhog-router:
|
||||||
|
condition: service_started
|
||||||
|
browserless:
|
||||||
|
condition: service_started
|
||||||
|
web:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: web
|
||||||
|
command: /compose/start
|
||||||
|
logging: *default-logging
|
||||||
|
volumes:
|
||||||
|
- ./compose:/compose
|
||||||
|
image: $REGISTRY_URL:$POSTHOG_APP_TAG
|
||||||
|
environment:
|
||||||
|
SITE_URL: https://$DOMAIN
|
||||||
|
LIVESTREAM_HOST: 'https://${DOMAIN}/livestream'
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
||||||
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
||||||
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: ${SESSION_RECORDING_V2_S3_ACCESS_KEY_ID}
|
||||||
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: ${SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY}
|
||||||
|
OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000
|
||||||
|
OBJECT_STORAGE_PUBLIC_ENDPOINT: https://$DOMAIN
|
||||||
|
SESSION_RECORDING_V2_S3_ENDPOINT: http://seaweedfs:8333
|
||||||
|
OBJECT_STORAGE_ENABLED: true
|
||||||
|
RECORDING_API_URL: http://recording-api:6738
|
||||||
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:-$POSTHOG_SECRET}
|
||||||
|
ENCRYPTION_SALT_KEYS: ${ENCRYPTION_SALT_KEYS}
|
||||||
|
OTEL_SERVICE_NAME: 'posthog'
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT: ''
|
||||||
|
OTEL_SDK_DISABLED: 'true'
|
||||||
|
OPT_OUT_CAPTURE: ${OPT_OUT_CAPTURE:-false}
|
||||||
|
# Blank sends no CSP reports. Point it at this instance's own /report/ endpoint to
|
||||||
|
# collect them yourself.
|
||||||
|
CSP_REPORT_ENDPOINT: ${CSP_REPORT_ENDPOINT:-}
|
||||||
|
DEPLOYMENT: 'hobby'
|
||||||
|
PERSONHOG_ADDR: 'personhog-router:50052'
|
||||||
|
PERSONHOG_ENABLED: 'true'
|
||||||
|
# Point Django at the Rust feature-flags container; the setting
|
||||||
|
# otherwise defaults to localhost:3001.
|
||||||
|
FEATURE_FLAGS_SERVICE_URL: 'http://feature-flags:3001'
|
||||||
|
# The logs product uses a dedicated ClickHouse cluster config (separate
|
||||||
|
# from CLICKHOUSE_HOST) whose host defaults to localhost and whose SECURE
|
||||||
|
# defaults to true outside DEBUG. Point it at the stack's ClickHouse over
|
||||||
|
# plaintext; the other logs-cluster settings already default correctly
|
||||||
|
# (native port 9000, user "default", empty password).
|
||||||
|
CLICKHOUSE_LOGS_CLUSTER_HOST: 'clickhouse'
|
||||||
|
CLICKHOUSE_LOGS_CLUSTER_SECURE: 'false'
|
||||||
|
# AI provider keys for PostHog's AI features. Blank unless set in .env.
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
- clickhouse
|
||||||
|
- kafka
|
||||||
|
- objectstorage
|
||||||
|
- seaweedfs
|
||||||
|
- personhog-router
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: plugins
|
||||||
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
SITE_URL: https://$DOMAIN
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
||||||
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
||||||
|
OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000
|
||||||
|
OBJECT_STORAGE_PUBLIC_ENDPOINT: https://$DOMAIN
|
||||||
|
OBJECT_STORAGE_ENABLED: true
|
||||||
|
CDP_REDIS_HOST: redis7
|
||||||
|
CDP_REDIS_PORT: 6379
|
||||||
|
LOGS_REDIS_HOST: redis7
|
||||||
|
LOGS_REDIS_PORT: 6379
|
||||||
|
LOGS_REDIS_TLS: 'false'
|
||||||
|
TRACES_REDIS_HOST: redis7
|
||||||
|
TRACES_REDIS_PORT: 6379
|
||||||
|
TRACES_REDIS_TLS: 'false'
|
||||||
|
# Every CDP process dual-writes to Valkey and refuses to start without a host.
|
||||||
|
CDP_VALKEY_HOST: valkey
|
||||||
|
CDP_VALKEY_PORT: 6379
|
||||||
|
ENCRYPTION_SALT_KEYS: ${ENCRYPTION_SALT_KEYS}
|
||||||
|
PERSONS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
||||||
|
BEHAVIORAL_COHORTS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT: ''
|
||||||
|
volumes:
|
||||||
|
- ./share:/share
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
- valkey
|
||||||
|
- clickhouse
|
||||||
|
- kafka
|
||||||
|
- objectstorage
|
||||||
|
- seaweedfs
|
||||||
|
|
||||||
|
ingestion-general:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: ingestion-general
|
||||||
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
PERSONS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
||||||
|
BEHAVIORAL_COHORTS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
||||||
|
ENCRYPTION_SALT_KEYS: ${ENCRYPTION_SALT_KEYS}
|
||||||
|
# The combined-mode AI consumer reads persons through PersonHog
|
||||||
|
PERSONHOG_ADDR: 'personhog-router:50052'
|
||||||
|
PERSONHOG_ENABLED: 'true'
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
- clickhouse
|
||||||
|
- kafka
|
||||||
|
# The combined AI consumer's blob store healthchecks the ai-blobs bucket at startup
|
||||||
|
- objectstorage
|
||||||
|
- personhog-router
|
||||||
|
|
||||||
|
ingestion-sessionreplay:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: ingestion-sessionreplay
|
||||||
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: ${SESSION_RECORDING_V2_S3_ACCESS_KEY_ID}
|
||||||
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: ${SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY}
|
||||||
|
SESSION_RECORDING_V2_S3_TIMEOUT_MS: 120000
|
||||||
|
SESSION_RECORDING_V2_S3_ENDPOINT: http://seaweedfs:8333
|
||||||
|
ENCRYPTION_SALT_KEYS: ${ENCRYPTION_SALT_KEYS}
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
- kafka
|
||||||
|
- seaweedfs
|
||||||
|
|
||||||
|
recording-api:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: recording-api
|
||||||
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
# Read side of the session recording V2 blob store. Must point at the same
|
||||||
|
# SeaweedFS the ingestion-sessionreplay writer uses — without these the
|
||||||
|
# service falls back to its localhost:8333 default, so every playback
|
||||||
|
# request 500s with ECONNREFUSED ("We're unable to play this recording").
|
||||||
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: ${SESSION_RECORDING_V2_S3_ACCESS_KEY_ID}
|
||||||
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: ${SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY}
|
||||||
|
SESSION_RECORDING_V2_S3_ENDPOINT: http://seaweedfs:8333
|
||||||
|
ENCRYPTION_SALT_KEYS: ${ENCRYPTION_SALT_KEYS}
|
||||||
|
# Must resolve to the same value as the web and worker services, which send it as
|
||||||
|
# X-Internal-Api-Secret. The JWT signing secret is deliberately left unset here, so hobby
|
||||||
|
# stays on the legacy shared secret until that scheme is rolled out.
|
||||||
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:-$POSTHOG_SECRET}
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
- clickhouse
|
||||||
|
- seaweedfs
|
||||||
|
|
||||||
|
ingestion-error-tracking:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: ingestion-error-tracking
|
||||||
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
ENCRYPTION_SALT_KEYS: ${ENCRYPTION_SALT_KEYS}
|
||||||
|
ERROR_TRACKING_CYMBAL_BASE_URL: 'http://cymbal:3302'
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
- kafka
|
||||||
|
|
||||||
|
ingestion-logs:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: ingestion-logs
|
||||||
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
LOGS_REDIS_TLS: 'false'
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
- kafka
|
||||||
|
|
||||||
|
ingestion-traces:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: ingestion-traces
|
||||||
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
TRACES_REDIS_TLS: 'false'
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
- kafka
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: proxy
|
||||||
|
logging: *default-logging
|
||||||
|
ports:
|
||||||
|
# nginx-proxy-manager owns host 80/443. Loopback only; NPM reaches
|
||||||
|
# this container over nginx-bridge as "posthog".
|
||||||
|
- '127.0.0.1:${POSTHOG_HTTP_PORT:-8002}:80'
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
nginx-bridge:
|
||||||
|
aliases:
|
||||||
|
- posthog
|
||||||
|
volumes:
|
||||||
|
- caddy-data:/data
|
||||||
|
- caddy-config:/config
|
||||||
|
environment:
|
||||||
|
CADDY_TLS_BLOCK: $TLS_BLOCK
|
||||||
|
CADDY_HOST: $CADDY_HOST
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
- livestream
|
||||||
|
objectstorage:
|
||||||
|
# SeaweedFS on the same objectstorage:19000 endpoint and credentials this service
|
||||||
|
# served when it ran MinIO, so no other service in this file changes.
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: objectstorage
|
||||||
|
restart: on-failure
|
||||||
|
logging: *default-logging
|
||||||
|
# The dev/CI bootstrap this replaces also registers an `anonymous` identity with
|
||||||
|
# Admin rights. Hobby must not, because Caddy proxies /posthog/* straight to this
|
||||||
|
# service: an anonymous identity opens every object to the internet. With only the
|
||||||
|
# posthog identity, SeaweedFS rejects unsigned requests and the presigned URLs
|
||||||
|
# Django hands out keep working. There is no duckgres here, so there is no
|
||||||
|
# ducklake-dev bucket either. The loop holds no shell variables on purpose:
|
||||||
|
# bin/upgrade-hobby pipes this file through envsubst, which collapses a
|
||||||
|
# doubled-dollar variable reference to a bare dollar sign.
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
(
|
||||||
|
while true; do
|
||||||
|
printf 's3.configure -access_key=object_storage_root_user -secret_key=object_storage_root_password -user=posthog -actions=Admin -apply\ns3.bucket.create -name posthog\ns3.bucket.create -name ai-blobs\n' \
|
||||||
|
| /usr/bin/weed shell -master=localhost:19001 >/dev/null 2>&1 || true
|
||||||
|
if echo 's3.bucket.list' | /usr/bin/weed shell -master=localhost:19001 2>/dev/null | grep -q ai-blobs \
|
||||||
|
&& echo 's3.configure' | /usr/bin/weed shell -master=localhost:19001 2>/dev/null | grep -q object_storage_root_user; then
|
||||||
|
touch /tmp/objectstorage-ready
|
||||||
|
echo "Objectstorage SeaweedFS ready with required buckets and identities"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
exec /usr/bin/weed "$$@"
|
||||||
|
- --
|
||||||
|
# A new volume. SeaweedFS cannot read the MinIO layout, so the old objects stay on
|
||||||
|
# the `objectstorage` volume until the operator copies them across with
|
||||||
|
# bin/migrate-storage-hobby or deletes the volume.
|
||||||
|
volumes:
|
||||||
|
- objectstorage-data:/data
|
||||||
|
ports:
|
||||||
|
# Localhost only. MinIO published this port on every interface.
|
||||||
|
- '127.0.0.1:19000:19000'
|
||||||
|
|
||||||
|
seaweedfs:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: seaweedfs
|
||||||
|
container_name: '${SEAWEEDFS_DOCKER_NAME:-seaweedfs-main}'
|
||||||
|
restart: on-failure
|
||||||
|
logging: *default-logging
|
||||||
|
ports:
|
||||||
|
- '127.0.0.1:8333:8333' # S3 API
|
||||||
|
- '127.0.0.1:9333:9333' # Master server
|
||||||
|
volumes:
|
||||||
|
- seaweedfs:/data
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
'CMD',
|
||||||
|
'sh',
|
||||||
|
'-c',
|
||||||
|
"echo 's3.bucket.list' | /usr/bin/weed shell -master=localhost:9333 2>&1 | grep -q posthog",
|
||||||
|
]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 30
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
browserless:
|
||||||
|
# Renders image exports and heatmap screenshots (no browser ships in the PostHog image).
|
||||||
|
image: ghcr.io/browserless/chromium:v2.51.2
|
||||||
|
restart: on-failure
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
||||||
|
LOG_LEVEL: warn
|
||||||
|
DEBUG: '-*'
|
||||||
|
|
||||||
|
asyncmigrationscheck:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: asyncmigrationscheck
|
||||||
|
image: $REGISTRY_URL:$POSTHOG_APP_TAG
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
SITE_URL: https://$DOMAIN
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
SKIP_ASYNC_MIGRATIONS_SETUP: 0
|
||||||
|
|
||||||
|
# Temporal containers
|
||||||
|
temporal:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: temporal
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
- ENABLE_ES=false
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:7233:7233
|
||||||
|
volumes:
|
||||||
|
- ./posthog/docker/temporal/dynamicconfig:/etc/temporal/config/dynamicconfig
|
||||||
|
elasticsearch:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: elasticsearch
|
||||||
|
logging: *default-logging
|
||||||
|
temporal-admin-tools:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: temporal-admin-tools
|
||||||
|
logging: *default-logging
|
||||||
|
depends_on:
|
||||||
|
- temporal
|
||||||
|
temporal-ui:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: temporal-ui
|
||||||
|
logging: *default-logging
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:8081:8080
|
||||||
|
depends_on:
|
||||||
|
temporal:
|
||||||
|
condition: service_started
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
temporal-django-worker:
|
||||||
|
command: /compose/temporal-django-worker
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: temporal-django-worker
|
||||||
|
logging: *default-logging
|
||||||
|
volumes:
|
||||||
|
- ./compose:/compose
|
||||||
|
image: $REGISTRY_URL:$POSTHOG_APP_TAG
|
||||||
|
environment:
|
||||||
|
SITE_URL: https://$DOMAIN
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
BROWSERLESS_CDP_URL: 'ws://browserless:3000'
|
||||||
|
BROWSERLESS_TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
||||||
|
HEATMAP_BROWSERLESS_URL: 'http://browserless:3000'
|
||||||
|
HEATMAP_BROWSERLESS_TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
||||||
|
HEATMAP_BROWSERLESS_SCREENSHOT_COOKIES_ENABLED: ${HEATMAP_BROWSERLESS_SCREENSHOT_COOKIES_ENABLED:-false}
|
||||||
|
# This worker runs the recording deletion Temporal activities, which call
|
||||||
|
# recording-api. Without these two the calls go out unauthenticated and
|
||||||
|
# recording-api rejects them with a 401.
|
||||||
|
RECORDING_API_URL: http://recording-api:6738
|
||||||
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:-$POSTHOG_SECRET}
|
||||||
|
# Point Django at the Rust feature-flags container; the setting
|
||||||
|
# otherwise defaults to localhost:3001.
|
||||||
|
FEATURE_FLAGS_SERVICE_URL: 'http://feature-flags:3001'
|
||||||
|
# The logs product uses a dedicated ClickHouse cluster config (separate
|
||||||
|
# from CLICKHOUSE_HOST) whose host defaults to localhost and whose SECURE
|
||||||
|
# defaults to true outside DEBUG. Point it at the stack's ClickHouse over
|
||||||
|
# plaintext; the other logs-cluster settings already default correctly
|
||||||
|
# (native port 9000, user "default", empty password). This worker runs
|
||||||
|
# the logs alert-check Temporal activities, which query ClickHouse under
|
||||||
|
# Workload.LOGS.
|
||||||
|
CLICKHOUSE_LOGS_CLUSTER_HOST: 'clickhouse'
|
||||||
|
CLICKHOUSE_LOGS_CLUSTER_SECURE: 'false'
|
||||||
|
# AI provider keys for PostHog's AI features. Blank unless set in .env.
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
- clickhouse
|
||||||
|
- kafka
|
||||||
|
- objectstorage
|
||||||
|
- seaweedfs
|
||||||
|
- temporal
|
||||||
|
- browserless
|
||||||
|
|
||||||
|
capture:
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: capture
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
capture-logs:
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: capture-logs
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
replay-capture:
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: replay-capture
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
property-defs-rs:
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: property-defs-rs
|
||||||
|
logging: *default-logging
|
||||||
|
depends_on:
|
||||||
|
kafka-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
livestream:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: livestream
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
- LIVESTREAM_JWT_SECRET=${POSTHOG_SECRET}
|
||||||
|
volumes:
|
||||||
|
- ./posthog/docker/livestream/configs-hobby.yml:/configs/configs.yml
|
||||||
|
|
||||||
|
personhog-replica:
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: personhog-replica
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
PRIMARY_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
personhog-router:
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: personhog-router
|
||||||
|
logging: *default-logging
|
||||||
|
depends_on:
|
||||||
|
- personhog-replica
|
||||||
|
|
||||||
|
feature-flags:
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: feature-flags
|
||||||
|
logging: *default-logging
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis7
|
||||||
|
|
||||||
|
hypercache-server:
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: hypercache-server
|
||||||
|
logging: *default-logging
|
||||||
|
depends_on:
|
||||||
|
- redis7
|
||||||
|
|
||||||
|
kafka-init:
|
||||||
|
image: docker.io/redpandadata/redpanda:v25.1.9
|
||||||
|
entrypoint: /bin/sh
|
||||||
|
logging: *default-logging
|
||||||
|
command:
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -x
|
||||||
|
echo "Waiting for Kafka broker to accept connections..."
|
||||||
|
TIMEOUT=60
|
||||||
|
ELAPSED=0
|
||||||
|
# Use rpk topic list as the readiness check - it's a simple operation
|
||||||
|
# that confirms the broker is accepting Kafka protocol requests
|
||||||
|
until rpk topic list --brokers kafka:9092 2>/dev/null; do
|
||||||
|
echo "Kafka broker not ready yet (elapsed: $${ELAPSED}s)..."
|
||||||
|
sleep 2
|
||||||
|
ELAPSED=$$((ELAPSED + 2))
|
||||||
|
if [ $$ELAPSED -ge $$TIMEOUT ]; then
|
||||||
|
echo "Timeout waiting for Kafka broker after $${TIMEOUT}s"
|
||||||
|
echo "Final attempt to list topics:"
|
||||||
|
rpk topic list --brokers kafka:9092 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Kafka broker is accepting requests, creating topics..."
|
||||||
|
# The topic list is docker/kafka/topics.txt, mounted below, shared with
|
||||||
|
# docker-compose.dev.yml so the two stacks cannot drift apart. That header
|
||||||
|
# explains what needs pre-creating.
|
||||||
|
# Note: $$ escapes $ for docker-compose variable substitution
|
||||||
|
TOPICS=$$(grep -vE '^[[:space:]]*#|^[[:space:]]*$$' /topics.txt)
|
||||||
|
for topic in $$TOPICS; do
|
||||||
|
if rpk topic create "$$topic" --brokers kafka:9092 -p 1 -r 1 2>&1; then
|
||||||
|
echo "Topic $$topic created successfully"
|
||||||
|
else
|
||||||
|
if rpk topic list --brokers kafka:9092 | awk '{print $$1}' | grep -qx "$$topic"; then
|
||||||
|
echo "Topic $$topic already exists, continuing"
|
||||||
|
else
|
||||||
|
echo "Failed to create topic $$topic"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Final topic list:"
|
||||||
|
rpk topic list --brokers kafka:9092
|
||||||
|
echo "Topics ready"
|
||||||
|
volumes:
|
||||||
|
- ./posthog/docker/kafka/topics.txt:/topics.txt:ro
|
||||||
|
depends_on:
|
||||||
|
kafka:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
cymbal:
|
||||||
|
image: ghcr.io/posthog/posthog/cymbal:master
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
args:
|
||||||
|
BIN: cymbal
|
||||||
|
restart: on-failure
|
||||||
|
logging: *default-logging
|
||||||
|
volumes:
|
||||||
|
- ./share:/share
|
||||||
|
environment:
|
||||||
|
KAFKA_HOSTS: 'kafka:9092'
|
||||||
|
OBJECT_STORAGE_BUCKET: posthog
|
||||||
|
OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
||||||
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
||||||
|
OBJECT_STORAGE_ENDPOINT: 'http://seaweedfs:8333'
|
||||||
|
OBJECT_STORAGE_FORCE_PATH_STYLE: 'true'
|
||||||
|
BIND_HOST: '0.0.0.0'
|
||||||
|
BIND_PORT: '3302'
|
||||||
|
DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
||||||
|
PERSONS_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
||||||
|
MAXMIND_DB_PATH: '/share/GeoLite2-City.mmdb'
|
||||||
|
REDIS_URL: 'redis://redis7:6379/'
|
||||||
|
ISSUE_BUCKETS_REDIS_URL: 'redis://redis7:6379/'
|
||||||
|
RUST_LOG: 'info'
|
||||||
|
# Processing offloads all symbol resolution to cymbal-resolution over gRPC. Both are
|
||||||
|
# mandatory: startup aborts when either is empty, so omitting them crash-loops cymbal.
|
||||||
|
CYMBAL_REMOTE_RESOLUTION_HOST: 'cymbal-resolution'
|
||||||
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET}
|
||||||
|
depends_on:
|
||||||
|
kafka-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
seaweedfs:
|
||||||
|
condition: service_healthy
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis7:
|
||||||
|
condition: service_healthy
|
||||||
|
cymbal-resolution:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
|
# The cymbal binary in resolution mode: the gRPC symbol-resolution service that cymbal's
|
||||||
|
# processing mode calls. Needs only Postgres and object storage — no Kafka or Redis — hence
|
||||||
|
# the narrower env than cymbal above.
|
||||||
|
cymbal-resolution:
|
||||||
|
image: ghcr.io/posthog/posthog/cymbal:master
|
||||||
|
build:
|
||||||
|
context: ./posthog/rust
|
||||||
|
args:
|
||||||
|
BIN: cymbal
|
||||||
|
restart: on-failure
|
||||||
|
logging: *default-logging
|
||||||
|
environment:
|
||||||
|
CYMBAL_MODE: resolution
|
||||||
|
GRPC_ADDRESS: '0.0.0.0:50061'
|
||||||
|
OBJECT_STORAGE_BUCKET: posthog
|
||||||
|
OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
||||||
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
||||||
|
OBJECT_STORAGE_ENDPOINT: 'http://seaweedfs:8333'
|
||||||
|
OBJECT_STORAGE_FORCE_PATH_STYLE: 'true'
|
||||||
|
DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
||||||
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET}
|
||||||
|
RUST_LOG: 'info'
|
||||||
|
depends_on:
|
||||||
|
seaweedfs:
|
||||||
|
condition: service_healthy
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
otel_network:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
zookeeper-data:
|
||||||
|
zookeeper-datalog:
|
||||||
|
zookeeper-logs:
|
||||||
|
objectstorage-data:
|
||||||
|
seaweedfs:
|
||||||
|
postgres-data:
|
||||||
|
clickhouse-data:
|
||||||
|
caddy-data:
|
||||||
|
caddy-config:
|
||||||
|
redis7-data:
|
||||||
|
kafka-data:
|
||||||
|
redpanda-data:
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Local source patches, applied to /code at container start.
|
||||||
|
|
||||||
|
Called from compose/start, compose/worker and compose/temporal-django-worker
|
||||||
|
before the app boots, so every recreated container comes back patched. (The old
|
||||||
|
approach patched running containers by hand and silently vanished on the next
|
||||||
|
`docker compose up`.)
|
||||||
|
|
||||||
|
Both patches exist because PostHog no longer sells self-hosted licenses, so the
|
||||||
|
license branches below are unreachable here. Both are gated on
|
||||||
|
MULTI_ORG_ENABLED (set in compose.override.yml), so unsetting it restores
|
||||||
|
upstream behaviour without un-patching. Only posthog/ files are touched, which
|
||||||
|
is MIT-licensed; ee/ is not involved.
|
||||||
|
|
||||||
|
Idempotent. If upstream changes shape, this warns loudly and lets the app start
|
||||||
|
unpatched rather than taking PostHog down. Check with:
|
||||||
|
docker compose logs web worker | grep LOCAL-PATCH
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
PATCHES = [
|
||||||
|
(
|
||||||
|
# Shows the "New project" / "New organization" buttons. Upstream gates
|
||||||
|
# them on preflight's can_create_org, which demands a license.
|
||||||
|
"can_create_org",
|
||||||
|
"/code/posthog/utils.py",
|
||||||
|
""" if settings.MULTI_ORG_ENABLED:
|
||||||
|
license = get_cached_instance_license()
|
||||||
|
if license is not None and AvailableFeature.ZAPIER in license.available_features:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning("You have configured MULTI_ORG_ENABLED, but not the required premium PostHog plan!")
|
||||||
|
""",
|
||||||
|
""" if settings.MULTI_ORG_ENABLED:
|
||||||
|
# Local patch, see compose/local-patches.py
|
||||||
|
return True
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
# Lifts the 1-project-per-org limit in the project API. Without a
|
||||||
|
# license, every org save and the periodic feature sync reset the org's
|
||||||
|
# features to [], and an org without `organizations_projects` may only
|
||||||
|
# have one project. With it and no `limit`, the cap is upstream's
|
||||||
|
# MAX_ALLOWED_PROJECTS_PER_ORG (2000).
|
||||||
|
"organizations_projects",
|
||||||
|
"/code/posthog/models/organization.py",
|
||||||
|
""" return self.available_product_features
|
||||||
|
|
||||||
|
def get_available_feature(""",
|
||||||
|
""" # Local patch, see compose/local-patches.py
|
||||||
|
if settings.MULTI_ORG_ENABLED and not self.get_available_feature("organizations_projects"):
|
||||||
|
self.available_product_features = [
|
||||||
|
*self.available_product_features,
|
||||||
|
{"key": "organizations_projects", "name": "Organizations projects"},
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.available_product_features
|
||||||
|
|
||||||
|
def get_available_feature(""",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, path, old, new in PATCHES:
|
||||||
|
src = open(path).read()
|
||||||
|
if new in src:
|
||||||
|
status = "already applied"
|
||||||
|
elif src.count(old) == 1:
|
||||||
|
open(path, "w").write(src.replace(old, new))
|
||||||
|
status = "applied"
|
||||||
|
else:
|
||||||
|
status = "PATTERN NOT FOUND, upstream changed, running UNPATCHED"
|
||||||
|
print(f"LOCAL-PATCH {name}: {status}", file=sys.stderr, flush=True)
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
python /compose/local-patches.py
|
||||||
|
/compose/wait
|
||||||
|
./bin/migrate
|
||||||
|
./bin/docker-server
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
python /compose/local-patches.py
|
||||||
|
./bin/temporal-django-worker
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
def loop():
|
||||||
|
print("Waiting for ClickHouse and Postgres to be ready")
|
||||||
|
try:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.connect(('clickhouse', 9000))
|
||||||
|
print("Clickhouse is ready")
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.connect(('db', 5432))
|
||||||
|
print("Postgres is ready")
|
||||||
|
except ConnectionRefusedError as e:
|
||||||
|
time.sleep(5)
|
||||||
|
loop()
|
||||||
|
|
||||||
|
loop()
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
python /compose/local-patches.py
|
||||||
|
exec ./bin/docker-worker-celery --with-scheduler
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: PostHog
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/PostHog
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
DB_PASSWD=CHANGE_ME
|
||||||
|
ROMM_AUTH_SECRET_KEY=CHANGE_ME
|
||||||
|
IGDB_CLIENT_ID=CHANGE_ME
|
||||||
|
IGDB_CLIENT_SECRET=CHANGE_ME
|
||||||
|
RETROACHIEVEMENTS_API_KEY=CHANGE_ME
|
||||||
|
STEAMGRIDDB_API_KEY=CHANGE_ME
|
||||||
|
OIDC_CLIENT_ID=CHANGE_ME
|
||||||
|
OIDC_CLIENT_SECRET=CHANGE_ME
|
||||||
|
MARIADB_ROOT_PASSWORD=CHANGE_ME
|
||||||
|
MARIADB_PASSWORD=CHANGE_ME
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
services:
|
||||||
|
romm:
|
||||||
|
image: rommapp/romm:latest
|
||||||
|
container_name: romm
|
||||||
|
hostname: romm
|
||||||
|
domainname: romm.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
restart: unless-stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
- DB_HOST=romm-db
|
||||||
|
- DB_NAME=romm
|
||||||
|
- DB_USER=gib
|
||||||
|
- DB_PASSWD=${DB_PASSWD}
|
||||||
|
- ROMM_AUTH_SECRET_KEY=${ROMM_AUTH_SECRET_KEY}
|
||||||
|
- IGDB_CLIENT_ID=${IGDB_CLIENT_ID}
|
||||||
|
- IGDB_CLIENT_SECRET=${IGDB_CLIENT_SECRET}
|
||||||
|
- RETROACHIEVEMENTS_API_KEY=${RETROACHIEVEMENTS_API_KEY}
|
||||||
|
- STEAMGRIDDB_API_KEY=${STEAMGRIDDB_API_KEY}
|
||||||
|
- HASHEOUS_API_ENABLED=true
|
||||||
|
- PLAYMATCH_API_ENABLED=true
|
||||||
|
- OIDC_ENABLED=true
|
||||||
|
- OIDC_PROVIDER=Gibs Auth
|
||||||
|
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
|
||||||
|
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
|
||||||
|
- OIDC_REDIRECT_URI=https://romm.gbrown.org/api/oauth/openid
|
||||||
|
- OIDC_SERVER_APPLICATION_URL=https://auth.gbrown.org/application/o/romm
|
||||||
|
#- ENABLE_RESCAN_ON_FILESYSTEM_CHANGE=true
|
||||||
|
volumes:
|
||||||
|
- ./volumes/romm/resources:/romm/resources # Resources fetched from IGDB (covers, screenshots, etc.)
|
||||||
|
- ./volumes/romm/redis_data:/redis-data # Cached data for background tasks
|
||||||
|
- /home/gib/Public/Server_Backup/ROMs:/romm/library # Your game library
|
||||||
|
- ./volumes/romm/assests:/romm/assets # Uploaded saves, states, etc.
|
||||||
|
- ./volumes/romm/config:/romm/config # Path where config.yml is stored
|
||||||
|
#ports: [8080:8080]
|
||||||
|
depends_on:
|
||||||
|
romm-db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: true
|
||||||
|
|
||||||
|
romm-db:
|
||||||
|
image: mariadb:latest
|
||||||
|
container_name: romm-db
|
||||||
|
hostname: romm-db
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
restart: unless-stopped
|
||||||
|
labels: [com.centurylinklabs.watchtower.enable=true]
|
||||||
|
environment:
|
||||||
|
- MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
|
||||||
|
- MARIADB_DATABASE=romm
|
||||||
|
- MARIADB_USER=gib
|
||||||
|
- MARIADB_PASSWORD=${MARIADB_PASSWORD}
|
||||||
|
volumes: [./volumes/mysql:/var/lib/mysql]
|
||||||
|
healthcheck:
|
||||||
|
test: [CMD, healthcheck.sh, --connect, --innodb_initialized]
|
||||||
|
start_period: 30s
|
||||||
|
start_interval: 10s
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Romm
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Romm
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Scrutiny — SMART disk health monitoring with historical trending.
|
||||||
|
#
|
||||||
|
# Omnibus image bundles the web UI, InfluxDB and the metrics collector in one
|
||||||
|
# container, which suits a single host.
|
||||||
|
#
|
||||||
|
# Needs raw device access: SYS_RAWIO for SMART ioctls on the SATA disks, and
|
||||||
|
# SYS_ADMIN additionally for NVMe. /run/udev is mounted read-only so scrutiny
|
||||||
|
# can resolve device metadata (model, serial, WWN).
|
||||||
|
#
|
||||||
|
# Devices listed explicitly rather than using privileged mode:
|
||||||
|
# sda-sdd 4x WDC WD40EFPX (4TB)
|
||||||
|
# sde-sdg 3x TOSHIBA MG08ACA14TE (14TB)
|
||||||
|
# sdh 1x WUH721414ALE601 (14TB)
|
||||||
|
# nvme0n1 WDS100T3X0C (1TB)
|
||||||
|
# nvme1n1 Samsung 990 PRO 2TB (root/LUKS)
|
||||||
|
# sdi (USB Mass Storage) is deliberately omitted.
|
||||||
|
#
|
||||||
|
# If a disk is added or replaced, add it to the devices: list and recreate.
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
scrutiny:
|
||||||
|
image: ghcr.io/analogj/scrutiny:master-omnibus
|
||||||
|
container_name: scrutiny
|
||||||
|
hostname: scrutiny
|
||||||
|
domainname: scrutiny.gbrown.org
|
||||||
|
networks: ['nginx-bridge']
|
||||||
|
#ports:
|
||||||
|
#- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- TZ=America/New_York
|
||||||
|
- SCRUTINY_WEB_INFLUXDB_RETENTION_POLICY=true
|
||||||
|
cap_add:
|
||||||
|
- SYS_RAWIO
|
||||||
|
- SYS_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/sda:/dev/sda
|
||||||
|
- /dev/sdb:/dev/sdb
|
||||||
|
- /dev/sdc:/dev/sdc
|
||||||
|
- /dev/sdd:/dev/sdd
|
||||||
|
- /dev/sde:/dev/sde
|
||||||
|
- /dev/sdf:/dev/sdf
|
||||||
|
- /dev/sdg:/dev/sdg
|
||||||
|
- /dev/sdh:/dev/sdh
|
||||||
|
- /dev/nvme0n1:/dev/nvme0n1
|
||||||
|
- /dev/nvme1n1:/dev/nvme1n1
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/config:/opt/scrutiny/config
|
||||||
|
- ./volumes/influxdb:/opt/scrutiny/influxdb
|
||||||
|
- /run/udev:/run/udev:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Scrutiny
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Scrutiny
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
COMPOSE_PROJECT_NAME=CHANGE_ME
|
||||||
|
COMPOSE_PROFILES=CHANGE_ME
|
||||||
|
SENTRY_EVENT_RETENTION_DAYS=CHANGE_ME
|
||||||
|
LAUNCHPAD_RPC_SHARED_SECRET=CHANGE_ME
|
||||||
|
SENTRY_BIND=CHANGE_ME
|
||||||
|
SENTRY_TASKWORKER_CONCURRENCY=CHANGE_ME
|
||||||
|
SENTRY_IMAGE=CHANGE_ME
|
||||||
|
SNUBA_IMAGE=CHANGE_ME
|
||||||
|
RELAY_IMAGE=CHANGE_ME
|
||||||
|
SYMBOLICATOR_IMAGE=CHANGE_ME
|
||||||
|
TASKBROKER_IMAGE=CHANGE_ME
|
||||||
|
VROOM_IMAGE=CHANGE_ME
|
||||||
|
UPTIME_CHECKER_IMAGE=CHANGE_ME
|
||||||
|
LAUNCHPAD_IMAGE=CHANGE_ME
|
||||||
|
HEALTHCHECK_INTERVAL=CHANGE_ME
|
||||||
|
HEALTHCHECK_TIMEOUT=CHANGE_ME
|
||||||
|
HEALTHCHECK_RETRIES=CHANGE_ME
|
||||||
|
HEALTHCHECK_START_PERIOD=CHANGE_ME
|
||||||
|
HEALTHCHECK_FILE_INTERVAL=CHANGE_ME
|
||||||
|
HEALTHCHECK_FILE_TIMEOUT=CHANGE_ME
|
||||||
|
HEALTHCHECK_FILE_RETRIES=CHANGE_ME
|
||||||
|
HEALTHCHECK_FILE_START_PERIOD=CHANGE_ME
|
||||||
|
SENTRY_MAIL_HOST=CHANGE_ME
|
||||||
|
SENTRY_SYSTEM_SECRET_KEY=CHANGE_ME
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
checkout: ~/Server/Sentry/
|
||||||
|
repo: https://github.com/getsentry/self-hosted
|
||||||
|
ref: tag 26.9.0
|
||||||
|
commit: 667094ad0b11bb27e6380fc757754c66516b59b4
|
||||||
|
|
||||||
|
docker-compose.yml and everything else tracked upstream is unchanged; sentry/config.yml and sentry/sentry.conf.py are the local config install.sh seeds from the examples.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Sentry
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Sentry
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# While a lot of configuration in Sentry can be changed via the UI, for all
|
||||||
|
# new-style config (as of 8.0) you can also declare values here in this file
|
||||||
|
# to enforce defaults or to ensure they cannot be changed via the UI. For more
|
||||||
|
# information see the Sentry documentation.
|
||||||
|
|
||||||
|
###############
|
||||||
|
# Mail Server #
|
||||||
|
###############
|
||||||
|
|
||||||
|
# mail.backend: 'smtp' # Use dummy if you want to disable email entirely
|
||||||
|
mail.host: 'smtp.usesend.gbrown.org'
|
||||||
|
mail.port: 587
|
||||||
|
mail.username: 'usesend'
|
||||||
|
mail.password: CHANGE_ME
|
||||||
|
# NOTE: `mail.use-tls` and `mail.use-ssl` are mutually exclusive and should not
|
||||||
|
# appear at the same time. Only uncomment one of them.
|
||||||
|
mail.use-tls: true
|
||||||
|
# mail.use-ssl: false
|
||||||
|
|
||||||
|
# NOTE: The following 2 configs (mail.from and mail.list-namespace) are set
|
||||||
|
# through SENTRY_MAIL_HOST in sentry.conf.py so remove those first if
|
||||||
|
# you want your values in this file to be effective!
|
||||||
|
|
||||||
|
# The email address to send on behalf of
|
||||||
|
mail.from: 'Sentry Admin <[email protected]>'
|
||||||
|
# mail.from: 'System Administrator <root@localhost>'
|
||||||
|
|
||||||
|
# The mailing list namespace for emails sent by this Sentry server.
|
||||||
|
# This should be a domain you own (often the same domain as the domain
|
||||||
|
# part of the `mail.from` configuration parameter value) or `localhost`.
|
||||||
|
mail.list-namespace: 'sentry.gbrown.org'
|
||||||
|
|
||||||
|
# If you'd like to configure email replies, enable this.
|
||||||
|
# mail.enable-replies: true
|
||||||
|
|
||||||
|
# When email-replies are enabled, this value is used in the Reply-To header
|
||||||
|
# mail.reply-hostname: ''
|
||||||
|
|
||||||
|
# If you're using mailgun for inbound mail, set your API key and configure a
|
||||||
|
# route to forward to /api/hooks/mailgun/inbound/
|
||||||
|
# Also don't forget to set `mail.enable-replies: true` above.
|
||||||
|
# mail.mailgun-api-key: ''
|
||||||
|
|
||||||
|
###################
|
||||||
|
# System Settings #
|
||||||
|
###################
|
||||||
|
|
||||||
|
# This is the main URL prefix where Sentry can be accessed.
|
||||||
|
# Sentry will use this to create links to its different parts of the web UI.
|
||||||
|
# This is most helpful if you are using an external reverse proxy.
|
||||||
|
system.url-prefix: https://sentry.gbrown.org
|
||||||
|
|
||||||
|
# Most of the time, this should NOT be changed. It's used for communication
|
||||||
|
# between containers. `web` is the container's name, and `9000` is the
|
||||||
|
# default port opened by the Sentry backend (this is NOT the public port).
|
||||||
|
#
|
||||||
|
# If you want to change the publicly exposed domain or port, you should change
|
||||||
|
# `system.url-prefix` above instead, along with `SENTRY_BIND` in `.env` file.
|
||||||
|
# Also see https://develop.sentry.dev/self-hosted/#productionalizing.
|
||||||
|
system.internal-url-prefix: 'http://web:9000'
|
||||||
|
|
||||||
|
# If this file ever becomes compromised, it's important to generate a new key.
|
||||||
|
# Changing this value will result in all current sessions being invalidated.
|
||||||
|
# A new key can be generated with `$ sentry config generate-secret-key`
|
||||||
|
#
|
||||||
|
# If you are using SENTRY_SYSTEM_SECRET_KEY that is being set on your `.env` or `.env.custom` file,
|
||||||
|
# you should remove this line below as it won't be used anyway.
|
||||||
|
|
||||||
|
# The ``redis.clusters`` setting is used, unsurprisingly, to configure Redis
|
||||||
|
# clusters. These clusters can be then referred to by name when configuring
|
||||||
|
# backends such as the cache, digests, or TSDB backend.
|
||||||
|
# redis.clusters:
|
||||||
|
# default:
|
||||||
|
# hosts:
|
||||||
|
# 0:
|
||||||
|
# host: 127.0.0.1
|
||||||
|
# port: 6379
|
||||||
|
|
||||||
|
################
|
||||||
|
# File storage #
|
||||||
|
################
|
||||||
|
|
||||||
|
# Uploaded media uses these `filestore` settings. The available
|
||||||
|
# backends are either `filesystem` or `s3`.
|
||||||
|
|
||||||
|
filestore.backend: 'filesystem'
|
||||||
|
filestore.options:
|
||||||
|
location: '/data/files'
|
||||||
|
dsym.cache-path: '/data/dsym-cache'
|
||||||
|
releasefile.cache-path: '/data/releasefile-cache'
|
||||||
|
|
||||||
|
# filestore.backend: 's3'
|
||||||
|
# filestore.options:
|
||||||
|
# bucket_name: 's3-bucket-name'
|
||||||
|
|
||||||
|
filestore.profiles-backend: 's3'
|
||||||
|
filestore.profiles-options:
|
||||||
|
bucket_acl: "private"
|
||||||
|
default_acl: "private"
|
||||||
|
access_key: CHANGE_ME
|
||||||
|
secret_key: CHANGE_ME
|
||||||
|
bucket_name: "profiles"
|
||||||
|
region_name: "us-east-1"
|
||||||
|
endpoint_url: "http://seaweedfs:8333"
|
||||||
|
addressing_style: "path"
|
||||||
|
signature_version: "s3v4"
|
||||||
|
|
||||||
|
symbolicator.enabled: true
|
||||||
|
symbolicator.options:
|
||||||
|
url: "http://symbolicator:3021"
|
||||||
|
|
||||||
|
transaction-events.force-disable-internal-project: true
|
||||||
|
|
||||||
|
######################
|
||||||
|
# GitHub Integration #
|
||||||
|
######################
|
||||||
|
|
||||||
|
# Refer to https://develop.sentry.dev/integrations/github/ for setup instructions.
|
||||||
|
|
||||||
|
# github-login.extended-permissions: ['repo']
|
||||||
|
# github-app.id: GITHUB_APP_ID
|
||||||
|
# github-app.name: 'GITHUB_APP_NAME'
|
||||||
|
# github-app.client-id: 'GITHUB_CLIENT_ID'
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# Slack Integration #
|
||||||
|
#####################
|
||||||
|
|
||||||
|
# Refer to https://develop.sentry.dev/integrations/slack/ for setup instructions.
|
||||||
|
|
||||||
|
# slack.client-id: <'client id'>
|
||||||
|
## If legacy-app is True use verification-token instead of signing-secret
|
||||||
|
|
||||||
|
|
||||||
|
#######################
|
||||||
|
# Discord Integration #
|
||||||
|
#######################
|
||||||
|
|
||||||
|
# Refer to https://develop.sentry.dev/integrations/discord/
|
||||||
|
|
||||||
|
# discord.application-id: "<application id>"
|
||||||
|
# discord.public-key: "<public key>"
|
||||||
|
|
||||||
|
###############
|
||||||
|
# Google Auth #
|
||||||
|
###############
|
||||||
|
|
||||||
|
# Refer to https://develop.sentry.dev/self-hosted/sso/#google-auth
|
||||||
|
|
||||||
|
# auth-google.client-id: "<client id>"
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
# This file is just Python, with a touch of Django which means
|
||||||
|
# you can inherit and tweak settings to your hearts content.
|
||||||
|
|
||||||
|
from sentry.conf.server import * # NOQA
|
||||||
|
|
||||||
|
BYTE_MULTIPLIER = 1024
|
||||||
|
UNITS = ("K", "M", "G")
|
||||||
|
|
||||||
|
|
||||||
|
def unit_text_to_bytes(text):
|
||||||
|
unit = text[-1].upper()
|
||||||
|
power = UNITS.index(unit) + 1
|
||||||
|
return float(text[:-1]) * (BYTE_MULTIPLIER**power)
|
||||||
|
|
||||||
|
|
||||||
|
# Generously adapted from pynetlinux: https://github.com/rlisagor/pynetlinux/blob/e3f16978855c6649685f0c43d4c3fcf768427ae5/pynetlinux/ifconfig.py#L197-L223
|
||||||
|
def get_internal_network():
|
||||||
|
import ctypes
|
||||||
|
import fcntl
|
||||||
|
import math
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
|
iface = b"eth0"
|
||||||
|
sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
ifreq = struct.pack(b"16sH14s", iface, socket.AF_INET, b"\x00" * 14)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ip = struct.unpack(
|
||||||
|
b"!I", struct.unpack(b"16sH2x4s8x", fcntl.ioctl(sockfd, 0x8915, ifreq))[2]
|
||||||
|
)[0]
|
||||||
|
netmask = socket.ntohl(
|
||||||
|
struct.unpack(b"16sH2xI8x", fcntl.ioctl(sockfd, 0x891B, ifreq))[2]
|
||||||
|
)
|
||||||
|
except IOError:
|
||||||
|
return ()
|
||||||
|
base = socket.inet_ntoa(struct.pack(b"!I", ip & netmask))
|
||||||
|
netmask_bits = 32 - int(round(math.log(ctypes.c_uint32(~netmask).value + 1, 2), 1))
|
||||||
|
return "{0:s}/{1:d}".format(base, netmask_bits)
|
||||||
|
|
||||||
|
|
||||||
|
INTERNAL_SYSTEM_IPS = (get_internal_network(),)
|
||||||
|
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "sentry.db.postgres",
|
||||||
|
"NAME": "postgres",
|
||||||
|
"USER": "postgres",
|
||||||
|
"PASSWORD": "",
|
||||||
|
"HOST": "pgbouncer",
|
||||||
|
"PORT": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# If you're expecting any kind of real traffic on Sentry, we highly recommend
|
||||||
|
# configuring the CACHES and Redis settings
|
||||||
|
|
||||||
|
###########
|
||||||
|
# General #
|
||||||
|
###########
|
||||||
|
|
||||||
|
# Instruct Sentry that this install intends to be run by a single organization
|
||||||
|
# and thus various UI optimizations should be enabled.
|
||||||
|
SENTRY_SINGLE_ORGANIZATION = True
|
||||||
|
|
||||||
|
# Sentry event retention days specifies how long events are retained in the database.
|
||||||
|
# This should be set on your `.env` or `.env.custom` file, instead of modifying
|
||||||
|
# the value here.
|
||||||
|
# NOTE: The longer the days, the more disk space is required.
|
||||||
|
SENTRY_OPTIONS["system.event-retention-days"] = int(
|
||||||
|
env("SENTRY_EVENT_RETENTION_DAYS", "90")
|
||||||
|
)
|
||||||
|
|
||||||
|
# The secret key is being used for various cryptographic operations, such as
|
||||||
|
# generating a CSRF token, session token, and registering Relay instances.
|
||||||
|
# The secret key value should be set on your `.env` or `.env.custom` file
|
||||||
|
# instead of modifying the value here.
|
||||||
|
#
|
||||||
|
# If the key ever becomes compromised, it's important to generate a new key.
|
||||||
|
# Changing this value will result in all current sessions being invalidated.
|
||||||
|
# A new key can be generated with `$ sentry config generate-secret-key`
|
||||||
|
if env("SENTRY_SYSTEM_SECRET_KEY"):
|
||||||
|
SENTRY_OPTIONS["system.secret-key"] = env("SENTRY_SYSTEM_SECRET_KEY", "")
|
||||||
|
|
||||||
|
# Self-hosted Sentry infamously has a lot of Docker containers required to make
|
||||||
|
# all the features work. Oftentimes, users don't use the full feature set that
|
||||||
|
# requires all the containers. This is a way to enable only the error monitoring
|
||||||
|
# feature which also reduces the amount of containers required to run Sentry.
|
||||||
|
#
|
||||||
|
# To make Sentry work with all features, set `COMPOSE_PROFILES` to `feature-complete`
|
||||||
|
# in your `.env` file. To enable only the error monitoring feature, set
|
||||||
|
# `COMPOSE_PROFILES` to `errors-only`.
|
||||||
|
#
|
||||||
|
# See https://develop.sentry.dev/self-hosted/optional-features/errors-only/
|
||||||
|
SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") != "feature-complete"
|
||||||
|
|
||||||
|
# When running in an air-gapped environment, set this to True to entirely disable
|
||||||
|
# external network calls and features that require Internet connectivity.
|
||||||
|
#
|
||||||
|
# Setting the value to False while running in an air-gapped environment will
|
||||||
|
# cause some containers to raise exceptions. One known example is fetching
|
||||||
|
# AI model prices from various public APIs.
|
||||||
|
SENTRY_AIR_GAP = False
|
||||||
|
|
||||||
|
# As of 25.9.0 (September 2025 release), Sentry enforces tighter restrictions
|
||||||
|
# of allowed IP addresses for outgoing requests. This is to prevent
|
||||||
|
# accidentally leaking sensitive information to third parties.
|
||||||
|
#
|
||||||
|
# By default, Sentry will not allow requests to private IP addresses.
|
||||||
|
# You can override this by configuring the allowed IP addresses here.
|
||||||
|
# Below is the default value, which is a list of IP addresses that are
|
||||||
|
# considered "private" and "reserved".
|
||||||
|
#
|
||||||
|
# SENTRY_DISALLOWED_IPS: tuple[str, ...] = (
|
||||||
|
# # https://en.wikipedia.org/wiki/Reserved_IP_addresses#IPv4
|
||||||
|
# "0.0.0.0/8",
|
||||||
|
# "10.0.0.0/8",
|
||||||
|
# "100.64.0.0/10",
|
||||||
|
# "127.0.0.0/8",
|
||||||
|
# "169.254.0.0/16",
|
||||||
|
# "172.16.0.0/12",
|
||||||
|
# "192.0.0.0/29",
|
||||||
|
# "192.0.2.0/24",
|
||||||
|
# "192.88.99.0/24",
|
||||||
|
# "192.168.0.0/16",
|
||||||
|
# "198.18.0.0/15",
|
||||||
|
# "198.51.100.0/24",
|
||||||
|
# "224.0.0.0/4",
|
||||||
|
# "240.0.0.0/4",
|
||||||
|
# "255.255.255.255/32",
|
||||||
|
# # https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses
|
||||||
|
# # Subnets match the IPv4 subnets above
|
||||||
|
# "::ffff:0:0/104",
|
||||||
|
# "::ffff:a00:0/104",
|
||||||
|
# "::ffff:6440:0/106",
|
||||||
|
# "::ffff:7f00:0/104",
|
||||||
|
# "::ffff:a9fe:0/112",
|
||||||
|
# "::ffff:ac10:0/108",
|
||||||
|
# "::ffff:c000:0/125",
|
||||||
|
# "::ffff:c000:200/120",
|
||||||
|
# "::ffff:c058:6300/120",
|
||||||
|
# "::ffff:c0a8:0/112",
|
||||||
|
# "::ffff:c612:0/111",
|
||||||
|
# "::ffff:c633:6400/120",
|
||||||
|
# "::ffff:e000:0/100",
|
||||||
|
# "::ffff:f000:0/100",
|
||||||
|
# "::ffff:ffff:ffff/128",
|
||||||
|
# # https://en.wikipedia.org/wiki/Reserved_IP_addresses#IPv6
|
||||||
|
# "::1/128",
|
||||||
|
# "::ffff:0:0/96",
|
||||||
|
# "64:ff9b::/96",
|
||||||
|
# "64:ff9b:1::/48",
|
||||||
|
# "100::/64",
|
||||||
|
# "2001:0000::/32",
|
||||||
|
# "2001:20::/28",
|
||||||
|
# "2001:db8::/32",
|
||||||
|
# "2002::/16",
|
||||||
|
# "fc00::/7",
|
||||||
|
# "fe80::/10",
|
||||||
|
# "ff00::/8",
|
||||||
|
# )
|
||||||
|
|
||||||
|
################
|
||||||
|
# Node Storage #
|
||||||
|
################
|
||||||
|
|
||||||
|
# Sentry uses an abstraction layer called "node storage" to store raw events.
|
||||||
|
# Previously, it used PostgreSQL as the backend, but this didn't scale for
|
||||||
|
# high-throughput environments. Read more about this in the documentation:
|
||||||
|
# https://develop.sentry.dev/backend/application-domains/nodestore/
|
||||||
|
#
|
||||||
|
# Through this setting, you can use the provided blob storage or
|
||||||
|
# your own S3-compatible API from your infrastructure.
|
||||||
|
# Other backend implementations for node storage developed by the community
|
||||||
|
# are available in public GitHub repositories.
|
||||||
|
|
||||||
|
SENTRY_NODESTORE = "sentry_nodestore_s3.S3PassthroughDjangoNodeStorage"
|
||||||
|
SENTRY_NODESTORE_OPTIONS = {
|
||||||
|
"compression": True,
|
||||||
|
"endpoint_url": "http://seaweedfs:8333",
|
||||||
|
"bucket_path": "nodestore",
|
||||||
|
"bucket_name": "nodestore",
|
||||||
|
"region_name": "us-east-1",
|
||||||
|
"aws_access_key_id": "sentry",
|
||||||
|
"aws_secret_access_key": "sentry",
|
||||||
|
}
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Redis #
|
||||||
|
#########
|
||||||
|
|
||||||
|
# Generic Redis configuration used as defaults for various things including:
|
||||||
|
# Buffers, Quotas, TSDB
|
||||||
|
|
||||||
|
SENTRY_OPTIONS["redis.clusters"] = {
|
||||||
|
"default": {
|
||||||
|
"hosts": {0: {"host": "redis", "password": "", "port": "6379", "db": "0"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Cache #
|
||||||
|
#########
|
||||||
|
|
||||||
|
# Sentry currently utilizes two separate mechanisms. While CACHES is not a
|
||||||
|
# requirement, it will optimize several high throughput patterns.
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "sentry.cache.backends.reconnectingmemcache.ReconnectingMemcache",
|
||||||
|
"LOCATION": ["memcached:11211"],
|
||||||
|
"TIMEOUT": 3600,
|
||||||
|
"OPTIONS": {"ignore_exc": True, "reconnect_age": 300},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# A primary cache is required for things such as processing events
|
||||||
|
SENTRY_CACHE = "sentry.cache.redis.RedisCache"
|
||||||
|
|
||||||
|
DEFAULT_KAFKA_OPTIONS = {
|
||||||
|
"bootstrap.servers": "kafka:9092",
|
||||||
|
"message.max.bytes": 50000000,
|
||||||
|
"socket.timeout.ms": 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
SENTRY_EVENTSTREAM = "sentry.eventstream.kafka.KafkaEventStream"
|
||||||
|
SENTRY_EVENTSTREAM_OPTIONS = {"producer_configuration": DEFAULT_KAFKA_OPTIONS}
|
||||||
|
|
||||||
|
KAFKA_CLUSTERS["default"] = DEFAULT_KAFKA_OPTIONS
|
||||||
|
|
||||||
|
###############
|
||||||
|
# Rate Limits #
|
||||||
|
###############
|
||||||
|
|
||||||
|
# Rate limits apply to notification handlers and are enforced per-project
|
||||||
|
# automatically.
|
||||||
|
|
||||||
|
SENTRY_RATELIMITER = "sentry.ratelimits.redis.RedisRateLimiter"
|
||||||
|
|
||||||
|
##################
|
||||||
|
# Update Buffers #
|
||||||
|
##################
|
||||||
|
|
||||||
|
# Buffers (combined with queueing) act as an intermediate layer between the
|
||||||
|
# database and the storage API. They will greatly improve efficiency on large
|
||||||
|
# numbers of the same events being sent to the API in a short amount of time.
|
||||||
|
# (read: if you send any kind of real data to Sentry, you should enable buffers)
|
||||||
|
|
||||||
|
SENTRY_BUFFER = "sentry.buffer.redis.RedisBuffer"
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Quotas #
|
||||||
|
##########
|
||||||
|
|
||||||
|
# Quotas allow you to rate limit individual projects or the Sentry install as
|
||||||
|
# a whole.
|
||||||
|
|
||||||
|
SENTRY_QUOTAS = "sentry.quotas.redis.RedisQuota"
|
||||||
|
|
||||||
|
########
|
||||||
|
# TSDB #
|
||||||
|
########
|
||||||
|
|
||||||
|
# The TSDB is used for building charts as well as making things like per-rate
|
||||||
|
# alerts possible.
|
||||||
|
|
||||||
|
SENTRY_TSDB = "sentry.tsdb.redissnuba.RedisSnubaTSDB"
|
||||||
|
|
||||||
|
#########
|
||||||
|
# SNUBA #
|
||||||
|
#########
|
||||||
|
|
||||||
|
SENTRY_SEARCH = "sentry.search.snuba.EventsDatasetSnubaSearchBackend"
|
||||||
|
SENTRY_SEARCH_OPTIONS = {}
|
||||||
|
SENTRY_TAGSTORE_OPTIONS = {}
|
||||||
|
|
||||||
|
###########
|
||||||
|
# Digests #
|
||||||
|
###########
|
||||||
|
|
||||||
|
# The digest backend powers notification summaries.
|
||||||
|
|
||||||
|
SENTRY_DIGESTS = "sentry.digests.backends.redis.RedisBackend"
|
||||||
|
|
||||||
|
##############
|
||||||
|
# Web Server #
|
||||||
|
##############
|
||||||
|
|
||||||
|
SENTRY_WEB_HOST = "0.0.0.0"
|
||||||
|
SENTRY_WEB_PORT = 9000
|
||||||
|
SENTRY_WEB_OPTIONS = {
|
||||||
|
"http": "%s:%s" % (SENTRY_WEB_HOST, SENTRY_WEB_PORT),
|
||||||
|
"protocol": "uwsgi",
|
||||||
|
# This is needed in order to prevent https://github.com/getsentry/sentry/blob/c6f9660e37fcd9c1bbda8ff4af1dcfd0442f5155/src/sentry/services/http.py#L70
|
||||||
|
"uwsgi-socket": None,
|
||||||
|
"so-keepalive": True,
|
||||||
|
# Keep this between 15s-75s as that's what Relay supports
|
||||||
|
"http-keepalive": 15,
|
||||||
|
"http-chunked-input": True,
|
||||||
|
# the number of web workers
|
||||||
|
"workers": 3,
|
||||||
|
"threads": 4,
|
||||||
|
"memory-report": False,
|
||||||
|
# The `harakiri` option terminates requests that take longer than the
|
||||||
|
# defined amount of time (in seconds) which can help avoid stuck workers
|
||||||
|
# caused by GIL issues or deadlocks.
|
||||||
|
# Ensure nginx `proxy_read_timeout` configuration (default: 90)
|
||||||
|
# on your `nginx.conf` file to be at least 5 seconds longer than this.
|
||||||
|
# "harakiri": 85,
|
||||||
|
# Some stuff so uwsgi will cycle workers sensibly
|
||||||
|
"max-requests": 100000,
|
||||||
|
"max-requests-delta": 500,
|
||||||
|
"max-worker-lifetime": 86400,
|
||||||
|
# Duplicate options from sentry default just so we don't get
|
||||||
|
# bit by sentry changing a default value that we depend on.
|
||||||
|
"thunder-lock": True,
|
||||||
|
"log-x-forwarded-for": False,
|
||||||
|
"buffer-size": 32768,
|
||||||
|
"limit-post": 209715200,
|
||||||
|
"disable-logging": True,
|
||||||
|
"reload-on-rss": 600,
|
||||||
|
"ignore-sigpipe": True,
|
||||||
|
"ignore-write-errors": True,
|
||||||
|
"disable-write-exception": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
###########
|
||||||
|
# SSL/TLS #
|
||||||
|
###########
|
||||||
|
|
||||||
|
# If you're using a reverse SSL proxy, you should enable the X-Forwarded-Proto
|
||||||
|
# header and enable the settings below
|
||||||
|
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
SOCIAL_AUTH_REDIRECT_IS_HTTPS = True
|
||||||
|
|
||||||
|
# End of SSL/TLS settings
|
||||||
|
|
||||||
|
########
|
||||||
|
# Mail #
|
||||||
|
########
|
||||||
|
|
||||||
|
SENTRY_OPTIONS["mail.list-namespace"] = env("SENTRY_MAIL_HOST", "localhost")
|
||||||
|
SENTRY_OPTIONS["mail.from"] = f"sentry@{SENTRY_OPTIONS['mail.list-namespace']}"
|
||||||
|
|
||||||
|
############
|
||||||
|
# Features #
|
||||||
|
############
|
||||||
|
|
||||||
|
# Sentry uses feature flags to enable certain features. Some features may
|
||||||
|
# require additional configuration or containers. To learn more about how
|
||||||
|
# Sentry uses feature flags, see https://develop.sentry.dev/backend/application-domains/feature-flags/
|
||||||
|
#
|
||||||
|
# The features listed here are stable and generally available on SaaS.
|
||||||
|
# To enable preview features, see https://develop.sentry.dev/self-hosted/configuration/#enabling-preview-features
|
||||||
|
|
||||||
|
SENTRY_FEATURES["projects:sample-events"] = False
|
||||||
|
SENTRY_FEATURES.update(
|
||||||
|
{
|
||||||
|
feature: True
|
||||||
|
for feature in (
|
||||||
|
"organizations:discover",
|
||||||
|
"organizations:global-views",
|
||||||
|
"organizations:issue-views",
|
||||||
|
"organizations:integrations-issue-basic",
|
||||||
|
"organizations:integrations-issue-sync",
|
||||||
|
"organizations:invite-members",
|
||||||
|
"organizations:sso-basic",
|
||||||
|
"organizations:sso-saml2",
|
||||||
|
"organizations:advanced-search",
|
||||||
|
"organizations:issue-platform",
|
||||||
|
"organizations:monitors",
|
||||||
|
"organizations:dashboards-mep",
|
||||||
|
"organizations:mep-rollout-flag",
|
||||||
|
"organizations:dashboards-rh-widget",
|
||||||
|
"organizations:dynamic-sampling",
|
||||||
|
"organizations:workflow-engine-ui",
|
||||||
|
"organizations:workflow-engine-rule-serializers",
|
||||||
|
"projects:custom-inbound-filters",
|
||||||
|
"projects:data-forwarding",
|
||||||
|
"projects:discard-groups",
|
||||||
|
"projects:plugins",
|
||||||
|
"projects:rate-limits",
|
||||||
|
"projects:servicehooks",
|
||||||
|
)
|
||||||
|
# Performance/Tracing/Spans related flags
|
||||||
|
+ (
|
||||||
|
"organizations:performance-view",
|
||||||
|
"organizations:span-stats",
|
||||||
|
"organizations:visibility-explore-view",
|
||||||
|
"organizations:visibility-explore-range-high",
|
||||||
|
"organizations:transaction-metrics-extraction",
|
||||||
|
"organizations:indexed-spans-extraction",
|
||||||
|
"organizations:insights-entry-points",
|
||||||
|
"organizations:insights-initial-modules",
|
||||||
|
"organizations:insights-addon-modules",
|
||||||
|
"organizations:insights-modules-use-eap",
|
||||||
|
"organizations:starfish-mobile-appstart",
|
||||||
|
"projects:span-metrics-extraction",
|
||||||
|
"projects:span-metrics-extraction-addons",
|
||||||
|
)
|
||||||
|
# Session Replay related flags
|
||||||
|
+ (
|
||||||
|
"organizations:session-replay",
|
||||||
|
)
|
||||||
|
# User Feedback related flags
|
||||||
|
+ (
|
||||||
|
"organizations:user-feedback-ui",
|
||||||
|
)
|
||||||
|
# Profiling related flags
|
||||||
|
+ (
|
||||||
|
"organizations:profiling",
|
||||||
|
"organizations:profiling-view",
|
||||||
|
)
|
||||||
|
# Continuous Profiling related flags
|
||||||
|
+ (
|
||||||
|
"organizations:continuous-profiling",
|
||||||
|
"organizations:continuous-profiling-stats",
|
||||||
|
)
|
||||||
|
# Uptime Monitoring related flags
|
||||||
|
+ (
|
||||||
|
"organizations:uptime",
|
||||||
|
"organizations:uptime-create-issues",
|
||||||
|
)
|
||||||
|
# Logs related flags
|
||||||
|
+ (
|
||||||
|
"organizations:ourlogs-enabled",
|
||||||
|
"organizations:ourlogs-ingestion",
|
||||||
|
"organizations:ourlogs-stats",
|
||||||
|
"organizations:ourlogs-replay-ui",
|
||||||
|
)
|
||||||
|
# Metrics related flags
|
||||||
|
+ (
|
||||||
|
"organizations:tracemetrics-enabled",
|
||||||
|
"organizations:tracemetrics-alerts",
|
||||||
|
"organizations:tracemetrics-ingestion",
|
||||||
|
"organizations:tracemetrics-equations-in-alerts",
|
||||||
|
"organizations:tracemetrics-equations-in-explore",
|
||||||
|
"organizations:tracemetrics-multi-metric-selection-in-dashboards",
|
||||||
|
"organizations:tracemetrics-units-ui",
|
||||||
|
"organizations:tracemetrics-stats-bytes-ui",
|
||||||
|
"organizations:tracemetrics-pii-scrubbing-ui",
|
||||||
|
)
|
||||||
|
# Transaction widgets/alerts/saved queries migration UI flags
|
||||||
|
+ (
|
||||||
|
"organizations:discover-saved-queries-deprecation",
|
||||||
|
"organizations:expose-migrated-discover-queries",
|
||||||
|
"organizations:performance-transaction-deprecation-banner",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
#######################
|
||||||
|
# MaxMind Integration #
|
||||||
|
#######################
|
||||||
|
|
||||||
|
GEOIP_PATH_MMDB = "/geoip/GeoLite2-City.mmdb"
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# Bitbucket Integration #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
# BITBUCKET_CONSUMER_KEY = 'YOUR_BITBUCKET_CONSUMER_KEY'
|
||||||
|
# BITBUCKET_CONSUMER_SECRET = 'YOUR_BITBUCKET_CONSUMER_SECRET'
|
||||||
|
|
||||||
|
##############################################
|
||||||
|
# Content Security Policy settings
|
||||||
|
##############################################
|
||||||
|
|
||||||
|
# CSP_REPORT_URI = "https://{your-sentry-installation}/api/{csp-project}/security/?sentry_key={sentry-key}"
|
||||||
|
CSP_REPORT_ONLY = True
|
||||||
|
|
||||||
|
# optional extra permissions
|
||||||
|
# https://django-csp.readthedocs.io/en/latest/configuration.html
|
||||||
|
# CSP_SCRIPT_SRC += ["example.com"]
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Sentry Endpoint Settings #
|
||||||
|
############################
|
||||||
|
|
||||||
|
# If your Sentry installation has different hostnames for ingestion and web UI,
|
||||||
|
# in which your web UI is accessible via private corporate network, yet your
|
||||||
|
# ingestion hostname is accessible from the public internet, you can uncomment
|
||||||
|
# this following options in order to have the ingestion hostname rendered
|
||||||
|
# correctly on the SDK configuration UI.
|
||||||
|
#
|
||||||
|
# SENTRY_ENDPOINT = "https://sentry.ingest.example.com"
|
||||||
|
|
||||||
|
#################
|
||||||
|
# CSRF Settings #
|
||||||
|
#################
|
||||||
|
|
||||||
|
# Since version 24.1.0, Sentry migrated to Django 4 which contains stricter CSRF protection.
|
||||||
|
# If you are accessing Sentry from multiple domains behind a reverse proxy, you should set
|
||||||
|
# this to match your IPs/domains. Ports should be included if you are using custom ports.
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = ["https://sentry.gbrown.org", "http://192.168.0.43:9000", "http://127.0.0.1:9000", "http://localhost:9000", 'https://sentry.gibbyb.com']
|
||||||
|
|
||||||
|
#################
|
||||||
|
# JS SDK Loader #
|
||||||
|
#################
|
||||||
|
|
||||||
|
# Configure Sentry JS SDK bundle URL template for Loader Scripts.
|
||||||
|
# Learn more about the Loader Scripts: https://docs.sentry.io/platforms/javascript/install/loader/
|
||||||
|
# If you wish to host your own JS SDK bundles, set `SETUP_JS_SDK_ASSETS` environment variable to `1`
|
||||||
|
# on your `.env` or `.env.custom` file. Then, replace the value below with your own public URL.
|
||||||
|
# For example: "https://sentry.example.com/js-sdk/%s/bundle%s.min.js"
|
||||||
|
#
|
||||||
|
# By default, the previous JS SDK assets version will be pruned during upgrades, if you wish
|
||||||
|
# to keep the old assets, set `SETUP_JS_SDK_KEEP_OLD_ASSETS` environment variable to any value on
|
||||||
|
# your `.env` or `.env.custom` file. The files should only be a few KBs, and this might be useful
|
||||||
|
# if you're using it directly like a CDN instead of using the loader script.
|
||||||
|
JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle%s.min.js"
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Spans #
|
||||||
|
#########
|
||||||
|
|
||||||
|
# Segments are processed by a taskworker task rather than the dedicated
|
||||||
|
# process-segments Kafka consumer, so there is no such consumer in
|
||||||
|
# docker-compose.yml. This is set explicitly (rather than relying on the default
|
||||||
|
# in sentry) because self-hosted and sentry are released on different cadences.
|
||||||
|
SENTRY_OPTIONS["spans.buffer.process-segments-task-rollout-rate"] = 1.0
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# Insights Settings #
|
||||||
|
#####################
|
||||||
|
|
||||||
|
# Since version 24.3.0, Insights features are available on self-hosted. For Requests module,
|
||||||
|
# there are scrubbing logic done on Relay to prevent high cardinality of stored HTTP hosts.
|
||||||
|
# However in self-hosted scenario, the amount of stored HTTP hosts might be consistent,
|
||||||
|
# and you may have allow list of hosts that you want to keep. Uncomment the following line
|
||||||
|
# to allow specific hosts. It might be IP addresses or domain names (without `http://` or `https://`).
|
||||||
|
|
||||||
|
# SENTRY_OPTIONS["relay.span-normalization.allowed_hosts"] = ["example.com", "192.168.10.1"]
|
||||||
|
|
||||||
|
##############
|
||||||
|
# Monitoring #
|
||||||
|
##############
|
||||||
|
|
||||||
|
# By default, Sentry uses dummy statsd monitoring backend that is a no-op.
|
||||||
|
# If you have a statsd server, you can utilize that to monitor self-hosted
|
||||||
|
# Sentry for "sentry"-related containers.
|
||||||
|
|
||||||
|
SENTRY_STATSD_ADDR = env("SENTRY_STATSD_ADDR")
|
||||||
|
if SENTRY_STATSD_ADDR:
|
||||||
|
host, _, port = SENTRY_STATSD_ADDR.partition(":")
|
||||||
|
port = int(port or 8125)
|
||||||
|
SENTRY_METRICS_BACKEND = 'sentry.metrics.statsd.StatsdMetricsBackend'
|
||||||
|
SENTRY_METRICS_OPTIONS: dict[str, Any] = {
|
||||||
|
'host': host,
|
||||||
|
'port': port,
|
||||||
|
}
|
||||||
|
# SENTRY_METRICS_SAMPLE_RATE = 1.0 # Adjust this to your needs, default is 1.0
|
||||||
|
# SENTRY_METRICS_PREFIX = "sentry." # Adjust this to your needs, default is "sentry."
|
||||||
|
|
||||||
|
#################################
|
||||||
|
# Self-monitoring (internal DSN) #
|
||||||
|
#################################
|
||||||
|
|
||||||
|
# Sentry reports its own errors to the "internal" project (id 1). By default it
|
||||||
|
# looks the key up in Postgres at startup, which fails in taskworker and cleanup
|
||||||
|
# child processes ("Sentry SDK not initialized: no DSN available"), so those ran
|
||||||
|
# without self-monitoring. Pinning the DSN skips the lookup. It goes through the
|
||||||
|
# stack's own nginx rather than the public hostname. The key is the internal
|
||||||
|
# project's public key, which is not a secret. Update it if that key is rotated.
|
||||||
|
SENTRY_SDK_CONFIG["sentry_mirror_dsn"] = "http://d6f5825aae7007c9ae5d3f9174cb54b5@nginx/1"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
infisical-slides:
|
||||||
|
image: tangramor/slidev:latest
|
||||||
|
container_name: infisical-slides
|
||||||
|
hostname: infisical-slides
|
||||||
|
domainname: infisical-presentation.gbrown.org
|
||||||
|
user: node
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
# ports:
|
||||||
|
#- "3030:3030"
|
||||||
|
# environment:
|
||||||
|
# - NPM_MIRROR="https://registry.npmirror.com
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./data:/slidev
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Slides/infisical
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Slides/infisical
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
watchtower:
|
||||||
|
image: nickfedor/watchtower:latest
|
||||||
|
container_name: watchtower
|
||||||
|
hostname: watchtower.gib
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: true
|
||||||
|
environment:
|
||||||
|
- WATCHTOWER_LABEL_ENABLE=true
|
||||||
|
- TZ=America/New_York
|
||||||
|
- WATCHTOWER_DISABLE_CONTAINERS=nextcloud-aio-borgbackup,nextcloud-aio-watchtower,nextcloud-aio-collabora,nextcloud-aio-talk,nextcloud-aio-database,nextcloud-aio-redis,nextcloud-aio-clamav,nextcloud-aio-fulltextsearch,nextcloud-aio-talk-recording,nextcloud-aio-imaginary,nextcloud-aio-docker-socket-proxy,nextcloud-aio-nextcloud,nextcloud-aio-notify-push,nextcloud-aio-apache,nextcloud-aio-mastercontainer
|
||||||
|
- WATCHTOWER_SCHEDULE=0 0 4 * * *
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Watchtower
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Watchtower
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
services:
|
||||||
|
whisper:
|
||||||
|
image: lscr.io/linuxserver/faster-whisper:gpu
|
||||||
|
container_name: whisper
|
||||||
|
hostname: whisper
|
||||||
|
domainname: whisper.gbrown.org
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
ports:
|
||||||
|
- "10300:10300"
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- TZ=America/New_York
|
||||||
|
- WHISPER_MODEL=distil-small.en
|
||||||
|
- WHISPER_BEAM=2
|
||||||
|
- WHISPER_LANG=en
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/whisper:/config
|
||||||
|
devices:
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
restart: unless-stopped
|
||||||
|
tty: true
|
||||||
|
security_opt: ['seccomp:unconfined','label:disable']
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
piper:
|
||||||
|
image: lscr.io/linuxserver/piper:gpu
|
||||||
|
container_name: piper
|
||||||
|
hostname: piper
|
||||||
|
domainname: piper.gbrown.org
|
||||||
|
networks:
|
||||||
|
- nginx-bridge
|
||||||
|
ports:
|
||||||
|
- "10200:10200"
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- TZ=America/New_York
|
||||||
|
- PIPER_VOICE=en_US-libritts-high
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/piper:/config
|
||||||
|
devices:
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
restart: unless-stopped
|
||||||
|
tty: true
|
||||||
|
security_opt: ['seccomp:unconfined','label:disable']
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Whisper
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/Whisper
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
services:
|
||||||
|
nextcloud-aio-mastercontainer:
|
||||||
|
image: ghcr.io/nextcloud-releases/all-in-one:latest
|
||||||
|
container_name: nextcloud-aio-mastercontainer
|
||||||
|
volumes:
|
||||||
|
- nextcloud_aio_mastercontainer:/mnt/docker-aio-config
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
network_mode: bridge
|
||||||
|
ports:
|
||||||
|
- 8070:8080
|
||||||
|
environment:
|
||||||
|
APACHE_PORT: 11000
|
||||||
|
APACHE_ADDITIONAL_NETWORK: nginx-bridge
|
||||||
|
# https://github.com/nextcloud/all-in-one#how-to-adjust-borgs-retention-policy
|
||||||
|
BORG_RETENTION_POLICY: --keep-within=7d --keep-weekly=4 --keep-monthly=6
|
||||||
|
# Host path for data directory. Can't change after initial run!
|
||||||
|
NEXTCLOUD_DATADIR: /home/gib/Public/Nextcloud/Server
|
||||||
|
# https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host
|
||||||
|
# NEXTCLOUD_MOUNT: /mnt/
|
||||||
|
# https://github.com/nextcloud/all-in-one#how-to-adjust-the-upload-limit-for-nextcloud
|
||||||
|
NEXTCLOUD_UPLOAD_LIMIT: 32G
|
||||||
|
# https://github.com/nextcloud/all-in-one#how-to-adjust-the-max-execution-time-for-nextcloud
|
||||||
|
NEXTCLOUD_MAX_TIME: 3600
|
||||||
|
# https://github.com/nextcloud/all-in-one#how-to-adjust-the-php-memory-limit-for-nextcloud
|
||||||
|
NEXTCLOUD_MEMORY_LIMIT: 4096M
|
||||||
|
# https://github.com/nextcloud/all-in-one#how-to-trust-user-defined-certification-authorities-ca
|
||||||
|
# NEXTCLOUD_TRUSTED_CACERTS_DIR: /path/to/my/cacerts
|
||||||
|
# https://github.com/nextcloud/all-in-one#how-to-change-the-nextcloud-apps-that-are-installed-on-the-first-startup
|
||||||
|
NEXTCLOUD_STARTUP_APPS: deck twofactor_totp calendar contacts notes
|
||||||
|
NEXTCLOUD_ENABLE_NVIDIA_GPU: true
|
||||||
|
SKIP_DOMAIN_VALIDATION: true
|
||||||
|
# TALK_PORT: 3478
|
||||||
|
# WATCHTOWER_DOCKER_SOCKET_PATH: /var/run/docker.sock
|
||||||
|
security_opt: ["seccomp:unconfined", "label:disable"]
|
||||||
|
devices: ["/dev/dri:/dev/dri"]
|
||||||
|
init: true
|
||||||
|
restart: unless-stopped
|
||||||
|
tty: true
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
nextcloud_aio_mastercontainer:
|
||||||
|
name: nextcloud_aio_mastercontainer # CANT CHANGE
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: nextcloud
|
||||||
|
After=network-online.target podman.socket
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/nextcloud
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -46,10 +46,6 @@ set -uo pipefail
|
|||||||
# podman pull <image> && systemctl --user restart podman-<name>.service
|
# podman pull <image> && systemctl --user restart podman-<name>.service
|
||||||
SKIP=(postgresql authentik watchtower)
|
SKIP=(postgresql authentik watchtower)
|
||||||
|
|
||||||
# Services built locally rather than pulled. There is no registry image to check,
|
|
||||||
# so pulling is pointless — they are skipped automatically when no `image:` line
|
|
||||||
# resolves, but listing them here keeps the log quiet and the intent obvious.
|
|
||||||
LOCAL_BUILD=(bang completeuphoria sierraandtyler)
|
|
||||||
|
|
||||||
# ⚠️ NOTE ON GITEA: it is deliberately NOT in SKIP. The three-day outage was caused
|
# ⚠️ NOTE ON GITEA: it is deliberately NOT in SKIP. The three-day outage was caused
|
||||||
# by watchtower's recreate mechanism, not by the act of updating — restarting the
|
# by watchtower's recreate mechanism, not by the act of updating — restarting the
|
||||||
@@ -90,8 +86,10 @@ list_units() {
|
|||||||
| sort
|
| sort
|
||||||
}
|
}
|
||||||
|
|
||||||
# Images declared in a service's compose.yml. Ignores commented lines and the
|
# Images declared in a service's compose.yml. Ignores commented lines, and the
|
||||||
# `build:` stanza, which has no pullable image.
|
# image of any compose service that has a `build:` stanza: that tag names a
|
||||||
|
# locally built image, so there is nothing in a registry to pull. Those
|
||||||
|
# services therefore resolve no images and are skipped without a log line.
|
||||||
#
|
#
|
||||||
# Image tags frequently reference variables from the service's .env, e.g.
|
# Image tags frequently reference variables from the service's .env, e.g.
|
||||||
# ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
# ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||||
@@ -102,8 +100,22 @@ list_units() {
|
|||||||
images_for() {
|
images_for() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
[ -f "$dir/compose.yml" ] || return 0
|
[ -f "$dir/compose.yml" ] || return 0
|
||||||
grep -oE '^[[:space:]]*image:[[:space:]]*[^[:space:]#]+' "$dir/compose.yml" 2>/dev/null \
|
awk '
|
||||||
| sed -E 's/^[[:space:]]*image:[[:space:]]*//' \
|
function flush() { if (img != "" && !build) print img; img = ""; build = 0 }
|
||||||
|
/^[[:space:]]*#/ { next }
|
||||||
|
/^services:[[:space:]]*$/ { in_services = 1; indent = -1; next }
|
||||||
|
in_services && /^[^[:space:]]/ { flush(); in_services = 0 }
|
||||||
|
in_services && NF {
|
||||||
|
match($0, /^ */); here = RLENGTH
|
||||||
|
if (indent < 0) indent = here
|
||||||
|
if (here == indent && $0 ~ /^ *[A-Za-z0-9_.-]+:[[:space:]]*$/) { flush(); next }
|
||||||
|
if ($0 ~ /^[[:space:]]+build:/) build = 1
|
||||||
|
if (match($0, /^[[:space:]]+image:[[:space:]]*[^[:space:]#]+/)) {
|
||||||
|
img = substr($0, RSTART, RLENGTH); sub(/^[[:space:]]+image:[[:space:]]*/, "", img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END { flush() }
|
||||||
|
' "$dir/compose.yml" 2>/dev/null \
|
||||||
| while IFS= read -r img; do
|
| while IFS= read -r img; do
|
||||||
case "$img" in
|
case "$img" in
|
||||||
*'${'*) ( set -a; [ -f "$dir/.env" ] && . "$dir/.env" >/dev/null 2>&1; set +a
|
*'${'*) ( set -a; [ -f "$dir/.env" ] && . "$dir/.env" >/dev/null 2>&1; set +a
|
||||||
@@ -124,8 +136,8 @@ if [ "${LIST:-0}" = "1" ]; then
|
|||||||
for svc in $(list_units); do
|
for svc in $(list_units); do
|
||||||
printf '%-22s' "$svc"
|
printf '%-22s' "$svc"
|
||||||
if in_list "$svc" "${SKIP[@]}"; then echo "SKIP (frozen)"; continue; fi
|
if in_list "$svc" "${SKIP[@]}"; then echo "SKIP (frozen)"; continue; fi
|
||||||
if in_list "$svc" "${LOCAL_BUILD[@]}"; then echo "SKIP (built locally)"; continue; fi
|
imgs="$(images_for "$(workdir_for "$svc")" | tr '\n' ' ')"
|
||||||
echo "$(images_for "$(workdir_for "$svc")" | tr '\n' ' ')"
|
echo "${imgs:-(built locally, nothing to pull)}"
|
||||||
done
|
done
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -142,7 +154,6 @@ for svc in $(list_units); do
|
|||||||
[ -n "$ONLY" ] && [ "$svc" != "$ONLY" ] && continue
|
[ -n "$ONLY" ] && [ "$svc" != "$ONLY" ] && continue
|
||||||
if [ -z "$ONLY" ]; then
|
if [ -z "$ONLY" ]; then
|
||||||
in_list "$svc" "${SKIP[@]}" && { log " $svc: frozen (SKIP list)"; continue; }
|
in_list "$svc" "${SKIP[@]}" && { log " $svc: frozen (SKIP list)"; continue; }
|
||||||
in_list "$svc" "${LOCAL_BUILD[@]}" && continue
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ⚠️ NEVER resurrect a service that is deliberately stopped. `systemctl restart`
|
# ⚠️ NEVER resurrect a service that is deliberately stopped. `systemctl restart`
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -18,3 +18,8 @@ AUTHENTIK_EMAIL__USE_SSL=false
|
|||||||
AUTHENTIK_EMAIL__TIMEOUT=10
|
AUTHENTIK_EMAIL__TIMEOUT=10
|
||||||
# Email address authentik will send from, should have a correct @domain
|
# Email address authentik will send from, should have a correct @domain
|
||||||
AUTHENTIK_EMAIL__FROM=[email protected]
|
AUTHENTIK_EMAIL__FROM=[email protected]
|
||||||
|
|
||||||
|
# Also set on the live machine
|
||||||
|
AUTHENTIK_IMAGE=CHANGE_ME
|
||||||
|
PG_DB=CHANGE_ME
|
||||||
|
PG_USER=CHANGE_ME
|
||||||
File renamed without changes.
-1
@@ -1,5 +1,4 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
RequiresMountsFor=/home/gib/Media
|
|
||||||
Description=Podman Compose: Authentik
|
Description=Podman Compose: Authentik
|
||||||
After=network-online.target podman.socket podman-postgresql.service
|
After=network-online.target podman.socket podman-postgresql.service
|
||||||
Wants=network-online.target podman-postgresql.service
|
Wants=network-online.target podman-postgresql.service
|
||||||
@@ -35,6 +35,11 @@ services:
|
|||||||
# No published port: NPM proxies to http://beszel:8090 over nginx-bridge.
|
# No published port: NPM proxies to http://beszel:8090 over nginx-bridge.
|
||||||
environment:
|
environment:
|
||||||
- TZ=America/New_York
|
- TZ=America/New_York
|
||||||
|
# Login is SSO-only through Authentik (OIDC provider "Gibs Auth", app slug
|
||||||
|
# beszel). The OAuth config lives in data.db (users collection options),
|
||||||
|
# not here. The PocketBase superuser panel at /_/ keeps its own password,
|
||||||
|
# and is the way back in if Authentik is down: flip this to false there.
|
||||||
|
- DISABLE_PASSWORD_AUTH=true
|
||||||
# Lets the hub reach a host-network agent. Under rootless podman this resolves to
|
# Lets the hub reach a host-network agent. Under rootless podman this resolves to
|
||||||
# the nginx-bridge gateway (172.18.0.1). Add the VPS in the hub UI with host
|
# the nginx-bridge gateway (172.18.0.1). Add the VPS in the hub UI with host
|
||||||
# "host.docker.internal", port 45876.
|
# "host.docker.internal", port 45876.
|
||||||
-1
@@ -1,5 +1,4 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
RequiresMountsFor=/home/gib/Media
|
|
||||||
Description=Podman Compose: Beszel
|
Description=Podman Compose: Beszel
|
||||||
After=network-online.target podman.socket
|
After=network-online.target podman.socket
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
INSTANCE_NAME=CHANGE_ME
|
||||||
|
INSTANCE_SECRET=CHANGE_ME
|
||||||
|
POSTGRES_URL=CHANGE_ME
|
||||||
|
CONVEX_CLOUD_ORIGIN=CHANGE_ME
|
||||||
|
CONVEX_SITE_ORIGIN=CHANGE_ME
|
||||||
|
DISABLE_BEACON=CHANGE_ME
|
||||||
|
REDACT_LOGS_TO_CLIENT=CHANGE_ME
|
||||||
|
DO_NOT_REQUIRE_SSL=CHANGE_ME
|
||||||
|
MCP_SECRET=CHANGE_ME
|
||||||
|
CONVEX_BACKEND_IMAGE=CHANGE_ME
|
||||||
|
CONVEX_DASHBOARD_IMAGE=CHANGE_ME
|
||||||
|
MCP_PUBLIC_URL=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_CONVEX_URL=CHANGE_ME
|
||||||
|
NEXT_PUBLIC_SITE_URL=CHANGE_ME
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
name: bill-tracker
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nginx-bridge:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
bill-tracker-next:
|
||||||
|
image: git.gbrown.org/gib/bill-tracker-next:latest
|
||||||
|
container_name: bill-tracker-next
|
||||||
|
hostname: bill-tracker-next
|
||||||
|
domainname: billtracker.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-https://billtracker.gbrown.org}
|
||||||
|
NEXT_PUBLIC_CONVEX_URL: ${NEXT_PUBLIC_CONVEX_URL:-https://api.billtracker.gbrown.org}
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
bill-tracker-mcp:
|
||||||
|
image: git.gbrown.org/gib/bill-tracker-mcp:latest
|
||||||
|
container_name: bill-tracker-mcp
|
||||||
|
hostname: bill-tracker-mcp
|
||||||
|
domainname: mcp.billtracker.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
volumes: [./data/mcp:/data:Z]
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
MCP_PUBLIC_URL: ${MCP_PUBLIC_URL:-https://mcp.billtracker.gbrown.org}
|
||||||
|
MCP_SECRET: ${MCP_SECRET:?Set MCP_SECRET in the VPS .env}
|
||||||
|
BILL_TRACKER_CONVEX_URL: ${NEXT_PUBLIC_CONVEX_URL:-https://api.billtracker.gbrown.org}
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
bill-tracker-backend:
|
||||||
|
image: ${CONVEX_BACKEND_IMAGE:-ghcr.io/get-convex/convex-backend@sha256:1f2044e3eac463ac78973b136c0baf72d4ada602611d853d6f99f280e29e0a98}
|
||||||
|
container_name: bill-tracker-backend
|
||||||
|
hostname: bill-tracker-backend
|
||||||
|
domainname: api.billtracker.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
volumes: [./data/convex:/convex/data:Z]
|
||||||
|
environment:
|
||||||
|
INSTANCE_NAME: ${INSTANCE_NAME:-bill_tracker}
|
||||||
|
INSTANCE_SECRET: ${INSTANCE_SECRET:?Set INSTANCE_SECRET in the VPS .env}
|
||||||
|
POSTGRES_URL: ${POSTGRES_URL:?Set POSTGRES_URL in the VPS .env}
|
||||||
|
CONVEX_CLOUD_ORIGIN: https://api.billtracker.gbrown.org
|
||||||
|
CONVEX_SITE_ORIGIN: https://convex.billtracker.gbrown.org
|
||||||
|
DISABLE_BEACON: 'true'
|
||||||
|
REDACT_LOGS_TO_CLIENT: 'true'
|
||||||
|
DO_NOT_REQUIRE_SSL: 'true'
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'curl', '-f', 'http://localhost:3210/version']
|
||||||
|
interval: 5s
|
||||||
|
start_period: 10s
|
||||||
|
stop_grace_period: 10s
|
||||||
|
stop_signal: SIGINT
|
||||||
|
|
||||||
|
bill-tracker-dashboard:
|
||||||
|
image: ${CONVEX_DASHBOARD_IMAGE:-ghcr.io/get-convex/convex-dashboard@sha256:284a2638e0c1a4ec0c2327d8219776f3a426ca5824b81686ae4d9454dc0ce8ed}
|
||||||
|
container_name: bill-tracker-dashboard
|
||||||
|
hostname: bill-tracker-dashboard
|
||||||
|
domainname: dashboard.billtracker.gbrown.org
|
||||||
|
networks: [nginx-bridge]
|
||||||
|
environment:
|
||||||
|
NEXT_PUBLIC_DEPLOYMENT_URL: https://api.billtracker.gbrown.org
|
||||||
|
depends_on:
|
||||||
|
bill-tracker-backend:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman Compose: Bill Tracker
|
||||||
|
RequiresMountsFor=/home/gib/Media
|
||||||
|
After=network-online.target podman.socket podman-postgresql.service
|
||||||
|
Wants=network-online.target podman-postgresql.service
|
||||||
|
StartLimitIntervalSec=300
|
||||||
|
StartLimitBurst=3
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=%h/Server/BillTracker
|
||||||
|
ExecStart=/usr/bin/podman compose up -d
|
||||||
|
ExecStop=/usr/bin/podman compose down
|
||||||
|
RemainAfterExit=yes
|
||||||
|
TimeoutStartSec=900
|
||||||
|
TimeoutStopSec=90
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
File renamed without changes.
@@ -7,7 +7,7 @@
|
|||||||
# BEFORE FIRST START, create its role and database on the SHARED postgres (this stack
|
# BEFORE FIRST START, create its role and database on the SHARED postgres (this stack
|
||||||
# no longer ships its own):
|
# no longer ships its own):
|
||||||
# podman exec -i postgresql psql -U npm <<'SQL'
|
# podman exec -i postgresql psql -U npm <<'SQL'
|
||||||
# CREATE ROLE calcom LOGIN PASSWORD '<see this service .env on the machine>';
|
# CREATE ROLE calcom LOGIN PASSWORD '617408b6790b546811cc38d0353ea506';
|
||||||
# CREATE DATABASE calcom OWNER calcom;
|
# CREATE DATABASE calcom OWNER calcom;
|
||||||
# SQL
|
# SQL
|
||||||
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql.
|
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql.
|
||||||
@@ -52,7 +52,7 @@ services:
|
|||||||
- EMAIL_SERVER_HOST=CHANGE_ME
|
- EMAIL_SERVER_HOST=CHANGE_ME
|
||||||
- EMAIL_SERVER_PORT=587
|
- EMAIL_SERVER_PORT=587
|
||||||
- EMAIL_SERVER_USER=CHANGE_ME
|
- EMAIL_SERVER_USER=CHANGE_ME
|
||||||
- EMAIL_SERVER_PASSWORD=${EMAIL_SERVER_PASSWORD}
|
- EMAIL_SERVER_PASSWORD=CHANGE_ME
|
||||||
labels:
|
labels:
|
||||||
com.centurylinklabs.watchtower.enable: "true"
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
tty: true
|
tty: true
|
||||||
File renamed without changes.
@@ -57,3 +57,8 @@ DO_NOT_REQUIRE_SSL=true
|
|||||||
#ACTIONS_USER_TIMEOUT_SECS=
|
#ACTIONS_USER_TIMEOUT_SECS=
|
||||||
#RUST_LOG=
|
#RUST_LOG=
|
||||||
#RUST_BACKTRACE=
|
#RUST_BACKTRACE=
|
||||||
|
|
||||||
|
# Also set on the live machine
|
||||||
|
BACKEND_PORT=CHANGE_ME
|
||||||
|
PORT=CHANGE_ME
|
||||||
|
SITE_PROXY_PORT=CHANGE_ME
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -9,7 +9,7 @@
|
|||||||
# 1. Create its role and database on the SHARED postgres (this stack no longer
|
# 1. Create its role and database on the SHARED postgres (this stack no longer
|
||||||
# ships its own):
|
# ships its own):
|
||||||
# podman exec -i postgresql psql -U npm <<'SQL'
|
# podman exec -i postgresql psql -U npm <<'SQL'
|
||||||
# CREATE ROLE documenso LOGIN PASSWORD '<see this service .env on the machine>';
|
# CREATE ROLE documenso LOGIN PASSWORD 'e2bf60b34aff8ce259e5ed42da46c9c6';
|
||||||
# CREATE DATABASE documenso OWNER documenso;
|
# CREATE DATABASE documenso OWNER documenso;
|
||||||
# SQL
|
# SQL
|
||||||
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql so a
|
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql so a
|
||||||
@@ -60,7 +60,7 @@ services:
|
|||||||
- NEXT_PRIVATE_SMTP_HOST=CHANGE_ME
|
- NEXT_PRIVATE_SMTP_HOST=CHANGE_ME
|
||||||
- NEXT_PRIVATE_SMTP_PORT=587
|
- NEXT_PRIVATE_SMTP_PORT=587
|
||||||
- NEXT_PRIVATE_SMTP_USERNAME=CHANGE_ME
|
- NEXT_PRIVATE_SMTP_USERNAME=CHANGE_ME
|
||||||
- NEXT_PRIVATE_SMTP_PASSWORD=${NEXT_PRIVATE_SMTP_PASSWORD}
|
- NEXT_PRIVATE_SMTP_PASSWORD=CHANGE_ME
|
||||||
- NEXT_PRIVATE_SMTP_FROM_NAME=Documenso
|
- NEXT_PRIVATE_SMTP_FROM_NAME=Documenso
|
||||||
- [email protected]
|
- [email protected]
|
||||||
labels:
|
labels:
|
||||||
File renamed without changes.
@@ -0,0 +1 @@
|
|||||||
|
AGENT_INGEST_SECRET=CHANGE_ME
|
||||||
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user