Panama learns what a server is: from a root login to running containers
A machine's role is now the interview's first question and the one answer Panama records. Servers get the same shell minus the screen: core packages, nvm, Bun, Claude Code and Codex (desktops get Codex too), linger, rootless ports from 80, firewalld, the nginx-bridge network, and a nightly image updater that replaced watchtower for cause. server/containers/ carries junior's 23 compose services -- secrets moved to per-machine .env files that never enter this public repo, every transformed compose proven to render byte-identical to what is live. 'panama server' enables, disables and relinks them; nothing here restarts a running service. 'boot --server' walks a fresh VPS from its root login to a normal install. Five new contracts pin the secrets rule, the catalog's shape, panama-server's behavior, the role plumbing, and the dotfile classification. Claude-Session: https://claude.ai/code/session_01NU5JGiN3JfzqrLQB6wmJ1E
This commit is contained in:
@@ -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,20 @@
|
||||
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]
|
||||
@@ -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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,70 @@
|
||||
# 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
|
||||
# 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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,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 '<see this service .env on the machine>';
|
||||
# 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=${EMAIL_SERVER_PASSWORD}
|
||||
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,59 @@
|
||||
# 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=
|
||||
@@ -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 '<see this service .env on the machine>';
|
||||
# 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=${NEXT_PRIVATE_SMTP_PASSWORD}
|
||||
- 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,25 @@
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
gitea:
|
||||
image: docker.io/gitea/gitea:latest
|
||||
container_name: gitea
|
||||
hostname: gitea
|
||||
domainname: git.gbrown.org
|
||||
networks:
|
||||
- nginx-bridge
|
||||
ports:
|
||||
- '2222:22'
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- USER_UID=1001
|
||||
- USER_GID=1003
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
volumes:
|
||||
- ./volume:/data:Z
|
||||
- /home/gib/Media/gitea-packages:/data/gitea/packages:Z
|
||||
- ./ssh:/data/git/.ssh:Z
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
@@ -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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,142 @@
|
||||
# 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
|
||||
@@ -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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,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.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
|
||||
@@ -0,0 +1,76 @@
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,52 @@
|
||||
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_PASSWORD: '<see this service .env on the machine>'
|
||||
#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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,2 @@
|
||||
PENPOT_SECRET_KEY=CHANGE_ME
|
||||
PENPOT_DATABASE_PASSWORD=CHANGE_ME
|
||||
@@ -0,0 +1,120 @@
|
||||
# Penpot — VPS (ROOTLESS PODMAN). Ported from the home server 2026-08-12.
|
||||
#
|
||||
# ⚠️ NOT STARTED. No systemd unit is enabled. It has never run on either machine, so
|
||||
# there is no data to migrate. Bring it up with:
|
||||
# systemctl --user enable --now podman-penpot.service
|
||||
#
|
||||
# BEFORE FIRST START, create its role and database on the SHARED postgres (this stack
|
||||
# no longer ships its own):
|
||||
# podman exec -i postgresql psql -U npm <<'SQL'
|
||||
# CREATE ROLE penpot LOGIN PASSWORD '<see this service .env on the machine>';
|
||||
# CREATE DATABASE penpot OWNER penpot;
|
||||
# SQL
|
||||
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql.
|
||||
#
|
||||
# NOTE: the home version used postgres:15-alpine with --data-checksums. The shared
|
||||
# instance is postgres 17 without checksums. Penpot does not care, and since there is
|
||||
# no existing data this is not a migration — but if you ever DO need checksums, they
|
||||
# can only be set at initdb time for the whole cluster.
|
||||
#
|
||||
# Redis stays local to this stack: Penpot uses it for transient session and
|
||||
# notification state only, so it is not worth centralising.
|
||||
#
|
||||
# Rootless adaptations from the home version:
|
||||
# - Dropped its private postgres:15-alpine; uses the shared instance (AGENTS.md §2).
|
||||
# - PENPOT_DATABASE_URI repointed from penpot-db to the shared postgresql.
|
||||
# - :Z on the assets bind mount. It is shared between frontend and backend, so it
|
||||
# uses LOWERCASE :z -- uppercase would give each container a private MCS category
|
||||
# and the second to start would lock the first out (AGENTS.md §6).
|
||||
# - /etc/localtime mounts removed in favour of TZ (AGENTS.md §8).
|
||||
#
|
||||
# Registration is disabled by default (PENPOT_FLAGS disable-registration), so create
|
||||
# the first account from the backend container before you can log in.
|
||||
#
|
||||
# NPM: proxy penpot.gbrown.org -> http://penpot-frontend:8080
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
penpot-frontend:
|
||||
image: docker.io/penpotapp/frontend:latest
|
||||
container_name: penpot-frontend
|
||||
hostname: penpot-frontend
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PENPOT_FLAGS=disable-registration enable-login-with-password disable-smtp
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/assets:/opt/data/assets:z
|
||||
depends_on:
|
||||
- penpot-backend
|
||||
- penpot-exporter
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
penpot-backend:
|
||||
image: docker.io/penpotapp/backend:latest
|
||||
container_name: penpot-backend
|
||||
hostname: penpot-backend
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PENPOT_FLAGS=disable-registration enable-login-with-password disable-smtp enable-prepl-server
|
||||
- PENPOT_PUBLIC_URI=https://penpot.gbrown.org
|
||||
- PENPOT_SECRET_KEY=${PENPOT_SECRET_KEY}
|
||||
# Shared postgres, not a private container
|
||||
- PENPOT_DATABASE_URI=postgresql://postgresql/penpot
|
||||
- PENPOT_DATABASE_USERNAME=penpot
|
||||
- PENPOT_DATABASE_PASSWORD=${PENPOT_DATABASE_PASSWORD}
|
||||
- PENPOT_REDIS_URI=redis://penpot-redis/0
|
||||
- PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
|
||||
- PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets
|
||||
- PENPOT_TELEMETRY_ENABLED=false
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/assets:/opt/data/assets:z
|
||||
depends_on:
|
||||
- penpot-redis
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
penpot-exporter:
|
||||
image: docker.io/penpotapp/exporter:latest
|
||||
container_name: penpot-exporter
|
||||
hostname: penpot-exporter
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PENPOT_PUBLIC_URI=http://penpot-frontend:8080
|
||||
- PENPOT_REDIS_URI=redis://penpot-redis/0
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
depends_on:
|
||||
- penpot-redis
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
penpot-redis:
|
||||
image: docker.io/library/redis:7-alpine
|
||||
container_name: penpot-redis
|
||||
hostname: penpot-redis
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
@@ -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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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 @@
|
||||
RUSTDESK_API_RUSTDESK_JWT_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=WLgvHhau6aa5nDPQutTHeQBpIrOEb8aPXByVBWQwkKc=
|
||||
- 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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,66 @@
|
||||
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
|
||||
@@ -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,58 @@
|
||||
# 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=
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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,22 @@
|
||||
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
|
||||
@@ -0,0 +1,88 @@
|
||||
name: usesend
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
container_name: usesend-redis
|
||||
hostname: usesend-redis
|
||||
networks:
|
||||
- nginx-bridge
|
||||
env_file: .env
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
volumes:
|
||||
- ./volumes/redis:/data:Z
|
||||
command:
|
||||
- redis-server
|
||||
- --maxmemory-policy
|
||||
- noeviction
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: usesend-storage
|
||||
hostname: usesend-storage
|
||||
networks:
|
||||
- nginx-bridge
|
||||
env_file: .env
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
volumes:
|
||||
- ./volumes/minio:/data:Z
|
||||
entrypoint: sh
|
||||
command: -c 'mkdir -p /data/unsend && minio server /data --console-address ':9001' --address ':9002''
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
smtp-server:
|
||||
image: usesend/smtp-proxy:v1.6.6
|
||||
container_name: usesend-smtp-server
|
||||
hostname: usesend-smtp
|
||||
networks:
|
||||
- nginx-bridge
|
||||
env_file: .env
|
||||
environment:
|
||||
- SMTP_AUTH_USERNAME=${SMTP_USER:?err}
|
||||
- USESEND_BASE_URL=${USESEND_URL:?err}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
usesend:
|
||||
image: usesend/usesend:v1.6.6
|
||||
container_name: usesend
|
||||
hostname: usesend
|
||||
domainname: ${USESEND_URL:?err}
|
||||
networks:
|
||||
- nginx-bridge
|
||||
env_file: .env
|
||||
environment:
|
||||
- PORT=${USESEND_PORT:-3000}
|
||||
- DATABASE_URL=${DATABASE_URL:?err}
|
||||
- NEXTAUTH_URL=${USESEND_URL:?err}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
|
||||
- AWS_ACCESS_KEY=${AWS_ACCESS_KEY:?err}
|
||||
- AWS_SECRET_KEY=${AWS_SECRET_KEY:?err}
|
||||
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:?err}
|
||||
- GITHUB_ID=${GITHUB_ID:?err}
|
||||
- GITHUB_SECRET=${GITHUB_SECRET:?err}
|
||||
- REDIS_URL=${REDIS_URL:?err}
|
||||
- NEXT_PUBLIC_IS_CLOUD=${NEXT_PUBLIC_IS_CLOUD:-false}
|
||||
- API_RATE_LIMIT=${API_RATE_LIMIT:-1}
|
||||
- SMTP_HOST=${SMTP_HOST:-smtp.usesend.gbrown.org}
|
||||
- SMTP_USER=${SMTP_USER:-usesend}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
@@ -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 @@
|
||||
ADMIN_TOKEN=CHANGE_ME
|
||||
@@ -0,0 +1,55 @@
|
||||
# Vaultwarden — VPS (ROOTLESS PODMAN) port of the home server's ~/Server/vaultwarden,
|
||||
# staged 2026-08-12.
|
||||
#
|
||||
# ⚠️ STAGED, NOT AUTHORITATIVE. The home instance is still live and still serving
|
||||
# vault.gbrown.org. This copy holds a point-in-time snapshot of the SQLite database.
|
||||
# DO NOT log into this one and add credentials before cutover -- the two would diverge
|
||||
# and one side's changes would be lost.
|
||||
#
|
||||
# Cutover procedure (do it in this order, it matters):
|
||||
# 1. stop vaultwarden on the home server (clean SQLite, no torn WAL)
|
||||
# 2. rsync -a server.gib:~/Server/vaultwarden/volume/ ~/Server/Vaultwarden/volume/
|
||||
# 3. systemctl --user restart podman-vaultwarden
|
||||
# 4. point vault.gbrown.org at the VPS, add the NPM proxy host
|
||||
# 5. verify a login + an item decrypts, THEN leave home stopped
|
||||
#
|
||||
# The SQLite copy taken while home was running is crash-consistent only. Step 1 is what
|
||||
# makes it clean -- don't skip it.
|
||||
#
|
||||
# Rootless adaptations:
|
||||
# - :Z on the data mount (SELinux Enforcing).
|
||||
# - /etc/localtime mount dropped in favour of TZ (AGENTS.md §8).
|
||||
# - No published port; NPM proxies to http://vaultwarden:80 over nginx-bridge.
|
||||
# - Image starts as root and drops privileges internally, so no userns_mode needed.
|
||||
#
|
||||
# NOTE: ADMIN_TOKEN is inline here, matching the home server's file verbatim. It grants
|
||||
# access to /admin. Worth moving to a mode-600 .env at some point -- compose files get
|
||||
# rsynced offsite by ~/Server/backup.
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
vaultwarden:
|
||||
image: docker.io/vaultwarden/server:latest
|
||||
container_name: vaultwarden
|
||||
hostname: vaultwarden
|
||||
domainname: vault.gbrown.org
|
||||
networks:
|
||||
- nginx-bridge
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- DOMAIN=https://vault.gbrown.org
|
||||
- LOGIN_RATELIMIT_MAX_BURST=10
|
||||
- LOGIN_RATELIMIT_SECONDS=60
|
||||
- WEB_VAULT_ENABLED=true
|
||||
- SIGNUPS_ALLOWED=false
|
||||
- WEBSOCKET_ENABLED=true
|
||||
- ADMIN_TOKEN=${ADMIN_TOKEN}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volume:/data:Z
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
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