Snapshot both servers: the VPS catalog moves to server/vps, the home server joins as server/home
The catalog in server/containers had drifted from the VPS. Eleven compose
files and twelve units no longer matched what runs, and two running
services, Gbrown_Agent and BillTracker, were missing. server/vps is now a
copy of the live files, and panama-server reads from it.
server/home holds the home server's services in the same shape. They still
run under Docker, so the podman units there are drafts for a later move, and
nothing installs from that tree. PostHog, Sentry and Plausible are checkouts
of upstream repositories. Only their local files are kept, next to an
UPSTREAM file naming the repository and the exact commit.
Every secret in both trees is a ${VAR} reference named in .env.example, or
CHANGE_ME in files that are not compose files. The live servers still hold
some of those values inline, so a live file can differ from its copy on
those lines alone. Existing .env.example files keep their comments, with new
keys appended at the end.
The ignore rules and the secrets contract now cover both trees, plus the
volume/ and volumes/ directories the home server uses.
This commit is contained in:
1 parent
a664cf68f5
commit
913a57f8a6
146 files changed
+5258
-354
No files matched your search
@@ -0,0 +1,44 @@
|
||||
# Adminer — database admin UI. VPS (ROOTLESS PODMAN), added 2026-08-12.
|
||||
#
|
||||
# On the home server this shipped alongside a MySQL container in the same compose
|
||||
# file. Here it is standalone and defaults to the shared PostgreSQL instance, which
|
||||
# is where the 14 databases now live (authentik, infisical_db, n8n, npm, the payload_*
|
||||
# and convex databases, usesend, lashaddict-payload, spoon_convex...).
|
||||
#
|
||||
# MySQL stays on the home server — nothing on the VPS uses it.
|
||||
#
|
||||
# It can still reach any other database reachable on nginx-bridge; ADMINER_DEFAULT_SERVER
|
||||
# only pre-fills the server field on the login form.
|
||||
#
|
||||
# ⚠️ SECURITY: Adminer has NO authentication of its own — it is a login form that
|
||||
# forwards credentials to whichever database you name. Publishing it means exposing a
|
||||
# database login prompt to the internet. Put it behind authentik forward-auth in NPM
|
||||
# (same pattern as sonarr/prowlarr on the home server), or restrict it to the LAN by
|
||||
# giving it only a UniFi record and no Cloudflare record.
|
||||
#
|
||||
# Rootless adaptations: no bind mounts (stateless, so no :Z needed), no published
|
||||
# ports — NPM proxies to http://adminer:8080 over nginx-bridge.
|
||||
#
|
||||
# NPM: adminer.gbrown.org -> http://adminer:8080
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
adminer:
|
||||
image: docker.io/library/adminer:latest
|
||||
container_name: adminer
|
||||
hostname: adminer
|
||||
domainname: adminer.gbrown.org
|
||||
networks:
|
||||
- nginx-bridge
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
# Pre-fills the server field; any nginx-bridge host can still be typed in.
|
||||
- ADMINER_DEFAULT_SERVER=postgresql
|
||||
- ADMINER_DESIGN=dracula
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Adminer
|
||||
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/Adminer
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,27 @@
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true # ALWAYS external; see AGENTS.md §5
|
||||
|
||||
services:
|
||||
agentchat:
|
||||
image: git.gbrown.org/gib/agentchat:latest
|
||||
container_name: agentchat
|
||||
hostname: agentchat
|
||||
domainname: agentchat.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
ports:
|
||||
# Loopback only: lets this host's own agent reach the hub at
|
||||
# http://127.0.0.1:8080 without going through NPM - the LAN access
|
||||
# list there blocks the VPS's public IP. Unreachable from elsewhere.
|
||||
- '127.0.0.1:8080:8080'
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- AGENTCHAT_DB=/data/agentchat.db
|
||||
volumes:
|
||||
- ./data:/data:z # lowercase :z — watchtower recreates DROP the relabel flag, and :Z-category files would lock the new container out (learned 2026-08-13); :z keeps them accessible
|
||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: wget -qO- http://localhost:8080/healthz || exit 1
|
||||
interval: 30s
|
||||
start_period: 10s
|
||||
@@ -0,0 +1,24 @@
|
||||
# TEMPLATE for a rootless user unit.
|
||||
# Install to ~/.config/systemd/user/podman-<name>.service then:
|
||||
# systemctl --user daemon-reload && systemctl --user enable --now podman-<name>.service
|
||||
# Requires linger: sudo loginctl enable-linger $USER
|
||||
[Unit]
|
||||
Description=Podman Compose: agentchat
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Agentchat
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,25 @@
|
||||
PG_PASS=CHANGE_ME
|
||||
AUTHENTIK_SECRET_KEY=CHANGE_ME
|
||||
AUTHENTIK_ERROR_REPORTING__ENABLED=true
|
||||
COMPOSE_PORT_HTTP=9000
|
||||
COMPOSE_PORT_HTTPS=9443
|
||||
AUTHENTIK_TAG=2026.2.2
|
||||
|
||||
# SMTP Host Emails are sent to
|
||||
AUTHENTIK_EMAIL__HOST=smtp.mail.me.com
|
||||
AUTHENTIK_EMAIL__PORT=587
|
||||
# Optionally authenticate (don't add quotation marks to your password)
|
||||
AUTHENTIK_EMAIL__USERNAME=CHANGE_ME
|
||||
AUTHENTIK_EMAIL__PASSWORD=CHANGE_ME
|
||||
# Use StartTLS
|
||||
AUTHENTIK_EMAIL__USE_TLS=true
|
||||
# Use SSL
|
||||
AUTHENTIK_EMAIL__USE_SSL=false
|
||||
AUTHENTIK_EMAIL__TIMEOUT=10
|
||||
# Email address authentik will send from, should have a correct @domain
|
||||
AUTHENTIK_EMAIL__FROM=[email protected]
|
||||
|
||||
# Also set on the live machine
|
||||
AUTHENTIK_IMAGE=CHANGE_ME
|
||||
PG_DB=CHANGE_ME
|
||||
PG_USER=CHANGE_ME
|
||||
@@ -0,0 +1,93 @@
|
||||
# Authentik — VPS (ROOTLESS PODMAN) copy of the home server's auth stack.
|
||||
#
|
||||
# This is a 1:1 copy of ~/Server/auth on server.gib, migrated 2026-08-11. The home
|
||||
# instance is still running and authoritative; this one has an independent database
|
||||
# restored from a dump taken at migration time, so THE TWO DIVERGE FROM THAT MOMENT ON.
|
||||
# Do not treat this as a hot standby -- it is a rehearsal/cutover target.
|
||||
#
|
||||
# Differences from the home Docker/root version, and why:
|
||||
#
|
||||
# :z on ./volumes/media (LOWERCASE, shared)
|
||||
# Both server and worker mount this same path. `:Z` assigns a PRIVATE SELinux MCS
|
||||
# category per container, so the second container to start would relabel it and
|
||||
# lock the first one out. Shared mounts must use `:z`. The per-container mounts
|
||||
# (server/custom-templates, worker/custom-templates, worker/certs) are exclusive
|
||||
# and correctly use `:Z`.
|
||||
#
|
||||
# No redis anywhere
|
||||
# authentik 2026.x dropped the Redis dependency (Postgres-backed now). The home
|
||||
# stack has no redis container and no AUTHENTIK_REDIS__* vars either -- verified,
|
||||
# not assumed. Do not "helpfully" add one.
|
||||
#
|
||||
# Postgres is the shared VPS instance
|
||||
# AUTHENTIK_POSTGRESQL__HOST=postgresql resolves over nginx-bridge to the same
|
||||
# container N8n and NPM use. Role + database `authentik` were created there with
|
||||
# the same PG_PASS as home, so .env needed no edits.
|
||||
#
|
||||
# .env is copied verbatim from home and contains AUTHENTIK_SECRET_KEY. That key MUST
|
||||
# match the one the database was encrypted with, or tokens and stored secrets break.
|
||||
# It is mode 600 -- never print it, never commit it.
|
||||
#
|
||||
# NOT YET SERVING auth.gbrown.org. That DNS record still points home. Cutover = create
|
||||
# an explicit auth.gbrown.org record pointing at this VPS (an explicit record overrides
|
||||
# the *.gbrown.org wildcard) plus an NPM proxy host to http://authentik-server:9000.
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2}
|
||||
container_name: authentik-server
|
||||
hostname: authentik-server
|
||||
domainname: auth.gbrown.org
|
||||
networks:
|
||||
- nginx-bridge
|
||||
command: server
|
||||
# Bound to the WireGuard address ONLY -- never listens on eth0, so this is not
|
||||
# reachable from the internet regardless of firewall state. Home's NPM uses it for
|
||||
# forward-auth (14 proxy hosts point at http://192.168.2.2:9000/outpost.goauthentik.io).
|
||||
# Do NOT change this to a bare "9000:9000"; that would expose it on the public
|
||||
# interface. Port 9443 is deliberately NOT published -- portainer already uses it.
|
||||
ports:
|
||||
- "192.168.2.2:9000:9000"
|
||||
environment:
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
TZ: America/New_York
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./volumes/server/custom-templates:/templates:Z
|
||||
- ./volumes/media:/data/media:z
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2}
|
||||
container_name: authentik-worker
|
||||
hostname: authentik-worker
|
||||
networks:
|
||||
- nginx-bridge
|
||||
command: worker
|
||||
environment:
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
TZ: America/New_York
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./volumes/media:/data/media:z
|
||||
- ./volumes/worker/certs:/certs:Z
|
||||
- ./volumes/worker/custom-templates:/templates:Z
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Authentik
|
||||
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/Authentik
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,75 @@
|
||||
# Beszel — lightweight host + container monitoring, VPS (ROOTLESS PODMAN) edition.
|
||||
#
|
||||
# Adapted from the home server's Docker/root version. Differences and why:
|
||||
#
|
||||
# docker.sock -> /run/user/1000/podman/podman.sock
|
||||
# Rootless podman's API socket. It exposes ONLY rootless containers, which is
|
||||
# exactly the set we want reported. No :Z on the socket mount -- relabeling the
|
||||
# live socket breaks podman. See AGENTS.md §6.
|
||||
#
|
||||
# security_opt: label:disable on the agent
|
||||
# Required under SELinux Enforcing to read that socket, which systemd recreates
|
||||
# each boot as user_tmp_t. Same treatment as portainer/uptime.
|
||||
#
|
||||
# EXTRA_FILESYSTEMS=/boot dropped
|
||||
# The home server has /boot on its own 974M partition. This VPS does not --
|
||||
# /dev/sda1 is the whole root fs and /boot/efi is a 64M ESP not worth alerting on.
|
||||
#
|
||||
# No hostname/domainname on the agent
|
||||
# Host networking forbids them, same as on the home server.
|
||||
#
|
||||
# The hub generates its keypair on first start at ./volumes/hub/id_ed25519.pub.
|
||||
# That public key is what goes in the agent's KEY below.
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
beszel:
|
||||
image: docker.io/henrygd/beszel:latest
|
||||
container_name: beszel
|
||||
hostname: beszel
|
||||
domainname: beszel.gibbyb.com
|
||||
networks: ['nginx-bridge']
|
||||
# No published port: NPM proxies to http://beszel:8090 over nginx-bridge.
|
||||
environment:
|
||||
- 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
|
||||
# the nginx-bridge gateway (172.18.0.1). Add the VPS in the hub UI with host
|
||||
# "host.docker.internal", port 45876.
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/hub:/beszel_data:Z
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
beszel-agent:
|
||||
image: docker.io/henrygd/beszel-agent:latest
|
||||
container_name: beszel-agent
|
||||
network_mode: host
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PORT=45876
|
||||
# This hub's public key. Newer Beszel writes only ./volumes/hub/id_ed25519 (private);
|
||||
# derive the public half with: ssh-keygen -y -f volumes/hub/id_ed25519
|
||||
# It is a PUBLIC key -- safe to keep in this file.
|
||||
- KEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDBWNqxKGRltqXJZg+wlDOtICeLo19ftQ6P3P8+uJg8y
|
||||
security_opt: ['label:disable']
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/agent:/var/lib/beszel-agent:Z
|
||||
- /run/user/1000/podman/podman.sock:/var/run/docker.sock:ro
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Beszel
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Beszel
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.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
|
||||
@@ -0,0 +1,5 @@
|
||||
NEXTAUTH_SECRET=CHANGE_ME
|
||||
CALENDSO_ENCRYPTION_KEY=CHANGE_ME
|
||||
DATABASE_URL=CHANGE_ME
|
||||
DATABASE_DIRECT_URL=CHANGE_ME
|
||||
EMAIL_SERVER_PASSWORD=CHANGE_ME
|
||||
@@ -0,0 +1,60 @@
|
||||
# Cal.com — 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-calcom.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 calcom LOGIN PASSWORD '617408b6790b546811cc38d0353ea506';
|
||||
# CREATE DATABASE calcom OWNER calcom;
|
||||
# SQL
|
||||
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql.
|
||||
#
|
||||
# Also unset: EMAIL_SERVER_HOST/USER/PASSWORD are still CHANGE_ME. Cal.com will run,
|
||||
# but booking confirmations and invitations will not send.
|
||||
#
|
||||
# Note Cal.com runs database migrations on first boot and can take several minutes to
|
||||
# become responsive. The unit allows 900s for this.
|
||||
#
|
||||
# Rootless adaptations from the home version:
|
||||
# - Dropped its private postgres:16-alpine; uses the shared instance (AGENTS.md §2).
|
||||
# - /etc/localtime mount removed in favour of TZ (AGENTS.md §8).
|
||||
# - No bind mounts remain, so no :Z is needed.
|
||||
#
|
||||
# NPM: proxy cal.gbrown.org -> http://calcom:3000
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
calcom:
|
||||
image: docker.io/calcom/cal.com:latest
|
||||
container_name: calcom
|
||||
hostname: calcom
|
||||
domainname: cal.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- NEXT_PUBLIC_WEBAPP_URL=https://cal.gbrown.org
|
||||
- NEXT_PUBLIC_WEBSITE_URL=https://cal.gbrown.org
|
||||
- NEXTAUTH_URL=https://cal.gbrown.org/api/auth
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY}
|
||||
# Shared postgres, not a private container
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- DATABASE_DIRECT_URL=${DATABASE_DIRECT_URL}
|
||||
- NEXT_PUBLIC_LICENSE_CONSENT=agree
|
||||
- ALLOWED_HOSTNAMES="cal.gbrown.org"
|
||||
- [email protected]
|
||||
- EMAIL_SERVER_HOST=CHANGE_ME
|
||||
- EMAIL_SERVER_PORT=587
|
||||
- EMAIL_SERVER_USER=CHANGE_ME
|
||||
- EMAIL_SERVER_PASSWORD=CHANGE_ME
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Cal.com
|
||||
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/CalCom
|
||||
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,64 @@
|
||||
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
|
||||
# Keep this file up-to-date when you add new variables to \`.env\`.
|
||||
# This file will be committed to version control, so make sure not to have any secrets in it.
|
||||
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
|
||||
|
||||
## Next App ##
|
||||
NODE_ENV=production
|
||||
SENTRY_AUTH_TOKEN=CHANGE_ME
|
||||
PAYLOAD_SECRET=CHANGE_ME
|
||||
PAYLOAD_DB_URL=CHANGE_ME
|
||||
NEXT_PUBLIC_SITE_URL=https://convexmonorepo.gbrown.org
|
||||
NEXT_PUBLIC_CONVEX_URL=https://api.convexmonorepo.gbrown.org # convex-backend:3210
|
||||
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.gbrown.org
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/3
|
||||
NEXT_PUBLIC_SENTRY_URL=https://sentry.gbrown.org
|
||||
NEXT_PUBLIC_SENTRY_ORG=sentry
|
||||
NEXT_PUBLIC_SENTRY_PROJECT_NAME=convexmonorepo-nextjs
|
||||
|
||||
## Convex ##
|
||||
CONVEX_SELF_HOSTED_URL=https://api.convexmonorepo.gbrown.org # convex-backend:3210
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY=CHANGE_ME
|
||||
# Convex Auth
|
||||
CONVEX_SITE_URL=https://convexmonorepo.gbrown.org # convex-backend:3211
|
||||
#CONVEX_SITE_URL=https://convexmonorepo.gbrown.org # convex-backend:3211
|
||||
USESEND_API_KEY=CHANGE_ME
|
||||
USESEND_URL=https://usesend.example.com
|
||||
USESEND_FROM_EMAIL='Convex Admin <[email protected]>'
|
||||
AUTH_AUTHENTIK_ID=CHANGE_ME
|
||||
AUTH_AUTHENTIK_SECRET=CHANGE_ME
|
||||
AUTH_AUTHENTIK_ISSUER=https://auth.gbrown.org/application/o/convexmonorepo/
|
||||
|
||||
## Docker Compose Variables for Next App ##
|
||||
NETWORK=nginx-bridge
|
||||
NEXT_CONTAINER_NAME=convexmonorepo-next
|
||||
NEXT_DOMAIN=convexmonorepo.gbrown.org
|
||||
NEXT_PORT=3000
|
||||
|
||||
## Docker Compose Variables for Self hosted Convex ##
|
||||
BACKEND_TAG=latest
|
||||
DASHBOARD_TAG=latest
|
||||
BACKEND_CONTAINER_NAME=convexmonorepo-backend
|
||||
DASHBOARD_CONTAINER_NAME=convexmonorepo-dashboard
|
||||
BACKEND_DOMAIN=convex.convexmonorepo.gbrown.org
|
||||
DASHBOARD_DOMAIN=dashboard.convexmonorepo.gbrown.org
|
||||
INSTANCE_NAME=convex
|
||||
#INSTANCE_SECRET=
|
||||
CONVEX_CLOUD_ORIGIN=https://api.convexmonorepo.gbrown.org
|
||||
CONVEX_SITE_ORIGIN=https://convex.convexmonorepo.gbrown.org
|
||||
NEXT_PUBLIC_DEPLOYMENT_URL=https://api.convexmonorepo.gbrown.org
|
||||
DISABLE_BEACON=true
|
||||
REDACT_LOGS_TO_CLIENT=true
|
||||
DO_NOT_REQUIRE_SSL=true
|
||||
#POSTGRES_URL= #postgresql://user:password@host:5432/db_name
|
||||
#BACKEND_PORT=
|
||||
#DASHBOARD_PORT
|
||||
#SITE_PROXY_PORT=
|
||||
#ACTIONS_USER_TIMEOUT_SECS=
|
||||
#RUST_LOG=
|
||||
#RUST_BACKTRACE=
|
||||
|
||||
# Also set on the live machine
|
||||
BACKEND_PORT=CHANGE_ME
|
||||
PORT=CHANGE_ME
|
||||
SITE_PROXY_PORT=CHANGE_ME
|
||||
@@ -0,0 +1,78 @@
|
||||
networks:
|
||||
nginx-bridge: # Change to network you plan to use
|
||||
external: true
|
||||
|
||||
services:
|
||||
convexmonorepo-next:
|
||||
image: git.gbrown.org/gib/convexmonorepo-next:latest
|
||||
container_name: convexmonorepo-next
|
||||
hostname: convexmonorepo-next
|
||||
domainname: ${NEXT_DOMAIN}
|
||||
networks: ['${NETWORK:-nginx-bridge}']
|
||||
#ports: ['${NEXT_PORT}:${NEXT_PORT}']
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV}
|
||||
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
||||
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:${NEXT_PORT:-3000}}
|
||||
- NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${BACKEND_PORT:-3210}}
|
||||
- NEXT_PUBLIC_PLAUSIBLE_URL=${NEXT_PUBLIC_PLAUSIBLE_URL:-https://plausible.gbrown.org}
|
||||
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||
- NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
|
||||
- NEXT_PUBLIC_SENTRY_ORG=${NEXT_PUBLIC_SENTRY_ORG:-sentry}
|
||||
- NEXT_PUBLIC_SENTRY_PROJECT_NAME=${NEXT_PUBLIC_SENTRY_PROJECT_NAME}
|
||||
- PAYLOAD_SECRET=${PAYLOAD_SECRET}
|
||||
- PAYLOAD_DB_URL=${PAYLOAD_DB_URL}
|
||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||
depends_on: ['convexmonorepo-backend']
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
convexmonorepo-backend:
|
||||
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
container_name: ${BACKEND_CONTAINER_NAME:-convex-backend}
|
||||
hostname: ${BACKEND_CONTAINER_NAME:-convex-backend}
|
||||
domainname: ${BACKEND_DOMAIN:-convex.gbrown.org}
|
||||
networks: ['${NETWORK:-nginx-bridge}']
|
||||
#user: '1000:1000'
|
||||
#ports: ['${BACKEND_PORT:-3210}:3210','${SITE_PROXY_PORT:-3211}:3211']
|
||||
volumes: [./data:/convex/data:z]
|
||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||
environment:
|
||||
- INSTANCE_NAME
|
||||
#- INSTANCE_SECRET
|
||||
- CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${BACKEND_PORT:-3210}}
|
||||
- CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${SITE_PROXY_PORT:-3211}}
|
||||
- DISABLE_BEACON=${DISABLE_BEACON:-true}
|
||||
- REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-true}
|
||||
- DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-false}
|
||||
#- POSTGRES_URL=${POSTGRES_URL}
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: curl -f http://localhost:3210/version
|
||||
interval: 5s
|
||||
start_period: 10s
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
|
||||
convexmonorepo-dashboard:
|
||||
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
container_name: ${DASHBOARD_CONTAINER_NAME:-convex-dashboard}
|
||||
hostname: ${DASHBOARD_CONTAINER_NAME:-convex-dashboard}
|
||||
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-convex.gbrown.org}}
|
||||
networks: ['${NETWORK:-nginx-bridge}']
|
||||
#user: 1000:1000
|
||||
#ports: ['${DASHBOARD_PORT:-6791}:6791']
|
||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||
environment:
|
||||
- NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${PORT:-3210}}
|
||||
depends_on:
|
||||
convexmonorepo-backend:
|
||||
condition: service_healthy
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: ConvexMonorepo
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/ConvexMonorepo
|
||||
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,6 @@
|
||||
NEXTAUTH_SECRET=CHANGE_ME
|
||||
NEXT_PRIVATE_ENCRYPTION_KEY=CHANGE_ME
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=CHANGE_ME
|
||||
NEXT_PRIVATE_DATABASE_URL=CHANGE_ME
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL=CHANGE_ME
|
||||
NEXT_PRIVATE_SMTP_PASSWORD=CHANGE_ME
|
||||
@@ -0,0 +1,72 @@
|
||||
# Documenso — VPS (ROOTLESS PODMAN). Ported from the home server 2026-08-12.
|
||||
#
|
||||
# ⚠️ NOT STARTED. No systemd unit is enabled for this. It has never run on either
|
||||
# machine, so there is no data to migrate. Bring it up with:
|
||||
# systemctl --user enable --now podman-documenso.service
|
||||
#
|
||||
# BEFORE FIRST START, two things must happen:
|
||||
#
|
||||
# 1. 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 documenso LOGIN PASSWORD 'e2bf60b34aff8ce259e5ed42da46c9c6';
|
||||
# CREATE DATABASE documenso OWNER documenso;
|
||||
# SQL
|
||||
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql so a
|
||||
# rebuild recreates them.
|
||||
#
|
||||
# 2. Provide a signing certificate at ./volumes/cert/cert.p12, or Documenso will
|
||||
# not start. Generate one with:
|
||||
# openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 3650 -nodes
|
||||
# openssl pkcs12 -export -out cert.p12 -inkey key.pem -in cert.pem -passout pass:
|
||||
#
|
||||
# Also unset: the four NEXT_PRIVATE_SMTP_* values are still CHANGE_ME. Documenso will
|
||||
# run without working email, but signature invitations will fail to send.
|
||||
#
|
||||
# Rootless adaptations from the home version:
|
||||
# - Dropped its private postgres:16-alpine; uses the shared instance (AGENTS.md §2).
|
||||
# - :Z on bind mounts (SELinux Enforcing).
|
||||
# - /etc/localtime mounts removed in favour of TZ (AGENTS.md §8).
|
||||
#
|
||||
# NPM: proxy docs.gbrown.org -> http://documenso:3000
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
documenso:
|
||||
image: docker.io/documenso/documenso:latest
|
||||
container_name: documenso
|
||||
hostname: documenso
|
||||
domainname: docs.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PORT=3000
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- NEXT_PRIVATE_ENCRYPTION_KEY=${NEXT_PRIVATE_ENCRYPTION_KEY}
|
||||
- NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY}
|
||||
- NEXT_PUBLIC_WEBAPP_URL=https://docs.gbrown.org
|
||||
- NEXTAUTH_URL=https://docs.gbrown.org
|
||||
# Shared postgres, not a private container
|
||||
- NEXT_PRIVATE_DATABASE_URL=${NEXT_PRIVATE_DATABASE_URL}
|
||||
- NEXT_PRIVATE_DIRECT_DATABASE_URL=${NEXT_PRIVATE_DIRECT_DATABASE_URL}
|
||||
- NEXT_PRIVATE_SIGNING_TRANSPORT=local
|
||||
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12
|
||||
- NEXT_PRIVATE_SIGNING_PASSPHRASE=
|
||||
- NEXT_PUBLIC_UPLOAD_TRANSPORT=database
|
||||
- NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
|
||||
- NEXT_PRIVATE_SMTP_HOST=CHANGE_ME
|
||||
- NEXT_PRIVATE_SMTP_PORT=587
|
||||
- NEXT_PRIVATE_SMTP_USERNAME=CHANGE_ME
|
||||
- NEXT_PRIVATE_SMTP_PASSWORD=CHANGE_ME
|
||||
- NEXT_PRIVATE_SMTP_FROM_NAME=Documenso
|
||||
- [email protected]
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/cert/cert.p12:/opt/documenso/cert.p12:Z
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Documenso
|
||||
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/Documenso
|
||||
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 @@
|
||||
AGENT_INGEST_SECRET=CHANGE_ME
|
||||
@@ -0,0 +1,43 @@
|
||||
# Hetzner VPS (junior.gib, rootless podman): ~/Server/Gbrown_Agent/compose.yml.
|
||||
# Follows ~/Server/AGENTS.md. AGENT_INGEST_SECRET lives in ./.env (never
|
||||
# committed). Started by podman-gbrown-agent.service, never by hand.
|
||||
#
|
||||
# SELinux:
|
||||
# - The podman socket is mounted with no relabel flag, ever (AGENTS.md §6).
|
||||
# - Reading it needs `label:disable`, the same exception portainer and uptime
|
||||
# have as container-management tooling.
|
||||
# - Because labeling is off, the NPM logs mount also takes no :z or :Z.
|
||||
# NPM mounts its data dir with :Z (a private MCS pair), so relabeling it here
|
||||
# would lock NPM out of its own data.
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
gbrown-agent:
|
||||
image: git.gbrown.org/gib/gbrown-agent:latest
|
||||
container_name: gbrown-agent
|
||||
hostname: gbrown-agent
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- HOST_ID=vps
|
||||
- INGEST_URL=https://convex.gbrown.org/ingest
|
||||
- AGENT_INGEST_SECRET=${AGENT_INGEST_SECRET}
|
||||
# Reach Convex over WireGuard, not Cloudflare: pin the hostname to the
|
||||
# Home Server's proxy (192.168.1.2, routed via the `Home` interface). The
|
||||
# URL stays https with the real hostname, so SNI and the certificate check
|
||||
# are unchanged.
|
||||
extra_hosts:
|
||||
- 'convex.gbrown.org:192.168.1.2'
|
||||
# The image runs as uid 1000 (bun). keep-id maps it to gib, who owns the
|
||||
# podman socket and the NPM logs (AGENTS.md §3.2). Never `user: 1000:1000`.
|
||||
userns_mode: 'keep-id:uid=1000,gid=1000'
|
||||
volumes:
|
||||
- /run/user/1000/podman/podman.sock:/var/run/docker.sock # no :Z, ever
|
||||
- /home/gib/Server/Nginx_Proxy_Manager/volumes/data/logs:/npm-logs:ro
|
||||
security_opt: ['label:disable', 'no-new-privileges:true']
|
||||
read_only: true
|
||||
cap_drop: [ALL]
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 10s
|
||||
@@ -0,0 +1,27 @@
|
||||
# Rootless user unit for the telemetry agent on junior.gib, from
|
||||
# ~/Server/podman-example.service. Install (as gib, never sudo):
|
||||
# cp podman-gbrown-agent.service ~/.config/systemd/user/
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable --now podman-gbrown-agent.service
|
||||
# Then add gbrown-agent to the lists in ~/Server/verify-rootless.sh, and to
|
||||
# update-containers.sh if it should follow nightly image updates.
|
||||
[Unit]
|
||||
Description=Podman Compose: gbrown telemetry agent
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Gbrown_Agent
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1 @@
|
||||
USESEND_API_KEY=CHANGE_ME
|
||||
@@ -0,0 +1,41 @@
|
||||
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
|
||||
# ── Mailer ────────────────────────────────────────────────────────────────
|
||||
# The image's entrypoint runs `environment-to-ini`, so GITEA__section__KEY
|
||||
# vars are written into /data/gitea/conf/app.ini on every start. This block
|
||||
# is therefore the source of truth -- hand-editing app.ini gets overwritten
|
||||
# on the next restart. It also means PASSWD lands in app.ini in plaintext
|
||||
# inside the volume; that is how Gitea works, not a misconfiguration.
|
||||
#
|
||||
# PROTOCOL=smtp+starttls matches port 587. The relay refuses AUTH until
|
||||
# STARTTLS succeeds, so plain `smtp` would fail with 538.
|
||||
- GITEA__mailer__ENABLED=true
|
||||
- GITEA__mailer__PROTOCOL=smtp+starttls
|
||||
- GITEA__mailer__SMTP_ADDR=smtp.usesend.gbrown.org
|
||||
- GITEA__mailer__SMTP_PORT=587
|
||||
- GITEA__mailer__USER=usesend
|
||||
- GITEA__mailer__PASSWD=${USESEND_API_KEY:?set USESEND_API_KEY in Gitea/.env}
|
||||
- GITEA__mailer__FROM=Gitea <[email protected]>
|
||||
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
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Gitea
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Gitea
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=90
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,30 @@
|
||||
# iSponsorBlockTV — VPS (ROOTLESS PODMAN) port of the home server's
|
||||
# ~/Server/isponsorblocktv, migrated 2026-08-12.
|
||||
#
|
||||
# Why this works off-LAN: the configured device is pinned by `screen_id`, which uses
|
||||
# YouTube's CLOUD lounge API rather than a direct LAN connection to the Apple TV. So it
|
||||
# does not need to be on the same network as the TV. (If you ever re-pair a device via
|
||||
# SSDP/mDNS discovery, that step DOES need LAN access and must be done from home.)
|
||||
#
|
||||
# Rootless adaptations:
|
||||
# - `user: 1000:1000` REPLACED with userns_mode keep-id. Under a rootless userns the
|
||||
# original would resolve to subuid 525287 and lose access to its own gib-owned data
|
||||
# directory -- the exact failure that kept n8n broken for two months. See AGENTS.md §3.2.
|
||||
# - :Z on the data mount (SELinux Enforcing).
|
||||
# - network_mode: host retained to match home. It works fine rootless.
|
||||
|
||||
services:
|
||||
isponsorblocktv:
|
||||
image: ghcr.io/dmunozv04/isponsorblocktv:latest
|
||||
container_name: iSponsorBlockTV
|
||||
network_mode: host
|
||||
userns_mode: "keep-id:uid=1000,gid=1000"
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
volumes:
|
||||
- './volumes/isponsorblocktv:/app/data:Z'
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: iSponsorBlockTV
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/ISponsorBlockTV
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,145 @@
|
||||
# Keys
|
||||
# Required key for platform encryption/decryption ops
|
||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
ENCRYPTION_KEY=CHANGE_ME
|
||||
|
||||
# JWT
|
||||
# Required secrets to sign JWT tokens
|
||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
AUTH_SECRET=CHANGE_ME
|
||||
|
||||
# Postgres creds
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_USER=infisical_user
|
||||
POSTGRES_DB=infisical_db
|
||||
|
||||
# Required
|
||||
DB_CONNECTION_URI=CHANGE_ME
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://infisical-redis:6379
|
||||
|
||||
# Website URL
|
||||
# Required
|
||||
SITE_URL=https://infisical.gbrown.org
|
||||
|
||||
# Mail/SMTP
|
||||
SMTP_HOST=smtp.mail.me.com
|
||||
SMTP_PORT=587
|
||||
SMTP_FROM_ADDRESS=[email protected]
|
||||
SMTP_FROM_NAME=Infisical Admin
|
||||
SMTP_USERNAME=CHANGE_ME
|
||||
SMTP_PASSWORD=CHANGE_ME
|
||||
|
||||
# CICD Integration
|
||||
CLIENT_ID_GITHUB=
|
||||
CLIENT_ID_GITHUB_APP=
|
||||
CLIENT_SLUG_GITHUB_APP=
|
||||
CLIENT_SECRET_GITHUB=
|
||||
CLIENT_SECRET_GITHUB_APP=
|
||||
CLIENT_ID_GITLAB=
|
||||
CLIENT_SECRET_GITLAB=
|
||||
CLIENT_PRIVATE_KEY_GITHUB_APP=
|
||||
CLIENT_APP_ID_GITHUB_APP=
|
||||
|
||||
# Sentry (optional) for monitoring errors
|
||||
SENTRY_DSN=
|
||||
|
||||
# Infisical Cloud-specific configs
|
||||
# Ignore - Not applicable for self-hosted version
|
||||
POSTHOG_HOST=
|
||||
POSTHOG_PROJECT_API_KEY=
|
||||
|
||||
# SSO-specific variables
|
||||
CLIENT_ID_GOOGLE_LOGIN=
|
||||
CLIENT_SECRET_GOOGLE_LOGIN=
|
||||
|
||||
CLIENT_ID_GITHUB_LOGIN=
|
||||
CLIENT_SECRET_GITHUB_LOGIN=
|
||||
|
||||
CLIENT_ID_GITLAB_LOGIN=
|
||||
CLIENT_SECRET_GITLAB_LOGIN=
|
||||
|
||||
CAPTCHA_SECRET=
|
||||
|
||||
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=false
|
||||
OTEL_EXPORT_TYPE=prometheus
|
||||
OTEL_EXPORT_OTLP_ENDPOINT=
|
||||
OTEL_OTLP_PUSH_INTERVAL=
|
||||
|
||||
OTEL_COLLECTOR_BASIC_AUTH_USERNAME=
|
||||
OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=
|
||||
|
||||
PLAIN_API_KEY=
|
||||
PLAIN_WISH_LABEL_IDS=
|
||||
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
|
||||
|
||||
# App Connections
|
||||
|
||||
# aws assume-role connection
|
||||
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID=
|
||||
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY=
|
||||
|
||||
# github oauth connection
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID=
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET=
|
||||
|
||||
#github app connection
|
||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID=
|
||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
|
||||
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||
INF_APP_CONNECTION_GITHUB_APP_ID=
|
||||
|
||||
#gitlab app connection
|
||||
INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID=
|
||||
INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET=
|
||||
|
||||
#github radar app connection
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_SLUG=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_ID=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET=
|
||||
|
||||
#gcp app connection
|
||||
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
|
||||
|
||||
# azure app connections
|
||||
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID=
|
||||
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET=
|
||||
|
||||
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID=
|
||||
INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET=
|
||||
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID=
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET=
|
||||
|
||||
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID=
|
||||
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET=
|
||||
|
||||
# heroku app connection
|
||||
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID=
|
||||
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET=
|
||||
|
||||
# datadog
|
||||
SHOULD_USE_DATADOG_TRACER=
|
||||
DATADOG_PROFILING_ENABLED=
|
||||
DATADOG_ENV=
|
||||
DATADOG_SERVICE=
|
||||
DATADOG_HOSTNAME=
|
||||
|
||||
# kubernetes
|
||||
KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN=false
|
||||
|
||||
# ClickHouse (optional) for audit log storage
|
||||
# CLICKHOUSE_URL=http://infisical:infisical@clickhouse:8123/infisical
|
||||
|
||||
# Also set on the live machine
|
||||
CLIENT_GITLAB_LOGIN_URL=CHANGE_ME
|
||||
@@ -0,0 +1,62 @@
|
||||
# Infisical — VPS (ROOTLESS PODMAN) port of the home server's ~/Server/infisical,
|
||||
# migrated 2026-08-12.
|
||||
#
|
||||
# CONSOLIDATED ONTO THE SHARED POSTGRES. Home runs a dedicated postgres:14 for infisical;
|
||||
# here it uses the shared `postgresql` container (PG 17) alongside authentik, n8n and NPM.
|
||||
# The PG14 dump restored into PG17 cleanly (no extensions, 749 tables, row counts verified
|
||||
# identical). One postgres to back up, patch and monitor instead of several.
|
||||
#
|
||||
# role/database: infisical_user / infisical_db (created in the shared instance)
|
||||
# DB_CONNECTION_URI in .env points at host `postgresql`, not the old `db`
|
||||
# REDIS_URL points at `infisical-redis` -- container_name is what netavark resolves
|
||||
# on nginx-bridge, so the home file's bare `redis` hostname does not work here
|
||||
#
|
||||
# redis stays local to this stack: it holds only cache/queue state, starts empty by
|
||||
# design, and is not worth centralising.
|
||||
#
|
||||
# Rootless adaptations:
|
||||
# - :Z on the redis data mount (SELinux Enforcing).
|
||||
# - No published ports; NPM proxies to http://infisical-backend:8080 over nginx-bridge.
|
||||
# - Images start as root and drop privileges internally, so no userns_mode needed.
|
||||
#
|
||||
# .env is copied verbatim from home (mode 600) and holds ENCRYPTION_KEY and AUTH_SECRET.
|
||||
# Those MUST match the database they encrypted -- never regenerate them on migrated data.
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: docker.io/library/redis:latest
|
||||
container_name: infisical-redis
|
||||
hostname: infisical-redis
|
||||
networks: ["nginx-bridge"]
|
||||
env_file: .env
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
- TZ=America/New_York
|
||||
volumes:
|
||||
- ./data/redis:/data:Z
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
|
||||
backend:
|
||||
image: docker.io/infisical/infisical:latest
|
||||
container_name: infisical-backend
|
||||
hostname: infisical-backend
|
||||
domainname: infisical.gbrown.org
|
||||
networks: ["nginx-bridge"]
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- TZ=America/New_York
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
depends_on:
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
stdin_open: true
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Infisical
|
||||
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/Infisical
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,72 @@
|
||||
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
|
||||
# Keep this file up-to-date when you add new variables to \`.env\`.
|
||||
# This file will be committed to version control, so make sure not to have any secrets in it.
|
||||
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
|
||||
|
||||
## Next.js ##
|
||||
NODE_ENV=production
|
||||
SENTRY_AUTH_TOKEN=CHANGE_ME
|
||||
PAYLOAD_SECRET=CHANGE_ME
|
||||
PAYLOAD_DB_URL=CHANGE_ME
|
||||
NEXT_PUBLIC_SITE_URL=https://lashaddict.gbrown.org
|
||||
NEXT_PUBLIC_CONVEX_URL=https://api.lashaddict.gbrown.org # convex-backend:3210
|
||||
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.gbrown.org
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/7
|
||||
NEXT_PUBLIC_SENTRY_URL=https://sentry.gbrown.org
|
||||
NEXT_PUBLIC_SENTRY_ORG=sentry
|
||||
NEXT_PUBLIC_SENTRY_PROJECT_NAME=lashaddict-next
|
||||
|
||||
## Convex ##
|
||||
CONVEX_SELF_HOSTED_URL=https://api.lashaddict.gbrown.org # convex-backend:3210
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY=CHANGE_ME
|
||||
# Convex Auth
|
||||
CONVEX_SITE_URL=https://lashaddict.gbrown.org
|
||||
USESEND_API_KEY=CHANGE_ME
|
||||
USESEND_URL=https://usesend.gbrown.org
|
||||
USESEND_FROM_EMAIL='Admin <[email protected]>'
|
||||
AUTH_AUTHENTIK_ID=CHANGE_ME
|
||||
AUTH_AUTHENTIK_SECRET=CHANGE_ME
|
||||
AUTH_AUTHENTIK_ISSUER=https://auth.gbrown.org/application/o/lashaddict/
|
||||
|
||||
## Docker Compose Variables for Next App ##
|
||||
NETWORK=nginx-bridge
|
||||
NEXT_CONTAINER_NAME=lashaddict-next
|
||||
NEXT_DOMAIN=lashaddict.gbrown.org
|
||||
#NEXT_PORT=
|
||||
|
||||
## Docker Compose Variables for Self hosted Convex ##
|
||||
BACKEND_TAG=latest
|
||||
DASHBOARD_TAG=latest
|
||||
BACKEND_CONTAINER_NAME=lashaddict-backend
|
||||
DASHBOARD_CONTAINER_NAME=lashaddict-dashboard
|
||||
BACKEND_DOMAIN=convex.lashaddict.gbrown.org
|
||||
DASHBOARD_DOMAIN=dashboard.lashaddict.gbrown.org
|
||||
INSTANCE_NAME=lashaddict_convex
|
||||
INSTANCE_SECRET=CHANGE_ME
|
||||
CONVEX_CLOUD_ORIGIN=https://api.lashaddict.gbrown.org
|
||||
CONVEX_SITE_ORIGIN=https://convex.lashaddict.gbrown.org
|
||||
NEXT_PUBLIC_DEPLOYMENT_URL=https://api.lashaddict.gbrown.org
|
||||
DISABLE_BEACON=true
|
||||
REDACT_LOGS_TO_CLIENT=true
|
||||
DO_NOT_REQUIRE_SSL=true
|
||||
POSTGRES_URL=CHANGE_ME
|
||||
#BACKEND_PORT=
|
||||
#DASHBOARD_PORT
|
||||
#SITE_PROXY_PORT=
|
||||
#ACTIONS_USER_TIMEOUT_SECS=
|
||||
#RUST_LOG=
|
||||
#RUST_BACKTRACE=
|
||||
|
||||
## Docker Compose Variables for Postgres ##
|
||||
POSTGRES_CONTAINER_NAME=lashaddict-postgres
|
||||
POSTGRES_USER=gib
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_DB=lashaddict-payload
|
||||
|
||||
# Also set on the live machine
|
||||
NEXT_PUBLIC_POSTHOG_KEY=CHANGE_ME
|
||||
NEXT_PUBLIC_POSTHOG_HOST=CHANGE_ME
|
||||
NEXT_PUBLIC_PLAUSIBLE_SRC=CHANGE_ME
|
||||
BACKEND_PORT=CHANGE_ME
|
||||
PORT=CHANGE_ME
|
||||
SITE_PROXY_PORT=CHANGE_ME
|
||||
@@ -0,0 +1,92 @@
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
lashaddict-next:
|
||||
image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest
|
||||
container_name: ${NEXT_CONTAINER_NAME}
|
||||
hostname: ${NEXT_CONTAINER_NAME}
|
||||
# Upstream DNS pinned to Cloudflare: Hetzner resolvers held a stale
|
||||
# NXDOMAIN for api.lashaddictstudio.com for hours after it was created
|
||||
# (2026-09-30). Container names still resolve through aardvark-dns.
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
domainname: ${NEXT_DOMAIN}
|
||||
networks:
|
||||
- ${NETWORK:-nginx-bridge}
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV}
|
||||
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
||||
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:${NEXT_PORT:-3000}}
|
||||
- NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-http://${BACKEND_CONTAINER_NAME:-lashaddict-backend}:${BACKEND_PORT:-3210}}
|
||||
- NEXT_PUBLIC_PLAUSIBLE_URL=${NEXT_PUBLIC_PLAUSIBLE_URL:-https://plausible.gbrown.org}
|
||||
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||
- NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
|
||||
- NEXT_PUBLIC_SENTRY_ORG=${NEXT_PUBLIC_SENTRY_ORG:-sentry}
|
||||
- NEXT_PUBLIC_SENTRY_PROJECT_NAME=${NEXT_PUBLIC_SENTRY_PROJECT_NAME}
|
||||
- PAYLOAD_SECRET=${PAYLOAD_SECRET}
|
||||
- PAYLOAD_DB_URL=${PAYLOAD_DB_URL}
|
||||
depends_on:
|
||||
- lashaddict-backend
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
lashaddict-backend:
|
||||
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
container_name: ${BACKEND_CONTAINER_NAME:-lashaddict-backend}
|
||||
# Same Cloudflare DNS pin as lashaddict-next (see its comment): the backend
|
||||
# fetches its own auth discovery URL, which Hetzner resolvers failed.
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
hostname: ${BACKEND_CONTAINER_NAME:-lashaddict-backend}
|
||||
domainname: ${BACKEND_DOMAIN:-lashaddict.gbrown.org}
|
||||
networks:
|
||||
- ${NETWORK:-nginx-bridge}
|
||||
volumes:
|
||||
- ./volumes/convex:/convex/data:z
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=true
|
||||
environment:
|
||||
- INSTANCE_NAME=${INSTANCE_NAME}
|
||||
- INSTANCE_SECRET=${INSTANCE_SECRET}
|
||||
- CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${BACKEND_PORT:-3210}}
|
||||
- CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${SITE_PROXY_PORT:-3211}}
|
||||
- DISABLE_BEACON=${DISABLE_BEACON:-true}
|
||||
- REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-true}
|
||||
- DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-false}
|
||||
- POSTGRES_URL=${POSTGRES_URL}
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: curl -f http://localhost:3210/version
|
||||
interval: 5s
|
||||
start_period: 10s
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
lashaddict-dashboard:
|
||||
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
container_name: ${DASHBOARD_CONTAINER_NAME:-lashaddict-dashboard}
|
||||
# Same Cloudflare DNS pin as lashaddict-next (see its comment): the backend
|
||||
# fetches its own auth discovery URL, which Hetzner resolvers failed.
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
hostname: ${DASHBOARD_CONTAINER_NAME:-lashaddict-dashboard}
|
||||
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-lashaddict.gbrown.org}}
|
||||
networks:
|
||||
- ${NETWORK:-nginx-bridge}
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=true
|
||||
environment:
|
||||
- NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://${BACKEND_CONTAINER_NAME:-lashaddict-backend}:${PORT:-3210}}
|
||||
depends_on:
|
||||
lashaddict-backend:
|
||||
condition: service_healthy
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: LashAddict
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/LashAddict
|
||||
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 @@
|
||||
GENERIC_TIMEZONE=America/New_York
|
||||
TZ=America/New_York
|
||||
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
|
||||
N8N_RUNNERS_ENABLED=true
|
||||
N8N_RUNNERS_MODE=external
|
||||
N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
|
||||
N8N_RUNNERS_AUTH_TOKEN=CHANGE_ME
|
||||
N8N_NATIVE_PYTHON_RUNNER=true
|
||||
DB_TYPE=postgresdb
|
||||
DB_POSTGRESDB_DATABASE=n8n
|
||||
DB_POSTGRESDB_HOST=postgresql
|
||||
DB_POSTGRESDB_PORT=5432
|
||||
DB_POSTGRESDB_USER=npm
|
||||
DB_POSTGRESDB_PASSWORD=CHANGE_ME
|
||||
N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
|
||||
N8N_RUNNERS_AUTH_TOKEN=CHANGE_ME
|
||||
@@ -0,0 +1,50 @@
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
n8n:
|
||||
image: docker.n8n.io/n8nio/n8n:latest
|
||||
container_name: n8n
|
||||
hostname: n8n
|
||||
domainname: n8n.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
#ports: ['5678:5678']
|
||||
env_file: [.env]
|
||||
environment:
|
||||
- GENERIC_TIMEZONE
|
||||
- TZ
|
||||
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS
|
||||
- N8N_RUNNERS_ENABLED
|
||||
- N8N_RUNNERS_MODE
|
||||
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS
|
||||
- N8N_RUNNERS_AUTH_TOKEN
|
||||
- N8N_NATIVE_PYTHON_RUNNER
|
||||
- DB_TYPE
|
||||
- DB_POSTGRESDB_DATABASE
|
||||
- DB_POSTGRESDB_HOST
|
||||
- DB_POSTGRESDB_PORT
|
||||
- DB_POSTGRESDB_USER
|
||||
- DB_POSTGRESDB_PASSWORD
|
||||
# Rootless: replaces `user: 1000:1000`, which under a rootless userns would have
|
||||
# resolved to subuid 525287 and lost access to the gib-owned ./data dir.
|
||||
userns_mode: "keep-id:uid=1000,gid=1000"
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- './data:/home/node/.n8n:Z'
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
task-runners:
|
||||
image: n8nio/runners:1.111.0
|
||||
container_name: n8n-runners
|
||||
# Was missing: without this the runner sits on the compose default network and
|
||||
# cannot resolve N8N_RUNNERS_TASK_BROKER_URI (http://n8n:5679).
|
||||
networks: ['nginx-bridge']
|
||||
env_file: [.env]
|
||||
environment:
|
||||
- N8N_RUNNERS_TASK_BROKER_URI
|
||||
- N8N_RUNNERS_AUTH_TOKEN
|
||||
depends_on:
|
||||
- n8n
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: N8n
|
||||
After=network-online.target podman-postgresql.service
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/N8n
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,5 @@
|
||||
DB_POSTGRES_HOST=postgresql
|
||||
DB_POSTGRES_PORT=5432
|
||||
DB_POSTGRES_USER=npm
|
||||
DB_POSTGRES_PASSWORD=CHANGE_ME
|
||||
DB_POSTGRES_NAME=npm
|
||||
@@ -0,0 +1,51 @@
|
||||
services:
|
||||
nginx-proxy-manager:
|
||||
image: jc21/nginx-proxy-manager:latest
|
||||
container_name: nginx-proxy-manager
|
||||
hostname: nginx-proxy-manager
|
||||
domainname: nginx.gibbyb.com
|
||||
networks: ['nginx-bridge']
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
# ⚠️ ONLY 81 moves. 80 and 443 above MUST stay on 0.0.0.0 — they are the
|
||||
# public front door and firewalld already restricts them to Cloudflare ranges.
|
||||
# 81 is the admin UI and is reached over WireGuard (http://192.168.2.2:81).
|
||||
- '192.168.2.2:81:81' # Admin Web Port — WireGuard only
|
||||
#- '21:21' # FTP
|
||||
#- '22:22' # SSH
|
||||
#- '25565:25565' # Minecraft
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- DB_POSTGRES_HOST
|
||||
- DB_POSTGRES_PORT
|
||||
- DB_POSTGRES_USER
|
||||
- DB_POSTGRES_PASSWORD
|
||||
- DB_POSTGRES_NAME
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
volumes:
|
||||
- ./volumes/data:/data:Z
|
||||
- ./volumes/letsencrypt:/etc/letsencrypt:Z
|
||||
#depends_on: [postgresql]
|
||||
|
||||
#postgresql:
|
||||
#image: postgres:17
|
||||
#container_name: nginx-proxy-manager-db
|
||||
#hostname: nginx-proxy-manager-db
|
||||
#networks: ['nginx-bridge']
|
||||
#environment:
|
||||
#POSTGRES_USER: 'npm'
|
||||
#POSTGRES_DB: 'npm'
|
||||
#labels:
|
||||
#com.centurylinklabs.watchtower.enable: 'true'
|
||||
#volumes:
|
||||
#- ./volumes/postgres:/var/lib/postgresql/data
|
||||
#restart: unless-stopped
|
||||
|
||||
networks:
|
||||
# Created out-of-band so all nine stacks agree and there is no create race at boot:
|
||||
# podman network create --subnet 172.18.0.0/24 nginx-bridge
|
||||
nginx-bridge:
|
||||
external: true
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Nginx Proxy Manager
|
||||
After=network-online.target podman-postgresql.service
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Nginx_Proxy_Manager
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,7 @@
|
||||
PENPOT_SECRET_KEY=CHANGE_ME
|
||||
PENPOT_DATABASE_PASSWORD=CHANGE_ME
|
||||
|
||||
# Also set on the live machine
|
||||
PENPOT_SMTP_PASSWORD=CHANGE_ME
|
||||
PENPOT_OIDC_CLIENT_ID=CHANGE_ME
|
||||
PENPOT_OIDC_CLIENT_SECRET=CHANGE_ME
|
||||
@@ -0,0 +1,183 @@
|
||||
# Penpot — VPS (ROOTLESS PODMAN). Ported from the home server 2026-08-12.
|
||||
#
|
||||
# First started 2026-09-21. Role + database created on the SHARED postgres, and the
|
||||
# same lines added to PostgreSQL/initdb/00-roles-and-databases.sql.template (note the
|
||||
# .template suffix -- the plain .sql path this header used to cite does not exist).
|
||||
# Managed by: systemctl --user enable --now podman-penpot.service
|
||||
#
|
||||
# Image tags are PINNED and watchtower is disabled on purpose. Penpot runs schema
|
||||
# migrations on boot and has shipped breaking major upgrades (1.x -> 2.x needed manual
|
||||
# steps), so it is a bad candidate for unattended updates. Bump the tags deliberately,
|
||||
# and back up the assets volume WITH the database -- a pg_dump alone will not restore.
|
||||
#
|
||||
# tty/stdin_open are false on every service here. A TTY makes tools in an entrypoint
|
||||
# think they are interactive; usesend hung silently on exactly that (2026-09-19).
|
||||
#
|
||||
# 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).
|
||||
#
|
||||
# Public sign-up is off (disable-registration). Accounts come from Authentik SSO
|
||||
# instead: enable-oidc-registration creates a profile on first login.
|
||||
#
|
||||
# NPM: proxy penpot.gbrown.org -> http://penpot-frontend:8080
|
||||
#
|
||||
# MCP (added 2026-09-23): `enable-mcp` on the FRONTEND makes its entrypoint write
|
||||
# /mcp/stream, /mcp/sse and /mcp/ws locations proxying to penpot-mcp:4401 (HTTP) and
|
||||
# :4402 (plugin websocket), so NPM needs no extra config. The flag is also on the
|
||||
# backend, matching upstream, where it gates per-user MCP keys (Account > Integrations).
|
||||
# Claude Code connects to https://penpot.gbrown.org/mcp/stream?userToken=<key>.
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
penpot-frontend:
|
||||
image: docker.io/penpotapp/frontend:2.17.2
|
||||
container_name: penpot-frontend
|
||||
hostname: penpot-frontend
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PENPOT_FLAGS=disable-registration disable-login-with-password enable-login-with-oidc enable-oidc-registration enable-smtp enable-mcp
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "false"
|
||||
volumes:
|
||||
- ./volumes/assets:/opt/data/assets:z
|
||||
depends_on:
|
||||
- penpot-backend
|
||||
- penpot-exporter
|
||||
- penpot-mcp
|
||||
tty: false
|
||||
stdin_open: false
|
||||
restart: unless-stopped
|
||||
|
||||
penpot-backend:
|
||||
image: docker.io/penpotapp/backend:2.17.2
|
||||
container_name: penpot-backend
|
||||
hostname: penpot-backend
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
# Heap deliberately UNCAPPED (Gib's call, 2026-09-21): the JVM takes ~25% of
|
||||
# host RAM, measuring ~2.3GB resident. Box has 16GB and the priority is
|
||||
# responsiveness, not footprint. If it gets tight, Penpot moves to the home
|
||||
# server rather than getting squeezed here. Set JVM_OPTS=-Xmx<n> to cap.
|
||||
- PENPOT_FLAGS=disable-registration disable-login-with-password enable-login-with-oidc enable-oidc-registration enable-smtp enable-prepl-server enable-mcp
|
||||
- PENPOT_PUBLIC_URI=https://penpot.gbrown.org
|
||||
# ── SMTP via the UseSend stack on this same host ──────────────────────
|
||||
# The proxy ADVERTISES "AUTH LOGIN PLAIN" before STARTTLS but actually refuses
|
||||
# it ("538 Must issue a STARTTLS command first"), so TLS is mandatory. It is
|
||||
# reached by the smtp.usesend.gbrown.org network alias rather than its
|
||||
# container name so the wildcard cert's hostname check passes.
|
||||
# Username is the proxy's SMTP_AUTH_USERNAME; password is a UseSend API key.
|
||||
# Max message size is 10MB.
|
||||
- PENPOT_SMTP_HOST=smtp.usesend.gbrown.org
|
||||
- PENPOT_SMTP_PORT=587
|
||||
- PENPOT_SMTP_USERNAME=usesend
|
||||
- PENPOT_SMTP_PASSWORD=${PENPOT_SMTP_PASSWORD:?set this in .env -- a UseSend API key}
|
||||
- PENPOT_SMTP_TLS=true
|
||||
- PENPOT_SMTP_SSL=false
|
||||
- PENPOT_SMTP_DEFAULT_FROM=Penpot <[email protected]>
|
||||
- PENPOT_SMTP_DEFAULT_REPLY_TO=Penpot <[email protected]>
|
||||
- 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 defaults to min-size=60 AND max-size=60 -- it eagerly opens 60
|
||||
# connections and never releases them. On the SHARED postgres (max_connections
|
||||
# = 100) that silently ate 60% of the cluster's slots and other apps started
|
||||
# getting "remaining connection slots are reserved for SUPERUSER". Undocumented
|
||||
# in Penpot's config guide; the numbers are visible in its startup log line
|
||||
# `app.db - initialize connection pool ... min-size=60, max-size=60`.
|
||||
- PENPOT_DATABASE_MIN_POOL_SIZE=4
|
||||
- PENPOT_DATABASE_MAX_POOL_SIZE=12
|
||||
- PENPOT_REDIS_URI=redis://penpot-redis/0
|
||||
- PENPOT_OBJECTS_STORAGE_BACKEND=fs
|
||||
- PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets
|
||||
- PENPOT_TELEMETRY_ENABLED=false
|
||||
# ── SSO via Authentik (app slug "penpot", any Authentik user) ────────
|
||||
# Login is SSO-only (disable-login-with-password). Existing profiles match
|
||||
# by email, and enable-oidc-registration creates a profile on first SSO
|
||||
# login, while disable-registration keeps the public sign-up form off.
|
||||
# Authority must keep its trailing slash; Penpot appends the discovery path.
|
||||
- PENPOT_OIDC_BASE_URI=https://auth.gbrown.org/application/o/penpot/
|
||||
- PENPOT_OIDC_CLIENT_ID=${PENPOT_OIDC_CLIENT_ID:?set this in .env}
|
||||
- PENPOT_OIDC_CLIENT_SECRET=${PENPOT_OIDC_CLIENT_SECRET:?set this in .env}
|
||||
- PENPOT_OIDC_SCOPES=openid profile email
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "false"
|
||||
volumes:
|
||||
- ./volumes/assets:/opt/data/assets:z
|
||||
depends_on:
|
||||
- penpot-redis
|
||||
tty: false
|
||||
stdin_open: false
|
||||
restart: unless-stopped
|
||||
|
||||
penpot-mcp:
|
||||
# Upstream image defaults: `node index.js --multi-user`, listening on 0.0.0.0.
|
||||
# No volumes and no published ports; only the frontend nginx talks to it.
|
||||
image: docker.io/penpotapp/mcp:2.17.2
|
||||
container_name: penpot-mcp
|
||||
hostname: penpot-mcp
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "false"
|
||||
tty: false
|
||||
stdin_open: false
|
||||
restart: unless-stopped
|
||||
|
||||
penpot-exporter:
|
||||
image: docker.io/penpotapp/exporter:2.17.2
|
||||
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
|
||||
# REQUIRED and missing from the original port: the exporter validates a
|
||||
# :secret-key at boot and crash-loops without it. Must match the backend.
|
||||
- PENPOT_SECRET_KEY=${PENPOT_SECRET_KEY}
|
||||
- PENPOT_REDIS_URI=redis://penpot-redis/0
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "false"
|
||||
depends_on:
|
||||
- penpot-redis
|
||||
tty: false
|
||||
stdin_open: false
|
||||
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: "false"
|
||||
tty: false
|
||||
stdin_open: false
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Penpot
|
||||
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/Penpot
|
||||
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,31 @@
|
||||
services:
|
||||
portainer:
|
||||
image: portainer/portainer-ee:latest
|
||||
container_name: portainer
|
||||
hostname: portainer
|
||||
domainname: port.gibbyb.com
|
||||
networks:
|
||||
- nginx-bridge
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
ports:
|
||||
# WireGuard address only — this admin UI has no business on the public
|
||||
# interface. It has its own login and the Hetzner firewall does not admit this
|
||||
# port, so this is defence in depth: it removes the dependency on a firewall rule
|
||||
# set that lives in a web console. See AGENTS.md §13.2.
|
||||
- 192.168.2.2:9443:9443
|
||||
# Required under SELinux Enforcing to reach the rootless podman socket, which
|
||||
# systemd recreates at each boot with a non-container label. The socket mount
|
||||
# deliberately has no :Z -- relabeling the live socket would break podman itself.
|
||||
security_opt: ['label:disable']
|
||||
volumes:
|
||||
- ./volumes/data:/data:Z
|
||||
- /run/user/1000/podman/podman.sock:/var/run/docker.sock
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Portainer
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Portainer
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,4 @@
|
||||
POSTGRES_USER=npm
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_NAME=npm
|
||||
POSTGRES_DB=npm
|
||||
@@ -0,0 +1,80 @@
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
postgresql:
|
||||
# pgvector image = stock postgres:17 plus the `vector` extension compiled in. Same
|
||||
# PostgreSQL 17.10 and same data directory format, so this is a drop-in swap with no
|
||||
# dump/restore needed.
|
||||
#
|
||||
# ⚠️ THE TAG MUST STAY -trixie. This is not cosmetic.
|
||||
#
|
||||
# The plain `pgvector/pgvector:pg17` tag is built on Debian 12 (bookworm, glibc 2.36).
|
||||
# The stock `postgres:17` this cluster was created under is Debian 13 (trixie,
|
||||
# glibc 2.41). glibc supplies the collation used to order every text index, and it is
|
||||
# NOT guaranteed stable across versions. Starting on bookworm made all 14 databases
|
||||
# report:
|
||||
# WARNING: database "npm" has a collation version mismatch
|
||||
# DETAIL: created using collation version 2.41, but the OS provides version 2.36
|
||||
# An index built under one collation and read under another can silently return wrong
|
||||
# results — missed rows in range scans, duplicate values slipping past unique
|
||||
# constraints. It does not error; it just quietly answers incorrectly.
|
||||
#
|
||||
# Using the -trixie build keeps glibc at 2.41, matching how the data was written, so
|
||||
# no REINDEX is required. If you ever must move to a different base, the correct
|
||||
# procedure is: REINDEX DATABASE <each>, then ALTER DATABASE <each> REFRESH COLLATION
|
||||
# VERSION — not simply silencing the warning.
|
||||
#
|
||||
# The extension is AVAILABLE but not enabled anywhere by default. To use it in a
|
||||
# database, enable it per-database (it is not cluster-wide):
|
||||
# podman exec postgresql psql -U npm -d <dbname> -c 'CREATE EXTENSION vector;'
|
||||
#
|
||||
# ⚠️ Do NOT let watchtower update this to a different pg major. It is already in
|
||||
# WATCHTOWER_DISABLE_CONTAINERS, which is what keeps that from happening.
|
||||
image: pgvector/pgvector:pg17-trixie
|
||||
container_name: postgresql
|
||||
# Rootless: maps host uid 1000 (gib) -> container uid 999 (postgres), so ./pg_data
|
||||
# stays gib-owned on the host. Postgres' entrypoint supports running as non-root
|
||||
# provided the data dir ownership matches, which this guarantees.
|
||||
userns_mode: "keep-id:uid=999,gid=999"
|
||||
hostname: postgresql
|
||||
domainname: pg.gbrown.org
|
||||
networks:
|
||||
- nginx-bridge
|
||||
# ⚠️ WIREGUARD ADDRESS ONLY. This was previously '5432:5432', which bound the
|
||||
# database to every interface including the public one — the only thing preventing
|
||||
# the internet from reaching a Postgres auth prompt was firewalld plus the Hetzner
|
||||
# cloud firewall. Two independent firewall rules were the sole barrier in front of
|
||||
# all 14 databases.
|
||||
#
|
||||
# Nothing needs the public binding: every consumer (authentik, infisical, adminer,
|
||||
# the payload/convex app stacks, gitea) resolves `postgresql` over the nginx-bridge
|
||||
# container network and never touches the published port at all. This mapping now
|
||||
# exists purely so you can point a desktop client at it across the tunnel.
|
||||
#
|
||||
# Same pattern as watchtower's API and authentik's 9000 — see AGENTS.md §5.
|
||||
ports: ['192.168.2.2:5432:5432']
|
||||
env_file: .env
|
||||
environment:
|
||||
- POSTGRES_USER
|
||||
- POSTGRES_PASSWORD
|
||||
- POSTGRES_DB
|
||||
- TZ=America/New_York
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./pg_data:/var/lib/postgresql/data:Z
|
||||
# Runs ONLY when pg_data is empty (fresh install). Recreates every role and
|
||||
# database this VPS needs, so the environment can be rebuilt from scratch without
|
||||
# hand-creating them. Does nothing to an existing database — safe to leave mounted.
|
||||
# The committed file is 00-roles-and-databases.sql.TEMPLATE with placeholder
|
||||
# passwords; copy it to .sql and fill in real values from each service's .env.
|
||||
- ./initdb:/docker-entrypoint-initdb.d:Z
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
start_period: 20s
|
||||
interval: 30s
|
||||
retries: 5
|
||||
timeout: 5s
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: PostgreSQL
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/PostgreSQL
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,4 @@
|
||||
RUSTDESK_API_RUSTDESK_JWT_KEY=CHANGE_ME
|
||||
|
||||
# Also set on the live machine
|
||||
RUSTDESK_API_RUSTDESK_KEY=CHANGE_ME
|
||||
@@ -0,0 +1,89 @@
|
||||
# RustDesk Server — VPS (ROOTLESS PODMAN) port of the home server's ~/Server/rustdesk,
|
||||
# migrated 2026-08-12.
|
||||
#
|
||||
# ⚠️ THIS SERVICE CANNOT GO BEHIND CLOUDFLARE'S PROXY.
|
||||
# RustDesk's ID/relay protocol is raw TCP/UDP on 21115-21119. Cloudflare's proxy only
|
||||
# carries HTTP/HTTPS; raw TCP needs Spectrum, which is not on this plan. So
|
||||
# rustdesk.gbrown.org MUST be a grey-cloud (DNS-only) record pointing at
|
||||
# 178.156.197.55, and these ports must be open in firewalld's public zone.
|
||||
#
|
||||
# Consequence, accepted deliberately: this publishes the VPS's real IP. That is
|
||||
# tolerable because the firewall still restricts 80/443 to Cloudflare ranges, so
|
||||
# knowing the IP does not grant access to any of the web services.
|
||||
#
|
||||
# The server keypair in ./volumes/server/id_ed25519 was carried over from the home
|
||||
# server. Its public half matches RUSTDESK_API_RUSTDESK_KEY below
|
||||
# (WLgvHhau6aa5nDPQutTHeQBpIrOEb8aPXByVBWQwkKc=), which is what lets existing clients
|
||||
# reconnect without being re-paired. NEVER regenerate it.
|
||||
#
|
||||
# Rootless adaptations:
|
||||
# - :Z on both bind mounts (SELinux Enforcing); each is exclusive to this container.
|
||||
# - All published ports are >1024 so no privileged-port handling is needed.
|
||||
# - The image starts as root under s6 and drops privileges internally, so no
|
||||
# userns_mode is required.
|
||||
#
|
||||
# PORT REFERENCE (all must be open in the firewall):
|
||||
# 21114/tcp web UI + API 21117/tcp hbbr relay
|
||||
# 21115/tcp hbbs NAT type test 21118/tcp websocket (web client)
|
||||
# 21116/tcp hbbs ID registration 21119/tcp websocket relay
|
||||
# 21116/udp hbbs heartbeat <-- UDP, easy to forget
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
rustdesk-server:
|
||||
image: docker.io/lejianwen/rustdesk-server-s6:latest
|
||||
container_name: rustdesk-server
|
||||
hostname: rustdesk-server
|
||||
domainname: rustdesk.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
ports:
|
||||
- 21114:21114
|
||||
- 21115:21115
|
||||
- 21116:21116
|
||||
- 21116:21116/udp
|
||||
- 21117:21117
|
||||
- 21118:21118
|
||||
- 21119:21119
|
||||
environment:
|
||||
- MUST_LOGIN=Y
|
||||
- TZ=America/New_York
|
||||
# SPLIT HOSTNAMES — this is deliberate, do not "simplify" it back to one name.
|
||||
# relay.gbrown.org DNS-only (grey cloud) -> 178.156.197.55
|
||||
# Carries the raw TCP/UDP protocol on 21115-21119.
|
||||
# Cloudflare's proxy only handles HTTP/HTTPS, so this
|
||||
# hostname MUST bypass it. That publishes the VPS IP,
|
||||
# which is acceptable: the firewall still restricts
|
||||
# 80/443 to Cloudflare ranges, so knowing the IP grants
|
||||
# no access to any web service.
|
||||
# rustdesk.gbrown.org Proxied -> NPM -> rustdesk-server:21114
|
||||
# The web console and API, over TLS via Cloudflare.
|
||||
# Keeping the API on the proxied name is what avoids sending login
|
||||
# credentials over plain HTTP.
|
||||
# RELAY is read by the s6 run script as `hbbs -r $RELAY`. It is what hbbs
|
||||
# hands back to clients when P2P hole-punching fails, so it MUST be set here:
|
||||
# the RUSTDESK_API_* vars below only configure the web console/API, not hbbs.
|
||||
# Left unset, the image defaults to `relay.example.com`, which resolves to
|
||||
# nothing -- so every connection needing a relay (i.e. every off-LAN,
|
||||
# off-WireGuard client behind CGNAT) silently fails.
|
||||
- RELAY=relay.gbrown.org
|
||||
- RUSTDESK_API_RUSTDESK_ID_SERVER=relay.gbrown.org
|
||||
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=relay.gbrown.org
|
||||
- RUSTDESK_API_RUSTDESK_API_SERVER=https://rustdesk.gbrown.org
|
||||
- RUSTDESK_API_RUSTDESK_KEY=${RUSTDESK_API_RUSTDESK_KEY}
|
||||
- RUSTDESK_API_RUSTDESK_JWT_KEY=${RUSTDESK_API_RUSTDESK_JWT_KEY}
|
||||
- RUSTDESK_API_LANG=en
|
||||
- RUSTDESK_API_APP_DISABLE_PWD_LOGIN=true
|
||||
- RUSTDESK_API_ADMIN_TITLE=Gib's Rustdesk
|
||||
- RUSTDESK_API_ADMIN_HELLO=<h1>Welcome to Gib's Rustdesk<h1>
|
||||
- ENCRYPTED_ONLY=1
|
||||
- RUSTDESK_API_RUSTDESK_WEBCLIENT_MAGIC_QUERYONLINE=1
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/server:/data:Z
|
||||
- ./volumes/api:/app/data:Z
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Rustdesk
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Rustdesk
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,69 @@
|
||||
AUTH_AUTHENTIK_ID=CHANGE_ME
|
||||
AUTH_AUTHENTIK_ISSUER="https://auth.gbrown.org/application/o/spoon/"
|
||||
AUTH_AUTHENTIK_SECRET=CHANGE_ME
|
||||
AUTH_GITHUB_ID="Iv23liygrKjd17rba96x"
|
||||
AUTH_GITHUB_SECRET=CHANGE_ME
|
||||
BACKEND_CONTAINER_NAME="spoon-backend"
|
||||
BACKEND_DOMAIN="convex.spoon.gbrown.org"
|
||||
BACKEND_PORT="3210"
|
||||
BACKEND_TAG="latest"
|
||||
CONVEX_CLOUD_ORIGIN="https://api.spoon.gbrown.org"
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY=CHANGE_ME
|
||||
CONVEX_SELF_HOSTED_URL="https://api.spoon.gbrown.org"
|
||||
CONVEX_SITE_ORIGIN="https://convex.spoon.gbrown.org"
|
||||
CONVEX_SITE_URL="https://spoon.gbrown.org"
|
||||
DASHBOARD_CONTAINER_NAME="spoon-dashboard"
|
||||
DASHBOARD_DOMAIN="dashboard.spoon.gbrown.org"
|
||||
DASHBOARD_PORT="6791"
|
||||
DASHBOARD_TAG="latest"
|
||||
DISABLE_BEACON="true"
|
||||
DO_NOT_REQUIRE_SSL="true"
|
||||
GITHUB_APP_CLIENT_ID="Iv23liygrKjd17rba96x"
|
||||
GITHUB_APP_CLIENT_SECRET=CHANGE_ME
|
||||
GITHUB_APP_ID="4111484"
|
||||
GITHUB_APP_INSTALLATION_ID="141786602"
|
||||
GITHUB_APP_OWNER="gibbyb"
|
||||
GITHUB_APP_PRIVATE_KEY=CHANGE_ME
|
||||
GITHUB_APP_SLUG="spoon-gbrown"
|
||||
GITHUB_APP_WEBHOOK_SECRET=CHANGE_ME
|
||||
INSTANCE_NAME="convex"
|
||||
NETWORK="nginx-bridge"
|
||||
NEXT_CONTAINER_NAME="spoon-next"
|
||||
NEXT_DOMAIN="spoon.gbrown.org"
|
||||
NEXT_PORT="3000"
|
||||
NEXT_PUBLIC_CONVEX_URL="https://api.spoon.gbrown.org"
|
||||
NEXT_PUBLIC_DEPLOYMENT_URL="https://api.spoon.gbrown.org"
|
||||
NEXT_PUBLIC_PLAUSIBLE_URL="https://plausible.gbrown.org"
|
||||
NEXT_PUBLIC_SENTRY_DSN="https://[email protected]/8"
|
||||
NEXT_PUBLIC_SENTRY_ORG="sentry"
|
||||
NEXT_PUBLIC_SENTRY_PROJECT_NAME="spoon-nextjs"
|
||||
NEXT_PUBLIC_SENTRY_URL="https://sentry.gbrown.org"
|
||||
NEXT_PUBLIC_SITE_URL="https://spoon.gbrown.org"
|
||||
NODE_ENV="production"
|
||||
POSTGRES_CONTAINER_NAME="spoon-postgres"
|
||||
POSTGRES_DB="spoon_convex"
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_PORT="5432"
|
||||
POSTGRES_URL=CHANGE_ME
|
||||
POSTGRES_USER="spoon"
|
||||
REDACT_LOGS_TO_CLIENT="true"
|
||||
SENTRY_AUTH_TOKEN=CHANGE_ME
|
||||
SITE_PROXY_PORT="3211"
|
||||
SPOON_ENCRYPTION_KEY=CHANGE_ME
|
||||
SPOON_WORKER_TOKEN=CHANGE_ME
|
||||
USESEND_API_KEY=CHANGE_ME
|
||||
USESEND_FROM_EMAIL="Spoon Admin <[email protected]>"
|
||||
USESEND_URL="https://usesend.gbrown.org"
|
||||
SPOON_AGENT_JOB_IMAGE=git.gbrown.org/gib/spoon-agent-job:latest
|
||||
SPOON_AGENT_JOB_TIMEOUT_MS="1800000"
|
||||
SPOON_AGENT_MAX_CONCURRENT_JOBS="1"
|
||||
SPOON_AGENT_NETWORK=nginx-bridge
|
||||
SPOON_AGENT_RUNTIME=docker
|
||||
SPOON_AGENT_WORKDIR=/var/lib/spoon-agent/work
|
||||
SPOON_AGENT_WORKER_ID=production-worker
|
||||
SPOON_AGENT_WORKER_HTTP_PORT="3921"
|
||||
SPOON_AGENT_WORKER_INTERNAL_TOKEN=CHANGE_ME
|
||||
SPOON_AGENT_WORKER_URL=http://spoon-agent-worker:3921
|
||||
|
||||
# Also set on the live machine
|
||||
PORT=CHANGE_ME
|
||||
@@ -0,0 +1,114 @@
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
spoon-next:
|
||||
image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest
|
||||
container_name: ${NEXT_CONTAINER_NAME}
|
||||
hostname: ${NEXT_CONTAINER_NAME}
|
||||
domainname: ${NEXT_DOMAIN}
|
||||
networks:
|
||||
- ${NETWORK:-nginx-bridge}
|
||||
pull_policy: missing
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV}
|
||||
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
||||
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:${NEXT_PORT:-3000}}
|
||||
- NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-http://${BACKEND_CONTAINER_NAME:-spoon-backend}:${BACKEND_PORT:-3210}}
|
||||
- NEXT_PUBLIC_PLAUSIBLE_URL=${NEXT_PUBLIC_PLAUSIBLE_URL:-https://plausible.gbrown.org}
|
||||
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||
- NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
|
||||
- NEXT_PUBLIC_SENTRY_ORG=${NEXT_PUBLIC_SENTRY_ORG:-sentry}
|
||||
- NEXT_PUBLIC_SENTRY_PROJECT_NAME=${NEXT_PUBLIC_SENTRY_PROJECT_NAME}
|
||||
- SPOON_AGENT_WORKER_URL=${SPOON_AGENT_WORKER_URL:-http://spoon-agent-worker:3921}
|
||||
- SPOON_AGENT_WORKER_INTERNAL_TOKEN=${SPOON_AGENT_WORKER_INTERNAL_TOKEN}
|
||||
- SPOON_WORKER_TOKEN=${SPOON_WORKER_TOKEN}
|
||||
depends_on:
|
||||
- spoon-backend
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=true
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
spoon-agent-worker:
|
||||
image: git.gbrown.org/gib/spoon-agent-worker:latest
|
||||
container_name: spoon-agent-worker
|
||||
hostname: spoon-agent-worker
|
||||
domainname: worker.${NEXT_DOMAIN:-spoon.gbrown.org}
|
||||
networks:
|
||||
- ${NETWORK:-nginx-bridge}
|
||||
pull_policy: missing
|
||||
environment:
|
||||
- GITHUB_APP_ID=${GITHUB_APP_ID}
|
||||
- GITHUB_APP_PRIVATE_KEY=${GITHUB_APP_PRIVATE_KEY}
|
||||
- NEXT_PUBLIC_CONVEX_URL=https://api.spoon.gbrown.org
|
||||
- SPOON_AGENT_WORKER_ID=${SPOON_AGENT_WORKER_ID:-production-worker}
|
||||
- SPOON_AGENT_JOB_IMAGE=${SPOON_AGENT_JOB_IMAGE:-git.gbrown.org/gib/spoon-agent-job:latest}
|
||||
- SPOON_AGENT_RUNTIME=docker
|
||||
- SPOON_AGENT_NETWORK=${NETWORK:-nginx-bridge}
|
||||
- SPOON_AGENT_WORKDIR=/var/lib/spoon-agent/work
|
||||
- SPOON_AGENT_HOST_WORKDIR=/var/lib/spoon-agent/work
|
||||
- SPOON_AGENT_WORKER_HTTP_PORT=${SPOON_AGENT_WORKER_HTTP_PORT:-3921}
|
||||
- SPOON_AGENT_WORKER_INTERNAL_TOKEN=${SPOON_AGENT_WORKER_INTERNAL_TOKEN}
|
||||
- SPOON_AGENT_MAX_CONCURRENT_JOBS=${SPOON_AGENT_MAX_CONCURRENT_JOBS:-1}
|
||||
- SPOON_AGENT_JOB_TIMEOUT_MS=${SPOON_AGENT_JOB_TIMEOUT_MS:-1800000}
|
||||
- SPOON_WORKER_TOKEN=${SPOON_WORKER_TOKEN}
|
||||
volumes:
|
||||
- /run/user/1000/podman/podman.sock:/var/run/docker.sock
|
||||
- ./volumes/agent-work:/var/lib/spoon-agent/work:z
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=true
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- label:disable
|
||||
spoon-backend:
|
||||
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
container_name: ${BACKEND_CONTAINER_NAME:-spoon-backend}
|
||||
hostname: ${BACKEND_CONTAINER_NAME:-spoon-backend}
|
||||
domainname: ${BACKEND_DOMAIN:-convex.spoon.gbrown.org}
|
||||
networks:
|
||||
- ${NETWORK:-nginx-bridge}
|
||||
volumes:
|
||||
- ./volumes/convex:/convex/data:z
|
||||
pull_policy: missing
|
||||
environment:
|
||||
- INSTANCE_NAME=${INSTANCE_NAME}
|
||||
- CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-spoon-backend}:${BACKEND_PORT:-3210}}
|
||||
- CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-spoon-backend}:${SITE_PROXY_PORT:-3211}}
|
||||
- DISABLE_BEACON=${DISABLE_BEACON:-true}
|
||||
- REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-true}
|
||||
- DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-false}
|
||||
- POSTGRES_URL=${POSTGRES_URL}
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=true
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: curl -f http://localhost:3210/version
|
||||
interval: 5s
|
||||
start_period: 10s
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
spoon-dashboard:
|
||||
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
container_name: ${DASHBOARD_CONTAINER_NAME:-spoon-dashboard}
|
||||
hostname: ${DASHBOARD_CONTAINER_NAME:-spoon-dashboard}
|
||||
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-spoon.gbrown.org}}
|
||||
networks:
|
||||
- ${NETWORK:-nginx-bridge}
|
||||
pull_policy: missing
|
||||
environment:
|
||||
- NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://${BACKEND_CONTAINER_NAME:-spoon-backend}:${PORT:-3210}}
|
||||
depends_on:
|
||||
spoon-backend:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=true
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Spoon
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Spoon
|
||||
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,63 @@
|
||||
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
|
||||
# Keep this file up-to-date when you add new variables to \`.env\`.
|
||||
# This file will be committed to version control, so make sure not to have any secrets in it.
|
||||
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
|
||||
|
||||
## Next App ##
|
||||
NODE_ENV=production
|
||||
SENTRY_AUTH_TOKEN=CHANGE_ME
|
||||
PAYLOAD_SECRET=CHANGE_ME
|
||||
PAYLOAD_DB_URL=CHANGE_ME
|
||||
NEXT_PUBLIC_SITE_URL=https://stpeteit.com
|
||||
NEXT_PUBLIC_CONVEX_URL=https://api.stpeteit.com # convex-backend:3210
|
||||
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.gbrown.org
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/5
|
||||
NEXT_PUBLIC_SENTRY_URL=https://sentry.gbrown.org
|
||||
NEXT_PUBLIC_SENTRY_ORG=sentry
|
||||
NEXT_PUBLIC_SENTRY_PROJECT_NAME=stpeteit-next
|
||||
|
||||
## Convex ##
|
||||
CONVEX_SELF_HOSTED_URL=https://api.stpeteit.com # convex-backend:3210
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY=CHANGE_ME
|
||||
# Convex Auth
|
||||
CONVEX_SITE_URL=https://stpeteit.com # convex-backend:3211
|
||||
USESEND_API_KEY=CHANGE_ME
|
||||
USESEND_URL=https://usesend.gbrown.org
|
||||
USESEND_FROM_EMAIL='St Pete IT Admin <[email protected]>'
|
||||
AUTH_AUTHENTIK_ID=CHANGE_ME
|
||||
AUTH_AUTHENTIK_SECRET=CHANGE_ME
|
||||
AUTH_AUTHENTIK_ISSUER=https://auth.gbrown.org/application/o/stpeteit/
|
||||
|
||||
## Docker Compose Variables for Next App ##
|
||||
NETWORK=nginx-bridge
|
||||
NEXT_CONTAINER_NAME=stpeteit-next
|
||||
NEXT_DOMAIN=stpeteit.com
|
||||
#NEXT_PORT=3000
|
||||
|
||||
## Docker Compose Variables for Self hosted Convex ##
|
||||
BACKEND_TAG=latest
|
||||
DASHBOARD_TAG=latest
|
||||
BACKEND_CONTAINER_NAME=stpeteit-backend
|
||||
DASHBOARD_CONTAINER_NAME=stpeteit-dashboard
|
||||
BACKEND_DOMAIN=convex.stpeteit.com
|
||||
DASHBOARD_DOMAIN=dashboard.stpeteit.com
|
||||
INSTANCE_NAME=stpeteit-convex
|
||||
#INSTANCE_SECRET=
|
||||
CONVEX_CLOUD_ORIGIN=https://api.stpeteit.com
|
||||
CONVEX_SITE_ORIGIN=https://convex.stpeteit.com
|
||||
NEXT_PUBLIC_DEPLOYMENT_URL=https://api.stpeteit.com
|
||||
DISABLE_BEACON=true
|
||||
REDACT_LOGS_TO_CLIENT=true
|
||||
DO_NOT_REQUIRE_SSL=true
|
||||
POSTGRES_URL=CHANGE_ME
|
||||
#BACKEND_PORT=
|
||||
#DASHBOARD_PORT
|
||||
#SITE_PROXY_PORT=
|
||||
#ACTIONS_USER_TIMEOUT_SECS=
|
||||
#RUST_LOG=
|
||||
#RUST_BACKTRACE=
|
||||
|
||||
# Also set on the live machine
|
||||
BACKEND_PORT=CHANGE_ME
|
||||
PORT=CHANGE_ME
|
||||
SITE_PROXY_PORT=CHANGE_ME
|
||||
@@ -0,0 +1,101 @@
|
||||
networks:
|
||||
nginx-bridge: # Change to network you plan to use
|
||||
external: true
|
||||
|
||||
services:
|
||||
stpeteit-next:
|
||||
image: git.gbrown.org/gib/stpeteit-next:latest
|
||||
container_name: stpeteit-next
|
||||
hostname: stpeteit-next
|
||||
domainname: ${NEXT_DOMAIN}
|
||||
networks: ['${NETWORK:-nginx-bridge}']
|
||||
#ports: ['${NEXT_PORT}:${NEXT_PORT}']
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-development}
|
||||
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
||||
- PAYLOAD_SECRET=${PAYLOAD_SECRET}
|
||||
- PAYLOAD_DB_URL=${PAYLOAD_DB_URL}
|
||||
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:${NEXT_PORT:-3000}}
|
||||
- NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${BACKEND_PORT:-3210}}
|
||||
- NEXT_PUBLIC_PLAUSIBLE_URL=${NEXT_PUBLIC_PLAUSIBLE_URL:-https://plausible.stpeteit.com}
|
||||
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||
- NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
|
||||
- NEXT_PUBLIC_SENTRY_ORG=${NEXT_PUBLIC_SENTRY_ORG:-sentry}
|
||||
- NEXT_PUBLIC_SENTRY_PROJECT_NAME=${NEXT_PUBLIC_SENTRY_PROJECT_NAME}
|
||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||
depends_on: ['stpeteit-backend']
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
stpeteit-backend:
|
||||
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
container_name: ${BACKEND_CONTAINER_NAME:-stpeteit-backend}
|
||||
hostname: ${BACKEND_CONTAINER_NAME:-stpeteit-backend}
|
||||
domainname: ${BACKEND_DOMAIN:-convex.stpeteit.com}
|
||||
networks: ['${NETWORK:-nginx-bridge}']
|
||||
#user: '1000:1000'
|
||||
#ports: ['${BACKEND_PORT:-3210}:3210','${SITE_PROXY_PORT:-3211}:3211']
|
||||
volumes: [./data:/convex/data:z]
|
||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||
environment:
|
||||
- INSTANCE_NAME
|
||||
#- INSTANCE_SECRET
|
||||
- CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${BACKEND_PORT:-3210}}
|
||||
- CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${SITE_PROXY_PORT:-3211}}
|
||||
- DISABLE_BEACON=${DISABLE_BEACON:-true}
|
||||
- REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-true}
|
||||
- DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-false}
|
||||
- POSTGRES_URL=${POSTGRES_URL}
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: curl -f http://localhost:3210/version
|
||||
interval: 5s
|
||||
start_period: 10s
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
|
||||
stpeteit-dashboard:
|
||||
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
container_name: ${DASHBOARD_CONTAINER_NAME:-stpeteit-dashboard}
|
||||
hostname: ${DASHBOARD_CONTAINER_NAME:-stpeteit-dashboard}
|
||||
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-convex.stpete.com}}
|
||||
networks: ['${NETWORK:-nginx-bridge}']
|
||||
#user: 1000:1000
|
||||
#ports: ['${DASHBOARD_PORT:-6791}:6791']
|
||||
labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||
environment:
|
||||
- NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${PORT:-3210}}
|
||||
depends_on:
|
||||
stpeteit-backend:
|
||||
condition: service_healthy
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
|
||||
#convexmonorepo-postgresql:
|
||||
#image: postgres:17
|
||||
#container_name: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres}
|
||||
#hostname: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres}
|
||||
#domainname: postgres.${NEXT_DOMAIN:-convexmonorepo.gbrown.org}
|
||||
#networks: ['${NETWORK:-nginx-bridge}']
|
||||
#ports: ['5432:5432']
|
||||
#environment:
|
||||
#- POSTGRES_USER=${POSTGRES_USER:-convexmonorepo}
|
||||
#- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
#- POSTGRES_DB=${POSTGRES_DB:-convexmonorepo_payload}
|
||||
#labels: ['com.centurylinklabs.watchtower.enable=true']
|
||||
#volumes: ['./volumes/postgres:/var/lib/postgresql/data:Z']
|
||||
#tty: true
|
||||
#stdin_open: true
|
||||
#restart: unless-stopped
|
||||
#healthcheck:
|
||||
#test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
#start_period: 20s
|
||||
#interval: 30s
|
||||
#retries: 5
|
||||
#timeout: 5s
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: StPeteIT
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/StPeteIT
|
||||
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,88 @@
|
||||
# Tools — small self-hosted utilities. VPS (ROOTLESS PODMAN) port of the home server's
|
||||
# ~/Server/tools stack, migrated 2026-08-12.
|
||||
#
|
||||
# Rootless adaptations:
|
||||
# - :Z on every bind mount (SELinux Enforcing). All mounts here are exclusive to a
|
||||
# single container, so uppercase :Z is correct -- see AGENTS.md §6.
|
||||
# - No ports published: NPM proxies to each container by name over nginx-bridge.
|
||||
#
|
||||
# All four images start as root and drop privileges internally, so no userns_mode is
|
||||
# needed (AGENTS.md §3.2).
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
convertx:
|
||||
image: ghcr.io/c4illin/convertx:latest
|
||||
container_name: convertx
|
||||
hostname: convertx
|
||||
domainname: convert.tools.gbrown.org
|
||||
networks: ["nginx-bridge"]
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- ALLOW_UNAUTHENTICATED=true
|
||||
- ACCOUNT_REGISTRATION=false
|
||||
- HIDE_HISTORY=true
|
||||
volumes:
|
||||
- ./volumes/convertx:/app/data:Z
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
|
||||
it-tools:
|
||||
image: docker.io/corentinth/it-tools:latest
|
||||
container_name: it-tools
|
||||
hostname: it-tools
|
||||
domainname: tools.gbrown.org
|
||||
networks: ["nginx-bridge"]
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
|
||||
bento-pdf:
|
||||
image: ghcr.io/alam00000/bentopdf:latest
|
||||
container_name: bento-pdf
|
||||
hostname: bento-pdf
|
||||
domainname: bento.tools.gbrown.org
|
||||
networks: ["nginx-bridge"]
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- VITE_BRAND_NAME="GibPDF"
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# pdf-tools:
|
||||
# image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
|
||||
# container_name: pdf-tools
|
||||
# hostname: pdf-tools
|
||||
# domainname: pdf.tools.gbrown.org
|
||||
# networks: ["nginx-bridge"]
|
||||
# environment:
|
||||
# - TZ=America/New_York
|
||||
# - LANGS=en_US
|
||||
# - DOCKER_ENABLE_SECURITY=true
|
||||
# volumes:
|
||||
# - ./volumes/pdf-tools/trainingData:/usr/share/tessdata:Z
|
||||
# - ./volumes/pdf-tools/extraConfigs:/configs:Z
|
||||
# - ./volumes/pdf-tools/customFiles:/customFiles:Z
|
||||
# - ./volumes/pdf-tools/logs:/logs/:Z
|
||||
# - ./volumes/pdf-tools/pipeline:/pipeline/:Z
|
||||
# labels:
|
||||
# com.centurylinklabs.watchtower.enable: "true"
|
||||
# tty: true
|
||||
# restart: unless-stopped
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Tools
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Tools
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,26 @@
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
uptime:
|
||||
image: louislam/uptime-kuma:latest
|
||||
container_name: uptime
|
||||
hostname: uptime
|
||||
domainname: uptime.gbrown.org
|
||||
networks: [nginx-bridge]
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
volumes:
|
||||
- ./volume:/app/data:Z
|
||||
- /run/user/1000/podman/podman.sock:/var/run/docker.sock
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
security_opt: ['seccomp:unconfined','label:disable']
|
||||
# WireGuard address only — this admin UI has no business on the public
|
||||
# interface. It has its own login and the Hetzner firewall does not admit this
|
||||
# port, so this is defence in depth: it removes the dependency on a firewall rule
|
||||
# set that lives in a web console. See AGENTS.md §13.2.
|
||||
ports: ['192.168.2.2:3001:3001']
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Uptime
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Uptime
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,26 @@
|
||||
API_RATE_LIMIT=1
|
||||
AUTH_AUTHENTIK_ID=CHANGE_ME
|
||||
AUTH_AUTHENTIK_SECRET=CHANGE_ME
|
||||
AUTH_AUTHENTIK_ISSUER="https://auth.gbrown.org/application/o/gibsend/"
|
||||
AWS_ACCESS_KEY=CHANGE_ME
|
||||
AWS_SECRET_KEY=CHANGE_ME
|
||||
AWS_DEFAULT_REGION=us-east-2
|
||||
DATABASE_URL=CHANGE_ME
|
||||
GITHUB_ID=Ov23liz34colyJed9t9S
|
||||
GITHUB_SECRET=CHANGE_ME
|
||||
MINIO_ROOT_USER=usesend
|
||||
MINIO_ROOT_PASSWORD=CHANGE_ME
|
||||
NEXTAUTH_SECRET=CHANGE_ME
|
||||
NEXT_PUBLIC_IS_CLOUD=false
|
||||
POSTGRES_USER=usesend
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_DB=usesend
|
||||
REDIS_URL=redis://usesend-redis:6379
|
||||
SMTP_HOST=smtp.usesend.gbrown.org
|
||||
SMTP_USER=usesend
|
||||
USESEND_PORT=3000
|
||||
USESEND_URL=https://usesend.gbrown.org
|
||||
|
||||
# Also set on the live machine
|
||||
AWS_ACCESS_KEY_ID=CHANGE_ME
|
||||
AWS_SECRET_ACCESS_KEY=CHANGE_ME
|
||||
@@ -0,0 +1,131 @@
|
||||
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:latest
|
||||
container_name: usesend-smtp-server
|
||||
hostname: usesend-smtp
|
||||
networks:
|
||||
nginx-bridge:
|
||||
# The proxy REQUIRES STARTTLS before it will accept AUTH, so clients must
|
||||
# reach it by a name the certificate actually covers. The wildcard is
|
||||
# *.gbrown.org, which matches ONE label -- hence smtp-usesend, not
|
||||
# smtp.usesend.
|
||||
#
|
||||
# Resolution: podman's aardvark-dns answers this name authoritatively for
|
||||
# containers ON nginx-bridge, handing back the container IP. NPM is not
|
||||
# involved at all -- it only reverse-proxies HTTP(S), and SMTP ports are
|
||||
# never published to the host, so the proxy is unreachable from outside.
|
||||
#
|
||||
# NOTE: there IS a wildcard *.gbrown.org record in Cloudflare, so this name
|
||||
# also resolves publicly (to Cloudflare, which serves nothing for it). The
|
||||
# consequence: if this container is STOPPED, aardvark-dns stops answering,
|
||||
# the lookup falls through to that public record, and clients dial
|
||||
# Cloudflare on 587 and hang until timeout instead of failing fast.
|
||||
aliases:
|
||||
# Both names resolve to this container and BOTH validate against npm-43,
|
||||
# which carries *.gbrown.org and *.usesend.gbrown.org. smtp.usesend is the
|
||||
# name UseSend's own UI advertises and is the target to standardise on;
|
||||
# smtp-usesend stays until every consumer has been moved across.
|
||||
- smtp-usesend.gbrown.org
|
||||
- smtp.usesend.gbrown.org
|
||||
env_file: .env
|
||||
environment:
|
||||
- SMTP_AUTH_USERNAME=${SMTP_USER:?err}
|
||||
- USESEND_BASE_URL=${USESEND_URL:?err}
|
||||
# The image's built-in default cert cannot complete a TLS 1.3 handshake at all
|
||||
# ("no suitable signature algorithm"), which silently made STARTTLS -- and so
|
||||
# ALL inbound SMTP -- impossible. These point it at the real wildcard cert.
|
||||
- USESEND_API_CERT_PATH=/certs/fullchain.pem
|
||||
- USESEND_API_KEY_PATH=/certs/privkey.pem
|
||||
ports:
|
||||
# Bound to the WireGuard address ONLY, never 0.0.0.0 (AGENTS.md §13.2). This is an
|
||||
# authenticated relay; putting it on the public interface would invite the internet to
|
||||
# brute-force it, and contradicts the Cloudflare-only posture in §7.
|
||||
# The `Home` wg interface sits in firewalld's `trusted` zone, so no rule is needed.
|
||||
# Rootless can bind these because net.ipv4.ip_unprivileged_port_start = 80.
|
||||
# Consumers on the home server reach it as smtp-usesend.gbrown.org (UniFi DNS maps
|
||||
# that name to 192.168.2.2) so the *.gbrown.org cert still validates by hostname.
|
||||
- "192.168.2.2:587:587"
|
||||
- "192.168.2.2:465:465"
|
||||
volumes:
|
||||
# Copied from NPM's letsencrypt store, NOT bind-mounted from it: NPM mounts that
|
||||
# tree with :Z (private SELinux category), so a second consumer would fight it.
|
||||
# TRADE-OFF: copies go stale. Refresh them when NPM renews (see certs/README).
|
||||
- ./certs:/certs:ro,Z
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
usesend:
|
||||
image: usesend/usesend:latest
|
||||
container_name: usesend
|
||||
hostname: usesend
|
||||
domainname: ${USESEND_URL:?err}
|
||||
networks:
|
||||
- nginx-bridge
|
||||
env_file: .env
|
||||
environment:
|
||||
- COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
- PORT=${USESEND_PORT:-3000}
|
||||
- DATABASE_URL=${DATABASE_URL:?err}
|
||||
- NEXTAUTH_URL=${USESEND_URL:?err}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:?err}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_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: false
|
||||
stdin_open: false
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: UseSend
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/UseSend
|
||||
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,6 @@
|
||||
ADMIN_TOKEN=CHANGE_ME
|
||||
|
||||
# Also set on the live machine
|
||||
USESEND_API_KEY=CHANGE_ME
|
||||
SSO_CLIENT_ID=CHANGE_ME
|
||||
SSO_CLIENT_SECRET=CHANGE_ME
|
||||
@@ -0,0 +1,77 @@
|
||||
# Vaultwarden — VPS (ROOTLESS PODMAN) port of the home server's ~/Server/vaultwarden,
|
||||
# staged 2026-08-12.
|
||||
#
|
||||
# This is the live instance serving vault.gbrown.org. The home server copy is retired.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# ADMIN_TOKEN lives in the mode-600 .env (which ~/Server/backup copies, so restores
|
||||
# have it). volume/config.json also holds admin_token and OVERRIDES the env value;
|
||||
# saving settings in the /admin panel writes it there. Keep the two in sync.
|
||||
|
||||
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:?set ADMIN_TOKEN in .env}
|
||||
# ── SSO via Authentik (app slug "vaultwarden", admin group only) ─────
|
||||
# SSO replaces the email step only. The vault is still unlocked with the
|
||||
# master password, because that is what the vault is encrypted with.
|
||||
# SSO_ONLY stays off so password login keeps working for everyone.
|
||||
# Existing accounts link by email, and Authentik must send
|
||||
# email_verified=true for that (its "email_custom" scope mapping does).
|
||||
# The authority needs its trailing slash.
|
||||
- SSO_ENABLED=true
|
||||
- SSO_ONLY=false
|
||||
- SSO_AUTHORITY=https://auth.gbrown.org/application/o/vaultwarden/
|
||||
- SSO_SCOPES=openid profile email offline_access
|
||||
- SSO_CLIENT_ID=${SSO_CLIENT_ID:?set SSO_CLIENT_ID in .env}
|
||||
- SSO_CLIENT_SECRET=${SSO_CLIENT_SECRET:?set SSO_CLIENT_SECRET in .env}
|
||||
# ── SMTP via the UseSend proxy on this host ───────────────────────────
|
||||
# These used to live in volume/config.json (written by the admin panel).
|
||||
# config.json OVERRIDES env, so those keys were removed from it; leaving
|
||||
# them would silently win over everything below.
|
||||
#
|
||||
# Reached by the smtp.usesend.gbrown.org network alias, not the container
|
||||
# name, so the *.gbrown.org wildcard cert passes hostname verification --
|
||||
# which is why ACCEPT_INVALID_* can stay false.
|
||||
#
|
||||
# FROM must be on a UseSend-verified domain. Bare gbrown.org is NOT
|
||||
# verified; vault.gbrown.org is.
|
||||
- SMTP_HOST=smtp.usesend.gbrown.org
|
||||
- SMTP_PORT=587
|
||||
- SMTP_SECURITY=starttls
|
||||
- SMTP_USERNAME=usesend
|
||||
- SMTP_PASSWORD=${USESEND_API_KEY:?set USESEND_API_KEY in .env}
|
||||
- [email protected]
|
||||
- SMTP_FROM_NAME=Gib's Vault Admin
|
||||
- SMTP_TIMEOUT=15
|
||||
- SMTP_ACCEPT_INVALID_CERTS=false
|
||||
- SMTP_ACCEPT_INVALID_HOSTNAMES=false
|
||||
- SMTP_EMBED_IMAGES=true
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volume:/data:Z
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Vaultwarden
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Vaultwarden
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
Reference in new issue
Block a user