226 changed files with 3305 additions and 9600 deletions

No files matched your search

+2 -2
View File
@@ -183,8 +183,8 @@ currently parked (dir present, unit not active) — imported anyway, noted as
parked in the catalog. parked in the catalog.
**Excluded:** Watchtower (retired — replaced by the update timer after it took **Excluded:** Watchtower (retired — replaced by the update timer after it took
gitea down; its dir stays on junior until cleanup), the app source gitea down; its dir stays on junior until cleanup), Completeuphoria,
checkouts such as sierraandtyler and Bang (deployed by CI from their own repos — sierraandtyler, Bang (source checkouts deployed by CI from their own repos —
Panama owning a second copy of their compose would drift), `ci/`, `logs/`, Panama owning a second copy of their compose would drift), `ci/`, `logs/`,
and junior's loose scripts other than `update-containers.sh` (§4). and junior's loose scripts other than `update-containers.sh` (§4).
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/panama
@@ -9,7 +9,7 @@ You are editing a running desktop, not a codebase that gets deployed later. Ever
repository is symlinked into `~/.config`, so a save is live the moment it lands. That single fact repository is symlinked into `~/.config`, so a save is live the moment it lands. That single fact
drives every rule below. drives every rule below.
Before large work, read these in order: Read before large work, in this order:
- `README.md` — layout, the `panama` command, how installing and updating work - `README.md` — layout, the `panama` command, how installing and updating work
- `config/dot/hypr/README.md` — the compositor config is **Lua, not hyprlang**; read "The one - `config/dot/hypr/README.md` — the compositor config is **Lua, not hyprlang**; read "The one
@@ -62,8 +62,7 @@ Every executable contract under `tests/` is classified in `tests/contracts.manif
`PANAMA_TEST_TIMEOUT_SECONDS` value. Failures print the contract's captured stdout and stderr. `PANAMA_TEST_TIMEOUT_SECONDS` value. Failures print the contract's captured stdout and stderr.
Successful stdout stays quiet. Successful stderr is surfaced as a warning. Successful stdout stays quiet. Successful stderr is surfaced as a warning.
- Contracts run directly too: `tests/setup/interview-contract`. - Contracts run directly too: `tests/setup/interview-contract`.
- After changing `PreferenceSchema.qml` or - After changing `PreferenceSchema.qml` or `services/SettingsRoutes.qml`, regenerate:
`config/dot/quickshell/services/SettingsRoutes.qml`, regenerate:
`config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and `config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and
`config/dot/quickshell/scripts/panama-settings-commands` (writes the launcher deep links). `config/dot/quickshell/scripts/panama-settings-commands` (writes the launcher deep links).
Both take `--check`; `tests/quickshell/settings-docs-contract` fails when stale. Both take `--check`; `tests/quickshell/settings-docs-contract` fails when stale.
+5 -11
View File
@@ -3,21 +3,15 @@
/config/bash/.env.lock /config/bash/.env.lock
# Server secrets and state never enter the repo: live .env files belong in # Server secrets and state never enter the repo: live .env files belong in
# ~/Server/<Name>/ (only compose.yml is symlinked back here), and data/ is # ~/Server/<Name>/ (only compose.yml is symlinked back here), and data/ is
# the bind-mount convention (the home server's older services use volume/ and # the bind-mount convention. Ignored anyway as a seatbelt -- the repo is
# volumes/). Ignored anyway as a seatbelt -- the repo is public, and # public, and tests/server/compose-secrets-contract is the check that this
# tests/server/compose-secrets-contract is the check that these lines still exist. # line still exists.
/server/**/.env /server/containers/**/.env
/server/**/data/ /server/containers/**/data/
/server/**/volume/
/server/**/volumes/
# Personal espanso triggers (name, email), seeded per-machine by setup-identity. # Personal espanso triggers (name, email), seeded per-machine by setup-identity.
/config/dot/espanso/match/identity.yml /config/dot/espanso/match/identity.yml
# Ignore backups of old config files # Ignore backups of old config files
/config/old /config/old
# MCP bearer tokens. The server list in user/agents/mcp/servers is tracked
# and names which variable carries each token; the tokens themselves are
# machine-local, because this repo is public.
/user/agents/mcp/env
# Ignore Wireguard config of course! # Ignore Wireguard config of course!
/config/wg/** /config/wg/**
# Ignore Neovim lazy-lock file # Ignore Neovim lazy-lock file
+5 -8
View File
@@ -102,13 +102,13 @@ in order, without stopping again:
| `interview` | Every prompt, before anything is installed. Answers last one run and are never written to a durable path | | `interview` | Every prompt, before anything is installed. Answers last one run and are never written to a durable path |
| `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), the package lists in `setup/packages/`, then whichever optional categories were chosen | | `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), the package lists in `setup/packages/`, then whichever optional categories were chosen |
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>`, and seeds the wallpaper, cursor theme and Firefox chrome | | `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>`, and seeds the wallpaper, cursor theme and Firefox chrome |
| `link-skills` | Links the agent skills in `skills/` into `~/.agents/skills` and `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash | | `link-skills` | Links the agent skills in `skills/` into `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash |
| `link-user` | Links the personal content in `user/` — agent instructions, SSH host aliases — but only on a machine that answered yes. See [user/README.md](user/README.md) | | `link-user` | Links the personal content in `user/` — agent instructions, SSH host aliases — but only on a machine that answered yes. See [user/README.md](user/README.md) |
| `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services | | `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services |
| `link-vicinae-scripts` | Publishes the Vicinae script commands | | `link-vicinae-scripts` | Publishes the Vicinae script commands |
| `setup-identity` | git config, `gh auth login`, an SSH key — whichever were asked for | | `setup-identity` | git config, `gh auth login`, an SSH key — whichever were asked for |
| `install-hardware` | NVIDIA, Secure Boot enrollment, Fedora's extras, firmware — each only if it was asked for. Last, because enrollment and firmware are consumed at the next boot | | `install-hardware` | NVIDIA, Secure Boot enrollment, Fedora's extras, firmware — each only if it was asked for. Last, because enrollment and firmware are consumed at the next boot |
| `setup-server` | Server role only: linger, unprivileged ports from 80, firewalld (80/443/81, opened only where nothing already grants them), the `nginx-bridge` network, and the nightly image-update timer | | `setup-server` | Server role only: linger, unprivileged ports from 80, firewalld (80/443/81), the `nginx-bridge` network, and the nightly image-update timer |
| `link-server` | Server role only: creates `~/Server` and refreshes the links of whatever services this machine has enabled | | `link-server` | Server role only: creates `~/Server` and refreshes the links of whatever services this machine has enabled |
A server runs the shared stages plus its own two, and skips the rest — A server runs the shared stages plus its own two, and skips the rest —
@@ -235,13 +235,10 @@ server/ The server role: compose services (one directory per
service), the nightly image updater, and its units. See service), the nightly image updater, and its units. See
server/README.md server/README.md
skills/ Agent skills for operating this desktop, linked into skills/ Agent skills for operating this desktop, linked into
~/.agents/skills and ~/.claude/skills ~/.claude/skills
setup/ setup/
apps/ Applications built from source, one file each apps/ Applications built from source, one file each
keys/ Pinned signing keys, for publishers that ship no fetchable lib/ Shared by more than one stage; the extras catalog reader
one; setup/keys/README.md records where each came from
lib/ Shared by more than one stage; the extras catalog reader,
the machine role, the verified ChatGPT repository
packages/ One package per line; extras/ holds the optional categories packages/ One package per line; extras/ holds the optional categories
scripts/ Run in order by ./install scripts/ Run in order by ./install
tests/ Contracts. See below tests/ Contracts. See below
@@ -250,7 +247,7 @@ docs/ Settings reference, and the design specs behind the work
## Tests ## Tests
192 of them, under `tests/`. `tests/contracts.manifest` classifies every 188 of them, under `tests/`. `tests/contracts.manifest` classifies every
contract by the capabilities it needs. Run the hermetic set, or grant a contract by the capabilities it needs. Run the hermetic set, or grant a
specific external capability when automation needs it: specific external capability when automation needs it:
+2 -8
View File
@@ -134,6 +134,7 @@ ${BOLD}Examples:${RESET}
$PROGRAM upgrade $PROGRAM upgrade
$PROGRAM apps $PROGRAM apps
$PROGRAM app $PROGRAM app
$PROGRAM app chatgpt-desktop
EOF EOF
} }
@@ -450,16 +451,11 @@ CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network p
contract_paths() { contract_paths() {
local candidate local candidate
# The manifest is kept in byte order, so both the discovery sort and the
# comparison below have to be byte order too. A UTF-8 collation folds the
# punctuation away -- `calendar_agenda_bridge_test.py` sorts before
# `calendar-agenda-helper-contract` under en_US and after it under C -- and
# a gate that passes or fails on the machine's LANG is not a gate.
while IFS= read -r candidate; do while IFS= read -r candidate; do
[[ -x "$candidate" || "$candidate" == *_test.py ]] || continue [[ -x "$candidate" || "$candidate" == *_test.py ]] || continue
printf 'tests/%s\n' "${candidate#"$PANAMA_DIR/tests/"}" printf 'tests/%s\n' "${candidate#"$PANAMA_DIR/tests/"}"
done < <(find "$PANAMA_DIR/tests" -type f \ done < <(find "$PANAMA_DIR/tests" -type f \
-not -path '*/fixtures/*' -not -path '*__pycache__*' | LC_ALL=C sort) -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
} }
contract_manifest_entries() { contract_manifest_entries() {
@@ -482,8 +478,6 @@ validate_contract_manifest() {
require_contract_manifest || return 1 require_contract_manifest || return 1
local manifest="$PANAMA_DIR/$CONTRACT_MANIFEST" local manifest="$PANAMA_DIR/$CONTRACT_MANIFEST"
# Byte order, for the same reason contract_paths sorts in it.
local LC_ALL=C
local line capabilities path extra previous_comment="" previous_was_comment=0 local line capabilities path extra previous_comment="" previous_was_comment=0
local previous_path="" capability discovered local previous_path="" capability discovered
local -a capability_list=() findings=() local -a capability_list=() findings=()
+3 -3
View File
@@ -2,7 +2,7 @@
# #
# panama-server – the compose services a server machine runs. # panama-server – the compose services a server machine runs.
# #
# The repository carries the catalog: server/vps/<Name>/ holds a # The repository carries the catalog: server/containers/<Name>/ holds a
# compose.yml, a podman-<name>.service user unit, and an .env.example naming # compose.yml, a podman-<name>.service user unit, and an .env.example naming
# what the service needs told. The machine carries the rest: ~/Server/<Name>/ # what the service needs told. The machine carries the rest: ~/Server/<Name>/
# is a real directory owning the .env (secrets never sit inside the checkout, # is a real directory owning the .env (secrets never sit inside the checkout,
@@ -27,7 +27,7 @@ PANAMA_DIR=$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)
# Overridable so the contract can point this at a fixture catalog and a # Overridable so the contract can point this at a fixture catalog and a
# throwaway HOME rather than the machine's real services. # throwaway HOME rather than the machine's real services.
SERVER_DIR="${PANAMA_SERVER_DIR:-$PANAMA_DIR/server/vps}" SERVER_DIR="${PANAMA_SERVER_DIR:-$PANAMA_DIR/server/containers}"
TARGET_DIR="${PANAMA_SERVER_TARGET:-$HOME/Server}" TARGET_DIR="${PANAMA_SERVER_TARGET:-$HOME/Server}"
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/panama/server-definitions" STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/panama/server-definitions"
@@ -46,7 +46,7 @@ err() { printf '%s✗%s %s\n' "${RED}${BOLD}" "$RESET" "$*" >&2; }
usage() { usage() {
cat <<EOF cat <<EOF
${BOLD}$PROGRAM${RESET} – manage the compose services in server/vps/ ${BOLD}$PROGRAM${RESET} – manage the compose services in server/containers/
${BOLD}Usage:${RESET} ${BOLD}Usage:${RESET}
$PROGRAM list Every service in the catalog, and its state here $PROGRAM list Every service in the catalog, and its state here
+89 -6
View File
@@ -45,6 +45,65 @@ checkout_command() {
fi fi
} }
# Git's index hints are performance promises, not trust evidence. In
# particular, assume-unchanged and skip-worktree can make porcelain status
# report a clean checkout whose files no longer match HEAD. Compare every
# tracked blob and Git mode with the verified commit before handing control to
# any file in the worktree.
checkout_matches_verified_commit() (
local checkout="$1" listing="" entry metadata mode type expected path actual
local link_target_with_sentinel link_target
trap '[[ -z "$listing" ]] || rm -f -- "$listing"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
listing="$(mktemp -u -t panama-boot-tree.XXXXXX)" || exit 1
umask 077
if ! (set -o noclobber; : >"$listing") 2>/dev/null; then
listing=""
exit 1
fi
checkout_command git -C "$checkout" ls-tree -rz --full-tree \
"$PANAMA_BOOT_REVISION" >"$listing" || exit 1
while IFS= read -r -d '' entry; do
[[ "$entry" == *$'\t'* ]] || exit 1
metadata="${entry%%$'\t'*}"
path="${entry#*$'\t'}"
read -r mode type expected <<<"$metadata"
[[ "$type" == blob && -n "$path" && "$path" != /* ]] || exit 1
case "$mode" in
100644) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
&& ! -x "$checkout/$path" ]] || exit 1 ;;
100755) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
&& -x "$checkout/$path" ]] || exit 1 ;;
120000)
[[ -L "$checkout/$path" ]] || exit 1
# hash-object given a pathname follows a symlink. Git's 120000 blob is
# the link text itself, including any trailing newlines, so preserve
# those bytes with a sentinel and hash stdin instead.
link_target_with_sentinel="$(
readlink -n -- "$checkout/$path" && printf .
)" || exit 1
[[ "$link_target_with_sentinel" == *. ]] || exit 1
link_target="${link_target_with_sentinel%.}"
actual="$(
printf '%s' "$link_target" \
| checkout_command git -C "$checkout" hash-object --stdin
)" || exit 1
[[ "$actual" == "$expected" ]] || exit 1
continue
;;
*) exit 1 ;;
esac
actual="$(checkout_command git -C "$checkout" hash-object --no-filters -- "$path")" \
|| exit 1
[[ "$actual" == "$expected" ]] || exit 1
done <"$listing"
)
prepare_panama_checkout() { prepare_panama_checkout() {
local checkout="$1" actual_head checkout_status local checkout="$1" actual_head checkout_status
@@ -110,6 +169,30 @@ for arg in "$@"; do
esac esac
done done
# Keep the worktree comparison at the last possible boundary. Checkout
# preparation may invoke several commands and return to the caller; performing
# the byte/mode/link check here ensures a change in that interval is rejected
# before any tracked file is executed.
verified_install_handoff() {
local use_tty="$1"
if ! checkout_matches_verified_commit "$PANAMA_PATH"; then
echo "boot: checkout files do not match PANAMA_BOOT_REVISION" >&2
return 1
fi
if [[ -n "$BOOTSTRAP_USER" ]]; then
if (( use_tty )); then
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
fi
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
fi
if (( use_tty )); then
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
fi
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
}
# The public bootstrap contract runs this branch as an ordinary user with a # The public bootstrap contract runs this branch as an ordinary user with a
# stubbed root identity. Keep its filesystem adapter unavailable to a real root # stubbed root identity. Keep its filesystem adapter unavailable to a real root
# shell so it cannot redirect a real installation by accident. # shell so it cannot redirect a real installation by accident.
@@ -471,7 +554,7 @@ if [[ "$(id -u)" -eq 0 ]]; then
if ! command -v git >/dev/null 2>&1; then if ! command -v git >/dev/null 2>&1; then
echo "Installing git, which the clone needs" echo "Installing git, which the clone needs"
dnf install -y git dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
fi fi
# Create or advance the checkout as the target user. A root-owned .git in a # Create or advance the checkout as the target user. A root-owned .git in a
@@ -481,15 +564,14 @@ if [[ "$(id -u)" -eq 0 ]]; then
prepare_panama_checkout "$PANAMA_PATH" prepare_panama_checkout "$PANAMA_PATH"
echo "Handing off to install as $username" echo "Handing off to install as $username"
exec runuser -u "$username" -- env PANAMA_PATH="$PANAMA_PATH" \ verified_install_handoff 1
"$PANAMA_PATH/install" --server </dev/tty
fi fi
# git is the one dependency the clone itself needs. Everything else -- gum # git is the one dependency the clone itself needs. Everything else -- gum
# included -- is bootstrapped by `install`. # included -- is bootstrapped by `install`.
if ! command -v git >/dev/null 2>&1; then if ! command -v git >/dev/null 2>&1; then
echo "Installing git, which the clone needs" echo "Installing git, which the clone needs"
sudo dnf install -y git sudo dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
fi fi
prepare_panama_checkout "$PANAMA_PATH" prepare_panama_checkout "$PANAMA_PATH"
@@ -500,7 +582,8 @@ prepare_panama_checkout "$PANAMA_PATH"
# so itself. # so itself.
# The probe actually opens /dev/tty rather than testing -r: a process with no # The probe actually opens /dev/tty rather than testing -r: a process with no
# controlling terminal passes -r and then fails the redirect. # controlling terminal passes -r and then fails the redirect.
handoff_tty=0
if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty handoff_tty=1
fi fi
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} verified_install_handoff "$handoff_tty"
+2 -3
View File
@@ -22,7 +22,6 @@ alias avante='nvim -c "lua vim.defer_fn(function()require(\"avante.api\").zen_mo
alias nlc='nvim leetcode.nvim' alias nlc='nvim leetcode.nvim'
alias clauded='claude --allow-dangerously-skip-permissions' alias clauded='claude --allow-dangerously-skip-permissions'
alias claudedr='claude --allow-dangerously-skip-permissions --resume' alias claudedr='claude --allow-dangerously-skip-permissions --resume'
alias klaude='CLAUDE_CONFIG_DIR="$HOME/.klaude" claude'
alias commit-repo='git add -A && git commit -m "Update stuff" && git push' alias commit-repo='git add -A && git commit -m "Update stuff" && git push'
# Docker Shortcuts # Docker Shortcuts
@@ -37,9 +36,9 @@ alias fprint-on='sudo authselect enable-feature with-fingerprint'
alias fprint-off='sudo authselect disable-feature with-fingerprint' alias fprint-off='sudo authselect disable-feature with-fingerprint'
# File system # File system
alias ls='eza -lh --group-directories-first --icons=auto' alias ls='eza -lh --group-directories-first --icons'
alias lsa='ls -a' alias lsa='ls -a'
alias lt='eza --tree --level=2 --long --icons=auto --git' alias lt='eza --tree --level=2 --long --icons --git'
alias lta='lt -a' alias lta='lt -a'
# Fedora's bat installs /usr/bin/bat; batcat is the Debian name. # Fedora's bat installs /usr/bin/bat; batcat is the Debian name.
alias ff="fzf --preview 'bat --style=numbers --color=always {}'" alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
+2 -17
View File
@@ -16,23 +16,8 @@ unset PANAMA_ENV
# Editor used by CLI # Editor used by CLI
export EDITOR="nvim" export EDITOR="nvim"
export SUDO_EDITOR="$EDITOR" export SUDO_EDITOR="$EDITOR"
# A graphical askpass, and only where there is a session to draw it in. export SSH_ASKPASS=/usr/bin/ksshaskpass
# SSH_ASKPASS_REQUIRE=prefer means ssh and git reach for the dialog BEFORE the export SSH_ASKPASS_REQUIRE=prefer
# terminal prompt, so on a headless machine the prompt that would have worked
# is skipped in favour of one that cannot open: `git push` then fails reading
# a username rather than asking for it. The desktop keeps the dialog; a
# server, a serial console and a plain SSH session fall back to the terminal.
if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ] && [ -x /usr/bin/ksshaskpass ]; then
export SSH_ASKPASS=/usr/bin/ksshaskpass
export SSH_ASKPASS_REQUIRE=prefer
else
# Cleared, not merely left unset. An askpass inherited from a parent that
# did have a display -- an agent's shell, a forwarded environment, a login
# that predates this guard -- is exactly as unusable here as one this file
# set itself, and declining to add it does nothing about a value already in
# the environment.
unset SSH_ASKPASS SSH_ASKPASS_REQUIRE
fi
# Define Paths # Define Paths
export CARGO_PATH="$HOME/.cargo" export CARGO_PATH="$HOME/.cargo"
+4 -14
View File
@@ -244,20 +244,10 @@ local shipped_mode = "4500x3000@60"
local shipped_scale = 1.5 local shipped_scale = 1.5
local shipped_transform = 0 local shipped_transform = 0
-- 8-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4 -- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
-- HBR3 and reliant on DSC, and this panel's link is marginal: every modeset -- HBR3, so this relies on DSC. If the display fails to light up or falls back to
-- retrains it and blanks the screen. 8bpc keeps headroom on the link. -- a lower mode, drop this to 8 first.
-- local shipped_bitdepth = 10
-- Related: directScanoutPolicy is 0 in Panama settings (2026-09-13). With
-- scanout on, a fullscreen game whose buffer depth differs from the desktop
-- (games ship both 8- and 10-bit swapchains) makes Hyprland change the output
-- format, and on amdgpu a format change is a full modeset. Compositing always
-- keeps the format fixed, so the link never retrains mid-game.
--
-- vrrPolicy is also 0 there. VRR on this panel loses sync and blacks out
-- (seen on GNOME in July 2026 and again here); a 60Hz panel gains little
-- from it anyway.
local shipped_bitdepth = 8
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header. -- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
local shipped_cm = "auto" local shipped_cm = "auto"
+2 -24
View File
@@ -125,16 +125,6 @@ hl.window_rule({
no_dim = true, no_dim = true,
}) })
-- Steam games never get true fullscreen. Everything Hyprland does only for a
-- real fullscreen window (direct scanout, VRR, tearing, auto HDR) makes this
-- panel's marginal DP link retrain and blank (2026-09-13). "1 2" keeps the
-- window maximized internally while the game believes it is fullscreen, which
-- is what borderless windowed looks like from the game's side.
hl.window_rule({
match = { class = "^steam_app_\\d+$" },
fullscreen_state = "1 2",
})
-- Steam itself is a normal window, but its transient popups are a mess. -- Steam itself is a normal window, but its transient popups are a mess.
hl.window_rule({ hl.window_rule({
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" }, match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
@@ -279,28 +269,16 @@ hl.layer_rule({
ignore_alpha = 0.2, ignore_alpha = 0.2,
}) })
-- Overview and local screen-reading UI dim the desktop behind them. -- Overview, capture and local screen-reading UI dim the desktop behind them.
hl.layer_rule({ hl.layer_rule({
name = "qs-overlay", name = "qs-overlay",
match = { namespace = "^qs-(overview|screen-intelligence)$" }, match = { namespace = "^qs-(overview|capture|screen-intelligence)$" },
blur = true, blur = true,
ignore_alpha = 0.4, ignore_alpha = 0.4,
dim_around = true, dim_around = true,
no_screen_share = true, no_screen_share = true,
}) })
-- The screenshot picker paints an opaque frozen frame edge to edge, so blur and
-- dim would be compositor work on pixels nobody sees. no_anim is load-bearing:
-- services/Capture.qml unmaps the picker and runs grim 90ms later, and the
-- 200ms layersOut fade would otherwise put the half-faded picker, dimming and
-- all, into every screenshot.
hl.layer_rule({
name = "qs-capture",
match = { namespace = "^qs-capture$" },
no_anim = true,
no_screen_share = true,
})
-- Notification toasts. Blurred like every other shell surface -- without this -- Notification toasts. Blurred like every other shell surface -- without this
-- the cards are a near-transparent fill sitting directly on the wallpaper and -- the cards are a near-transparent fill sitting directly on the wallpaper and
-- read as washed out rather than as glass. -- read as washed out rather than as glass.
@@ -1805,7 +1805,7 @@ Singleton {
"org.mozilla.thunderbird_esr", "com.slack.Slack", "org.mozilla.thunderbird_esr", "com.slack.Slack",
"app.bluebubbles.BlueBubbles", "rustdesk", "app.bluebubbles.BlueBubbles", "rustdesk",
"io.podman_desktop.PodmanDesktop", "com.anthropic.Claude", "io.podman_desktop.PodmanDesktop", "com.anthropic.Claude",
"chatgpt", "md.obsidian.Obsidian", "codex-desktop", "md.obsidian.Obsidian",
"com.obsproject.Studio", "steam" "com.obsproject.Studio", "steam"
] ]
}, },
@@ -27,13 +27,9 @@ PanelWindow {
right: true right: true
} }
// Ignore, and nothing else: reserve no space (or every window on the // Fullscreen overlays must not reserve space, or every window on the
// workspace resizes as the picker opens and closes) and respect nobody's // workspace gets resized as the picker opens and closes.
// (or the window starts under the bar, the full-output freeze frame is exclusiveZone: 0
// squeezed into a shorter box, and every selection lands one bar-height
// off in the real capture). Never pair this with exclusiveZone; see
// tests/quickshell/exclusion-idiom-contract.
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua WlrLayershell.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
@@ -57,6 +57,7 @@ PanelWindow {
anchors { top: true; bottom: true; left: true; right: true } anchors { top: true; bottom: true; left: true; right: true }
color: "transparent" color: "transparent"
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind // The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
@@ -55,6 +55,7 @@ PanelWindow {
color: "transparent" color: "transparent"
// A dock that reserved space would not be intellihiding. // A dock that reserved space would not be intellihiding.
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
// Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename. // Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
@@ -40,6 +40,7 @@ PanelWindow {
anchors { top: true; bottom: true; left: true; right: true } anchors { top: true; bottom: true; left: true; right: true }
color: "transparent" color: "transparent"
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
// Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted // Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted
@@ -44,6 +44,7 @@ PanelWindow {
// Reserve nothing and respect nothing: the glow is drawn over the whole // Reserve nothing and respect nothing: the glow is drawn over the whole
// output including under the bar and the dock, which is what makes it // output including under the bar and the dock, which is what makes it
// visible from wherever the eyes happen to be. // visible from wherever the eyes happen to be.
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
color: "transparent" color: "transparent"
@@ -29,6 +29,7 @@ PanelWindow {
screen: root.modelData screen: root.modelData
anchors.bottom: true anchors.bottom: true
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3 margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
implicitWidth: root.desiredWidth implicitWidth: root.desiredWidth
implicitHeight: 64 implicitHeight: 64
@@ -22,6 +22,7 @@ PanelWindow {
} }
color: "transparent" color: "transparent"
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename. // Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
@@ -34,23 +34,11 @@ Item {
visible: root.rowCount > 0 visible: root.rowCount > 0
implicitHeight: rows.implicitHeight implicitHeight: rows.implicitHeight
// Probing I2C takes on the order of a second, so it waits until the quick // Probing I2C takes on the order of a second, so it waits until the panel
// settings panel is actually on screen rather than running at shell // is actually on screen rather than running at shell startup. Monitors do
// startup. Monitors do not come and go, so once is enough. // not come and go, so once is enough.
// onVisibleChanged: if (visible && !Brightness.scanned) Brightness.refresh()
// The trigger is the panel opening, not this item becoming visible: on a Component.onCompleted: if (root.visible && !Brightness.scanned) Brightness.refresh()
// machine with no backlight, `visible` stays false until a scan has found
// a DDC display, so keying the scan off it would never scan at all.
Connections {
target: ShellState
function onQuickSettingsOpenChanged(): void { root.scanIfOpen(); }
}
Component.onCompleted: root.scanIfOpen()
function scanIfOpen(): void {
if (ShellState.quickSettingsOpen && !Brightness.scanned)
Brightness.refresh();
}
// `-m` is the machine-readable form: name,class,current,percent,max // `-m` is the machine-readable form: name,class,current,percent,max
Process { Process {
@@ -22,6 +22,7 @@ Variants {
implicitWidth: 260 implicitWidth: 260
implicitHeight: 172 implicitHeight: 172
color: "transparent" color: "transparent"
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
mask: Region {} mask: Region {}
@@ -383,13 +383,6 @@ Lua-configured Hyprland, prints the refusal to stdout, and exits 0. `eval` exits
0 on syntax and runtime errors too. The only trustworthy signal that a write 0 on syntax and runtime errors too. The only trustworthy signal that a write
landed is reading the value back. landed is reading the value back.
**`exclusionMode: ExclusionMode.Ignore` goes alone.** Quickshell's `exclusiveZone`
setter flips `exclusionMode` back to `Normal` as a side effect, so a window that
declares both is at the mercy of which property the QML engine applies last.
The 2026-09-14 Qt update changed that order and every full-screen overlay slid
under the bar. `tests/quickshell/exclusion-idiom-contract` fails any file that
pairs them.
**The Settings window is tiled.** `implicitWidth` is a hint; the layout decides, **The Settings window is tiled.** `implicitWidth` is a hint; the layout decides,
and it ranges from a half-screen split to the full display. `SliderRow` stacks and it ranges from a half-screen split to the full display. `SliderRow` stacks
its control under the label below 520px. Test narrow. its control under the label below 520px. Test narrow.
@@ -78,6 +78,7 @@ PanelWindow {
anchors { top: true; bottom: true; left: true; right: true } anchors { top: true; bottom: true; left: true; right: true }
color: "transparent" color: "transparent"
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "qs-popover-welcome" WlrLayershell.namespace: "qs-popover-welcome"
+1 -4
View File
@@ -341,10 +341,7 @@ esac'
property bool record: false property bool record: false
property bool intelligence: false property bool intelligence: false
// A handful of frames at 60Hz, enough for the compositor to recomposite // A handful of frames at 60Hz, enough for the compositor to recomposite
// the output without the overlay on it. Only enough because the // the output without the overlay on it.
// qs-capture layer rule in hypr/rules.lua sets no_anim: with the
// default 200ms layersOut fade the half-faded picker would still be
// on screen when grim reads it.
interval: 90 interval: 90
onTriggered: { onTriggered: {
const r = commitDelay.rect; const r = commitDelay.rect;
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env zsh
# The Mac mini's shell. macOS stays on zsh: its bash is 3.2, and
# config/bash/shell leans on Linux-only tools (stat -c, ksshaskpass, the nvm
# profile script), so this is a small zsh counterpart rather than a port.
# Linked to ~/.zshrc. Homebrew's PATH comes from ~/.zprofile (brew shellenv).
export PANAMA_PATH="$HOME/.local/share/Panama"
export EDITOR="nvim"
export SUDO_EDITOR="$EDITOR"
export BUN_INSTALL="$HOME/.bun"
export PATH="$HOME/.local/bin:$BUN_INSTALL/bin:$PANAMA_PATH/bin:$PATH"
# History shared across sessions, so tmux panes see each other's commands.
HISTFILE="$HOME/.zsh_history"
HISTSIZE=50000
SAVEHIST=50000
setopt SHARE_HISTORY HIST_IGNORE_DUPS HIST_IGNORE_SPACE
# Emacs keys, like bash's readline: ctrl-a/e/r work as they do on Linux.
bindkey -e
autoload -Uz compinit && compinit
[ -s "$BUN_INSTALL/_bun" ] && source "$BUN_INSTALL/_bun"
# Same SSH behaviour as config/bash/shell: an SSH login lands in a tmux
# session that survives a dropped connection. PANAMA_SSH_TMUX=off opts out.
if [[ -n "$SSH_CONNECTION" && -z "$TMUX" && -o interactive &&
"${PANAMA_SSH_TMUX:-on}" != "off" ]] && command -v tmux >/dev/null 2>&1; then
exec tmux new-session -A -s main
fi
alias c="clear"
alias n="nvim"
alias n.="nvim ."
alias ..="cd .."
alias ...="cd ../.."
alias ....="cd ../../.."
alias letscode="ranger ~/Documents/Documents/Code"
alias commit-repo='git add -A && git commit -m "Update stuff" && git push'
alias ls='eza -lh --group-directories-first --icons=auto'
alias lsa='ls -a'
alias lt='eza --tree --level=2 --long --icons=auto --git'
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
compress() { tar -czf "${1%/}.tar.gz" "${1%/}"; }
alias decompress="tar -xzf"
# Xcode cannot sign from an SSH session while the login keychain is locked
# (errSecInternalComponent). Run this once per session before a build.
alias unlock-keychain='security unlock-keychain ~/Library/Keychains/login.keychain-db'
eval "$(zoxide init zsh)"
source <(fzf --zsh)
eval "$(oh-my-posh init zsh --config "$PANAMA_PATH/config/dot/ohmyposh/gib.omp.json")"
+106 -105
View File
@@ -71,7 +71,7 @@ source "$PANAMA_PATH/bin/ascii"
# time. On an upgrade it is worth running only when its package lists or # time. On an upgrade it is worth running only when its package lists or
# reviewed installer trust inputs changed, so this hashes them and remembers # reviewed installer trust inputs changed, so this hashes them and remembers
# the result. The framed, sorted stream includes top-level package files, the # the result. The framed, sorted stream includes top-level package files, the
# package-stage adapter, every helper it sources, and regular provenance files; # package-stage adapter, the provenance helper, and regular provenance files;
# both relative paths and bytes are part of the state. # both relative paths and bytes are part of the state.
# #
# A content hash rather than a git range, because Panama is developed in place: # A content hash rather than a git range, because Panama is developed in place:
@@ -88,42 +88,75 @@ source "$PANAMA_PATH/bin/ascii"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
PACKAGES_HASH="$STATE_DIR/packages-hash" PACKAGES_HASH="$STATE_DIR/packages-hash"
hash_packages() { _collect_package_inputs() {
local file relative size fixed_input digest local destination="$1" raw="${1}.raw"
[[ -d "$PANAMA_PATH/setup/packages" \
for fixed_input in \ && ! -L "$PANAMA_PATH/setup/packages" \
"$PANAMA_PATH/setup/scripts/install-packages" \ && -d "$PANAMA_PATH/setup/provenance" \
"$PANAMA_PATH/setup/lib/artifact-provenance" \ && ! -L "$PANAMA_PATH/setup/provenance" ]] || return 1
"$PANAMA_PATH/setup/lib/chatgpt-package" \ {
"$PANAMA_PATH/setup/lib/extras-catalog" \ printf '%s\0' \
"$PANAMA_PATH/setup/lib/machine-role"; do "$PANAMA_PATH/setup/scripts/install-packages" \
[[ -f "$fixed_input" && ! -L "$fixed_input" && -r "$fixed_input" ]] || return 1 "$PANAMA_PATH/setup/lib/artifact-provenance" \
done "$PANAMA_PATH/setup/lib/extras-catalog" \
"$PANAMA_PATH/setup/lib/machine-role" || exit 1
digest="$( # extras/ is deliberately excluded. A symlink or other non-directory
{ # object at this level is still an input error, not something discovery may
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -print0 || exit 1 # silently omit.
printf '%s\0' \ find "$PANAMA_PATH/setup/packages" -mindepth 1 -maxdepth 1 \
"$PANAMA_PATH/setup/scripts/install-packages" \ ! -type d -print0 || exit 1
"$PANAMA_PATH/setup/lib/artifact-provenance" \ find "$PANAMA_PATH/setup/provenance" -mindepth 1 \
"$PANAMA_PATH/setup/lib/chatgpt-package" \ ! -type d -print0 || exit 1
"$PANAMA_PATH/setup/lib/extras-catalog" \ } >"$raw" || return 1
"$PANAMA_PATH/setup/lib/machine-role" || exit 1 LC_ALL=C sort -z "$raw" >"$destination"
find "$PANAMA_PATH/setup/provenance" -type f -print0 || exit 1
} | LC_ALL=C sort -z | while IFS= read -r -d '' file; do
relative="${file#"$PANAMA_PATH"/}"
size="$(wc -c <"$file")" || exit 1
printf '%s\0%s\0' "$relative" "$size" || exit 1
cat -- "$file" || exit 1
printf '\0' || exit 1
done | sha256sum | cut -d' ' -f1
)" || return 1
printf '%s\n' "$digest"
} }
packages_needed() { _write_package_manifest() {
local current_hash="$1" recorded_hash local inputs="$1" destination="$2" file relative digest
: >"$destination" || return 1
while IFS= read -r -d '' file; do
[[ -f "$file" && ! -L "$file" && -r "$file" ]] || return 1
relative="${file#"$PANAMA_PATH"/}"
[[ "$relative" != "$file" ]] || return 1
digest="$(sha256sum -- "$file" | awk '{ print $1 }')" || return 1
[[ "$digest" =~ ^[0-9a-f]{64}$ ]] || return 1
printf '%s\0%s\0' "$relative" "$digest" >>"$destination" || return 1
done <"$inputs"
}
# Read every input twice from the same enumerated set. A file or path that
# changes while the snapshot is built cannot produce a receipt.
hash_packages() (
local work="" inputs_before inputs_after manifest_before manifest_after
trap '[[ -z "$work" ]] || rm -rf -- "$work"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
work="$(mktemp -u -d -t panama-packages-hash.XXXXXX)" || exit 1
if ! mkdir -m 700 -- "$work"; then
work=""
exit 1
fi
inputs_before="$work/inputs-before"
inputs_after="$work/inputs-after"
manifest_before="$work/manifest-before"
manifest_after="$work/manifest-after"
_collect_package_inputs "$inputs_before" || exit 1
_write_package_manifest "$inputs_before" "$manifest_before" || exit 1
_collect_package_inputs "$inputs_after" || exit 1
cmp -s -- "$inputs_before" "$inputs_after" || exit 1
_write_package_manifest "$inputs_after" "$manifest_after" || exit 1
cmp -s -- "$manifest_before" "$manifest_after" || exit 1
sha256sum -- "$manifest_before" | awk '{ print $1 }'
)
PACKAGE_START_HASH=""
packages_needed() {
local current_hash recorded_hash
current_hash="$(hash_packages)" || return 2
PACKAGE_START_HASH="$current_hash"
(( FORCE_PACKAGES )) && return 0 (( FORCE_PACKAGES )) && return 0
(( UPGRADE )) || return 0 (( UPGRADE )) || return 0
[[ -r "$PACKAGES_HASH" ]] || return 0 [[ -r "$PACKAGES_HASH" ]] || return 0
@@ -134,52 +167,41 @@ packages_needed() {
# Written only after the stage succeeds, mirroring the rule panama-migrate # Written only after the stage succeeds, mirroring the rule panama-migrate
# documents for its markers: a step that did not complete has not happened, and # documents for its markers: a step that did not complete has not happened, and
# recording it as done hides it forever. # recording it as done hides it forever.
record_packages_hash() { record_packages_hash() (
local starting_hash="$1" current_hash temporary_hash local expected_hash="$1" current_hash temporary_hash=""
trap '[[ -z "$temporary_hash" ]] || rm -f -- "$temporary_hash"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
current_hash="$(hash_packages)" || return 1 current_hash="$(hash_packages)" || return 1
[[ "$current_hash" == "$starting_hash" ]] || return 1 [[ "$current_hash" == "$expected_hash" ]] || return 1
mkdir -p "$STATE_DIR" mkdir -p "$STATE_DIR"
temporary_hash="$(mktemp "$STATE_DIR/.packages-hash.XXXXXX")" || return 1 temporary_hash="$(mktemp -u "$STATE_DIR/.packages-hash.XXXXXX")" || return 1
if printf '%s\n' "$starting_hash" >"$temporary_hash"; then umask 077
mv -f -- "$temporary_hash" "$PACKAGES_HASH" if ! (set -o noclobber; : >"$temporary_hash") 2>/dev/null; then
else temporary_hash=""
rm -f -- "$temporary_hash"
return 1 return 1
fi fi
} printf '%s\n' "$expected_hash" >"$temporary_hash" || return 1
mv -f -- "$temporary_hash" "$PACKAGES_HASH" || return 1
temporary_hash=""
)
# Repository trust is checked before the installer can reach its bootstrap DNF. # Repository trust is checked before the installer can reach its bootstrap DNF.
# Status 78 is reserved for a trust-root failure. It suppresses every stage that # Status 78 is reserved for a trust-root failure and is propagated unchanged so
# opens DNF -- install-hardware included, which would otherwise pull drivers # no later stage, especially install-hardware, can invoke DNF with that repo.
# through the very repository in doubt -- and the run still exits 78 at the end.
#
# It suppresses nothing else. Linking dotfiles, skills and user content reads no
# repository and installs no package, and a machine whose Terra is in question
# still wants its configuration. Refusing the safe work because the unsafe work
# is unavailable does not make the machine safer, it just leaves the machine
# unconfigured with no way to fix itself. Exiting here instead meant link-skills
# never ran on a machine whose Terra was merely unadopted, so ~/.claude/skills
# stayed the whole-directory symlink it had been before skills were linked one
# by one, and not one shipped skill was reachable.
TERRA_TRUST_FAILURE_STATUS=78 TERRA_TRUST_FAILURE_STATUS=78
DNF_STAGES=(install-packages change-settings install-hardware)
package_trust_refused=0
stage_opens_dnf() {
local candidate="$1" dnf_stage
for dnf_stage in "${DNF_STAGES[@]}"; do
[[ "$candidate" == "$dnf_stage" ]] && return 0
done
return 1
}
trust_preflight="$PANAMA_PATH/setup/scripts/install-packages" trust_preflight="$PANAMA_PATH/setup/scripts/install-packages"
if [[ ! -x "$trust_preflight" ]]; then if [[ ! -x "$trust_preflight" ]]; then
printf 'install: package repository trust preflight is unavailable\n' >&2 printf 'install: package repository trust preflight is unavailable\n' >&2
package_trust_refused=1 exit "$TERRA_TRUST_FAILURE_STATUS"
elif ! "$trust_preflight" --trust-preflight; then fi
if "$trust_preflight" --trust-preflight; then
:
else
trust_status=$?
printf 'install: package repository trust preflight failed\n' >&2 printf 'install: package repository trust preflight failed\n' >&2
package_trust_refused=1 exit "$trust_status"
fi fi
# ── The interview ──────────────────────────────────────────────────────────── # ── The interview ────────────────────────────────────────────────────────────
@@ -199,7 +221,7 @@ fi
# Gated exactly like the interview itself: under --upgrade no questions are # Gated exactly like the interview itself: under --upgrade no questions are
# asked, so nothing here is used, and a machine that cannot install gum must # asked, so nothing here is used, and a machine that cannot install gum must
# not have that stop an upgrade that never needed it. # not have that stop an upgrade that never needed it.
if (( ! UPGRADE && ! package_trust_refused )); then if (( ! UPGRADE )); then
bootstrap=() bootstrap=()
command -v gum >/dev/null 2>&1 || bootstrap+=(gum) command -v gum >/dev/null 2>&1 || bootstrap+=(gum)
# The probe tools serve only the hardware questions, which a server is never # The probe tools serve only the hardware questions, which a server is never
@@ -214,7 +236,8 @@ if (( ! UPGRADE && ! package_trust_refused )); then
fi fi
if (( ${#bootstrap[@]} > 0 )); then if (( ${#bootstrap[@]} > 0 )); then
echo "Installing what the setup questions are built on: ${bootstrap[*]}" echo "Installing what the setup questions are built on: ${bootstrap[*]}"
sudo dnf install -y "${bootstrap[@]}" >/dev/null || { sudo dnf install -y --repo=fedora --repo=updates \
--from-repo=fedora,updates "${bootstrap[@]}" >/dev/null || {
echo "Could not install ${bootstrap[*]}, so the setup questions cannot be asked." >&2 echo "Could not install ${bootstrap[*]}, so the setup questions cannot be asked." >&2
exit 1 exit 1
} }
@@ -277,7 +300,12 @@ gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null || true
# is unset and each stage takes the empty-answer path it already documents -- # is unset and each stage takes the empty-answer path it already documents --
# which is why this is a flag rather than a rewrite of seven stage scripts. # which is why this is a flag rather than a rewrite of seven stage scripts.
if (( ! UPGRADE )); then if (( ! UPGRADE )); then
PANAMA_ANSWERS="$(mktemp -t panama-answers.XXXXXX)" PANAMA_ANSWERS="$(mktemp -u -t panama-answers.XXXXXX)" || exit 1
umask 077
if ! (set -o noclobber; : >"$PANAMA_ANSWERS") 2>/dev/null; then
PANAMA_ANSWERS=""
exit 1
fi
export PANAMA_ANSWERS export PANAMA_ANSWERS
if ! PANAMA_ROLE_PRESET="$ROLE_PRESET" "$PANAMA_PATH/setup/scripts/interview"; then if ! PANAMA_ROLE_PRESET="$ROLE_PRESET" "$PANAMA_PATH/setup/scripts/interview"; then
@@ -343,7 +371,7 @@ fi
if [[ "$PANAMA_ROLE" == server ]]; then if [[ "$PANAMA_ROLE" == server ]]; then
STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity) STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity)
else else
STAGES=(install-packages link-dotfiles link-skills link-user link-mcp change-settings link-vicinae-scripts setup-identity install-hardware) STAGES=(install-packages link-dotfiles link-skills link-user change-settings link-vicinae-scripts setup-identity install-hardware)
fi fi
# The two an upgrade drops. Both exist only to act on interview answers, and # The two an upgrade drops. Both exist only to act on interview answers, and
@@ -366,16 +394,11 @@ for stage in "${STAGES[@]}"; do
script="$PANAMA_PATH/setup/scripts/$stage" script="$PANAMA_PATH/setup/scripts/$stage"
[[ -x "$script" ]] || continue [[ -x "$script" ]] || continue
printf '\n=== %s ===\n' "$stage" printf '\n=== %s ===\n' "$stage"
if (( package_trust_refused )) && stage_opens_dnf "$stage"; then
echo "Skipped: the package repository trust check refused package work."
continue
fi
if [[ "$stage" == install-packages ]]; then if [[ "$stage" == install-packages ]]; then
package_start_hash=""
package_state_status=0 package_state_status=0
package_start_hash="$(hash_packages)" || package_state_status=2 packages_needed || package_state_status=$?
if (( package_state_status == 0 )); then package_start_hash="$PACKAGE_START_HASH"
packages_needed "$package_start_hash" || package_state_status=$?
fi
if (( package_state_status == 1 )); then if (( package_state_status == 1 )); then
echo "The package lists have not changed since the last run; skipping." echo "The package lists have not changed since the last run; skipping."
echo "Run with --packages to install them anyway." echo "Run with --packages to install them anyway."
@@ -395,12 +418,9 @@ for stage in "${STAGES[@]}"; do
fi fi
else else
stage_status=$? stage_status=$?
# A configuration change between the preflight and this stage. Suppress the
# remaining DNF stages, keep the safe ones, and carry the status to the end.
if [[ "$stage" == install-packages && "$stage_status" -eq "$TERRA_TRUST_FAILURE_STATUS" ]]; then if [[ "$stage" == install-packages && "$stage_status" -eq "$TERRA_TRUST_FAILURE_STATUS" ]]; then
printf '!!! %s stopped on an untrusted package repository\n' "$stage" >&2 printf '!!! %s stopped on an untrusted package repository\n' "$stage" >&2
package_trust_refused=1 exit "$stage_status"
continue
fi fi
failed+=("$stage") failed+=("$stage")
printf '!!! %s failed\n' "$stage" >&2 printf '!!! %s failed\n' "$stage" >&2
@@ -426,16 +446,8 @@ done
# written for -- and baselining would skip every one of them forever. Every # written for -- and baselining would skip every one of them forever. Every
# migration is self-guarding and a no-op where it does not apply, so running # migration is self-guarding and a no-op where it does not apply, so running
# them is the safe direction. # them is the safe direction.
#
# Held back when package work was refused. A migration is free to run a DNF
# transaction -- the ChatGPT package replacement does exactly that -- so the
# repositories have to be trustworthy before any of them is allowed to run.
# They are not marked applied either, so the next run still has them pending.
migrate="$PANAMA_PATH/bin/panama-migrate" migrate="$PANAMA_PATH/bin/panama-migrate"
if (( package_trust_refused )) && [[ -x "$migrate" ]]; then if [[ -x "$migrate" ]]; then
printf '\n=== migrations ===\n'
echo "Skipped: the package repository trust check refused package work."
elif [[ -x "$migrate" ]]; then
printf '\n=== migrations ===\n' printf '\n=== migrations ===\n'
if (( UPGRADE )) || [[ -d "$STATE_DIR/migrations" ]]; then if (( UPGRADE )) || [[ -d "$STATE_DIR/migrations" ]]; then
"$migrate" run || failed+=(migrations) "$migrate" run || failed+=(migrations)
@@ -483,17 +495,6 @@ else
retry='./install' retry='./install'
fi fi
# Reported last and on its own, because it is not an ordinary stage failure:
# everything safe did run, and what did not run is named rather than buried in a
# list. The exit status stays 78 so a caller can still tell the two apart.
if (( package_trust_refused )); then
printf 'Package work was refused: the Terra repository configuration on this\n' >&2
printf 'machine is not one Panama can verify. Skipped: %s\n' "${DNF_STAGES[*]}" >&2
printf 'Everything that touches no repository was still applied.\n' >&2
printf 'Inspect it with: panama diagnose\n' >&2
exit "$TERRA_TRUST_FAILURE_STATUS"
fi
if (( ${#failed[@]} == 0 )); then if (( ${#failed[@]} == 0 )); then
if (( UPGRADE )); then if (( UPGRADE )); then
echo "Panama is up to date." echo "Panama is up to date."
-64
View File
@@ -1,64 +0,0 @@
#!/usr/bin/env bash
# replace the community ChatGPT Desktop build with the official OpenAI package
#
# `panama app chatgpt-desktop` used to build a community wrapper (codex-desktop)
# from the upstream macOS disk image, complete with a local rebuild daemon.
# OpenAI ships an official Linux RPM now, and the installer takes that instead;
# this repairs machines still carrying the community build. The official
# package goes on before the community one comes off, so a failure part-way
# leaves the machine with an app, never without one.
#
# Rules, because the runner cannot enforce them:
#
# * Safe to run twice. The marker records success, not intent.
# * Tolerant of the repair already being correct -- the user may have fixed
# it by hand, or a later ./install may have put it back.
# * Root work goes through `panama-sudo --reason "..."`, never bare sudo,
# so the password prompt names the repair.
# * Exit non-zero to be retried at the next login. Exit zero only when the
# machine is genuinely in the state this describes.
set -euo pipefail
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
# Machines that never had the community build are already correct. The official
# app is the installer's job, not this one's.
rpm -q codex-desktop >/dev/null 2>&1 || exit 0
# The same verified repository the installer establishes: the pinned signing
# key, then a repository that names it, so dnf checks OpenAI's signature before
# root installs anything. See setup/lib/chatgpt-package.
# shellcheck source=../setup/lib/chatgpt-package
source "$PANAMA_PATH/setup/lib/chatgpt-package"
sudo_cmd=(sudo)
if [[ -t 0 && -x "$PANAMA_PATH/bin/panama-sudo" ]]; then
sudo_cmd=(
"$PANAMA_PATH/bin/panama-sudo" --reason
"Replacing the community-built ChatGPT Desktop (codex-desktop) with the official OpenAI package"
--
)
fi
# The official package first, so the machine is never left without one.
if ! rpm -q chatgpt >/dev/null 2>&1; then
chatgpt_install_repository "${sudo_cmd[@]}"
"${sudo_cmd[@]}" dnf install -y chatgpt
fi
# The community package's updater is a user unit; stop it before dnf removes
# the unit file out from under it. Removal also takes the app in /opt, both
# binaries, and the polkit policy the local rebuilds needed.
systemctl --user disable --now codex-update-manager.service 2>/dev/null || true
"${sudo_cmd[@]}" dnf remove -y codex-desktop
systemctl --user daemon-reload 2>/dev/null || true
# The rebuild state the updater kept; the official package needs none of it.
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/codex-update-manager" \
"${XDG_CACHE_HOME:-$HOME/.cache}/codex-runtimes" \
"${XDG_CONFIG_HOME:-$HOME/.config}/codex-update-manager" \
"${XDG_STATE_HOME:-$HOME/.local/state}/codex-update-manager"
echo "Replaced the community codex-desktop build with the official chatgpt package."
+6 -33
View File
@@ -6,34 +6,13 @@ screen, plus rootless podman compose services managed as systemd user units.
``` ```
server/ server/
vps/ The catalog panama-server installs from, one directory containers/ One directory per service: compose.yml, the
per service: compose.yml, the podman-<name>.service user podman-<name>.service user unit, and an .env.example
unit, and an .env.example naming what the service needs told naming what the service needs told
home/ The home server's services in the same shape (see below)
scripts/ update-containers, the nightly image updater scripts/ update-containers, the nightly image updater
systemd/ The units behind it, linked by setup-server systemd/ The units behind it, linked by setup-server
``` ```
## The home server
`server/home/` mirrors `~/Server` on the home server, where the services still
run under Docker. Each directory has the same three files as the VPS catalog,
and the unit is drafted for the eventual move to rootless podman. It has never
run, and the composes still use Docker-only features (host networking, the
GPU, the Docker socket) that need converting first. Nothing here is
installable: panama-server reads `server/vps/` only, so a service moves into
the catalog when it actually moves to podman.
Paths keep their nesting (`server/home/Agents/Gitea` is `~/Server/Agents/Gitea`).
Three services are checkouts of upstream repositories. For those, only the
local files are kept, and an `UPSTREAM` file names the repository and the exact
commit the checkout sits at: `PostHog`, `Sentry` and `Plausible`.
Both trees are snapshots of the live machines with every secret replaced by a
`${VAR}` named in `.env.example`, or by `CHANGE_ME` in files that are not
compose files. The live servers still carry some secrets inline, so a live
compose and its copy here can differ on those lines alone.
## The contract with ~/Server ## The contract with ~/Server
The repository carries the definitions; the machine carries the state. The repository carries the definitions; the machine carries the state.
@@ -56,18 +35,12 @@ is what `panama server enable` refuses to start on.
## Conventions ## Conventions
- Service directories are TitleCase, matching `~/Server` as it has always - Service directories are TitleCase, matching `~/Server` as it has always
looked: `server/vps/Gitea` becomes `~/Server/Gitea`. looked: `server/containers/Gitea` becomes `~/Server/Gitea`.
- Bind mounts live under `./data/` in new services. Imported services keep - Bind mounts live under `./data/` in new services. Imported services keep
the volume path they were running with until their cutover, because the the volume path they were running with until their cutover, because the
repo copy must render identically to the live one — renames happen with repo copy must render identically to the live one — renames happen with
the service stopped, not in git. the service stopped, not in git.
- Only 80, 443 and 81 are open, and `setup-server` opens each one only where - Only 80, 443 and 81 are open (setup-server's doing). Everything else is
nothing already grants it. A port reachable through an enabled service or an
accepting rich rule — a CDN-scoped grant, say — is left exactly as it is,
because turning a narrow grant into a public one during an unattended
`panama update` is not a decision an installer gets to make. A machine that
manages its own firewall opts out for good with
`echo manual > ~/.local/state/panama/firewall`. Everything else is
reverse-proxied over the `nginx-bridge` network by container name, with no reverse-proxied over the `nginx-bridge` network by container name, with no
published ports of its own. A service that truly needs another host port published ports of its own. A service that truly needs another host port
says so in a README in its directory, and the port is opened by hand. says so in a README in its directory, and the port is opened by hand.
@@ -94,7 +67,7 @@ ones — and verify it came back up. One service at a time, quiet hours.
## Adding a service ## Adding a service
Add `server/vps/<Name>/` with the three files, modelled on any Add `server/containers/<Name>/` with the three files, modelled on any
existing service. The unit is the standard oneshot `podman compose up -d` existing service. The unit is the standard oneshot `podman compose up -d`
shape (`WorkingDirectory=%h/Server/<Name>`); the shape contract pins the shape (`WorkingDirectory=%h/Server/<Name>`); the shape contract pins the
details. Secrets go in `.env.example` as `KEY=CHANGE_ME` lines. details. Secrets go in `.env.example` as `KEY=CHANGE_ME` lines.
File renamed without changes.
File renamed without changes.
@@ -18,8 +18,3 @@ AUTHENTIK_EMAIL__USE_SSL=false
AUTHENTIK_EMAIL__TIMEOUT=10 AUTHENTIK_EMAIL__TIMEOUT=10
# Email address authentik will send from, should have a correct @domain # Email address authentik will send from, should have a correct @domain
AUTHENTIK_EMAIL__FROM=[email protected] AUTHENTIK_EMAIL__FROM=[email protected]
# Also set on the live machine
AUTHENTIK_IMAGE=CHANGE_ME
PG_DB=CHANGE_ME
PG_USER=CHANGE_ME
File renamed without changes.
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Authentik Description=Podman Compose: Authentik
After=network-online.target podman.socket podman-postgresql.service After=network-online.target podman.socket podman-postgresql.service
Wants=network-online.target podman-postgresql.service Wants=network-online.target podman-postgresql.service
@@ -35,11 +35,6 @@ services:
# No published port: NPM proxies to http://beszel:8090 over nginx-bridge. # No published port: NPM proxies to http://beszel:8090 over nginx-bridge.
environment: environment:
- TZ=America/New_York - TZ=America/New_York
# Login is SSO-only through Authentik (OIDC provider "Gibs Auth", app slug
# beszel). The OAuth config lives in data.db (users collection options),
# not here. The PocketBase superuser panel at /_/ keeps its own password,
# and is the way back in if Authentik is down: flip this to false there.
- DISABLE_PASSWORD_AUTH=true
# Lets the hub reach a host-network agent. Under rootless podman this resolves to # Lets the hub reach a host-network agent. Under rootless podman this resolves to
# the nginx-bridge gateway (172.18.0.1). Add the VPS in the hub UI with host # the nginx-bridge gateway (172.18.0.1). Add the VPS in the hub UI with host
# "host.docker.internal", port 45876. # "host.docker.internal", port 45876.
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Beszel Description=Podman Compose: Beszel
After=network-online.target podman.socket After=network-online.target podman.socket
Wants=network-online.target Wants=network-online.target
File renamed without changes.
@@ -7,7 +7,7 @@
# BEFORE FIRST START, create its role and database on the SHARED postgres (this stack # BEFORE FIRST START, create its role and database on the SHARED postgres (this stack
# no longer ships its own): # no longer ships its own):
# podman exec -i postgresql psql -U npm <<'SQL' # podman exec -i postgresql psql -U npm <<'SQL'
# CREATE ROLE calcom LOGIN PASSWORD '617408b6790b546811cc38d0353ea506'; # CREATE ROLE calcom LOGIN PASSWORD '<see this service .env on the machine>';
# CREATE DATABASE calcom OWNER calcom; # CREATE DATABASE calcom OWNER calcom;
# SQL # SQL
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql. # Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql.
@@ -52,7 +52,7 @@ services:
- EMAIL_SERVER_HOST=CHANGE_ME - EMAIL_SERVER_HOST=CHANGE_ME
- EMAIL_SERVER_PORT=587 - EMAIL_SERVER_PORT=587
- EMAIL_SERVER_USER=CHANGE_ME - EMAIL_SERVER_USER=CHANGE_ME
- EMAIL_SERVER_PASSWORD=CHANGE_ME - EMAIL_SERVER_PASSWORD=${EMAIL_SERVER_PASSWORD}
labels: labels:
com.centurylinklabs.watchtower.enable: "true" com.centurylinklabs.watchtower.enable: "true"
tty: true tty: true
@@ -57,8 +57,3 @@ DO_NOT_REQUIRE_SSL=true
#ACTIONS_USER_TIMEOUT_SECS= #ACTIONS_USER_TIMEOUT_SECS=
#RUST_LOG= #RUST_LOG=
#RUST_BACKTRACE= #RUST_BACKTRACE=
# Also set on the live machine
BACKEND_PORT=CHANGE_ME
PORT=CHANGE_ME
SITE_PROXY_PORT=CHANGE_ME
File renamed without changes.
@@ -9,7 +9,7 @@
# 1. Create its role and database on the SHARED postgres (this stack no longer # 1. Create its role and database on the SHARED postgres (this stack no longer
# ships its own): # ships its own):
# podman exec -i postgresql psql -U npm <<'SQL' # podman exec -i postgresql psql -U npm <<'SQL'
# CREATE ROLE documenso LOGIN PASSWORD 'e2bf60b34aff8ce259e5ed42da46c9c6'; # CREATE ROLE documenso LOGIN PASSWORD '<see this service .env on the machine>';
# CREATE DATABASE documenso OWNER documenso; # CREATE DATABASE documenso OWNER documenso;
# SQL # SQL
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql so a # Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql so a
@@ -60,7 +60,7 @@ services:
- NEXT_PRIVATE_SMTP_HOST=CHANGE_ME - NEXT_PRIVATE_SMTP_HOST=CHANGE_ME
- NEXT_PRIVATE_SMTP_PORT=587 - NEXT_PRIVATE_SMTP_PORT=587
- NEXT_PRIVATE_SMTP_USERNAME=CHANGE_ME - NEXT_PRIVATE_SMTP_USERNAME=CHANGE_ME
- NEXT_PRIVATE_SMTP_PASSWORD=CHANGE_ME - NEXT_PRIVATE_SMTP_PASSWORD=${NEXT_PRIVATE_SMTP_PASSWORD}
- NEXT_PRIVATE_SMTP_FROM_NAME=Documenso - NEXT_PRIVATE_SMTP_FROM_NAME=Documenso
- [email protected] - [email protected]
labels: labels:
+25
View File
@@ -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
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: iSponsorBlockTV Description=Podman Compose: iSponsorBlockTV
After=network-online.target podman.socket After=network-online.target podman.socket
Wants=network-online.target Wants=network-online.target
@@ -140,6 +140,3 @@ KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN=false
# ClickHouse (optional) for audit log storage # ClickHouse (optional) for audit log storage
# CLICKHOUSE_URL=http://infisical:infisical@clickhouse:8123/infisical # CLICKHOUSE_URL=http://infisical:infisical@clickhouse:8123/infisical
# Also set on the live machine
CLIENT_GITLAB_LOGIN_URL=CHANGE_ME
File renamed without changes.
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Infisical Description=Podman Compose: Infisical
After=network-online.target podman.socket podman-postgresql.service After=network-online.target podman.socket podman-postgresql.service
Wants=network-online.target podman-postgresql.service Wants=network-online.target podman-postgresql.service
@@ -62,11 +62,3 @@ POSTGRES_CONTAINER_NAME=lashaddict-postgres
POSTGRES_USER=gib POSTGRES_USER=gib
POSTGRES_PASSWORD=CHANGE_ME POSTGRES_PASSWORD=CHANGE_ME
POSTGRES_DB=lashaddict-payload POSTGRES_DB=lashaddict-payload
# Also set on the live machine
NEXT_PUBLIC_POSTHOG_KEY=CHANGE_ME
NEXT_PUBLIC_POSTHOG_HOST=CHANGE_ME
NEXT_PUBLIC_PLAUSIBLE_SRC=CHANGE_ME
BACKEND_PORT=CHANGE_ME
PORT=CHANGE_ME
SITE_PROXY_PORT=CHANGE_ME
@@ -6,12 +6,6 @@ services:
image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest
container_name: ${NEXT_CONTAINER_NAME} container_name: ${NEXT_CONTAINER_NAME}
hostname: ${NEXT_CONTAINER_NAME} hostname: ${NEXT_CONTAINER_NAME}
# Upstream DNS pinned to Cloudflare: Hetzner resolvers held a stale
# NXDOMAIN for api.lashaddictstudio.com for hours after it was created
# (2026-09-30). Container names still resolve through aardvark-dns.
dns:
- 1.1.1.1
- 1.0.0.1
domainname: ${NEXT_DOMAIN} domainname: ${NEXT_DOMAIN}
networks: networks:
- ${NETWORK:-nginx-bridge} - ${NETWORK:-nginx-bridge}
@@ -35,11 +29,6 @@ services:
lashaddict-backend: lashaddict-backend:
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest} image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
container_name: ${BACKEND_CONTAINER_NAME:-lashaddict-backend} container_name: ${BACKEND_CONTAINER_NAME:-lashaddict-backend}
# Same Cloudflare DNS pin as lashaddict-next (see its comment): the backend
# fetches its own auth discovery URL, which Hetzner resolvers failed.
dns:
- 1.1.1.1
- 1.0.0.1
hostname: ${BACKEND_CONTAINER_NAME:-lashaddict-backend} hostname: ${BACKEND_CONTAINER_NAME:-lashaddict-backend}
domainname: ${BACKEND_DOMAIN:-lashaddict.gbrown.org} domainname: ${BACKEND_DOMAIN:-lashaddict.gbrown.org}
networks: networks:
@@ -69,11 +58,6 @@ services:
lashaddict-dashboard: lashaddict-dashboard:
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest} image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
container_name: ${DASHBOARD_CONTAINER_NAME:-lashaddict-dashboard} container_name: ${DASHBOARD_CONTAINER_NAME:-lashaddict-dashboard}
# Same Cloudflare DNS pin as lashaddict-next (see its comment): the backend
# fetches its own auth discovery URL, which Hetzner resolvers failed.
dns:
- 1.1.1.1
- 1.0.0.1
hostname: ${DASHBOARD_CONTAINER_NAME:-lashaddict-dashboard} hostname: ${DASHBOARD_CONTAINER_NAME:-lashaddict-dashboard}
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-lashaddict.gbrown.org}} domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-lashaddict.gbrown.org}}
networks: networks:
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: N8n Description=Podman Compose: N8n
After=network-online.target podman-postgresql.service After=network-online.target podman-postgresql.service
Wants=network-online.target Wants=network-online.target
@@ -37,6 +37,7 @@ services:
#networks: ['nginx-bridge'] #networks: ['nginx-bridge']
#environment: #environment:
#POSTGRES_USER: 'npm' #POSTGRES_USER: 'npm'
#POSTGRES_PASSWORD: '<see this service .env on the machine>'
#POSTGRES_DB: 'npm' #POSTGRES_DB: 'npm'
#labels: #labels:
#com.centurylinklabs.watchtower.enable: 'true' #com.centurylinklabs.watchtower.enable: 'true'
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Nginx Proxy Manager Description=Podman Compose: Nginx Proxy Manager
After=network-online.target podman-postgresql.service After=network-online.target podman-postgresql.service
Wants=network-online.target Wants=network-online.target
+2
View File
@@ -0,0 +1,2 @@
PENPOT_SECRET_KEY=CHANGE_ME
PENPOT_DATABASE_PASSWORD=CHANGE_ME
+120
View File
@@ -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
File renamed without changes.
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Portainer Description=Podman Compose: Portainer
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: PostgreSQL Description=Podman Compose: PostgreSQL
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
+1
View File
@@ -0,0 +1 @@
RUSTDESK_API_RUSTDESK_JWT_KEY=CHANGE_ME
@@ -72,7 +72,7 @@ services:
- RUSTDESK_API_RUSTDESK_ID_SERVER=relay.gbrown.org - RUSTDESK_API_RUSTDESK_ID_SERVER=relay.gbrown.org
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=relay.gbrown.org - RUSTDESK_API_RUSTDESK_RELAY_SERVER=relay.gbrown.org
- RUSTDESK_API_RUSTDESK_API_SERVER=https://rustdesk.gbrown.org - RUSTDESK_API_RUSTDESK_API_SERVER=https://rustdesk.gbrown.org
- RUSTDESK_API_RUSTDESK_KEY=${RUSTDESK_API_RUSTDESK_KEY} - RUSTDESK_API_RUSTDESK_KEY=WLgvHhau6aa5nDPQutTHeQBpIrOEb8aPXByVBWQwkKc=
- RUSTDESK_API_RUSTDESK_JWT_KEY=${RUSTDESK_API_RUSTDESK_JWT_KEY} - RUSTDESK_API_RUSTDESK_JWT_KEY=${RUSTDESK_API_RUSTDESK_JWT_KEY}
- RUSTDESK_API_LANG=en - RUSTDESK_API_LANG=en
- RUSTDESK_API_APP_DISABLE_PWD_LOGIN=true - RUSTDESK_API_APP_DISABLE_PWD_LOGIN=true
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Rustdesk Description=Podman Compose: Rustdesk
After=network-online.target podman.socket After=network-online.target podman.socket
Wants=network-online.target Wants=network-online.target
@@ -64,6 +64,3 @@ SPOON_AGENT_WORKER_ID=production-worker
SPOON_AGENT_WORKER_HTTP_PORT="3921" SPOON_AGENT_WORKER_HTTP_PORT="3921"
SPOON_AGENT_WORKER_INTERNAL_TOKEN=CHANGE_ME SPOON_AGENT_WORKER_INTERNAL_TOKEN=CHANGE_ME
SPOON_AGENT_WORKER_URL=http://spoon-agent-worker:3921 SPOON_AGENT_WORKER_URL=http://spoon-agent-worker:3921
# Also set on the live machine
PORT=CHANGE_ME
File renamed without changes.
@@ -56,8 +56,3 @@ POSTGRES_URL=CHANGE_ME
#ACTIONS_USER_TIMEOUT_SECS= #ACTIONS_USER_TIMEOUT_SECS=
#RUST_LOG= #RUST_LOG=
#RUST_BACKTRACE= #RUST_BACKTRACE=
# Also set on the live machine
BACKEND_PORT=CHANGE_ME
PORT=CHANGE_ME
SITE_PROXY_PORT=CHANGE_ME
File renamed without changes.
@@ -19,7 +19,7 @@ services:
container_name: convertx container_name: convertx
hostname: convertx hostname: convertx
domainname: convert.tools.gbrown.org domainname: convert.tools.gbrown.org
networks: ["nginx-bridge"] networks: ['nginx-bridge']
environment: environment:
- TZ=America/New_York - TZ=America/New_York
- ALLOW_UNAUTHENTICATED=true - ALLOW_UNAUTHENTICATED=true
@@ -37,7 +37,7 @@ services:
container_name: it-tools container_name: it-tools
hostname: it-tools hostname: it-tools
domainname: tools.gbrown.org domainname: tools.gbrown.org
networks: ["nginx-bridge"] networks: ['nginx-bridge']
environment: environment:
- TZ=America/New_York - TZ=America/New_York
labels: labels:
@@ -45,12 +45,33 @@ services:
tty: true tty: true
restart: unless-stopped 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: bento-pdf:
image: ghcr.io/alam00000/bentopdf:latest image: ghcr.io/alam00000/bentopdf:latest
container_name: bento-pdf container_name: bento-pdf
hostname: bento-pdf hostname: bento-pdf
domainname: bento.tools.gbrown.org domainname: bento.tools.gbrown.org
networks: ["nginx-bridge"] networks: ['nginx-bridge']
environment: environment:
- TZ=America/New_York - TZ=America/New_York
- PUID=1000 - PUID=1000
@@ -61,28 +82,7 @@ services:
tty: true tty: true
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080"] test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:8080']
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
# pdf-tools:
# image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
# container_name: pdf-tools
# hostname: pdf-tools
# domainname: pdf.tools.gbrown.org
# networks: ["nginx-bridge"]
# environment:
# - TZ=America/New_York
# - LANGS=en_US
# - DOCKER_ENABLE_SECURITY=true
# volumes:
# - ./volumes/pdf-tools/trainingData:/usr/share/tessdata:Z
# - ./volumes/pdf-tools/extraConfigs:/configs:Z
# - ./volumes/pdf-tools/customFiles:/customFiles:Z
# - ./volumes/pdf-tools/logs:/logs/:Z
# - ./volumes/pdf-tools/pipeline:/pipeline/:Z
# labels:
# com.centurylinklabs.watchtower.enable: "true"
# tty: true
# restart: unless-stopped
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Tools Description=Podman Compose: Tools
After=network-online.target podman.socket After=network-online.target podman.socket
Wants=network-online.target Wants=network-online.target
File renamed without changes.
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Uptime Description=Podman Compose: Uptime
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
@@ -20,7 +20,3 @@ SMTP_HOST=smtp.usesend.gbrown.org
SMTP_USER=usesend SMTP_USER=usesend
USESEND_PORT=3000 USESEND_PORT=3000
USESEND_URL=https://usesend.gbrown.org USESEND_URL=https://usesend.gbrown.org
# Also set on the live machine
AWS_ACCESS_KEY_ID=CHANGE_ME
AWS_SECRET_ACCESS_KEY=CHANGE_ME
+88
View File
@@ -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 @@
ADMIN_TOKEN=CHANGE_ME
+55
View File
@@ -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
@@ -1,4 +1,5 @@
[Unit] [Unit]
RequiresMountsFor=/home/gib/Media
Description=Podman Compose: Vaultwarden Description=Podman Compose: Vaultwarden
After=network-online.target podman.socket After=network-online.target podman.socket
Wants=network-online.target Wants=network-online.target
-48
View File
@@ -1,48 +0,0 @@
# Beszel AGENT ONLY — the hub now lives on the VPS.
#
# The hub (web UI + database) was moved to the VPS on 2026-08-11 and is reachable at
# https://beszel.gibbyb.com. This host keeps only the agent, which collects metrics and
# serves them to that hub over the WireGuard link (VPS 192.168.2.2 -> here 192.168.1.2:45876).
#
# KEY below is the VPS hub's public key. If the VPS hub is ever rebuilt its keypair
# changes, and this value must be updated to match:
# ssh-keygen -y -f ~/Server/Beszel/volumes/hub/id_ed25519 (run on the VPS)
#
# Previous config, including the local hub service, is in compose.yml.bak-2026-08-11.
#
# The agent uses network_mode: host — that is the documented setup and is what lets it
# read real host network interfaces rather than a container's veth. Docker forbids setting
# hostname/domainname together with host networking, so those keys are absent.
services:
beszel-agent:
image: henrygd/beszel-agent-nvidia:latest
container_name: beszel-agent
network_mode: host
# Give the agent read access to NVIDIA's management interface so Beszel can
# report GPU utilization, temperature, power draw, and VRAM usage.
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [utility]
environment:
- TZ=America/New_York
- PORT=45876
- KEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDBWNqxKGRltqXJZg+wlDOtICeLo19ftQ6P3P8+uJg8y
# /boot is a separate 974M partition (nvme1n1p1), not part of the LUKS
# root the agent finds on its own. It filling up is what broke a 1032
# package dnf transaction on 2026-08-09, so it is worth alerting on.
- EXTRA_FILESYSTEMS=/boot
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/agent:/var/lib/beszel-agent
- /var/run/docker.sock:/var/run/docker.sock:ro
- /boot:/boot:ro
- /etc/localtime:/etc/localtime:ro
tty: true
stdin_open: true
restart: unless-stopped
@@ -1,18 +0,0 @@
[Unit]
Description=Podman Compose: Agents/Beszel
After=network-online.target podman.socket
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=%h/Server/Agents/Beszel
ExecStart=/usr/bin/podman compose up -d
ExecStop=/usr/bin/podman compose down
RemainAfterExit=yes
TimeoutStartSec=900
TimeoutStopSec=90
Restart=on-failure
RestartSec=30
[Install]
WantedBy=default.target
-1
View File
@@ -1 +0,0 @@
GITEA_RUNNER_REGISTRATION_TOKEN=CHANGE_ME
-153
View File
@@ -1,153 +0,0 @@
# Gitea Actions runners — 3 parallel runners, each with its own dind sidecar.
#
# Each runner gets an isolated Docker daemon rather than the host socket, so a
# CI job cannot reach the other ~180 containers on this box. Cost: each dind
# keeps its own image cache under ./volumes/runner-N/dind.
#
# The pre-existing host runner (/home/gib/.local/bin/act_runner, name
# "server-gib") is intentionally left running.
#
# Registration token generated with:
# docker exec -u git gitea gitea actions generate-runner-token
# It is reusable — all three runners register with the same one.
networks:
nginx-bridge:
external: true
x-runner-env: &runner-env
TZ: America/New_York
GITEA_INSTANCE_URL: https://git.gbrown.org
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
GITEA_RUNNER_LABELS: "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04,ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: /certs/client
CONFIG_FILE: /data/config.yml
x-dind-env: &dind-env
TZ: America/New_York
DOCKER_TLS_CERTDIR: /certs
services:
gitea-runner-1:
image: gitea/act_runner:latest
container_name: gitea-runner-1
hostname: gitea-runner-1
domainname: git.gbrown.org
networks: ['nginx-bridge']
environment:
<<: *runner-env
GITEA_RUNNER_NAME: gitea-runner-1
DOCKER_HOST: tcp://gitea-runner-1-dind:2376
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/runner-1/data:/data
- ./volumes/runner-1/certs:/certs
- /etc/localtime:/etc/localtime:ro
depends_on:
- gitea-runner-1-dind
tty: true
stdin_open: true
restart: unless-stopped
gitea-runner-1-dind:
image: docker:dind
container_name: gitea-runner-1-dind
hostname: gitea-runner-1-dind
domainname: git.gbrown.org
networks: ['nginx-bridge']
privileged: true
environment:
<<: *dind-env
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/runner-1/certs:/certs
- ./volumes/runner-1/dind:/var/lib/docker
- /etc/localtime:/etc/localtime:ro
tty: true
stdin_open: true
restart: unless-stopped
gitea-runner-2:
image: gitea/act_runner:latest
container_name: gitea-runner-2
hostname: gitea-runner-2
domainname: git.gbrown.org
networks: ['nginx-bridge']
environment:
<<: *runner-env
GITEA_RUNNER_NAME: gitea-runner-2
DOCKER_HOST: tcp://gitea-runner-2-dind:2376
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/runner-2/data:/data
- ./volumes/runner-2/certs:/certs
- /etc/localtime:/etc/localtime:ro
depends_on:
- gitea-runner-2-dind
tty: true
stdin_open: true
restart: unless-stopped
gitea-runner-2-dind:
image: docker:dind
container_name: gitea-runner-2-dind
hostname: gitea-runner-2-dind
domainname: git.gbrown.org
networks: ['nginx-bridge']
privileged: true
environment:
<<: *dind-env
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/runner-2/certs:/certs
- ./volumes/runner-2/dind:/var/lib/docker
- /etc/localtime:/etc/localtime:ro
tty: true
stdin_open: true
restart: unless-stopped
gitea-runner-3:
image: gitea/act_runner:latest
container_name: gitea-runner-3
hostname: gitea-runner-3
domainname: git.gbrown.org
networks: ['nginx-bridge']
environment:
<<: *runner-env
GITEA_RUNNER_NAME: gitea-runner-3
DOCKER_HOST: tcp://gitea-runner-3-dind:2376
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/runner-3/data:/data
- ./volumes/runner-3/certs:/certs
- /etc/localtime:/etc/localtime:ro
depends_on:
- gitea-runner-3-dind
tty: true
stdin_open: true
restart: unless-stopped
gitea-runner-3-dind:
image: docker:dind
container_name: gitea-runner-3-dind
hostname: gitea-runner-3-dind
domainname: git.gbrown.org
networks: ['nginx-bridge']
privileged: true
environment:
<<: *dind-env
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/runner-3/certs:/certs
- ./volumes/runner-3/dind:/var/lib/docker
- /etc/localtime:/etc/localtime:ro
tty: true
stdin_open: true
restart: unless-stopped
@@ -1,18 +0,0 @@
[Unit]
Description=Podman Compose: Agents/Gitea
After=network-online.target podman.socket
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=%h/Server/Agents/Gitea
ExecStart=/usr/bin/podman compose up -d
ExecStop=/usr/bin/podman compose down
RemainAfterExit=yes
TimeoutStartSec=900
TimeoutStopSec=90
Restart=on-failure
RestartSec=30
[Install]
WantedBy=default.target
-39
View File
@@ -1,39 +0,0 @@
services:
# portainer:
# image: portainer/portainer-ee:latest
# container_name: portainer
# hostname: portainer
# domainname: port.gbrown.org
# networks:
# - nginx-bridge
# labels:
# com.centurylinklabs.watchtower.enable: "true"
# ports:
# - 9443:9443
# volumes:
# - ./volumes/data:/data
# - /var/run/docker.sock:/var/run/docker.sock
# tty: true
# stdin_open: true
# restart: unless-stopped
portainer-agent:
image: portainer/agent:2.33.2
container_name: portainer-agent
hostname: portainer-agent
networks:
- nginx-bridge
labels:
com.centurylinklabs.watchtower.enable: "true"
ports: ["9001:9001"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
- /:/host
tty: true
stdin_open: true
restart: unless-stopped
networks:
nginx-bridge:
external: true
@@ -1,18 +0,0 @@
[Unit]
Description=Podman Compose: Agents/Portainer
After=network-online.target podman.socket
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=%h/Server/Agents/Portainer
ExecStart=/usr/bin/podman compose up -d
ExecStop=/usr/bin/podman compose down
RemainAfterExit=yes
TimeoutStartSec=900
TimeoutStopSec=90
Restart=on-failure
RestartSec=30
[Install]
WantedBy=default.target
-2
View File
@@ -1,2 +0,0 @@
RTSP_PASSWORD=CHANGE_ME
PLUS_API_KEY=CHANGE_ME
-79
View File
@@ -1,79 +0,0 @@
networks:
nginx-bridge:
external: true
services:
frigate:
image: ghcr.io/blakeblackshear/frigate:0.17.1-tensorrt # For Nvidia GPU
container_name: frigate
hostname: frigate
domainname: frigate.gbrown.org
networks: [nginx-bridge]
ports:
#- "8971:8971"
- "5000:5000"
#- "8554:8554"
#- "8555:8555"
# Check host system's actual render/video/plugdev group IDs with 'getent group render', 'getent group video', and 'getent group plugdev'
# Must add these exact IDs in container's group_add section or OpenVINO GPU acceleration will fail
group_add:
- "105" # render
- "110" # render
- "39" # video
- "46" # plugdev
shm_size: "256mb"
env_file:
- .env
environment:
- TZ=America/New_York
- PGID=1000
- PUID=1000
- FRIGATE_RTSP_PASSWORD=${RTSP_PASSWORD}
#- HSA_OVERRIDE_GFX_VERSION=11.0.0
- PLUS_API_KEY=${PLUS_API_KEY}
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/frigate/config:/config:rw
- ./volumes/frigate/data:/usr/local/nginx/conf:rw
- /home/gib/Public/Server_Backup/frigate:/media/frigate
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
- /etc/localtime:/etc/localtime:ro
# - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB
devices:
- /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
#- /dev/kfd:/dev/kfd
stop_grace_period: 30s
restart: unless-stopped
tty: true
security_opt: ['seccomp:unconfined','label:disable','systempaths=unconfined']
#privileged: true
runtime: nvidia
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
mqtt:
image: eclipse-mosquitto:2
container_name: mqtt
hostname: mqtt
domainname: mqtt.gbrown.org
networks: [nginx-bridge]
ports: ["1883:1883"]
#env_file: [.env]
environment: [TZ=America/New_York]
labels:
com.centurylinklabs.watchtower.enable: "true"
volumes:
- ./volumes/mosquitto/config:/mosquitto/config:rw
- ./volumes/mosquitto/data:/mosquitto/data:rw
- ./volumes/mosquitto/log:/mosquitto/log:rw
tty: true
security_opt: ['seccomp:unconfined','label:disable','systempaths=unconfined']
restart: unless-stopped
@@ -1,18 +0,0 @@
[Unit]
Description=Podman Compose: Frigate
After=network-online.target podman.socket
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=%h/Server/Frigate
ExecStart=/usr/bin/podman compose up -d
ExecStop=/usr/bin/podman compose down
RemainAfterExit=yes
TimeoutStartSec=900
TimeoutStopSec=90
Restart=on-failure
RestartSec=30
[Install]
WantedBy=default.target
Loaded 100 of 226 files, more files were not shown because too many files have changed in this diff. Show more