Compare commits

...
195 Commits
Author SHA1 Message Date
poprhythm 15640da229 minecraft: comment out immersive portals server 2026-07-29 23:44:06 +00:00
poprhythm 7468df2b8d watchtower: reduce poll interval to 24h to cut notification spam 2026-07-20 13:25:05 +00:00
poprhythm f665865762 cleanup: remove quizmaster and leafweb (unused/orphaned), add watchtower monitor-only labels, minor consistency fixes
- quizmaster: removed entirely (test service, no longer used); also removed Portainer stack and /srv/quizmaster data
- leafweb: removed orphaned MSSQL compose file (no running container, no Portainer stack, no data on disk)
- authelia, gitea, couchdb, portainer, immich (server + ml): add com.centurylinklabs.watchtower.monitor-only=true so watchtower still notifies on new versions without auto-applying them
- ladder: restore restart: unless-stopped (was commented out)
- rackpeek: add missing TZ env var
2026-07-20 02:19:32 +00:00
poprhythm 2f1a988ac3 security: remove hardcoded secrets, drop unneeded privileged mode, restrict internal-only ports to localhost
- watchtower: move Telegram token/chat ID to .env instead of plaintext in compose
- plex: remove stale/dead PLEX_CLAIM token
- uptime-kuma: mount docker.sock read-only (only needs to read container state)
- home-assistant: drop privileged: true (no device passthrough configured)
- glances: drop privileged: true, bind dashboard port to 127.0.0.1
- couchdb, docker-registry, leafweb: bind ports to 127.0.0.1 (proxy/localhost-only access confirmed, no direct LAN clients)
2026-07-20 01:49:56 +00:00
poprhythm 362643a064 minecraft: immersive: fix chat (disable enforce-secure-profile), op OlympicJumperL, allow flight 2026-07-12 17:27:07 +00:00
poprhythm eac385cc70 minecraft: immersive: force gamemode creative on login 2026-07-12 17:22:13 +00:00
poprhythm 586b48457b minecraft: immersive: set creative mode, enable command blocks, bump JVM heap to 3G 2026-07-12 17:18:41 +00:00
poprhythm 4febd0c303 minecraft: rename container to minecraft-immersive 2026-07-12 17:09:32 +00:00
poprhythm ac6946ccb5 minecraft: rename service key to immersive:, restore server name to Immersive Portals 2026-07-12 17:09:16 +00:00
poprhythm 6a2d24b6ea minecraft: bump immersive: to 1.20.4, pin gravity-api-fork to v1.3.0-mc1.20.4 2026-07-12 17:08:11 +00:00
poprhythm 69d6640d54 minecraft: rename modded to immersive:, set seed, add server icon 2026-07-12 16:58:36 +00:00
poprhythm 089f7eafa9 minecraft: add Immersive Portals SMP, comment out creative, rename terra to Terraforma SMP 2026-07-12 16:53:31 +00:00
poprhythm b9a050f8ab immich: migrate postgres from deprecated pgvecto.rs to VectorChord
immich-server auto-updated to v3.0.1 via watchtower but postgres stayed
pinned to the old pgvecto.rs image, causing a crash loop (missing vector
extension). Per official Immich migration guide.
2026-07-04 01:41:10 +00:00
poprhythm cbbcb76fcf ollama/open-webui: fix OIDC auth loop and session key persistence
open-webui was sending OAuth credentials via both client_secret_basic
(header) and client_secret_post (body) simultaneously, which Authelia
rejects. Setting OAUTH_TOKEN_ENDPOINT_AUTH_METHOD=client_secret_post
forces a single method and matches the updated Authelia client config.

WEBUI_SECRET_KEY is now a stable env var so watchtower image updates
no longer invalidate all user sessions.
2026-07-02 02:26:26 +00:00
poprhythm cfefcbfe1b minecraft: add Empire SMP server, comment out Battle Royale 2026-06-30 15:36:03 +00:00
poprhythm 8f3e7614fa inbox-zero: pin to v1.4.12 (latest tag has broken build, missing node_modules) 2026-06-15 01:56:41 +00:00
poprhythm 82cc6117fb homebox: add HBOX_AUTH_API_KEY_PEPPER env var (required by new version) 2026-06-15 01:36:40 +00:00
poprhythm 8087d19d7f nginx-proxy-acme: replace broken symlinks with empty files
These vhost.d entries were symlinks to a non-existent 'public' file.
Portainer refuses to clone repos containing symlinks, blocking all
git-based stack redeployments. Replace with empty regular files
(equivalent behavior: no extra nginx config = public access).
2026-06-15 01:33:20 +00:00
poprhythm 8871c8354b audiobookshelf: switch audiobooks mount from Docker NFS volume to systemd automount bind mount
NFS named volumes go ESTALE when unRAID drives spin down and Docker cannot recover.
systemd automount handles NFS reconnection transparently; Docker just sees a bind mount.
2026-05-05 20:27:13 +00:00
poprhythm 1ac9f06e29 ollama: restrict port 11434 to 127.0.0.1 (LAN security audit) 2026-05-03 01:40:33 +00:00
poprhythm cb17443ab6 couchdb: mount local.ini for persistent CORS config (entrypoint chowns config files, :ro breaks startup) 2026-04-26 23:39:08 +00:00
poprhythm 98d64139fe couchdb: mount persistent CORS config to survive container updates 2026-04-26 23:20:59 +00:00
poprhythm ca170af736 watchtower: remove debug logging now that scan/update/cleanup is confirmed working 2026-04-26 22:08:21 +00:00
poprhythm a925d61e83 watchtower: add debug logging + disable self-update to diagnose scanned=0 2026-04-26 17:18:05 +00:00
poprhythm 2c90e4218b watchtower: switch to nickfedor/watchtower fork (containrrr archived Dec 2025, Docker API 1.25 incompatible) 2026-04-26 17:02:34 +00:00
poprhythm ba8f1f7668 minecraft: update battle server seed to 9037888329883360986 2026-04-05 22:38:42 +00:00
poprhythm c1c86e3ab2 minecraft: add ViaVersion to battle server for cross-version client support 2026-04-03 16:22:08 +00:00
poprhythm 26c89ebb39 minecraft: fix MODRINTH_PROJECTS separator (comma not space) 2026-04-03 16:18:22 +00:00
poprhythm d4a23d0d6f minecraft: add Paper + EzCountdown + WorldResetPlugin to battle server 2026-04-03 16:16:09 +00:00
poprhythm 5353df05fb minecraft: add battle server on port 25573 for kids PVP
Survival mode, seed 854721609616927000, PVP enabled.
30-min resource gather then battle.
2026-04-03 16:10:09 +00:00
poprhythm 76e02bf26d authelia: add ownCloud iOS OIDC client to git config 2026-03-29 18:59:51 +00:00
poprhythm c58ffb1b68 romm: pass ScreenScraper credentials via Portainer env vars 2026-03-20 22:03:49 +00:00
poprhythm 03d4dae8b5 inbox-zero: add REDIS_URL env var for email-stream / Deep Clean
REDIS_URL was not set, causing "REDIS_URL is not set" errors in
/api/email-stream which powers the Deep Clean full-inbox scan.
2026-03-14 14:34:45 +00:00
poprhythm 9dee75e2a0 docs: update NAS connection strategies for Immich and nas_library/nas_family mounts 2026-03-14 02:10:52 +00:00
poprhythm b2d7f82c53 immich: migrate upload volume from NFS named volume to CIFS systemd bind mount
Replace fragile Docker NFS named volume with /mnt/nas_family/immich-library
bind mount backed by systemd mnt-nas_family.mount (permanent CIFS).
2026-03-14 02:04:33 +00:00
poprhythm f1a9346861 map-frontend: fix @protomaps/basemaps version (4 doesn't exist, use 5) 2026-03-13 14:24:44 +00:00
poprhythm a483c2e7fc map-frontend: use absolute /srv paths for nginx config and html mounts 2026-03-13 14:19:57 +00:00
poprhythm cd232d93ac map-frontend: fix nginx config mount (dir instead of file) 2026-03-13 14:14:55 +00:00
poprhythm ef1d136a3e Add map-frontend (nginx + MapLibre GL), make pmtiles internal-only
- map-frontend: nginx serves MapLibre GL static app, proxies /tiles/ to pmtiles internally
- pmtiles: remove host port binding (internal to npm-network only), update public-url
- 5 themes: light, dark, grayscale, white, black
2026-03-13 14:14:16 +00:00
poprhythm 662b558d21 torrents: document NPM web UI instance labels (OPEN/VPN badges) 2026-03-13 03:36:52 +00:00
poprhythm d7d20cd080 torrents: add nas_library bind mount to qbittorrent_open 2026-03-12 20:53:36 +00:00
poprhythm 0b4571205a Add kiwix and pmtiles services with NAS library share mounts 2026-03-12 19:50:18 +00:00
poprhythm 97ff83a246 romm: add arcade (FBNeo) to populate-roms, add IGDB credentials
100 arcade titles spanning Pac-Man/Namco classics, Street Fighter,
Mortal Kombat, Metal Slug, King of Fighters, Samurai Shodown,
Fatal Fury, beat-em-ups (TMNT, Simpsons, X-Men), and more.

Also adds IGDB_CLIENT_ID/SECRET env vars for metadata scraping.
2026-03-10 01:54:53 +00:00
poprhythm 0fe632e1b5 stable-diffusion: disable ai-dock service portal auth 2026-03-09 03:09:44 +00:00
poprhythm aa686d0827 romm: add GB, GBA, and Amiga to populate-roms script
32 Game Boy, 36 GBA, and 41 Amiga titles. Amiga includes the full
Lemmings trilogy plus Turrican, Monkey Island, Chaos Engine, Worms,
Sensible Soccer, and more. Note: Amiga requires Kickstart firmware
(not in No-Intro) to actually run.
2026-03-09 02:13:53 +00:00
poprhythm 597ed26f89 romm: add IGDB credentials, add populate-roms script 2026-03-08 23:56:44 +00:00
poprhythm 9d084f3f19 Add RomM retro game library manager
MariaDB backend, library on NAS via /mnt/nas_games CIFS automount,
EmulatorJS built-in for browser-based play.
2026-03-08 22:06:53 +00:00
poprhythm adcaa39e41 calibre: move SQLite DBs to local SSD, sync library to NAS via lsyncd 2026-03-08 00:03:17 +00:00
poprhythm 964c8bedf3 Configure Ollama to bind on all interfaces for remote access 2026-03-07 03:41:52 +00:00
poprhythm 35a6715f01 Add Firefly III data import plan
Documents FIDI + SimpleFIN approach for Capital One and Citi bank import,
including OFX Direct Connect option for Citi and the FIDI compose snippet.
2026-03-06 20:06:22 +00:00
poprhythm 2925305ded Add Firefly III personal finance manager
PostgreSQL backend, npm-network integration, uploads/db on /srv/firefly-iii/.
2026-03-06 19:47:04 +00:00
poprhythm a92d4d21d5 Add Gmail filter cleanup script to remove dead/redundant filters 2026-03-06 18:47:10 +00:00
poprhythm 06646b5dee Fix gmail-label-cleanup: reduce batch size to 100 (API limit) 2026-03-06 16:04:15 +00:00
poprhythm 1bb41f5cc9 Fix gmail-label-cleanup: use GmailLabel methods instead of GmailApp 2026-03-06 16:03:20 +00:00
poprhythm 3c6e1588e7 Add Gmail label cleanup script for inbox-zero migration 2026-03-06 16:00:22 +00:00
poprhythm d50e696162 minecraft: disable hardcore+survival, add mem_limits, keep terra active 2026-03-06 13:09:19 +00:00
poprhythm 9bd4b70960 Fix inbox-zero: OLLAMA_BASE_URL must include /api path prefix 2026-03-06 04:27:59 +00:00
poprhythm 1e13be7739 Fix inbox-zero: bind web server to 0.0.0.0 for npm-network access 2026-03-06 04:02:04 +00:00
poprhythm b7b9dc91b4 Fix portainer.sh deploy: forward extra args (env vars) to cmd_deploy 2026-03-06 03:55:53 +00:00
poprhythm a08c7ffbcb Add inbox-zero service (Gmail AI assistant, Ollama backend) 2026-03-06 03:46:45 +00:00
poprhythm b615c503cd Fix clue-picker: remove host port binding (NPM handles ingress) 2026-03-04 20:11:40 +00:00
poprhythm 460afd7221 Add clue-picker service (docker-registry image, port 3001) 2026-03-04 20:07:33 +00:00
poprhythm 9a9d0562ae Fix ocis: replace Docker NFS volume with systemd automount bind mount
Same pattern as calibre fix — Docker NFS named volumes with soft mount
fail when unRAID drives spin down. Use systemd NFS automount at
/mnt/nas_owncloud instead; hard mount blocks on spin-up rather than EIO.
2026-03-02 03:59:43 +00:00
poprhythm aee517463d Fix calibre: switch to NFS systemd automount bind mounts
CIFS uid/gid mismatch caused Permission denied on files created via NFS
(cert.key, .gnupg, etc. owned by uid=99 with mode 600/700; SMB user
can't access them server-side). NFS maps uid/gid directly, avoiding the
issue. Use hard mount so spin-up blocks briefly instead of returning EIO.
2026-03-02 03:45:31 +00:00
poprhythm eb5f9117c8 Fix calibre: use /mnt/media (consolidate with existing mount point)
/mnt/media already existed; no need for a separate /mnt/nas_media.
Same CIFS share (//192.168.1.192/media), shorter bind mount paths.
2026-03-02 01:15:19 +00:00
poprhythm ebd813afcc Fix calibre: replace NFS volumes with systemd automount bind mounts
SQLite (Calibre Notes DB) fails with SQLITE_IOERR on soft NFS when unRAID
drives spin down. Replace Docker NFS named volumes with bind mounts to
/mnt/nas_media (CIFS via systemd automount), same pattern as backrest and
filebrowser-colleen-hd. systemd handles drive reconnection independently.
2026-03-02 01:06:29 +00:00
poprhythm d186aec6e5 Switch ocis NAS share from SMB to NFS Docker named volume
Replace /mnt/nas_owncloud CIFS bind mount with nas_owncloud external
NFS volume (192.168.1.192:/mnt/user/owncloud). Removes dependency on
systemd CIFS automount units.
2026-03-01 14:56:25 +00:00
poprhythm e2050fee96 Fix Spoolman: remove user override, add PUID/PGID env vars 2026-03-01 04:29:31 +00:00
poprhythm 860b7772cc Add Spoolman filament spool manager 2026-03-01 04:27:40 +00:00
poprhythm 0cd293e763 Fix ownCloud Desktop OIDC client config in Authelia
- Change public: true → false (desktop app sends client_secret_basic)
- Add client_secret reference to secrets file
- Add token_endpoint_auth_method: client_secret_basic
- Add response_types: [code] (required for offline_access scope)
- Add grant_types: [authorization_code, refresh_token] (required to issue refresh_token)
2026-03-01 02:53:14 +00:00
poprhythm ec56e1f68e Fix ownCloud Desktop client_id (missing '69' suffix) 2026-03-01 01:12:12 +00:00
poprhythm 9d5f5cec43 Add ownCloud Desktop OIDC client to Authelia
Uses the well-known desktop app client_id with localhost redirect URI
for PKCE authentication flow (RFC 8252 loopback).
2026-03-01 01:11:13 +00:00
poprhythm 650be93503 Add get-env/set-env commands to portainer.sh; fix redeploy env var wipe
- get-env: display env vars for a named stack
- set-env: merge KEY=VALUE pairs into a stack's env vars and redeploy
  (uses git/redeploy endpoint with pullImage:false for git-linked stacks)
- redeploy: now preserves existing env vars by including them in the
  git/redeploy payload (previously wiped them when env was omitted)
2026-03-01 01:02:49 +00:00
poprhythm 6ad7e74c39 Add portainer.sh management script (list/redeploy/deploy) 2026-03-01 00:46:48 +00:00
poprhythm 4f1c422d4d Store oCIS user files on unRAID owncloud share 2026-03-01 00:40:38 +00:00
poprhythm f03033b176 Set OCIS_ADMIN_USER_ID for poprhythm 2026-02-28 23:42:52 +00:00
poprhythm 294bbde259 Add CSP config to allow Authelia OIDC connections from browser 2026-02-28 23:39:08 +00:00
poprhythm dab1267e90 Add CORS config to Authelia OIDC for browser-based PKCE clients 2026-02-28 23:33:12 +00:00
poprhythm fc886e89e0 Fix oCIS init: pipe answer to avoid interactive prompt 2026-02-28 23:22:28 +00:00
poprhythm 827675d847 Add oCIS cloud storage with Authelia OIDC
Deploy ownCloud Infinite Scale as a self-hosted cloud storage service
at cloud.kolpacksoftware.com using Authelia as the external OIDC IdP.
Configures a PKCE public client (no secret required).
2026-02-28 23:19:28 +00:00
poprhythm 1c8547d5da Remove picoshare and dashy (unused services) 2026-02-28 23:00:39 +00:00
poprhythm 44c773c943 Mount Docker config into Watchtower for private registry auth 2026-02-28 16:22:07 +00:00
poprhythm 3417fdcfaa Exclude unupdatable containers from Watchtower
- dns-updateip: local image not on any registry
- immich redis/postgres: pinned by digest, no floating tag
2026-02-28 16:07:23 +00:00
poprhythm dce25c6ed4 Add Telegram notifications to Watchtower via Shoutrrr 2026-02-28 15:57:43 +00:00
poprhythm acfde6f040 Upgrade Uptime Kuma from v1 to v2 2026-02-28 14:58:33 +00:00
poprhythm 10d008636c Enable watchtower image cleanup after updates 2026-02-28 14:50:11 +00:00
poprhythm 66664a5925 Fix open-webui OIDC token auth method to client_secret_basic
Open WebUI (authlib) sends credentials in the Authorization header
(client_secret_basic), not the POST body. Authelia was rejecting it.
2026-02-27 03:08:30 +00:00
poprhythm bfb71d67d2 Add Immich OIDC client to Authelia 2026-02-27 03:01:56 +00:00
poprhythm 696398a863 Document OIDC JWK key handling in git config
path:// scheme not supported for key field in Authelia 4.38.
Host copy inlines the PEM as a YAML block scalar written via Python.
2026-02-26 20:49:19 +00:00
poprhythm e6d4b0a349 Revert OIDC client secrets to file-based approach
Bcrypt hashes contain $ signs which Portainer interpolates when storing
as env vars, truncating the values. Use {{ secret "file" }} template
syntax instead — hashes live in /srv/authelia/config/secrets/ on the
host, written via Python to avoid shell interpolation.

Only $ -safe values (hex strings) remain as env vars.
2026-02-26 20:03:03 +00:00
poprhythm 39f6ca8530 Fix Authelia OIDC client secret syntax to use template filter
expand-env double-processes substituted values so $ in bcrypt hashes
get re-expanded. Switch back to template filter with {{ env "VAR" }}
syntax which returns values as-is.
2026-02-26 20:00:44 +00:00
poprhythm 907d214b5c Fix Authelia OIDC config to use expand-env filter with ${VAR} substitution
- Switch X_AUTHELIA_CONFIG_FILTERS from template to expand-env so ${VAR}
  syntax in config files is actually substituted
- Add missing env var pass-throughs for OIDC HMAC secret and client secrets
- Update git config client_secret fields to use ${VAR} syntax (matching host)
- Update .env.example to document all required Portainer env vars
2026-02-26 19:57:55 +00:00
poprhythm 6e62d9ba2f Fix obico model_cache mount path to /model_cache/ml_api 2026-02-26 17:04:22 +00:00
poprhythm a6f2d4c4b2 Mount model_cache volume for Obico ML model weights
Model weights (~193MB each, darknet + onnx) are stored at
/srv/obico/config/model_cache/ and mounted at /model_cache in the
container since they are not baked into the imagegenius image.
2026-02-26 16:46:28 +00:00
poprhythm 196613f684 Add Obico server for 3D printer failure detection
imagegenius monolithic image with NVIDIA GPU (GTX 1660 SUPER) via cuda tag.
Includes internal Redis, npm-network for reverse proxy, DJANGO_SECRET_KEY
as env var placeholder for Portainer.
2026-02-26 16:26:34 +00:00
poprhythm 5e91f0c68b Fix Authelia OIDC client secrets using template secret files
- Switch from unsupported \${VAR} substitution to {{ secret "..." }} template syntax
- Enable X_AUTHELIA_CONFIG_FILTERS=template in compose
- Client secrets now loaded from /config/secrets/oidc_* files on host
- Use PBKDF2-SHA512 hashes (not bcrypt, not plaintext)
2026-02-26 02:04:52 +00:00
poprhythm 0ce030275b Add OIDC secret placeholders to .env.example files
- Add OIDC HMAC and client secrets to authelia/.env.example
- Create ollama/.env.example with OIDC client secret placeholder
2026-02-26 01:42:34 +00:00
poprhythm 645d908ca5 Add Authelia OIDC SSO to Open WebUI
- Add open-webui OIDC client to Authelia config
- Configure open-webui with OIDC env vars pointing to Authelia
- Secret managed via AUTHELIA_OIDC_CLIENT_SECRET_OPEN_WEBUI env var in Portainer
2026-02-26 01:37:28 +00:00
poprhythm 94819639dd Add LiveSync synchronization method step to Obsidian setup guide 2026-02-24 18:02:18 +00:00
poprhythm e923fe0655 Add Obsidian LiveSync setup guide with verified step-by-step instructions 2026-02-24 17:55:10 +00:00
poprhythm fecb4831ad Add Brave API key to openclaw, fix uptime-kuma auth and TCP monitors
- openclaw: expose BRAVE_API_KEY env var for web search tool
- uptime-kuma: prefer username/password auth (API key token auth unreliable)
- uptime-kuma: add TCP monitor type support to manage_monitors.py
2026-02-23 19:14:01 +00:00
poprhythm c4157023f6 Add token_endpoint_auth_method: client_secret_post to linkding OIDC client 2026-02-23 19:00:11 +00:00
poprhythm 38d5edc37f Fix linkding OIDC env var names: drop LD_ prefix from provider settings 2026-02-23 18:56:00 +00:00
poprhythm d0037cf4cd Add Linkding OIDC via Authelia, fix jwks key name in config
- linkding: add OIDC env vars pointing to Authelia as identity provider
- authelia/config: fix issuer_private_keys → jwks (correct key for 4.38.x)
  and replace non-functional template function with host-managed note
2026-02-23 18:45:33 +00:00
poprhythm 4c838e1989 code-server: mount /srv/authelia/config for editing users_database.yaml 2026-02-23 15:25:26 +00:00
poprhythm 3ec74c1f69 Restrict wildcard rule to admins; jsmith (guests) can only access ultralytics 2026-02-23 14:41:18 +00:00
poprhythm 4ec70062ce revert 877d916c15
revert Update ultralytics/docker-compose.yml
2026-02-23 13:43:05 +00:00
poprhythm 877d916c15 Update ultralytics/docker-compose.yml 2026-02-23 13:40:28 +00:00
poprhythm 4293022561 Fix authelia: default_redirection_url must differ from authelia_url 2026-02-22 20:26:00 +00:00
poprhythm 9f98a4081e Add Authelia SSO, remove authentik, restrict ultralytics port
- Add authelia/ stack: Authelia 4.38 + Redis 7-alpine on isolated
  authelia-internal bridge; Authelia also on npm-network for NPM
  forward-auth. Secrets via env vars (not committed).
- Add authelia/config/configuration.yaml: file-based users, SQLite
  storage, one_factor policy for *.kolpacksoftware.com
- Add **/users_database.yaml to .gitignore (host-only secret)
- Remove authentik/ (non-functional leftover)
- ultralytics: bind port 8501 to 127.0.0.1 only (auth enforced via NPM)
2026-02-22 20:21:11 +00:00
poprhythm 717ba151e5 Rename leafweb-vision to ultralytics 2026-02-22 19:50:12 +00:00
poprhythm 7ef6d248da Load yolo-plant-leaf-detection model as default in Streamlit UI
Run streamlit directly to pass model path via -- arg, loading
yolo11x_leaf.pt as the preselected model in the sidebar dropdown.
2026-02-21 19:58:49 +00:00
poprhythm aa9560569f Fix leafweb-vision: use YAML list form for command to avoid parse errors 2026-02-21 19:53:32 +00:00
poprhythm 697818845d Fix leafweb-vision: patch cv2.destroyAllWindows via sed at startup
opencv-python-headless swap doesn't override conda's cv2. Instead,
sed-patch the one offending line in streamlit_inference.py directly.
2026-02-21 19:51:49 +00:00
poprhythm eebb5d11d0 Fix leafweb-vision OpenCV headless error on cv2.destroyAllWindows
Replace opencv-python with opencv-python-headless so GUI calls are
no-ops instead of crashing in a display-less container.
2026-02-21 19:49:00 +00:00
poprhythm 3eeaa8ff6d Fix leafweb-vision: use yolo solutions inference + install streamlit at startup
streamlit-predict was removed in newer ultralytics; yolo solutions inference
is the current equivalent. Installs streamlit on startup via pip cache volume.
2026-02-21 19:46:00 +00:00
poprhythm 0aae4324a8 Add leafweb-vision service with YOLO Streamlit web UI
Runs ultralytics/ultralytics with yolo streamlit-predict on port 8501,
NVIDIA GPU passthrough, and npm-network for reverse proxy access.
2026-02-21 19:39:59 +00:00
poprhythm 1944589989 Revert OLLAMA_NUM_CTX (not a valid env var; using Modelfile instead) 2026-02-20 22:27:49 +00:00
poprhythm 03bf4bc05c Set OLLAMA_NUM_CTX=16384 to prevent prompt truncation for openclaw 2026-02-20 22:23:15 +00:00
poprhythm 2ab1cccdc8 Switch openclaw default model to qwen2.5:7b (supports tools) 2026-02-20 22:11:54 +00:00
poprhythm 0b38b19d20 Fix openclaw model contextWindow to 32768 (minimum is 16000) 2026-02-20 22:08:29 +00:00
poprhythm 18b202909c Rebuild openclaw from npm instead of premade image, configure Ollama
- Add Dockerfile using node:22-bookworm-slim + npm install -g openclaw@latest
- Update docker-compose.yml: use local build, add OLLAMA_API_KEY=ollama-local,
  remove legacy OPENCLAW_AGENT_PROVIDER/MODEL/OLLAMA_BASE_URL env vars
- Add setup.sh to create openclaw.json with explicit Ollama provider config

Key fixes vs previous attempt:
- Config file is openclaw.json (not config.json or auth-profiles.json)
- models.providers.ollama needs baseUrl with /v1 suffix + explicit model list
- OLLAMA_API_KEY env var is required to opt in to Ollama support
- reasoning:false on models prevents 400 errors from Ollama
2026-02-20 22:03:43 +00:00
poprhythm e9bf43469c Switch backrest and filebrowser-colleen-hd to systemd automount bind mount
Replace Docker CIFS named volumes with bind mounts to /mnt/nas_backup,
which is managed by a systemd automount unit. This handles drive spin-down
reconnection independently of Docker container lifecycle.
2026-02-20 21:30:57 +00:00
poprhythm bb41473360 Replace hard (ignored) with echo_interval=300 for CIFS spin-down resilience 2026-02-20 21:06:25 +00:00
poprhythm 03a2ce647b Add hard+noserverino to CIFS mounts to survive unRAID drive spin-down 2026-02-20 21:02:54 +00:00
poprhythm c84175640f Fix CIFS mount permissions: use uid=0/noperm for root-running containers 2026-02-19 02:33:43 +00:00
poprhythm 8523f597da Switch filebrowser-colleen-hd from NFS to SMB for reliable reconnection after drive spin-up 2026-02-18 19:18:46 +00:00
poprhythm 2544694586 Move hardcoded secrets to .env files for dns-updateip, docker-registry, openclaw
- dns-updateip: APIKEY now uses ${APIKEY} substitution
- docker-registry: SECRET_KEY_BASE now uses ${SECRET_KEY_BASE} substitution
- openclaw: OPENCLAW_GATEWAY_TOKEN now uses ${OPENCLAW_GATEWAY_TOKEN} substitution
- Add .env.example templates for backrest, dns-updateip, docker-registry, openclaw
2026-02-18 19:02:01 +00:00
poprhythm 5f704441fb Switch backrest backup mount from NFS to SMB for reliable reconnection after drive spin-up 2026-02-18 13:45:52 +00:00
poprhythm 3474043bb9 Update README with working ttyd web terminal configuration 2026-02-16 03:53:08 +00:00
poprhythm 9a7b00780c Set baud rate to 56k (7000 bytes/sec) for better balance 2026-02-16 03:35:02 +00:00
poprhythm ab465c0234 Add 2400 baud rate throttling for classic BBS nostalgia 2026-02-16 03:34:29 +00:00
poprhythm 54f3c37e7c Increase font size to 18 and set custom ORMG BBS title 2026-02-16 03:30:58 +00:00
poprhythm 9fbd6f6900 Enable write mode in ttyd with -W flag 2026-02-16 03:28:44 +00:00
poprhythm d7f7f19fbd Revert to simple busybox telnet - users can set ANSI in BBS settings 2026-02-16 03:26:44 +00:00
poprhythm eaaf15339d Use socat for raw TCP connection to BBS 2026-02-16 03:24:19 +00:00
poprhythm cba9531a71 Switch ttyd to use SSH instead of telnet for proper ANSI support 2026-02-16 03:23:24 +00:00
poprhythm 9f0d0249cd Use inetutils-telnet for proper terminal type negotiation 2026-02-16 03:22:18 +00:00
poprhythm 57150c9e5c Configure ttyd with xterm-256color for ANSI support 2026-02-16 03:20:45 +00:00
poprhythm 058247465a Fix ttyd container - use alpine image with busybox telnet 2026-02-16 03:19:20 +00:00
poprhythm e846a9c1cb Add ttyd web terminal for browser-based BBS access 2026-02-16 03:13:58 +00:00
poprhythm 23498377fc Add ORMG branding customization for Synchronet BBS
- Add custom ANSI art with "ORMG" branding instead of "Synchronet"
- Replace "bbs software" with "door games" in welcome screen
- Include original synch.ans as backup
- Update README with customization instructions
- Custom branding applied to /srv/synchronet/ctrl/answer.ans
2026-02-15 03:32:20 +00:00
poprhythm 752de89955 Change Synchronet HTTPS port to 8444 (8443 in use by code-server) 2026-02-15 02:46:09 +00:00
poprhythm eb56dd2513 Add Synchronet setup helper script 2026-02-15 02:42:05 +00:00
poprhythm 15901a6ff7 Add Synchronet BBS with Legend of the Red Dragon
- Configure bbsio/synchronet:3.19c Docker image
- Expose web interface (ports 8023/8443) and fTelnet websocket terminals
- Include telnet (23), SSH (2222), and classic BBS protocols
- LORD (Legend of the Red Dragon) included as built-in door game
- Add comprehensive README with setup and configuration instructions
2026-02-15 02:40:17 +00:00
poprhythm 0b34690bc5 Add user directive to RackPeek for proper permissions 2026-02-14 02:48:41 +00:00
poprhythm a712132eef Change RackPeek port to 8081 (8080 used by qbittorrent) 2026-02-14 02:47:36 +00:00
poprhythm 68f97de1ea Add RackPeek infrastructure documentation tool
RackPeek is a CLI tool to discover, manage, and document IT infrastructure
and home lab setups. It will help track hardware, services, networks, and
their relationships in a scriptable way.
2026-02-14 02:44:59 +00:00
poprhythm 7ab8533910 Simplify audiobookshelf volume configuration
Use local storage for application data and single NFS volume:
- /srv/audiobookshelf/config - local application config
- /srv/audiobookshelf/metadata - local metadata database
- NFS volume: /mnt/user/media/audiobooks (contains library/ and untagged/ subdirs)
2026-02-10 19:31:00 +00:00
poprhythm 2bf8895740 Configure audiobookshelf to use NFS volumes
Switch from local bind mounts to NFS volumes on 192.168.1.192:
- config: /mnt/user/media/audiobooks/config
- metadata: /mnt/user/media/audiobooks/metadata
- library: /mnt/user/media/audiobooks/library
- untagged: /mnt/user/media/audiobooks/untagged

Follows the same NFS pattern used by calibre for media storage.
2026-02-10 19:28:37 +00:00
poprhythm 24b6e247a6 Add audiobookshelf setup
Configure audiobookshelf for audiobook library management with:
- Library path: /mnt/media/audiobooks/library
- Untagged imports: /mnt/media/audiobooks/untagged
- Reverse proxy integration via npm-network
- Accessible at audiobookshelf.kolpacksoftware.com
2026-02-10 19:19:36 +00:00
poprhythm 2d91aab062 Add Home Assistant setup
Home automation platform for smart home device integration. Configured with:
- nginx reverse proxy integration at homeassistant.kolpacksoftware.com
- Data persistence at /srv/home-assistant
- Privileged mode for device access
2026-02-09 15:44:04 +00:00
poprhythm f0902944e0 Document OpenClaw troubleshooting attempts
Add comprehensive troubleshooting documentation for OpenClaw auth issues:
- All configuration attempts with Ollama and Anthropic
- Error messages and findings
- Links to relevant GitHub issues and documentation
- Recommendations for future attempts

Issue: Gateway mode fails with 'No API key found' despite correct auth-profiles.json configuration with both Ollama and real Anthropic API key.
2026-02-09 03:03:56 +00:00
poprhythm c7733147b3 Hardcode OpenClaw gateway token in docker-compose
Token is safe to commit as this is a private repo
2026-02-09 02:33:22 +00:00
poprhythm 716718504d Configure OpenClaw to use Ollama with llama3
Add environment variables to specify Ollama as the agent provider:
- OPENCLAW_AGENT_PROVIDER=ollama
- OPENCLAW_AGENT_MODEL=llama3
- OPENCLAW_OLLAMA_BASE_URL=http://ollama:11434
2026-02-09 02:32:23 +00:00
poprhythm 8209caecff Fix OpenClaw architecture mismatch - specify linux/amd64 platform
Adds explicit platform: linux/amd64 to prevent pulling ARM64 image on x86_64 systems, which causes "Exec format error"
2026-02-09 02:04:57 +00:00
poprhythm 68e3f673e8 Add OpenClaw setup documentation and utilities
- SETUP.md: Complete deployment guide with WebSocket configuration
- portainer-setup.sh: Automated Portainer stack creation script
- fix-permissions.sh: Storage directory permission fixer
- .gitignore: Ensure .credentials file is not committed

Note: WebSocket support must be enabled in nginx-proxy-manager for OpenClaw to work.
2026-02-08 21:45:31 +00:00
poprhythm d66fc86287 Add explicit --bind lan flag to OpenClaw gateway command
CLI flag takes precedence over env vars and config, ensuring
the gateway binds to LAN interface for nginx reverse proxy access.
2026-02-08 21:42:57 +00:00
poprhythm c2a25c2185 Add OPENCLAW_GATEWAY_BIND=lan environment variable
Fix 502 error by configuring OpenClaw gateway to bind on LAN interface
instead of localhost only, allowing nginx reverse proxy to connect.
2026-02-08 21:29:57 +00:00
poprhythm 7c4393828d Update OpenClaw for git-linked deployment with Ollama
- Switch to official ghcr.io/openclaw/openclaw:main image
- Remove port mappings in favor of nginx reverse proxy
- Add reverse proxy environment variables (VIRTUAL_HOST, etc.)
- Configure for Ollama integration instead of Anthropic API
- Add PUID/PGID for proper file permissions
2026-02-08 21:08:23 +00:00
poprhythm a6c4302c00 Add comprehensive Uptime Kuma management documentation
Complete documentation for monitor and tag management workflows:

New documentation:
- README.md: Quick reference and complete workflow guide
- TAG_MANAGEMENT.md: Comprehensive tag management strategies
- tag_monitor.sh: Quick CLI script to tag monitors

Updated documentation:
- API.md: Added tag management section with quick reference

Coverage:
- Adding monitors with tags (complete workflow)
- Updating and deleting monitors
- Tag management strategies and patterns
- Common tagging patterns by service type
- Troubleshooting guide
- Best practices and maintenance tasks

All common operations are now documented with examples.
2026-02-08 20:23:10 +00:00
poprhythm f926ff733c Fix and complete Uptime Kuma monitor tagging
Successfully tagged all 12 monitors with service categories:
- Used add_monitor_tag API method instead of edit_monitor
- All hardware/monitoring, application, and infrastructure monitors now tagged
- Added verification and fix scripts for debugging

New files:
- check_tags.py: Verify tag assignments
- fix_missing_tags.py: Add specific missing tags
- test_single_tag.py: Debug single monitor tagging
- TAG_SUMMARY.md: Complete documentation of all tags and assignments

All monitors are now properly organized by service category.
2026-02-08 20:18:58 +00:00
poprhythm aefe76478a Add service category tagging script for Uptime Kuma
Creates and assigns service category tags to all monitors:
- Hardware/monitoring: Server fan monitors (4 monitors)
- Application: Web apps, database, media services (3 monitors)
- Infrastructure: Storage, virtualization, VMs (5 monitors)

Tags with color coding:
- hardware (red), monitoring (orange)
- application (blue), web (cyan), database (purple), media (pink)
- infrastructure (green), storage (light green), virtualization (teal), vm (blue grey)
2026-02-08 20:10:57 +00:00
poprhythm 3a990fc686 Add Uptime Kuma API management tooling
Implements programmatic monitor management via Socket.IO API:
- manage_monitors.py: CLI script for CRUD operations on monitors
- API.md: Comprehensive API usage documentation
- SETUP.md: Step-by-step setup and verification guide

Key features:
- Supports API key and username/password authentication
- List, add, update, delete monitors via CLI
- SSL verification disabled for self-signed certificates
- Monitor types: HTTP, TCP, ping, DNS, docker
2026-02-08 19:59:43 +00:00
poprhythm f46d493ca2 Update dashy, gitea, homebox, docker-registry for git-linked deployment 2026-02-08 03:23:38 +00:00
poprhythm dab1385ca7 Fix calibre PUID/PGID to match NFS share (99/100) 2026-02-08 03:11:37 +00:00
poprhythm 038767ef76 Update calibre compose for production deployment 2026-02-08 03:07:41 +00:00
poprhythm 4d089966d8 Update linkding compose for production deployment 2026-02-08 03:01:37 +00:00
poprhythm da970d1ed5 Update ladder/docker-compose.yml 2026-02-06 03:22:51 +00:00
poprhythm 8a5be5989f Add ladder/docker-compose.yml 2026-02-06 03:14:17 +00:00
poprhythm da7a0295e3 added comfyui 2026-02-04 02:55:08 +00:00
poprhythm 201349a6fd forge a fork of webui 2026-02-03 03:21:12 +00:00
poprhythm 8ab2ff68a1 another sd try 2026-02-03 03:18:53 +00:00
poprhythm 8bab95d2ef add openclaw 2026-02-03 02:42:59 +00:00
poprhythm 94b5587831 update sd 2026-02-03 02:17:40 +00:00
poprhythm 8e878cec0d add stable diffusion 2026-02-03 02:09:53 +00:00
poprhythm 42bf219965 Fix glances GPU access using Ubuntu image and nvidia runtime
Alpine-based image (latest-full) is incompatible with NVIDIA libraries
due to musl/glibc mismatch. Switch to ubuntu-latest-full and use
runtime: nvidia for GPU monitoring support.
2026-01-31 20:10:40 +00:00
poprhythm 947f5fd348 Add GPU monitoring support to glances
Enable NVIDIA runtime and GPU visibility for GPU performance metrics.
2026-01-31 19:52:58 +00:00
poprhythm 3426193bc2 Use runtime:nvidia for GPU access
Replace deploy.resources.reservations.devices block with runtime: nvidia
for better compatibility with Portainer standalone mode.
2026-01-31 19:38:05 +00:00
poprhythm fda2a9b8d8 Update ollama configuration
- Add npm-network to ollama and open-webui services
- Add timezone (America/New_York) to both services
- Change open-webui image tag from main to latest
2026-01-31 19:20:50 +00:00
poprhythm edf45229dc Add ollama/docker-compose.yml 2026-01-31 19:14:29 +00:00
poprhythm b721e0108c Add GPU capabilities 2026-01-29 03:35:17 +00:00
poprhythm e0c8ce3d8a Update uptime-kuma/docker-compose.yaml 2026-01-26 15:04:29 +00:00
poprhythm fddda863de update mqtt-network 2026-01-25 15:44:51 +00:00
poprhythm 4a0c703fc4 add mqtt-network for uptime-kuma 2026-01-25 15:38:03 +00:00
poprhythm 22166f116a Merge branch 'main' of https://gitea.kolpacksoftware.com/homelab/docker-infrastructure 2026-01-25 14:45:51 +00:00
poprhythm 73d80ea2b2 uptime-kuma config 2026-01-25 14:43:24 +00:00
poprhythm d84e412532 Add nginx-proxy-acme 2026-01-25 14:39:16 +00:00
poprhythm 57590e63b1 add claude.md 2026-01-25 14:33:27 +00:00
poprhythm a76803a4ea Update couchdb/docker-compose.yml 2026-01-09 19:29:26 +00:00
poprhythm 9f1626b5c3 Update couchdb/docker-compose.yml 2026-01-09 19:27:28 +00:00
poprhythm a2bb987ea5 Update couchdb/docker-compose.yml 2026-01-09 19:22:24 +00:00
poprhythm 7153f1cd5e Update couchdb/docker-compose.yml 2026-01-09 19:08:29 +00:00
poprhythm 9691e50075 Update couchdb/docker-compose.yml 2026-01-09 19:06:14 +00:00
poprhythm 6d7567acee Add couchdb/docker-compose.yml 2026-01-09 18:57:06 +00:00
124 changed files with 6314 additions and 310 deletions
+3
View File
@@ -0,0 +1,3 @@
.credentials
**/.env
**/users_database.yaml
+81
View File
@@ -0,0 +1,81 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
This is a homelab Docker infrastructure repository containing ~30 self-hosted services. Each service has its own directory with a `docker-compose.yaml` (or `.yml`) file. All services use pre-built Docker images - there are no custom Dockerfiles.
## Common Commands
```bash
# Start a service
cd <service-name> && docker compose up -d
# Stop a service
cd <service-name> && docker compose down
# View logs
docker compose logs -f <service-name>
# Reload nginx proxy config after changing vhost.d or conf.d
docker exec nginx-proxy nginx -s reload
```
## Architecture
### Network Architecture
All proxied services connect to `npm-network`, an external Docker network shared with the nginx reverse proxy. Services define this in their compose file:
```yaml
networks:
npm-network:
external: true
```
Some services have additional internal networks (e.g., `mqtt-network` for mosquitto).
### Reverse Proxy (nginx-proxy-acme)
The primary ingress is `nginx-proxy-acme/`, which auto-discovers Docker containers and issues Let's Encrypt certificates. To expose a service:
```yaml
environment:
- VIRTUAL_HOST=myapp.kolpacksoftware.com
- VIRTUAL_PORT=8080
- LETSENCRYPT_HOST=myapp.kolpacksoftware.com
```
**Access control:**
- Private hosts: Include `vhost.d/private` which restricts to 192.168.1.0/24 and Docker networks
- Public hosts: No vhost.d file (only default security headers apply)
- To make a host private: `echo 'include /etc/nginx/vhost.d/private;' | sudo tee /srv/nginx-proxy-acme/vhost.d/<hostname>`
**Static backends** (non-Docker services) are configured in `conf.d/static-upstreams.conf` and require adding the domain to the `static-certs` container's environment variables.
### Storage Patterns
- **Bind mounts**: Most services use `/srv/<service-name>/` paths
- **NFS mounts**: Some services (immich, calibre, backrest, filebrowser-colleen-hd) use NFS volumes pointing to 192.168.1.4 or 192.168.1.192
### Automation
- **watchtower**: Monitors and auto-updates container images every 5 minutes
- **netdata**: Real-time monitoring with alerting at port 19999
## Key Configuration Locations
| Path | Purpose |
|------|---------|
| `/srv/nginx-proxy-acme/vhost.d/` | Per-host nginx config (access control) |
| `/srv/nginx-proxy-acme/conf.d/` | Global nginx config (WAF rules, static backends) |
| `<service>/docker-compose.yaml` | Service definition |
| `<service>/.env` | Service-specific environment (when present) |
## Environment Variables
Common patterns across services:
- `PUID=1000`, `PGID=1000` for file permissions
- `TZ=America/New_York` for timezone
- `VIRTUAL_HOST`, `VIRTUAL_PORT`, `LETSENCRYPT_HOST` for proxy integration
+123
View File
@@ -0,0 +1,123 @@
# NAS Connection Strategies
Summary of how each service connects to the unRAID NAS (192.168.1.192 / 192.168.1.4).
---
## Strategy Overview
| Strategy | Services | Reliability | Notes |
|----------|----------|-------------|-------|
| Docker NFS named volume | Plex, qBittorrent, Audiobookshelf | ⚠️ Fragile | Soft mount; stale handles on drive spin-down |
| systemd permanent mount + bind | Calibre (import), oCIS, Immich | ✅ Solid | Hard NFS; permanent; no idle cycling |
| systemd automount + bind | Backrest, Filebrowser-Colleen-HD | ✅ Solid | CIFS; TimeoutIdleSec=0; never unmounts once triggered |
| Local SSD + lsyncd sync | Calibre (library) | ✅ Best | SQLite never touches NFS; NAS copy is read-only replica |
---
## Per-Service Details
### Plex
- **Strategy**: Docker NFS named volume (`nas_media`, external)
- **NAS path**: `192.168.1.192:/mnt/user/media`
- **Mount options**: `nfsvers=4,soft,nolock,timeo=14,rsize=8192,wsize=8192`
- **Container path**: `/data`
- **Risk**: `soft` + `timeo=14` means EIO after 14 retries × 0.1s = ~1.4s timeout. If unRAID drives spin down, Plex gets I/O errors. Works in practice because Plex opens/closes file handles per-request rather than holding them open.
### qBittorrent (all 3 instances: open, vpn, vpn2)
- **Strategy**: Docker NFS named volume (`nas_media`, same as Plex)
- **NAS path**: `192.168.1.192:/mnt/user/media`
- **Container path**: `/data`
- **Risk**: Same as Plex. Active downloads write continuously — more exposure to spin-down EIO than Plex.
### Calibre
- **Strategy (library)**: Local SSD + lsyncd one-way sync to NAS
- Local: `/srv/calibre/library` → container `/config/Calibre Library`
- lsyncd syncs to `/mnt/nas_books/calibre/Calibre Library` within ~15 seconds
- SQLite (`metadata.db`, `notes.db`) never touches NFS — eliminates `apsw.IOError`
- **Strategy (import)**: systemd permanent NFS mount + Docker bind mount
- Host: `/mnt/nas_books/calibre-import` (permanent NFS, no automount)
- Container: `/calibre-import`
- NAS path: `192.168.1.192:/mnt/user/media/books`
- **Mount options**: `nfsvers=3,hard,rw,noatime`
- **Why permanent (not automount)**: Docker bind mounts snapshot the mount reference at container start. If autofs cycles between restarts, the container gets a stale handle. Permanent mount avoids this entirely.
- **Manual book add** (when auto-add fails):
```bash
docker stop calibre
docker run --rm \
-v /srv/calibre/library:/config/Calibre\ Library \
-v /mnt/nas_books/calibre-import:/calibre-import \
lscr.io/linuxserver/calibre:latest \
sh -c "cd /opt/calibre && LD_LIBRARY_PATH=/opt/calibre ./calibredb add \
--library-path '/config/Calibre Library' '/calibre-import/<book>' 2>&1"
docker start calibre
```
### Filebrowser-Colleen-HD
- **Strategy**: systemd CIFS automount + Docker bind mount
- **Host mount**: `/mnt/nas_backup` (CIFS `//192.168.1.192/backup`)
- **Container path**: `/folder`
- **Mount options**: `vers=3.0,uid=0,gid=0,noperm,noserverino,soft,TimeoutIdleSec=0`
- **Why CIFS**: Container runs as root; CIFS with `uid=0,gid=0` maps all files to root
- **TimeoutIdleSec=0**: Never auto-unmounts once triggered; safe for persistent container access
### Backrest (backup service)
- **Strategy**: systemd CIFS automount + Docker bind mount (same mount as Filebrowser)
- **Host mount**: `/mnt/nas_backup/nas-docker-data`
- **Container path**: `/backup-export`
- **Same CIFS mount as Filebrowser-Colleen-HD** (`mnt-nas_backup`)
### oCIS (ownCloud Infinite Scale)
- **Strategy**: systemd permanent NFS mount + Docker bind mount
- **Host mount**: `/mnt/nas_owncloud` (permanent NFS, no automount)
- **Container path**: `/var/lib/ocis/storage/users`
- **NAS path**: `192.168.1.192:/mnt/user/owncloud`
- **Mount options**: `nfsvers=3,hard,rw,noatime`
- **Local config/index**: `/srv/ocis/data` and `/srv/ocis/config` stay on local SSD
### Immich
- **Strategy**: systemd permanent NFS mount + Docker bind mount
- **Host mount**: `/mnt/nas_family` (NFS `192.168.1.192:/mnt/user/family`)
- **Container path**: `/usr/src/app/upload` → `/mnt/nas_family/immich-library`
- **Mount options**: `nfsvers=3,hard,rw,noatime`
- **Why permanent (not automount)**: Drive doesn't spin down; permanent mount avoids any autofs cycling risk
- **Database**: PostgreSQL on local SSD (`/var/lib/postgresql/data`) — correct, not on NFS
### Audiobookshelf
- **Strategy**: Docker NFS named volume
- **NAS path**: `192.168.1.192:/mnt/user/media/audiobooks`
- **Container path**: `/audiobooks`
- **Config/metadata**: Local SSD (`/srv/audiobookshelf/`) — correct
---
## systemd Mount Units
| Unit | Type | Share | Status |
|------|------|-------|--------|
| `mnt-nas_books.mount` | NFS permanent | `:/mnt/user/media/books` | enabled, always up |
| `mnt-nas_owncloud.mount` | NFS permanent | `:/mnt/user/owncloud` | enabled, always up |
| `mnt-nas_family.mount` | NFS permanent | `:/mnt/user/family` | enabled, always up |
| `mnt-nas_library.mount` | CIFS automount | `//192.168.1.192/library` | triggered on access, TimeoutIdleSec=0 |
| `mnt-nas_library.automount` | CIFS automount | — | enabled |
| `mnt-nas_backup.mount` | CIFS automount | `//192.168.1.192/backup` | triggered on access, TimeoutIdleSec=0 |
| `mnt-nas_backup.automount` | CIFS automount | — | enabled |
---
## Known Issues & Gotchas
- **Docker NFS named volumes**: The `nas_media` volume uses `soft,nolock,timeo=14,nfsvers=4`. This is fragile — short timeout means EIO on spin-up. Plex/qBittorrent survive because they open/close handles per request. If either starts having I/O errors, migrate to the systemd permanent mount pattern.
- **Docker bind mounts + automount = stale handles**: Docker snapshots the mount reference at container start. If autofs cycles (unmount + remount) between container restarts, the container's bind mount goes stale while the host sees the path fine. Fix: use permanent `.mount` (no `.automount`) for any share that Docker containers bind-mount.
- **SQLite on NFS**: Never store SQLite databases (calibre `metadata.db`/`notes.db`, any app DB) on NFS. Even with `hard` mounts and NLM locking, transient NFS errors cause `SQLITE_IOERR`. Keep SQLite on local SSD; sync non-SQLite files to NAS if sharing is needed.
- **CIFS vs NFS for file permissions**: CIFS enforces server-side ACLs based on the SMB user. Files created via NFS by uid=99 with mode 600 are inaccessible via CIFS. Use NFS when container needs uid-mapped access to NFS-created files.
- **lsyncd for NAS sync**: inotify-based, syncs within ~15 seconds of any write. Config at `/etc/lsyncd/lsyncd.conf.lua`. Log at `/var/log/lsyncd.log`.
---
## Recommended Migration (future)
Plex, qBittorrent, Immich, and Audiobookshelf all use the fragile Docker NFS named volume pattern. The safer pattern is systemd permanent mount + Docker bind mount (as used by calibre-import and oCIS). This would involve:
1. Create `/mnt/nas_media` systemd permanent mount unit
2. Update compose files to use bind mounts instead of the external `nas_media` volume
3. `docker volume rm nas_media` after redeployment
+23
View File
@@ -0,0 +1,23 @@
networks:
default:
external:
name: npm-network
services:
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
container_name: audiobookshelf
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
- VIRTUAL_HOST=audiobookshelf.kolpacksoftware.com
- VIRTUAL_PORT=80
- LETSENCRYPT_HOST=audiobookshelf.kolpacksoftware.com
volumes:
- /srv/audiobookshelf/config:/config
- /srv/audiobookshelf/metadata:/metadata
- /mnt/nas_audiobooks:/audiobooks
ports:
- 13378:80
restart: unless-stopped
+26
View File
@@ -0,0 +1,26 @@
# Authelia secrets — set all of these in Portainer stack environment variables
# Generate random values with: openssl rand -hex 32
# Core secrets (safe as env vars — no $ signs in values)
AUTHELIA_JWT_SECRET=
AUTHELIA_SESSION_SECRET=
AUTHELIA_STORAGE_ENCRYPTION_KEY=
# OIDC HMAC secret (safe as env var — hex string, no $ signs)
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET=
# OIDC client secrets are NOT stored as env vars.
# Bcrypt hashes contain $ characters which Portainer/shell interpolates, truncating them.
# Instead, store hashes as files on the host:
# /srv/authelia/config/secrets/oidc_open_webui <- bcrypt hash of open-webui client secret
# /srv/authelia/config/secrets/oidc_linkding <- bcrypt hash of linkding client secret
#
# Write them with Python (not shell) to avoid $ interpolation:
# docker run --rm -v /srv/authelia/config:/config python:3-alpine python3 -c "
# open('/config/secrets/oidc_open_webui','w').write('<bcrypt-hash>')
# open('/config/secrets/oidc_linkding','w').write('<bcrypt-hash>')
# "
# Generate a bcrypt hash:
# docker run --rm authelia/authelia:4.38 authelia crypto hash generate bcrypt --password <plaintext>
#
# Note: the OIDC JWK private key is also host-managed inline in /srv/authelia/config/configuration.yml
+179
View File
@@ -0,0 +1,179 @@
server:
address: 0.0.0.0:9091
log:
level: info
totp:
issuer: kolpacksoftware.com
webauthn:
disable: true
authentication_backend:
file:
path: /config/users_database.yaml
password:
algorithm: argon2id
access_control:
default_policy: deny
rules:
- domain: auth.kolpacksoftware.com
policy: bypass
- domain: ultralytics.kolpacksoftware.com
policy: one_factor
- domain: "*.kolpacksoftware.com"
policy: one_factor
subject: "group:admins"
session:
cookies:
- domain: kolpacksoftware.com
authelia_url: https://auth.kolpacksoftware.com
default_redirection_url: https://kolpacksoftware.com
name: authelia_session
expiration: 1h
inactivity: 5m
redis:
host: authelia-redis
port: 6379
storage:
local:
path: /config/db.sqlite3
notifier:
filesystem:
filename: /config/notifications.txt
regulation:
max_retries: 3
find_time: 2m
ban_time: 5m
identity_providers:
oidc:
hmac_secret: ${AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET}
cors:
endpoints:
- authorization
- token
- revocation
- introspection
- userinfo
allowed_origins_from_client_redirect_uris: true
jwks:
- key_id: main
algorithm: RS256
use: sig
# key is host-managed — never commit to git
# Host copy inlines the PEM content as a YAML block scalar (key: |)
# using Python to avoid shell $ interpolation of the PEM content.
# Generate with: openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out /srv/authelia/config/oidc.key
clients:
- client_id: open-webui
client_name: Open WebUI
client_secret: '{{ secret "/config/secrets/oidc_open_webui" }}'
public: false
authorization_policy: one_factor
token_endpoint_auth_method: client_secret_basic
redirect_uris:
- https://open-webui.kolpacksoftware.com/oauth/oidc/callback
scopes:
- openid
- profile
- email
userinfo_signed_response_alg: none
- client_id: immich
client_name: Immich
client_secret: '{{ secret "/config/secrets/oidc_immich" }}'
public: false
authorization_policy: one_factor
token_endpoint_auth_method: client_secret_post
redirect_uris:
- https://immich.kolpacksoftware.com/auth/login
- app.immich:///oauth-callback
scopes:
- openid
- profile
- email
userinfo_signed_response_alg: none
- client_id: linkding
client_name: Linkding
client_secret: '{{ secret "/config/secrets/oidc_linkding" }}'
public: false
authorization_policy: one_factor
token_endpoint_auth_method: client_secret_post
redirect_uris:
- https://linkding.kolpacksoftware.com/oidc/callback/
scopes:
- openid
- profile
- email
- groups
userinfo_signed_response_alg: none
- client_id: ocis
client_name: ownCloud Infinite Scale
public: true
require_pkce: true
pkce_challenge_method: S256
authorization_policy: one_factor
redirect_uris:
- https://cloud.kolpacksoftware.com/
- https://cloud.kolpacksoftware.com/oidc-callback.html
- https://cloud.kolpacksoftware.com/oidc-silent-redirect.html
- https://cloud.kolpacksoftware.com/apps/openidconnect/redirect
scopes:
- openid
- profile
- email
- groups
- offline_access
userinfo_signed_response_alg: none
- client_id: mxd5OQDk6es5LzOzRvidJNfXLUZS2oN3oUFeXPP8LpPrhx3UroJFduGEYIBOxkY1
client_name: ownCloud iOS
client_secret: '{{ secret "/config/secrets/oidc_ocis_ios" }}'
public: false
require_pkce: true
pkce_challenge_method: S256
token_endpoint_auth_method: client_secret_basic
authorization_policy: one_factor
response_types:
- code
grant_types:
- authorization_code
- refresh_token
redirect_uris:
- oc://ios.owncloud.com
scopes:
- openid
- profile
- email
- offline_access
userinfo_signed_response_alg: none
- client_id: xdXOt13JKxym1B1QcEncf2XDkLAexMBFwiT9j6EfhhHFJhs2KM9jbjTmf8JBXE69
client_name: ownCloud Desktop
client_secret: '{{ secret "/config/secrets/oidc_ocis_desktop" }}'
public: false
require_pkce: true
pkce_challenge_method: S256
token_endpoint_auth_method: client_secret_basic
authorization_policy: one_factor
response_types:
- code
grant_types:
- authorization_code
- refresh_token
redirect_uris:
- http://127.0.0.1
scopes:
- openid
- profile
- email
- offline_access
userinfo_signed_response_alg: none
+39
View File
@@ -0,0 +1,39 @@
services:
authelia:
container_name: authelia
image: authelia/authelia:4.38
restart: unless-stopped
labels:
- com.centurylinklabs.watchtower.monitor-only=true
volumes:
- /srv/authelia/config:/config
environment:
- TZ=America/New_York
- AUTHELIA_JWT_SECRET=${AUTHELIA_JWT_SECRET}
- AUTHELIA_SESSION_SECRET=${AUTHELIA_SESSION_SECRET}
- AUTHELIA_STORAGE_ENCRYPTION_KEY=${AUTHELIA_STORAGE_ENCRYPTION_KEY}
- AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET=${AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET}
# OIDC client secrets are NOT passed as env vars — bcrypt hashes contain $ which
# Portainer/shell interpolates. Stored as files in /srv/authelia/config/secrets/ instead.
- X_AUTHELIA_CONFIG_FILTERS=template
networks:
- npm-network
- authelia-internal
depends_on:
- authelia-redis
authelia-redis:
container_name: authelia-redis
image: redis:7-alpine
restart: unless-stopped
command: --save 60 1 --loglevel warning
volumes:
- /srv/authelia/redis:/data
networks:
- authelia-internal
networks:
npm-network:
external: true
authelia-internal:
driver: bridge
-95
View File
@@ -1,95 +0,0 @@
networks:
default:
external:
name: nginx-network
services:
postgresql:
environment:
POSTGRES_DB: ${PG_DB:-authentik}
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
healthcheck:
interval: 30s
retries: 5
start_period: 20s
test:
- CMD-SHELL
- pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
timeout: 5s
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
volumes:
- database:/var/lib/postgresql/data
redis:
command: --save 60 1 --loglevel warning
healthcheck:
interval: 30s
retries: 5
start_period: 20s
test:
- CMD-SHELL
- redis-cli ping | grep PONG
timeout: 3s
image: docker.io/library/redis:alpine
restart: unless-stopped
volumes:
- redis:/data
server:
command: server
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
environment:
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.3}
ports:
- ${COMPOSE_PORT_HTTP:-9000}:9000
- ${COMPOSE_PORT_HTTPS:-9443}:9443
restart: unless-stopped
volumes:
- ./media:/media
- ./custom-templates:/templates
worker:
command: worker
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
environment:
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.3}
restart: unless-stopped
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
volumes:
database:
driver: local
driver_opts:
type: 'none'
o: 'bind'
device: '/srv/authentik/database'
redis:
driver: local
driver_opts:
type: 'none'
o: 'bind'
device: '/srv/authentik/redis'
+2
View File
@@ -0,0 +1,2 @@
SMB_USERNAME=your_smb_username
SMB_PASSWORD=your_smb_password
+2 -11
View File
@@ -1,13 +1,5 @@
version: "3.8" version: "3.8"
volumes:
backup-mnt:
driver: local
driver_opts:
type: nfs
o: "addr=192.168.1.192,rw,noatime,rsize=8192,wsize=8192,tcp,timeo=14,nolock,soft,nfsvers=4"
device: " :/mnt/user/backup/nas-docker-data"
services: services:
backrest: backrest:
image: garethgeorge/backrest:latest image: garethgeorge/backrest:latest
@@ -20,8 +12,7 @@ services:
- /srv/backrest/tmp:/tmp - /srv/backrest/tmp:/tmp
- /srv/backrest/rclone:/root/.config/rclone # Mount for rclone config (needed when using rclone remotes) - /srv/backrest/rclone:/root/.config/rclone # Mount for rclone config (needed when using rclone remotes)
- /srv:/srv-data # Mount local paths to backup - /srv:/srv-data # Mount local paths to backup
#- /mnt/backup-export:/backup-export # NFS share backup destination - /mnt/nas_backup/nas-docker-data:/backup-export
- backup-mnt:/backup-export # NFS share backup destination
environment: environment:
- BACKREST_DATA=/data - BACKREST_DATA=/data
- BACKREST_CONFIG=/config/config.json - BACKREST_CONFIG=/config/config.json
@@ -30,4 +21,4 @@ services:
- TZ=America/New_York - TZ=America/New_York
ports: ports:
- "9898:9898" - "9898:9898"
restart: unless-stopped restart: unless-stopped
+15 -24
View File
@@ -1,41 +1,32 @@
# https://thebloody.cloud/posts/Installing-Web-Calibre-in-Docker/
# SQLite databases (metadata.db, notes.db) live on local SSD to avoid NFS IOError.
# lsyncd syncs /srv/calibre/library → /mnt/nas_books/calibre/Calibre Library
# within ~15s so other devices see an up-to-date read-only replica on the NAS.
# calibre-import stays on NFS as the drop folder for new books.
networks: networks:
default: default:
external: external:
name: npm-network name: npm-network
volumes:
config:
driver: local
driver_opts:
type: nfs
o: "addr=192.168.1.4,rw,nolock,soft"
device: ":/srv/books/calibre"
import:
driver: local
driver_opts:
type: nfs
o: "addr=192.168.1.4,rw,nolock,soft"
device: ":/srv/books/calibre-import"
services: services:
calibre: calibre:
image: lscr.io/linuxserver/calibre:latest image: lscr.io/linuxserver/calibre:latest
container_name: calibre container_name: calibre
security_opt: security_opt:
- seccomp:unconfined #optional - seccomp:unconfined
#security-opt:
# - seccomp:unconfined
environment: environment:
- PUID=1024 - PUID=99
- PGID=1024 - PGID=100
- TZ=America/New_York - TZ=America/New_York
- PASSWORD= #optional - VIRTUAL_HOST=calibre.kolpacksoftware.com
- CLI_ARGS= #optional - VIRTUAL_PORT=8080
- LETSENCRYPT_HOST=calibre.kolpacksoftware.com
volumes: volumes:
- config:/config - /srv/calibre/config:/config
- import:/calibre-import - /srv/calibre/library:/config/Calibre Library
- /mnt/nas_books/calibre-import:/calibre-import
ports: ports:
- 8090:8080 - 8090:8080
- 8091:8081 - 8091:8081
restart: unless-stopped restart: unless-stopped
+11
View File
@@ -0,0 +1,11 @@
services:
clue-picker:
image: docker-registry.kolpacksoftware.com/clue-picker:latest
container_name: clue-picker
restart: unless-stopped
networks:
- npm-network
networks:
npm-network:
external: true
+1
View File
@@ -27,6 +27,7 @@ services:
- /srv/nginx-proxy/data:/mnt/nginx-proxy - /srv/nginx-proxy/data:/mnt/nginx-proxy
- /srv/tsa-chapter-organizer-rms:/mnt/tsa-chapter-organizer-rms - /srv/tsa-chapter-organizer-rms:/mnt/tsa-chapter-organizer-rms
- /srv/glances:/mnt/glances - /srv/glances:/mnt/glances
- /srv/authelia/config:/mnt/authelia
ports: ports:
- 8443:8443 - 8443:8443
restart: unless-stopped restart: unless-stopped
+23
View File
@@ -0,0 +1,23 @@
services:
comfyui:
container_name: comfyui
image: ghcr.io/ai-dock/comfyui:latest-cuda
restart: unless-stopped
ports:
- 8188:8188
environment:
- COMFYUI_ARGS=
- AUTO_UPDATE=false
- TZ=America/New_York
# Optional - for gated models
# - HF_TOKEN=your_token
# - CIVITAI_TOKEN=your_token
volumes:
- /srv/comfyui/workspace:/workspace
runtime: nvidia
networks:
- npm-network
networks:
npm-network:
external: true
+22
View File
@@ -0,0 +1,22 @@
services:
couchserver:
image: couchdb
container_name: couchdb
restart: always
labels:
- com.centurylinklabs.watchtower.monitor-only=true
ports:
- "127.0.0.1:5984:5984"
environment:
- COUCHDB_USER=admin
- COUCHDB_PASSWORD=${ADMIN_PASSWORD}
volumes:
- /srv/couchdb-data:/opt/couchdb/data
- /srv/couchdb-config/local.ini:/opt/couchdb/etc/local.ini
networks:
- npm-network
networks:
default:
npm-network:
external: true
-31
View File
@@ -1,31 +0,0 @@
networks:
default:
external:
name: npm-network
services:
dashy:
# To build from source, replace 'image: lissy93/dashy' with 'build: .'
# build: .
image: lissy93/dashy
container_name: dashy
# Pass in your config file below, by specifying the path on your host machine
volumes:
- /srv/dashy:/app/user-data
ports:
- 9000:8080
# Set any environmental variables
environment:
- NODE_ENV=production
# Specify your user ID and group ID. You can find this by running `id -u` and `id -g`
# - UID=1000
# - GID=1000
# Specify restart policy
restart: unless-stopped
# Configure healthchecks
healthcheck:
test: ['CMD', 'node', '/app/services/healthcheck']
interval: 1m30s
timeout: 10s
retries: 3
start_period: 40s
+1
View File
@@ -0,0 +1 @@
APIKEY=your_dnsexit_api_key
+3 -1
View File
@@ -8,7 +8,9 @@ services:
stdin_open: true stdin_open: true
tty: true tty: true
labels:
- com.centurylinklabs.watchtower.enable=false
environment: environment:
- APIKEY=s5P998CqUu5bqAukdS1dW57EfjgFlX - APIKEY=${APIKEY}
- HOST=kolpacksoftware.com,rmstsa.org,popcyclical.com - HOST=kolpacksoftware.com,rmstsa.org,popcyclical.com
- INTERVAL=1h - INTERVAL=1h
+1
View File
@@ -0,0 +1 @@
SECRET_KEY_BASE=generate_with_openssl_rand_-hex_64
+43
View File
@@ -0,0 +1,43 @@
networks:
default:
external:
name: npm-network
services:
registry-browser:
container_name: registry-browser
image: klausmeyer/docker-registry-browser:latest
ports:
- 5080:8080
environment:
- DOCKER_REGISTRY_URL=https://docker-registry.kolpacksoftware.com/
- PUBLIC_REGISTRY_URL=https://docker-registry.kolpacksoftware.com/
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
- ENABLE_DELETE_IMAGES=true
- VIRTUAL_HOST=docker-registry-ui.kolpacksoftware.com
- VIRTUAL_PORT=8080
- LETSENCRYPT_HOST=docker-registry-ui.kolpacksoftware.com
registry-server:
image: registry:latest
container_name: registry-server
restart: always
environment:
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin=[https://docker-registry.kolpacksoftware.com/]
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods=[HEAD,GET,OPTIONS,DELETE]
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials=[true]
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers=[Authorization,Accept,Cache-Control]
- REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers=[Docker-Content-Digest]
- REGISTRY_STORAGE_DELETE_ENABLED=true
- REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=KSC Docker Registry
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/registry.password
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/data
- VIRTUAL_HOST=docker-registry.kolpacksoftware.com
- VIRTUAL_PORT=5000
- LETSENCRYPT_HOST=docker-registry.kolpacksoftware.com
volumes:
- /srv/registry/data:/data
- /srv/registry/registry.password:/auth/registry.password
ports:
- 127.0.0.1:5000:5000
+2
View File
@@ -0,0 +1,2 @@
SMB_USERNAME=your_smb_username
SMB_PASSWORD=your_smb_password
+2 -10
View File
@@ -6,7 +6,7 @@ services:
FILEBROWSER_CONFIG: "data/config.yaml" FILEBROWSER_CONFIG: "data/config.yaml"
TZ: "America/New_York" TZ: "America/New_York"
volumes: volumes:
- backup-mnt:/folder - /mnt/nas_backup:/folder
- /srv/filebrowser-colleen-hd:/home/filebrowser/data - /srv/filebrowser-colleen-hd:/home/filebrowser/data
ports: ports:
- 3427:80 - 3427:80
@@ -14,15 +14,7 @@ services:
- npm-network - npm-network
restart: unless-stopped restart: unless-stopped
volumes:
backup-mnt:
driver: local
driver_opts:
type: nfs
o: "addr=192.168.1.192,rw,nolock,soft"
device: ":/mnt/user/backup/"
networks: networks:
default: default:
npm-network: npm-network:
external: true external: true
+6
View File
@@ -0,0 +1,6 @@
# Generate with: openssl rand -hex 16 | head -c 32
APP_KEY=
APP_URL=https://firefly.kolpacksoftware.com
DB_PASSWORD=
+65
View File
@@ -0,0 +1,65 @@
# Firefly III Data Import Plan
## Bank Accounts
| Institution | Type | Export Format | Automation |
|-------------|------|---------------|------------|
| Capital One | Bank/Credit Card | CSV only (dropped OFX) | SimpleFIN |
| Citibank | Credit Card | CSV + QFX/OFX Direct Connect | SimpleFIN or OFX |
## Phase 1: Manual Import via FIDI (Free)
**Deploy FIDI** as a companion container to Firefly III.
- Image: `fireflyiii/data-importer:latest`
- Needs: `FIREFLY_III_URL`, `FIREFLY_III_ACCESS_TOKEN` (generate in Firefly UI under Profile → OAuth)
- Port: 8383 (internal, no need to expose publicly)
- Add to `firefly-iii/docker-compose.yaml` as a second service on the internal network
**Workflow:**
1. Log into Capital One → Account → Download transactions → CSV
2. Log into Citi → Account → Download transactions → QFX or CSV
3. Go to FIDI UI → upload file → map columns → import
**Citi OFX Direct Connect** (optional, skips manual download):
- Server: `https://www.citi.com/ofxdirect`
- Requires Quicken-compatible credentials (may need to call Citi to enable)
- FIDI supports OFX connections natively
## Phase 2: Automation via SimpleFIN (Optional, ~$1.50/mo)
SimpleFIN Bridge aggregates US bank data including Capital One and Citi.
**Setup:**
1. Sign up at https://bridge.simplefin.org/
2. Get a SimpleFIN token
3. Deploy a bridge script (e.g. `simplefin-to-firefly` community tool) as a cron container
4. Script polls SimpleFIN → pushes new transactions to Firefly III API
**Firefly III API base URL:** `https://firefly.kolpacksoftware.com/api/v1`
**Auth:** Personal Access Token (Profile → OAuth → Personal Access Tokens)
## FIDI Compose Addition
Add to `firefly-iii/docker-compose.yaml`:
```yaml
fidi:
image: fireflyiii/data-importer:latest
container_name: firefly-iii-fidi
restart: unless-stopped
depends_on:
- app
environment:
- FIREFLY_III_URL=http://app:8080
- VANITY_URL=https://firefly.kolpacksoftware.com
- FIREFLY_III_ACCESS_TOKEN=${FIDI_ACCESS_TOKEN}
- TZ=America/New_York
ports:
- 8383:8080
networks:
- internal
```
- `FIDI_ACCESS_TOKEN`: Generate in Firefly III UI → Profile → OAuth → Personal Access Tokens
- FIDI runs on internal network only — access via `http://<host-ip>:8383` or add to npm-network if you want a public URL
+45
View File
@@ -0,0 +1,45 @@
services:
app:
image: fireflyiii/core:latest
container_name: firefly-iii
restart: unless-stopped
depends_on:
- db
environment:
- APP_KEY=${APP_KEY}
- APP_URL=${APP_URL}
- TRUSTED_PROXIES=**
- DB_CONNECTION=pgsql
- DB_HOST=db
- DB_PORT=5432
- DB_DATABASE=firefly
- DB_USERNAME=firefly
- DB_PASSWORD=${DB_PASSWORD}
- TZ=America/New_York
volumes:
- /srv/firefly-iii/upload:/var/www/html/storage/upload
ports:
- 8282:8080
networks:
- default
- internal
db:
image: postgres:16-alpine
container_name: firefly-iii-db
restart: unless-stopped
environment:
- POSTGRES_DB=firefly
- POSTGRES_USER=firefly
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- /srv/firefly-iii/db:/var/lib/postgresql/data
networks:
- internal
networks:
default:
external:
name: npm-network
internal:
driver: bridge
+6 -5
View File
@@ -1,5 +1,3 @@
version: "3"
networks: networks:
default: default:
external: external:
@@ -9,16 +7,19 @@ services:
server: server:
image: docker.gitea.com/gitea:nightly image: docker.gitea.com/gitea:nightly
container_name: gitea container_name: gitea
labels:
- com.centurylinklabs.watchtower.monitor-only=true
environment: environment:
- USER_UID=1000 - USER_UID=1000
- USER_GID=1000 - USER_GID=1000
- VIRTUAL_HOST=gitea.kolpacksoftware.com
- VIRTUAL_PORT=3000
- LETSENCRYPT_HOST=gitea.kolpacksoftware.com
restart: always restart: always
# networks:
# - gitea
volumes: volumes:
- /srv/gitea-data:/data - /srv/gitea-data:/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
ports: ports:
- "3001:3000" - "3001:3000"
- "222:22" - "222:22"
+5 -5
View File
@@ -1,19 +1,19 @@
version: '3'
services: services:
glances: glances:
container_name: glances container_name: glances
image: 'nicolargo/glances:latest-full' image: 'nicolargo/glances:ubuntu-latest-full'
restart: unless-stopped restart: unless-stopped
privileged: true
ports: ports:
- 61208-61209:61208-61209 - 127.0.0.1:61208-61209:61208-61209
environment: environment:
- TZ=${TZ} - TZ=${TZ}
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
- "GLANCES_OPT=-C /glances/conf/glances.conf -w" - "GLANCES_OPT=-C /glances/conf/glances.conf -w"
volumes: volumes:
- '/var/run/docker.sock:/var/run/docker.sock:ro' - '/var/run/docker.sock:/var/run/docker.sock:ro'
- '/run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro' - '/run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro'
- /srv/glances/glances.conf:/glances/conf/glances.conf - /srv/glances/glances.conf:/glances/conf/glances.conf
pid: host pid: host
runtime: nvidia
+68
View File
@@ -0,0 +1,68 @@
# Home Assistant
Home automation platform that integrates with hundreds of smart home devices and services.
## Access
- **URL**: https://homeassistant.kolpacksoftware.com
- **Local**: http://localhost:8123 (if needed)
## Initial Setup
1. Navigate to the URL above
2. Create your admin account on first launch
3. Follow the onboarding wizard to:
- Set your location
- Choose initial integrations
- Configure devices
## Configuration
- **Data directory**: `/srv/home-assistant/`
- **Config file**: `/srv/home-assistant/configuration.yaml`
- **Logs**: `docker logs home-assistant`
## Network Considerations
The container runs on `npm-network` and connects through the nginx reverse proxy.
**For device discovery**: Some integrations (like HomeKit, mDNS-based devices) work best with host networking. If you encounter discovery issues:
1. Stop the container via Portainer
2. Edit `docker-compose.yml` and replace the networks section with:
```yaml
network_mode: host
```
3. Remove the VIRTUAL_HOST environment variables (not needed with host mode)
4. Redeploy via Portainer
5. Configure nginx-proxy-manager manually to proxy to localhost:8123
## Common Integrations
- **Smart lights**: Philips Hue, LIFX, TP-Link
- **Smart speakers**: Google Home, Alexa
- **IoT devices**: Zigbee, Z-Wave, MQTT
- **Media**: Plex, Sonos, Chromecast
- **Weather, calendars, notifications**: Many built-in integrations
## Useful Commands
```bash
# View logs
docker logs -f home-assistant
# Restart container
docker restart home-assistant
# Check configuration
docker exec home-assistant hass --script check_config
# Access container shell
docker exec -it home-assistant /bin/bash
```
## Resources
- [Official Documentation](https://www.home-assistant.io/docs/)
- [Community Forum](https://community.home-assistant.io/)
- [Integrations List](https://www.home-assistant.io/integrations/)
+20
View File
@@ -0,0 +1,20 @@
networks:
default:
external:
name: npm-network
services:
homeassistant:
image: ghcr.io/home-assistant/home-assistant:stable
container_name: home-assistant
volumes:
- /srv/home-assistant:/config
- /etc/localtime:/etc/localtime:ro
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
- VIRTUAL_HOST=homeassistant.kolpacksoftware.com
- VIRTUAL_PORT=8123
- LETSENCRYPT_HOST=homeassistant.kolpacksoftware.com
restart: unless-stopped
+9 -7
View File
@@ -1,15 +1,17 @@
services: services:
homebox: homebox:
# image: ghcr.io/sysadminsmedia/homebox:latest
image: ghcr.io/sysadminsmedia/homebox:latest-rootless image: ghcr.io/sysadminsmedia/homebox:latest-rootless
container_name: homebox container_name: homebox
restart: always restart: always
environment: environment:
- HBOX_LOG_LEVEL=info - HBOX_LOG_LEVEL=info
- HBOX_LOG_FORMAT=text - HBOX_LOG_FORMAT=text
- HBOX_WEB_MAX_FILE_UPLOAD=10 - HBOX_WEB_MAX_FILE_UPLOAD=10
# Please consider allowing analytics to help us improve Homebox (basic computer information, no personal data) - HBOX_OPTIONS_ALLOW_ANALYTICS=false
- HBOX_OPTIONS_ALLOW_ANALYTICS=false - HBOX_AUTH_API_KEY_PEPPER=${HBOX_AUTH_API_KEY_PEPPER}
- VIRTUAL_HOST=homebox.kolpacksoftware.com
- VIRTUAL_PORT=7745
- LETSENCRYPT_HOST=homebox.kolpacksoftware.com
volumes: volumes:
- /srv/homebox-data:/data/ - /srv/homebox-data:/data/
ports: ports:
@@ -18,4 +20,4 @@ services:
networks: networks:
default: default:
external: external:
name: npm-network name: npm-network
+11 -11
View File
@@ -16,9 +16,10 @@ services:
# extends: # extends:
# file: hwaccel.transcoding.yml # file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
labels:
- com.centurylinklabs.watchtower.monitor-only=true
volumes: volumes:
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file - /mnt/nas_family/immich-library:/usr/src/app/upload
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- stack.env - stack.env
@@ -42,6 +43,8 @@ services:
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
# file: hwaccel.ml.yml # file: hwaccel.ml.yml
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable # service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
labels:
- com.centurylinklabs.watchtower.monitor-only=true
volumes: volumes:
- model-cache:/cache - model-cache:/cache
env_file: env_file:
@@ -53,13 +56,17 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8 image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
labels:
- com.centurylinklabs.watchtower.enable=false
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
labels:
- com.centurylinklabs.watchtower.enable=false
environment: environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
@@ -68,24 +75,17 @@ services:
volumes: volumes:
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
shm_size: 128mb
healthcheck: healthcheck:
test: >- test: >-
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m interval: 5m
#start_interval: 30s #start_interval: 30s
start_period: 5m start_period: 5m
command: >-
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
restart: always restart: always
volumes: volumes:
model-cache: model-cache:
library:
driver: local
driver_opts:
type: nfs
o: "addr=192.168.1.192,rw,nolock,soft"
device: ":/mnt/user/family/immich-library"
networks: networks:
default: default:
+134
View File
@@ -0,0 +1,134 @@
services:
db:
image: postgres:16
restart: unless-stopped
environment:
- POSTGRES_USER=inboxzero
- POSTGRES_DB=inboxzero
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U inboxzero']
interval: 10s
timeout: 5s
retries: 5
volumes:
- /srv/inbox-zero/postgres:/var/lib/postgresql/data
networks:
- inbox-zero-network
redis:
image: redis:7
restart: unless-stopped
volumes:
- /srv/inbox-zero/redis:/data
networks:
- inbox-zero-network
serverless-redis-http:
image: hiett/serverless-redis-http:latest
restart: unless-stopped
environment:
SRH_MODE: env
SRH_TOKEN: ${UPSTASH_REDIS_TOKEN}
SRH_CONNECTION_STRING: "redis://redis:6379"
depends_on:
- redis
networks:
- inbox-zero-network
web:
image: ghcr.io/elie222/inbox-zero:v1.4.12
pull_policy: always
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}
NEXT_PUBLIC_BYPASS_PREMIUM_CHECKS: "true"
NEXT_PUBLIC_EMAIL_SEND_ENABLED: "true"
DATABASE_URL: postgresql://inboxzero:${POSTGRES_PASSWORD}@db:5432/inboxzero?schema=public
DIRECT_URL: postgresql://inboxzero:${POSTGRES_PASSWORD}@db:5432/inboxzero?schema=public
UPSTASH_REDIS_URL: http://serverless-redis-http:80
UPSTASH_REDIS_TOKEN: ${UPSTASH_REDIS_TOKEN}
REDIS_URL: redis://redis:6379
INTERNAL_API_URL: http://web:3000
AUTH_SECRET: ${AUTH_SECRET}
EMAIL_ENCRYPT_SECRET: ${EMAIL_ENCRYPT_SECRET}
EMAIL_ENCRYPT_SALT: ${EMAIL_ENCRYPT_SALT}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
GOOGLE_PUBSUB_TOPIC_NAME: ${GOOGLE_PUBSUB_TOPIC_NAME}
GOOGLE_PUBSUB_VERIFICATION_TOKEN: ${GOOGLE_PUBSUB_VERIFICATION_TOKEN}
CRON_SECRET: ${CRON_SECRET}
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
API_KEY_SALT: ${API_KEY_SALT}
HOSTNAME: "0.0.0.0"
# Ollama AI provider
DEFAULT_LLM_PROVIDER: ollama
DEFAULT_LLM_MODEL: ${OLLAMA_MODEL}
ECONOMY_LLM_PROVIDER: ollama
ECONOMY_LLM_MODEL: ${OLLAMA_MODEL}
OLLAMA_BASE_URL: http://ollama:11434/api
networks:
- inbox-zero-network
- npm-network
cron:
image: alpine:latest
restart: unless-stopped
command: >
sh -c "
apk add --no-cache curl &&
(
while true; do
curl -s -X GET 'http://web:3000/api/cron/scheduled-actions' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
sleep 900
done
) &
(
while true; do
curl -s -X GET 'http://web:3000/api/cron/automation-jobs' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
sleep 900
done
) &
(
while true; do
curl -s -X GET 'http://web:3000/api/follow-up-reminders' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
sleep 3600
done
) &
(
while true; do
curl -s -X GET 'http://web:3000/api/resend/digest/all' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
sleep 1800
done
) &
(
while true; do
curl -s -X GET 'http://web:3000/api/meeting-briefs' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
sleep 900
done
) &
(
while true; do
curl -s -X GET 'http://web:3000/api/watch/all' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
sleep 21600
done
) &
wait
"
environment:
CRON_SECRET: ${CRON_SECRET}
depends_on:
- web
networks:
- inbox-zero-network
networks:
inbox-zero-network:
driver: bridge
npm-network:
external: true
+261
View File
@@ -0,0 +1,261 @@
/**
* Gmail Filter Cleanup Script
* Run from https://script.google.com
*
* SETUP REQUIRED:
* Before running, enable the Gmail Advanced Service:
* 1. Click "+" next to "Services" in the left sidebar
* 2. Find "Gmail API" and add it
*
* Run dryRun() first to see what will be deleted.
* Run deleteFilters() to actually delete them.
*/
// Patterns to match against filter "from" criteria.
// Any filter whose "from" contains one of these strings will be deleted.
var DELETE_FROM_PATTERNS = [
// Dead services
'mozy.com',
'ingdirect.com',
'stumbleupon.com',
'plaxo.com',
'jott.com',
'svpply.com',
'alice@email.aliceupdates.com',
'telltalegames.com',
'gymboree.com',
'landofnod.com',
'evganews@evga.com',
'thinkgeek.com',
'recyclebank.com',
'sharebuilder.com',
'zulily.com',
// Political / campaign (no longer relevant)
'barackobama.com',
'berniesanders.com',
'democrats.org',
'rockthevote.com',
'rockthevote',
// No longer relevant (uncertain ones user confirmed)
'vacationsurvey@vargaresearch.com',
'5h4rpd0g@gmail.com',
'mdegan@jacsmechanical.com',
'ispcm-conf.org',
'tntechnology.org',
'Jason Mechler',
// Duplicates / inbox-zero handles these via AI
'nytimes@email.newyorktimes.com',
'nytimes@e.newyorktimesinfo.com',
'redcross-email@usa.redcross.org',
'redcross-email@redcross.org',
'redcross@theamericanredcross.org',
'xfinity@info.xfinity.com',
'xfinity@emails.xfinity.com',
'kirkusreviews.com',
'smarttoys@aol.com',
'spotify@email.news.spotifymail.com',
'no-reply@news.spotifymail.com',
'newsletters@audible.com',
'fightingback@messages.cancer.org',
'@messages.cancer.org',
// Newsletter/marketing — inbox-zero AI handles these
'eff.org',
'rccetimes.com',
'group-digests@linkedin.com',
'info.knologyupclose.com',
'newsletter@theorangepeel.net',
'microsoft@e-mail.microsoft.com',
'newsletters-no-reply@myfonts.com',
'act@credoaction.com',
'yourpowertip@powershell.com',
'email@email.microsoftemail.com',
'ramit@iwillteachyoutoberich.com',
'mkt@manning.com',
'deals@livingsocial.com',
'idera.com',
'newsletter@kirkusreviews.com',
'email@earthfare-email.com',
'gameinfo@email2.telltalegames.com',
'no-reply@mail.goodreads.com',
'angieslist@members.angieslist.com',
'reply@e.taxact.com',
'josabank@shop.josbank.com',
'frenchpaperco@gmail.com',
'csaimages@frenchpaper.com',
'mailman@xoxide.com',
'info@petsafe.net',
'flor@news.flor.com',
'info@launchtn.org',
'alumni@tntech.edu',
'info@rockthevote.com',
'payscalemonitor@mail.payscale.com',
'statefarm@e.statefarm.com',
'mailer@svpply.com',
'noreply@dontcrack.com',
'info@paperandpolkadots.com',
'dennis@adirondackguitar.com',
'Yamaha_Corporation_of_America@mail.vresp.com',
'carters.email@e.carters.com',
'FTD@email.ftd.com',
'info@email.glassdoor.com',
'reply@glassdoor.com',
'customerservice@1800petmeds.com',
'news@crownclub.regmovies.com',
'autonationspecials@autonation.chtah.com',
'EddieBauerEmail@em.eddiebauer.com',
'email@style.ballarddesigns.com',
'barnesandnoble@m.bn.com',
'sales@xoxide.com',
'tripadvisor.com',
'noreply@youtube.com',
'donotreply@proguitarshop.com',
'info@knoxvilleacademyofmusic.com',
'artistupdates@reverbnation.com',
'promotion@mwave.com',
'deansbeans.com',
'express@e.express.com',
'alice@email.aliceupdates.com',
'ispcm-conf.org',
'no_reply@jetbrains.com',
'noreply@roddenberry.com',
'yourhondadealer@ahmdcms.com',
'mdegan@jacsmechanical.com',
];
// Subject patterns — delete filters matching these subjects
// (only when combined with a dead/irrelevant from, handled separately below)
// For now these are standalone subject-only filters to delete
var DELETE_SUBJECT_PATTERNS = [
'Abandonia Newsletter',
];
// List patterns (mailing list ID in filter query)
var DELETE_LIST_PATTERNS = [
'oakridgemoms.googlegroups.com',
'14-days.googlegroups.com',
'120331169_84238_19051',
'andrewbird.fanbridge.com',
];
// Specific from+subject combos — delete if BOTH match
var DELETE_FROM_SUBJECT_PAIRS = [
{ from: 'linkedin.com', subject: 'endorsed' },
{ from: 'updates@linkedin.com', subject: null }, // skip inbox only, no label
{ from: 'connections@linkedin.com', subject: null }, // linkedin connection spam
{ from: 'noreply@plaxo.com', subject: 'birthday' },
{ from: 'confirm+ye22mger@facebookmail.com', subject: null }, // old specific fb filter
];
// ─────────────────────────────────────────────────────────────────────────────
function getFiltersToDelete_() {
var response = Gmail.Users.Settings.Filters.list('me');
var filters = response.filter || [];
var toDelete = [];
filters.forEach(function(f) {
var from = (f.criteria && f.criteria.from) ? f.criteria.from.toLowerCase() : '';
var subject = (f.criteria && f.criteria.subject) ? f.criteria.subject.toLowerCase() : '';
var query = (f.criteria && f.criteria.query) ? f.criteria.query.toLowerCase() : '';
var shouldDelete = false;
var reason = '';
// Check from patterns
DELETE_FROM_PATTERNS.forEach(function(p) {
if (from.indexOf(p.toLowerCase()) !== -1) {
shouldDelete = true;
reason = 'from matches: ' + p;
}
});
// Check subject patterns
if (!shouldDelete) {
DELETE_SUBJECT_PATTERNS.forEach(function(p) {
if (subject.indexOf(p.toLowerCase()) !== -1) {
shouldDelete = true;
reason = 'subject matches: ' + p;
}
});
}
// Check list patterns in query
if (!shouldDelete) {
DELETE_LIST_PATTERNS.forEach(function(p) {
if (query.indexOf(p.toLowerCase()) !== -1) {
shouldDelete = true;
reason = 'list matches: ' + p;
}
});
}
// Check from+subject pairs
if (!shouldDelete) {
DELETE_FROM_SUBJECT_PAIRS.forEach(function(pair) {
var fromMatch = pair.from ? from.indexOf(pair.from.toLowerCase()) !== -1 : true;
var subjectMatch = pair.subject ? subject.indexOf(pair.subject.toLowerCase()) !== -1 : true;
if (fromMatch && subjectMatch && (pair.from || pair.subject)) {
shouldDelete = true;
reason = 'pair match: from=' + pair.from + ' subject=' + pair.subject;
}
});
}
if (shouldDelete) {
toDelete.push({ id: f.id, criteria: f.criteria, action: f.action, reason: reason });
}
});
return { all: filters, toDelete: toDelete };
}
function dryRun() {
Logger.log('=== DRY RUN — no filters will be deleted ===');
var result = getFiltersToDelete_();
Logger.log('Total filters: ' + result.all.length);
Logger.log('Filters that WOULD be deleted: ' + result.toDelete.length);
Logger.log('');
result.toDelete.forEach(function(f) {
Logger.log('DELETE [' + f.id + ']');
Logger.log(' From: ' + (f.criteria.from || ''));
Logger.log(' Subject: ' + (f.criteria.subject || ''));
Logger.log(' Query: ' + (f.criteria.query || ''));
Logger.log(' Reason: ' + f.reason);
Logger.log('');
});
Logger.log('=== Filters that will be KEPT ===');
var kept = result.all.filter(function(f) {
return !result.toDelete.some(function(d) { return d.id === f.id; });
});
kept.forEach(function(f) {
Logger.log('KEEP [' + f.id + '] from=' + (f.criteria.from || '') +
' subject=' + (f.criteria.subject || '') +
' query=' + (f.criteria.query || ''));
});
}
function deleteFilters() {
Logger.log('=== Deleting filters ===');
var result = getFiltersToDelete_();
Logger.log('Deleting ' + result.toDelete.length + ' of ' + result.all.length + ' filters...');
var deleted = 0;
result.toDelete.forEach(function(f) {
try {
Gmail.Users.Settings.Filters.remove('me', f.id);
Logger.log('Deleted [' + f.id + '] from=' + (f.criteria.from || '') + ' (' + f.reason + ')');
deleted++;
Utilities.sleep(200);
} catch(e) {
Logger.log('ERROR deleting [' + f.id + ']: ' + e.message);
}
});
Logger.log('Done. Deleted ' + deleted + ' filters.');
Logger.log('Remaining: ' + (result.all.length - deleted) + ' filters.');
}
+235
View File
@@ -0,0 +1,235 @@
/**
* Gmail Label Cleanup Script
* Run from https://script.google.com
*
* What this does:
* 1. Merges old labels into inbox-zero categories (relabels all threads)
* 2. Archives + relabels cvlp threads to Newsletter
* 3. Merges purchases into Receipt, deletes purchases
* 4. Moves donations top-level, selling under business
* 5. Deletes contract label (threads stay under business)
* 6. Deletes empty/unused labels
*
* Safe to run multiple times - skips labels that don't exist.
*/
// ─── STEP 1: Merge old labels into inbox-zero categories ─────────────────────
function mergeLabels() {
var merges = [
{ from: 'promo', to: 'Marketing' },
{ from: 'mailing list', to: 'Newsletter' },
{ from: 'twitter', to: 'Notification' },
{ from: 'facebook', to: 'Notification' },
{ from: 'tech', to: 'Newsletter' },
{ from: 'To Reply', to: 'Awaiting Reply' },
];
merges.forEach(function(m) {
var fromLabel = GmailApp.getUserLabelByName(m.from);
if (!fromLabel) {
Logger.log('Skipping (not found): ' + m.from);
return;
}
var toLabel = GmailApp.getUserLabelByName(m.to);
if (!toLabel) {
Logger.log('Creating label: ' + m.to);
toLabel = GmailApp.createLabel(m.to);
}
Logger.log('Merging ' + m.from + ' -> ' + m.to);
var threads = fromLabel.getThreads(0, 100);
var total = 0;
while (threads.length > 0) {
toLabel.addToThreads(threads);
fromLabel.removeFromThreads(threads);
total += threads.length;
Utilities.sleep(1000);
threads = fromLabel.getThreads(0, 100);
}
Logger.log('Moved ' + total + ' threads: ' + m.from + ' -> ' + m.to);
fromLabel.deleteLabel();
Logger.log('Deleted label: ' + m.from);
});
}
// ─── STEP 2: cvlp → Newsletter + archive ─────────────────────────────────────
function archiveCvlp() {
var cvlpLabel = GmailApp.getUserLabelByName('cvlp');
if (!cvlpLabel) {
Logger.log('cvlp label not found, skipping');
return;
}
var newsletterLabel = GmailApp.getUserLabelByName('Newsletter');
if (!newsletterLabel) {
newsletterLabel = GmailApp.createLabel('Newsletter');
}
Logger.log('Processing cvlp: relabeling to Newsletter and archiving...');
var threads = cvlpLabel.getThreads(0, 100);
var total = 0;
while (threads.length > 0) {
newsletterLabel.addToThreads(threads);
cvlpLabel.removeFromThreads(threads);
threads.forEach(function(t) { t.moveToArchive(); });
total += threads.length;
Utilities.sleep(1000);
threads = cvlpLabel.getThreads(0, 100);
}
Logger.log('Archived ' + total + ' cvlp threads');
cvlpLabel.deleteLabel();
Logger.log('Deleted label: cvlp');
}
// ─── STEP 3: Merge purchases into Receipt ────────────────────────────────────
function mergePurchasesIntoReceipt() {
var purchasesLabel = GmailApp.getUserLabelByName('purchases');
if (!purchasesLabel) {
Logger.log('purchases label not found, skipping');
return;
}
var receiptLabel = GmailApp.getUserLabelByName('Receipt');
if (!receiptLabel) {
receiptLabel = GmailApp.createLabel('Receipt');
}
Logger.log('Merging purchases -> Receipt...');
var threads = purchasesLabel.getThreads(0, 100);
var total = 0;
while (threads.length > 0) {
receiptLabel.addToThreads(threads);
purchasesLabel.removeFromThreads(threads);
total += threads.length;
Utilities.sleep(1000);
threads = purchasesLabel.getThreads(0, 100);
}
Logger.log('Moved ' + total + ' threads: purchases -> Receipt');
purchasesLabel.deleteLabel();
Logger.log('Deleted label: purchases');
}
// ─── STEP 4: Restructure business nested labels ───────────────────────────────
function restructureBusinessLabels() {
// Move donations top-level: rename business/donations -> donations
var bizDonations = GmailApp.getUserLabelByName('business/donations');
if (bizDonations) {
var donationsLabel = GmailApp.getUserLabelByName('donations');
if (!donationsLabel) {
donationsLabel = GmailApp.createLabel('donations');
}
Logger.log('Moving business/donations -> donations (top-level)');
var threads = bizDonations.getThreads(0, 100);
var total = 0;
while (threads.length > 0) {
donationsLabel.addToThreads(threads);
bizDonations.removeFromThreads(threads);
total += threads.length;
Utilities.sleep(1000);
threads = bizDonations.getThreads(0, 100);
}
Logger.log('Moved ' + total + ' threads to top-level donations');
bizDonations.deleteLabel();
} else {
Logger.log('business/donations not found - skipping');
}
// Move selling under business: rename selling -> business/selling
var sellingLabel = GmailApp.getUserLabelByName('selling');
if (sellingLabel) {
var bizSelling = GmailApp.getUserLabelByName('business/selling');
if (!bizSelling) {
bizSelling = GmailApp.createLabel('business/selling');
}
Logger.log('Moving selling -> business/selling');
var threads = sellingLabel.getThreads(0, 100);
var total = 0;
while (threads.length > 0) {
bizSelling.addToThreads(threads);
sellingLabel.removeFromThreads(threads);
total += threads.length;
Utilities.sleep(1000);
threads = sellingLabel.getThreads(0, 100);
}
Logger.log('Moved ' + total + ' threads to business/selling');
sellingLabel.deleteLabel();
}
// Delete contract label (threads already have business label)
var contractLabel = GmailApp.getUserLabelByName('business/contract');
if (!contractLabel) {
contractLabel = GmailApp.getUserLabelByName('contract');
}
if (contractLabel) {
Logger.log('Removing contract label from all threads...');
var threads = contractLabel.getThreads(0, 100);
var total = 0;
while (threads.length > 0) {
contractLabel.removeFromThreads(threads);
total += threads.length;
Utilities.sleep(1000);
threads = contractLabel.getThreads(0, 100);
}
contractLabel.deleteLabel();
Logger.log('Deleted contract label (' + total + ' threads unaffected)');
}
}
// ─── STEP 5: Delete unused labels ────────────────────────────────────────────
function deleteUnusedLabels() {
var toDelete = [
'Notes',
'[Mailbox]',
'Later',
'To Buy',
'To Read',
'To Watch',
'Cold Email',
'Oak Ridge Moms',
'music',
];
toDelete.forEach(function(name) {
var label = GmailApp.getUserLabelByName(name);
if (!label) {
Logger.log('Not found (already gone?): ' + name);
return;
}
var threads = label.getThreads(0, 100);
var total = 0;
while (threads.length > 0) {
label.removeFromThreads(threads);
total += threads.length;
Utilities.sleep(500);
threads = label.getThreads(0, 100);
}
if (total > 0) {
Logger.log('Removed label from ' + total + ' threads: ' + name);
}
label.deleteLabel();
Logger.log('Deleted: ' + name);
});
}
// ─── RUN ALL ──────────────────────────────────────────────────────────────────
function runAll() {
Logger.log('=== Starting Gmail label cleanup ===');
Logger.log('--- Step 1: Merging labels ---');
mergeLabels();
Logger.log('--- Step 2: Archiving cvlp ---');
archiveCvlp();
Logger.log('--- Step 3: Merging purchases into Receipt ---');
mergePurchasesIntoReceipt();
Logger.log('--- Step 4: Restructuring business labels ---');
restructureBusinessLabels();
Logger.log('--- Step 5: Deleting unused labels ---');
deleteUnusedLabels();
Logger.log('=== Done! ===');
}
+16
View File
@@ -0,0 +1,16 @@
services:
kiwix:
image: ghcr.io/kiwix/kiwix-serve:latest
container_name: kiwix
restart: unless-stopped
command: '*.zim'
ports:
- "8085:8080"
volumes:
- /mnt/nas_library/kiwix/zim:/data
networks:
- npm-network
networks:
npm-network:
external: true
+28
View File
@@ -0,0 +1,28 @@
version: '3'
services:
ladder:
image: ghcr.io/everywall/ladder:latest
container_name: ladder
#build: .
restart: unless-stopped
#command: sh -c ./ladder
environment:
- PORT=8080
- RULESET=https://raw.githubusercontent.com/everywall/ladder-rules/main/ruleset.yaml
#- RULESET=/app/ruleset.yaml
#- ALLOWED_DOMAINS=example.com,example.org
#- ALLOWED_DOMAINS_RULESET=false
#- EXPOSE_RULESET=true
#- PREFORK=false
#- DISABLE_FORM=false
#- FORM_PATH=/app/form.html
#- X_FORWARDED_FOR=66.249.66.1
#- USER_AGENT=Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
#- USERPASS=foo:bar
#- LOG_URLS=true
#- GODEBUG=netdns=go
ports:
- "8910:8080"
volumes:
- /srv/ladder/ruleset.yaml:/app/ruleset.yaml
- /srv/ladder/handlers/form.html:/app/form.html
-10
View File
@@ -1,10 +0,0 @@
version: '3.8'
services:
sql-server:
image: mcr.microsoft.com/mssql/server
container_name: leafweb-sql-server
environment:
SA_PASSWORD: ${SA_PASSWORD}
ACCEPT_EULA: Y
ports:
- "1433:1433"
+17 -5
View File
@@ -1,15 +1,27 @@
services: services:
linkding: linkding:
container_name: "${LD_CONTAINER_NAME:-linkding}" container_name: linkding
image: sissbruecker/linkding:latest image: sissbruecker/linkding:latest
ports: ports:
- "${LD_HOST_PORT:-9090}:9090" - "9090:9090"
volumes: volumes:
- "${LD_HOST_DATA_DIR:-./data}:/etc/linkding/data" - /srv/linkding-data:/etc/linkding/data
environment:
- VIRTUAL_HOST=linkding.kolpacksoftware.com
- VIRTUAL_PORT=9090
- LETSENCRYPT_HOST=linkding.kolpacksoftware.com
# Authelia OIDC (LD_ENABLE_OIDC uses LD_ prefix; the rest do not)
- LD_ENABLE_OIDC=True
- OIDC_OP_AUTHORIZATION_ENDPOINT=https://auth.kolpacksoftware.com/api/oidc/authorization
- OIDC_OP_TOKEN_ENDPOINT=https://auth.kolpacksoftware.com/api/oidc/token
- OIDC_OP_USER_ENDPOINT=https://auth.kolpacksoftware.com/api/oidc/userinfo
- OIDC_OP_JWKS_ENDPOINT=https://auth.kolpacksoftware.com/jwks.json
- OIDC_RP_CLIENT_ID=linkding
- OIDC_RP_CLIENT_SECRET=${LINKDING_OIDC_CLIENT_SECRET}
- OIDC_RP_SIGN_ALGO=RS256
restart: unless-stopped restart: unless-stopped
networks: networks:
default: default:
external: external:
name: npm-network name: npm-network
+16
View File
@@ -0,0 +1,16 @@
services:
map-frontend:
image: nginx:alpine
container_name: map-frontend
restart: unless-stopped
ports:
- "8087:80"
volumes:
- /srv/map-frontend/nginx-conf:/etc/nginx/conf.d:ro
- /srv/map-frontend/html:/usr/share/nginx/html:ro
networks:
- npm-network
networks:
npm-network:
external: true
+112
View File
@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maps</title>
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.css">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
#map { width: 100%; height: 100vh; }
#theme-control {
position: absolute;
top: 10px;
left: 10px;
z-index: 1;
}
#theme-control select {
appearance: none;
background: #fff;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.15);
cursor: pointer;
font-family: system-ui, sans-serif;
font-size: 13px;
padding: 6px 28px 6px 10px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23666'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
}
#theme-control select.dark {
background-color: #1a1a1a;
color: #eee;
border-color: rgba(255,255,255,0.2);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23aaa'/%3E%3C/svg%3E");
}
</style>
</head>
<body>
<div id="map"></div>
<div id="theme-control">
<select id="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="grayscale">Grayscale</option>
<option value="white">White</option>
<option value="black">Black</option>
</select>
</div>
<script src="https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.js"></script>
<script type="module">
import { layers, namedFlavor } from 'https://esm.sh/@protomaps/basemaps@5';
const TILESET = 'north-america';
function buildStyle(theme) {
return {
version: 8,
glyphs: 'https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf',
sprite: `https://protomaps.github.io/basemaps-assets/sprites/v4/${theme}`,
sources: {
protomaps: {
type: 'vector',
tiles: [`${window.location.origin}/tiles/${TILESET}/{z}/{x}/{y}.mvt`],
minzoom: 0,
maxzoom: 15,
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a>'
}
},
layers: layers('protomaps', namedFlavor(theme), { lang: 'en' })
};
}
const map = new maplibregl.Map({
container: 'map',
style: buildStyle('light'),
center: [-98, 39],
zoom: 4
});
map.addControl(new maplibregl.NavigationControl(), 'top-right');
map.addControl(new maplibregl.ScaleControl(), 'bottom-left');
map.addControl(new maplibregl.FullscreenControl(), 'top-right');
map.addControl(new maplibregl.GeolocateControl({
positionOptions: { enableHighAccuracy: true },
trackUserLocation: true
}), 'top-right');
const themeSelect = document.getElementById('theme');
themeSelect.addEventListener('change', e => {
const theme = e.target.value;
const { lng, lat } = map.getCenter();
const zoom = map.getZoom();
const bearing = map.getBearing();
const pitch = map.getPitch();
themeSelect.className = (theme === 'dark' || theme === 'black') ? 'dark' : '';
map.setStyle(buildStyle(theme));
map.once('styledata', () => {
map.jumpTo({ center: [lng, lat], zoom, bearing, pitch });
});
});
</script>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
server {
listen 80;
# Proxy tile requests to the internal pmtiles server
location /tiles/ {
proxy_pass http://pmtiles:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# Serve the static frontend
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ =404;
}
}
+113 -41
View File
@@ -20,30 +20,99 @@ services:
environment: environment:
EULA: "true" EULA: "true"
SEED: 46942827301 SEED: 46942827301
SERVER_NAME: "Ourcraft" SERVER_NAME: "Terraforma SMP"
MEMORY: "4G" MEMORY: "4G"
OPS: "Jimcognito" OPS: "Jimcognito"
#TYPE: "FABRIC" #TYPE: "FABRIC"
volumes: volumes:
- /srv/minecraft-data:/data - /srv/minecraft-data:/data
stdin_open: true stdin_open: true
tty: true tty: true
restart: unless-stopped restart: unless-stopped
creative: mem_limit: 6g
# creative:
# image: itzg/minecraft-server:latest
# container_name: minecraft-creative
# ports:
# - "25566:25565"
# environment:
# EULA: "true"
# SEED: 8486672581758651406
# SERVER_NAME: "Creative Kolpacks"
# volumes:
# - /srv/minecraft-data-creative:/data
# stdin_open: true
# tty: true
# restart: unless-stopped
# mem_limit: 2g
# battle:
# image: itzg/minecraft-server:latest
# container_name: minecraft-battle
# ports:
# - "25573:25565"
# environment:
# EULA: "true"
# SEED: "9037888329883360986"
# SERVER_NAME: "Battle Royale"
# MODE: "survival"
# PVP: "true"
# OPS: "Jimcognito"
# TYPE: "PAPER"
# MODRINTH_PROJECTS: "ezcountdown,worldresetplugin,viaversion"
# volumes:
# - /srv/minecraft-battle:/data
# stdin_open: true
# tty: true
# restart: unless-stopped
# mem_limit: 2g
# immersive:
# image: itzg/minecraft-server:latest
# container_name: minecraft-immersive
# ports:
# - "55567:25565"
# environment:
# EULA: "true"
# TYPE: "FABRIC"
# VERSION: "1.20.4"
# SERVER_NAME: "Immersive Portals"
# SEED: "302814868814545"
# MODE: "creative"
# FORCE_GAMEMODE: "true"
# ENABLE_COMMAND_BLOCK: "true"
# ALLOW_FLIGHT: "true"
# ENFORCE_SECURE_PROFILE: "false"
# MEMORY: "3G"
# OPS: "Jimcognito,OlympicJumperL"
# MODRINTH_PROJECTS: "fabric-api,pehkui,immersiveportals,gravity-api-fork:v1.3.0-mc1.20.4"
# MODRINTH_DOWNLOAD_DEPENDENCIES: "required"
# ICON: "https://cdn.modrinth.com/data/zJpHMkdD/0b2fa76a843b96f8bba1eb9d41f18f9f8ed4ce18_96.webp"
# volumes:
# - /srv/minecraft-modded:/data
# stdin_open: true
# tty: true
# restart: unless-stopped
# mem_limit: 4g
empire-smp:
image: itzg/minecraft-server:latest image: itzg/minecraft-server:latest
container_name: minecraft-creative container_name: minecraft-empire-smp
ports: ports:
- "25566:25565" - "31415:25565"
environment: environment:
EULA: "true" EULA: "true"
SEED: 8486672581758651406 SEED: "-4690695632699825725"
SERVER_NAME: "Creative Kolpacks" SERVER_NAME: "Empire SMP"
MODE: "survival"
PVP: "false"
OPS: "Jimcognito"
volumes: volumes:
- /srv/minecraft-data-creative:/data - /srv/minecraft-empire-smp:/data
stdin_open: true stdin_open: true
tty: true tty: true
restart: unless-stopped restart: unless-stopped
mem_limit: 2g
epic-bases: epic-bases:
image: itzg/minecraft-server:latest image: itzg/minecraft-server:latest
container_name: minecraft-pvp0 container_name: minecraft-pvp0
@@ -60,39 +129,42 @@ services:
stdin_open: true stdin_open: true
tty: true tty: true
restart: unless-stopped restart: unless-stopped
hardcore: mem_limit: 2g
image: itzg/minecraft-server:latest # hardcore temporarily disabled to reduce swap pressure
container_name: minecraft-hardcore # hardcore:
ports: # image: itzg/minecraft-server:latest
- "25568:25565" # container_name: minecraft-hardcore
environment: # ports:
EULA: "true" # - "25568:25565"
SEED: "-3149142039438819319" # environment:
SERVER_NAME: "Hardcore" # EULA: "true"
OPS: "Jimcognito,MagicSpaceCat" # SEED: "-3149142039438819319"
TYPE: "FABRIC" # SERVER_NAME: "Hardcore"
MAX_WORLD_SIZE: 415 # OPS: "Jimcognito,MagicSpaceCat"
HARDCORE: true # TYPE: "FABRIC"
PVP: false # MAX_WORLD_SIZE: 415
volumes: # HARDCORE: true
- /srv/minecraft-data-hardcore:/data # PVP: false
stdin_open: true # volumes:
tty: true # - /srv/minecraft-data-hardcore:/data
restart: unless-stopped # stdin_open: true
survival: # tty: true
image: itzg/minecraft-server:latest # restart: unless-stopped
container_name: minecraft-survival-smp # survival temporarily disabled to reduce swap pressure
ports: # survival:
- "25571:25565" # image: itzg/minecraft-server:latest
environment: # container_name: minecraft-survival-smp
EULA: "true" # ports:
SERVER_NAME: "Survival SMP" # - "25571:25565"
OPS: "Jimcognito,OlympicJumperL" # environment:
volumes: # EULA: "true"
- /srv/minecraft-data-survival-smp:/data # SERVER_NAME: "Survival SMP"
stdin_open: true # OPS: "Jimcognito,OlympicJumperL"
tty: true # volumes:
restart: unless-stopped # - /srv/minecraft-data-survival-smp:/data
# stdin_open: true
# tty: true
# restart: unless-stopped
# spells: # spells:
# image: itzg/minecraft-server # image: itzg/minecraft-server
# container_name: minecraft-spells # container_name: minecraft-spells
+1 -2
View File
@@ -28,7 +28,6 @@ services:
networks: networks:
mqtt-network: mqtt-network:
driver: bridge external: true
default:
npm-network: npm-network:
external: true external: true
+2
View File
@@ -0,0 +1,2 @@
# Let's Encrypt notification email
LETSENCRYPT_EMAIL=your-email@example.com
+142
View File
@@ -0,0 +1,142 @@
# nginx-proxy + acme-companion
Automated reverse proxy with Let's Encrypt SSL certificates.
## Setup
1. Create the data directories:
```bash
sudo mkdir -p /srv/nginx-proxy-acme/{certs,vhost.d,html,conf.d,acme}
```
2. Copy configuration files:
```bash
sudo cp conf/conf.d/* /srv/nginx-proxy-acme/conf.d/
sudo cp conf/vhost.d/* /srv/nginx-proxy-acme/vhost.d/
```
3. Configure environment:
```bash
cp .env.example .env
# Edit .env with your email
```
4. Start the proxy:
```bash
docker compose up -d
```
## Architecture
- **nginx-proxy**: Reverse proxy with auto-discovery of Docker containers
- **acme-companion**: Automatic Let's Encrypt certificate management
- **static-certs**: Dummy container that triggers cert issuance for non-container backends
## Security
All hosts receive these security features via `vhost.d/default`:
- **HSTS**: Strict-Transport-Security header (1 year)
- **Security headers**: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy
- **Block exploits**: WAF rules against SQL injection, XSS, file injection, spam, bad user agents
## Access Control
### Private Hosts (IP restricted)
Private hosts include `vhost.d/private` which restricts access to:
- 192.168.1.0/24 (local network)
- 172.16.0.0/12 (Docker networks)
To make a new host private, create a vhost.d file:
```bash
echo 'include /etc/nginx/vhost.d/private;' | sudo tee /srv/nginx-proxy-acme/vhost.d/myapp.kolpacksoftware.com
```
### Public Hosts
Public hosts have no vhost.d file (only `default` applies). They get security headers and block-exploits but no IP restrictions.
Current public hosts:
- chd.kolpacksoftware.com
- linkding.kolpacksoftware.com
- organizer.rmstsa.org
- ridge-resources.org
- rmstsa.org / www.rmstsa.org
- share.kolpacksoftware.com
- vikunja.kolpacksoftware.com
## Adding a Proxied Service
Add these environment variables to any container:
```yaml
services:
myapp:
image: myapp:latest
environment:
- VIRTUAL_HOST=myapp.kolpacksoftware.com
- VIRTUAL_PORT=8080
- LETSENCRYPT_HOST=myapp.kolpacksoftware.com
networks:
- npm-network
networks:
npm-network:
external: true
```
Then if private, add the vhost.d file as shown above.
## Static IP Backends
Services running on physical hosts or VMs (not Docker) are configured in `conf.d/static-upstreams.conf`:
| Domain | Backend |
|--------|---------|
| portainer.kolpacksoftware.com | 172.17.0.1:9443 |
| btt-cb1.kolpacksoftware.com | 192.168.1.173:80 |
| hats.kolpacksoftware.com | 192.168.1.66:9999 |
| pve-nas.kolpacksoftware.com | 192.168.1.245:8006 |
| unraid.kolpacksoftware.com | 192.168.1.192:80 |
All static backends are private (IP restricted).
To add a new static backend:
1. Add server block to `conf.d/static-upstreams.conf`
2. Add domain to `static-certs` container's VIRTUAL_HOST and LETSENCRYPT_HOST in docker-compose.yaml
3. Reload: `docker exec nginx-proxy nginx -s reload`
## Directory Structure
```
/srv/nginx-proxy-acme/
├── acme/ # acme.sh state (auto-managed)
├── certs/ # SSL certificates (auto-managed)
├── conf.d/
│ ├── block-exploits.conf # WAF rules
│ └── static-upstreams.conf # Static IP backend server blocks
├── html/ # ACME challenge files (auto-managed)
└── vhost.d/
├── default # Security headers + HSTS + block-exploits (all hosts)
├── private # IP allowlist (private hosts include this)
├── docker-registry.* # Special config (auth headers + private)
└── <hostname> # Per-host includes (usually just 'include private')
```
## Reload Config
After changing vhost.d or conf.d files:
```bash
docker exec nginx-proxy nginx -s reload
```
## Multiple Domains
For multiple domains on one cert:
```yaml
environment:
- VIRTUAL_HOST=example.com,www.example.com
- LETSENCRYPT_HOST=example.com,www.example.com
```
Create vhost.d entries for each domain if private.
@@ -0,0 +1,134 @@
# Block SQL injections
set $block_sql_injections 0;
if ($query_string ~ "union.*select.*\(") {
set $block_sql_injections 1;
}
if ($query_string ~ "union.*all.*select.*") {
set $block_sql_injections 1;
}
if ($query_string ~ "concat.*\(") {
set $block_sql_injections 1;
}
if ($block_sql_injections = 1) {
return 403;
}
# Block file injections
set $block_file_injections 0;
if ($query_string ~ "[a-zA-Z0-9_]=http://") {
set $block_file_injections 1;
}
if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") {
set $block_file_injections 1;
}
if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") {
set $block_file_injections 1;
}
if ($block_file_injections = 1) {
return 403;
}
# Block common exploits
set $block_common_exploits 0;
if ($query_string ~ "(<|%3C).*script.*(>|%3E)") {
set $block_common_exploits 1;
}
if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") {
set $block_common_exploits 1;
}
if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") {
set $block_common_exploits 1;
}
if ($query_string ~ "proc/self/environ") {
set $block_common_exploits 1;
}
if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") {
set $block_common_exploits 1;
}
if ($query_string ~ "base64_(en|de)code\(.*\)") {
set $block_common_exploits 1;
}
if ($block_common_exploits = 1) {
return 403;
}
# Block spam
set $block_spam 0;
if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") {
set $block_spam 1;
}
if ($block_spam = 1) {
return 403;
}
# Block user agents
set $block_user_agents 0;
if ($http_user_agent ~ "Indy Library") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "libwww-perl") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "GetRight") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "GetWeb!") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "Go!Zilla") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "Download Demon") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "Go-Ahead-Got-It") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "TurnitinBot") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "GrabNet") {
set $block_user_agents 1;
}
if ($block_user_agents = 1) {
return 403;
}
@@ -0,0 +1,219 @@
# Static upstream configurations for non-container backends
# These hosts forward to IP addresses instead of Docker containers
# ------------------------------------------------------------------------------
# portainer.kolpacksoftware.com -> 172.17.0.1:9443 (HTTPS backend via Docker bridge)
# ------------------------------------------------------------------------------
upstream portainer-backend {
server 172.17.0.1:9443;
}
server {
listen 80;
server_name portainer.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name portainer.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/portainer.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/portainer.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
location / {
proxy_pass https://portainer-backend;
proxy_ssl_verify off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# ------------------------------------------------------------------------------
# btt-cb1.kolpacksoftware.com -> 192.168.1.173:80
# ------------------------------------------------------------------------------
upstream btt-cb1-backend {
server 192.168.1.173:80;
}
server {
listen 80;
server_name btt-cb1.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name btt-cb1.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/btt-cb1.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/btt-cb1.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
location / {
proxy_pass http://btt-cb1-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# ------------------------------------------------------------------------------
# hats.kolpacksoftware.com -> 192.168.1.66:9999
# ------------------------------------------------------------------------------
upstream hats-backend {
server 192.168.1.66:9999;
}
server {
listen 80;
server_name hats.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name hats.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/hats.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/hats.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
location / {
proxy_pass http://hats-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# ------------------------------------------------------------------------------
# pve-nas.kolpacksoftware.com -> 192.168.1.245:8006 (HTTPS backend - Proxmox)
# ------------------------------------------------------------------------------
upstream pve-nas-backend {
server 192.168.1.245:8006;
}
server {
listen 80;
server_name pve-nas.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name pve-nas.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/pve-nas.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/pve-nas.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
# Proxmox needs larger buffers
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
location / {
proxy_pass https://pve-nas-backend;
proxy_ssl_verify off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# ------------------------------------------------------------------------------
# unraid.kolpacksoftware.com -> 192.168.1.192:80
# ------------------------------------------------------------------------------
upstream unraid-backend {
server 192.168.1.192:80;
}
server {
listen 80;
server_name unraid.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name unraid.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/unraid.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/unraid.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
location / {
proxy_pass http://unraid-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
+12
View File
@@ -0,0 +1,12 @@
# Default configuration applied to all virtual hosts
# Security headers and WAF rules - applied to ALL hosts (public and private)
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Include block-exploits rules
include /etc/nginx/conf.d/block-exploits.conf;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1,6 @@
# Docker registry needs Authorization header passthrough
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
# Private access control
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
+7
View File
@@ -0,0 +1,7 @@
# Access control for private hosts
# Symlink this file to restrict access to local networks only
allow 192.168.1.0/24;
allow 172.16.0.0/12;
# allow 162.10.130.22; # Netskope - uncomment if needed for corporate VPN
deny all;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
+48
View File
@@ -0,0 +1,48 @@
services:
nginx-proxy:
image: nginxproxy/nginx-proxy:1.6
container_name: nginx-proxy
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
- /srv/nginx-proxy-acme/certs:/etc/nginx/certs:ro
- /srv/nginx-proxy-acme/vhost.d:/etc/nginx/vhost.d
- /srv/nginx-proxy-acme/html:/usr/share/nginx/html
- /srv/nginx-proxy-acme/conf.d/static-upstreams.conf:/etc/nginx/conf.d/static-upstreams.conf:ro
- /srv/nginx-proxy-acme/conf.d/block-exploits.conf:/etc/nginx/conf.d/block-exploits.conf:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
environment:
- TRUST_DOWNSTREAM_PROXY=false
acme-companion:
image: nginxproxy/acme-companion:2.4
container_name: acme-companion
restart: unless-stopped
volumes_from:
- nginx-proxy
volumes:
- /srv/nginx-proxy-acme/certs:/etc/nginx/certs:rw
- /srv/nginx-proxy-acme/acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- DEFAULT_EMAIL=${LETSENCRYPT_EMAIL}
depends_on:
- nginx-proxy
# Dummy container to trigger certificate issuance for static IP backends
# This container does nothing but hold env vars for acme-companion to detect
static-certs:
image: alpine:3.19
container_name: static-certs
restart: unless-stopped
command: ["sleep", "infinity"]
environment:
- VIRTUAL_HOST=portainer.kolpacksoftware.com,btt-cb1.kolpacksoftware.com,hats.kolpacksoftware.com,pve-nas.kolpacksoftware.com,unraid.kolpacksoftware.com
- LETSENCRYPT_HOST=portainer.kolpacksoftware.com,btt-cb1.kolpacksoftware.com,hats.kolpacksoftware.com,pve-nas.kolpacksoftware.com,unraid.kolpacksoftware.com
networks:
default:
name: npm-network
external: true
+6
View File
@@ -0,0 +1,6 @@
# Obico Server environment variables
# Copy to .env and fill in values, OR set in Portainer stack environment
# Generate after first start with:
# docker exec -it obico python manage.py gen_site_secret
DJANGO_SECRET_KEY=change-me-generate-with-manage.py-gen_site_secret
+51
View File
@@ -0,0 +1,51 @@
services:
obico-redis:
image: redis:7.2-alpine
container_name: obico-redis
restart: unless-stopped
networks:
- obico-internal
obico:
# :cuda tag attempts GPU but falls back to CPU if CUDA version mismatch
# (libcudart.so.11.0 required; GTX 1660 SUPER with driver 590 has CUDA 12).
# ML inference still works on CPU - adequate for a single printer.
image: ghcr.io/imagegenius/obico:cuda
container_name: obico
restart: unless-stopped
depends_on:
- obico-redis
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
- REDIS_URL=redis://obico-redis:6379
# HOST_IP must exactly match the URL used to access Obico (no trailing slash, no http://)
# Wrong value causes HTTP 500 errors on all page loads
- HOST_IP=obico.kolpacksoftware.com
- SITE_USES_HTTPS=True
- CSRF_TRUSTED_ORIGINS=["https://obico.kolpacksoftware.com"]
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
# Set to True temporarily to register your account, then back to False
- ACCOUNT_ALLOW_SIGN_UP=False
volumes:
- /srv/obico/config:/config
- /srv/obico/config/model_cache:/model_cache/ml_api
ports:
- "3334:3334"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
networks:
- npm-network
- obico-internal
networks:
npm-network:
external: true
obico-internal:
driver: bridge
+127
View File
@@ -0,0 +1,127 @@
Obsidian + Self-Hosted LiveSync Setup
======================================
This guide walks through setting up Obsidian with the Self-hosted LiveSync
plugin, syncing to CouchDB at https://couchdb.kolpacksoftware.com.
The web-based Obsidian instance is accessible at:
https://obsidian.kolpacksoftware.com
--------------------------------------------------------------------------------
DOCKER COMPOSE (obsidian/docker-compose.yml)
--------------------------------------------------------------------------------
services:
obsidian:
image: lscr.io/linuxserver/obsidian:latest
container_name: obsidian
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- /srv/obsidian-config:/config
ports:
- 3050:3000
- 3051:3001
shm_size: "1gb"
restart: unless-stopped
networks:
default:
external:
name: npm-network
Notes:
- Port 3050 = HTTP (KasmVNC web UI)
- Port 3051 = HTTPS (KasmVNC web UI)
- Vault files are stored inside the container at /config/obsidian/
--------------------------------------------------------------------------------
STEP 1: CREATE THE VAULT
--------------------------------------------------------------------------------
1. Open Obsidian on your machine.
2. On the welcome screen, click "Create new vault".
3. Name it: Obsidian Vault
4. Choose a location on your disk (default is fine).
5. Click "Create".
--------------------------------------------------------------------------------
STEP 2: DISABLE THE BUILT-IN SYNC PLUGIN
--------------------------------------------------------------------------------
1. Open Settings (gear icon, bottom-left).
2. Click "Core plugins" in the left sidebar.
3. Find "Sync" in the list and toggle it OFF.
--------------------------------------------------------------------------------
STEP 3: INSTALL SELF-HOSTED LIVESYNC PLUGIN
--------------------------------------------------------------------------------
1. In Settings, click "Community plugins".
2. If prompted, click "Turn on community plugins".
3. Click "Browse" and search for: Self-hosted LiveSync
4. Click the result, then click "Install".
5. Once installed, click "Enable".
--------------------------------------------------------------------------------
STEP 4: CONFIGURE LIVESYNC (ADDING A DEVICE)
--------------------------------------------------------------------------------
On the existing/primary device (e.g. https://obsidian.kolpacksoftware.com):
1. Go to Settings → Self-hosted LiveSync.
2. Click the wizard icon and select "Setup".
3. Click "Copy" to generate a Setup URI.
4. Set a passcode when prompted.
5. Copy the URI to your clipboard.
On the new device:
1. Go to Settings → Self-hosted LiveSync.
2. Click the wizard icon.
3. Select "I am adding a device".
4. Select "Use the copied setup URI".
5. Paste the Setup URI and enter the passcode.
6. Click "Restart and Fetch Data".
7. When prompted, select "This vault is empty, initialize with remote data".
8. Click "I understand the risks" to confirm.
Obsidian will restart and sync the vault contents down from CouchDB.
--------------------------------------------------------------------------------
STEP 5: ENABLE LIVESYNC MODE
--------------------------------------------------------------------------------
Once the initial sync completes:
1. Go to Settings → Self-hosted LiveSync.
2. Find "Synchronization Method".
3. Set it to "LiveSync".
This enables real-time continuous sync across all connected devices.
--------------------------------------------------------------------------------
TROUBLESHOOTING
--------------------------------------------------------------------------------
- "CORS error" in browser console:
CouchDB needs CORS enabled. Go to https://couchdb.kolpacksoftware.com/_utils
(Fauxton UI), open Config, and under "cors" set:
enable_cors = true
origins = *
credentials = true
methods = GET, PUT, POST, HEAD, DELETE
headers = accept, authorization, content-type, origin, referer
- "401 Unauthorized":
Verify admin username/password in LiveSync settings.
- Sync not starting:
Toggle the plugin off and back on, or restart Obsidian.
Check the LiveSync log panel (Settings → Self-hosted LiveSync → Log).
- KasmVNC clipboard issues:
Use the clipboard icon in the KasmVNC toolbar to paste text into
the Obsidian window.
+38
View File
@@ -0,0 +1,38 @@
services:
ocis:
image: owncloud/ocis:latest
container_name: ocis
restart: unless-stopped
entrypoint:
- /bin/sh
command:
- "-c"
- "echo 'no' | ocis init || true; exec ocis server"
environment:
- TZ=America/New_York
- OCIS_URL=https://cloud.kolpacksoftware.com
- PROXY_TLS=false
- PROXY_HTTP_ADDR=0.0.0.0:9200
- OCIS_OIDC_ISSUER=https://auth.kolpacksoftware.com
- WEB_OIDC_CLIENT_ID=ocis
- PROXY_OIDC_REWRITE_WELLKNOWN=true
- PROXY_OIDC_ACCESS_TOKEN_VERIFY_METHOD=none
- PROXY_USER_OIDC_CLAIM=preferred_username
- PROXY_AUTOPROVISION_ACCOUNTS=true
- OCIS_EXCLUDE_RUN_SERVICES=idp
- OCIS_LOG_LEVEL=warn
- DEMO_USERS=false
- OCIS_ADMIN_USER_ID=2e7e8fed-7de6-44d6-bc12-772599b711e4
- PROXY_CSP_CONFIG_FILE_LOCATION=/etc/ocis/csp.yaml
volumes:
- /srv/ocis/data:/var/lib/ocis
- /mnt/nas_owncloud:/var/lib/ocis/storage/users
- /srv/ocis/config:/etc/ocis
ports:
- 9200:9200
networks:
- npm-network
networks:
npm-network:
external: true
+2
View File
@@ -0,0 +1,2 @@
# Open WebUI OIDC — must match AUTHELIA_OIDC_CLIENT_SECRET_OPEN_WEBUI in Authelia stack
AUTHELIA_OIDC_CLIENT_SECRET_OPEN_WEBUI=
+46
View File
@@ -0,0 +1,46 @@
services:
ollama:
container_name: ollama
image: ollama/ollama:latest
restart: unless-stopped
ports:
- 127.0.0.1:11434:11434
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
- TZ=America/New_York
- OLLAMA_HOST=0.0.0.0
volumes:
- /srv/ollama:/root/.ollama
runtime: nvidia
networks:
- npm-network
# Optional: Web UI for Ollama
open-webui:
container_name: open-webui
image: ghcr.io/open-webui/open-webui:latest
restart: unless-stopped
ports:
- 3000:8080
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- TZ=America/New_York
- ENABLE_OAUTH_SIGNUP=true
- OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true
- OAUTH_PROVIDER_NAME=Authelia
- OPENID_PROVIDER_URL=https://auth.kolpacksoftware.com/.well-known/openid-configuration
- OAUTH_CLIENT_ID=open-webui
- OAUTH_CLIENT_SECRET=${AUTHELIA_OIDC_CLIENT_SECRET_OPEN_WEBUI}
- OAUTH_TOKEN_ENDPOINT_AUTH_METHOD=client_secret_post
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
volumes:
- /srv/open-webui:/app/backend/data
depends_on:
- ollama
networks:
- npm-network
networks:
npm-network:
external: true
+2
View File
@@ -0,0 +1,2 @@
# OpenClaw Gateway Token (auto-generated or set manually)
OPENCLAW_GATEWAY_TOKEN=your_gateway_token
+17
View File
@@ -0,0 +1,17 @@
FROM node:22-bookworm-slim
# Install git and ca-certificates required by openclaw's npm dependencies
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/*
# Rewrite any SSH GitHub URLs to HTTPS so npm install works without SSH keys
RUN git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
# Install openclaw from npm (same approach as the official docker-setup.sh)
RUN npm install -g openclaw@latest --loglevel=error
ENV HOME=/home/node
ENV TERM=xterm-256color
# Run as non-root node user (uid 1000), same as official image
USER node
WORKDIR /home/node
+129
View File
@@ -0,0 +1,129 @@
# OpenClaw Setup Guide
## Prerequisites
1. **Create storage directories:**
```bash
sudo mkdir -p /srv/openclaw/config /srv/openclaw/workspace
sudo chown -R 1000:1000 /srv/openclaw
```
2. **Create .env file on host:**
The `.env` file is already created locally at `openclaw/.env` with a generated token. You need to copy it to the deployment location:
```bash
# Copy .env to the openclaw directory where Portainer will use it
# Option 1: If running locally
cp openclaw/.env /path/to/portainer/stack/openclaw/.env
# Option 2: Add environment variables directly in Portainer stack
# (see Portainer setup below)
```
## Portainer Stack Setup
### Method 1: Via Portainer Web UI
1. Go to Portainer → Stacks → Add Stack
2. Choose "Git Repository"
3. Fill in:
- **Name:** openclaw
- **Repository URL:** `https://gitea.kolpacksoftware.com/homelab/docker-infrastructure.git`
- **Reference:** `refs/heads/main`
- **Compose path:** `openclaw/docker-compose.yml`
- **Auto update:** Enabled (5m interval recommended)
4. Add environment variables:
- `OPENCLAW_GATEWAY_TOKEN`: `27d4e63adce6c8f7c5396e8ca3f9ec5e6ff590077247fb11da03a8684ee3c711`
5. Deploy the stack
### Method 2: Via Portainer API
```bash
# Set your Portainer API token
export PORTAINER_TOKEN="your-token-here"
# Create the stack (Docker Compose mode)
curl -k -X POST "https://localhost:9443/api/stacks/create/standalone/repository" \
-H "X-API-Key: $PORTAINER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "openclaw",
"endpointId": 2,
"repositoryURL": "https://gitea.kolpacksoftware.com/homelab/docker-infrastructure.git",
"repositoryReferenceName": "refs/heads/main",
"composeFile": "openclaw/docker-compose.yml",
"repositoryAuthentication": false,
"autoUpdate": {
"interval": "5m"
},
"env": [
{
"name": "OPENCLAW_GATEWAY_TOKEN",
"value": "27d4e63adce6c8f7c5396e8ca3f9ec5e6ff590077247fb11da03a8684ee3c711"
}
]
}'
```
## Nginx Proxy Manager Configuration
Set up a proxy host for:
- **Domain:** openclaw.kolpacksoftware.com
- **Forward Hostname / IP:** openclaw (container name)
- **Forward Port:** 18789
- **⚠️ WebSocket Support:** Enable "WebSocket Support" toggle (REQUIRED - OpenClaw uses WebSocket protocol)
- **SSL:** Request Let's Encrypt certificate
- **Access List:** Create/use "Private Network Only" to restrict to 192.168.1.0/24
## Ollama Integration
Make sure Ollama is running and has models pulled:
```bash
# Check Ollama status
docker ps | grep ollama
# Pull a model if needed (from ollama directory)
docker exec -it ollama ollama pull llama3
# or
docker exec -it ollama ollama pull mistral
```
## Running the Onboarding Wizard
After the stack is deployed, run the onboarding wizard:
```bash
docker exec -it openclaw npx openclaw onboard
```
During onboarding:
1. Select **Ollama** as your model provider
2. Enter Ollama base URL: `http://ollama:11434` (since they're on the same Docker network)
3. Select which model to use (e.g., llama3, mistral)
4. Configure any messaging channels you want (optional)
## Accessing OpenClaw
- **Control UI:** https://openclaw.kolpacksoftware.com
- **Gateway Token:** `27d4e63adce6c8f7c5396e8ca3f9ec5e6ff590077247fb11da03a8684ee3c711`
## Troubleshooting
⚠️ **KNOWN ISSUE (2026-02-09)**: OpenClaw gateway mode in Docker consistently fails with "No API key found for provider 'anthropic'" despite correct auth-profiles.json configuration. See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed information about all attempted solutions.
**Current Status**: Container runs and is accessible, but cannot execute AI requests with either Ollama or Anthropic API.
Check logs:
```bash
docker logs openclaw -f
```
Check if OpenClaw can reach Ollama:
```bash
docker exec -it openclaw curl http://ollama:11434
```
For detailed troubleshooting history and configuration attempts, see:
- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** - Complete troubleshooting log
+292
View File
@@ -0,0 +1,292 @@
# OpenClaw Troubleshooting Log
## Issue Summary
OpenClaw gateway mode consistently fails with "No API key found for provider 'anthropic'" despite correct configuration of auth-profiles.json with both Ollama and Anthropic API keys.
**Date**: 2026-02-09
**OpenClaw Version**: `ghcr.io/openclaw/openclaw:main` (2026.2.6-3)
**Environment**: Docker on x86_64 Linux
## Errors Encountered
### 1. Architecture Mismatch (SOLVED)
**Error**: `[FATAL tini (7)] exec docker-entrypoint.sh failed: Exec format error`
**Cause**: Docker image defaults to ARM64 architecture
**Solution**: Add `platform: linux/amd64` to docker-compose.yml
```yaml
services:
openclaw:
platform: linux/amd64
```
### 2. Device Pairing Required (SOLVED)
**Error**: `disconnected (1008): pairing required`
**Solution**: Approve pending device pairing requests
```bash
docker exec openclaw npx openclaw devices list
docker exec openclaw npx openclaw devices approve <REQUEST_ID>
```
### 3. API Key Not Found (UNSOLVED)
**Error**:
```
Error: No API key found for provider "anthropic".
Auth store: /home/node/.openclaw/agents/main/agent/auth-profiles.json
```
**This error persists despite all configuration attempts below.**
## Configuration Attempts
### Attempt 1: Ollama Integration via Environment Variables
Added environment variables to docker-compose.yml:
```yaml
environment:
- OPENCLAW_AGENT_PROVIDER=ollama
- OPENCLAW_AGENT_MODEL=llama3
- OPENCLAW_OLLAMA_BASE_URL=http://ollama:11434
```
**Result**: ❌ Gateway still defaulted to Anthropic
### Attempt 2: Main Config with Ollama
Created `/srv/openclaw/config/config.json`:
```json
{
"agents": {
"defaults": {
"provider": "ollama",
"model": "llama3",
"authProfile": "ollama"
}
},
"ollama": {
"baseURL": "http://ollama:11434"
}
}
```
**Result**: ❌ Gateway ignored configuration
### Attempt 3: OpenAI-Compatible Ollama Configuration
Based on GitHub issues showing Ollama uses OpenAI-compatible API:
```json
{
"models": {
"providers": {
"ollama": {
"baseUrl": "http://ollama:11434/v1",
"api": "openai-completions",
"models": ["llama3", "qwen3-coder"]
}
}
}
}
```
**Result**: ❌ Gateway still tried to use Anthropic
### Attempt 4: Dummy Anthropic Token for Ollama Redirect
Created auth-profiles.json with anthropic profile pointing to Ollama:
```json
{
"anthropic": {
"provider": "openai",
"baseURL": "http://ollama:11434/v1",
"apiKey": "sk-ollama-dummy"
}
}
```
**Result**: ❌ Still complained about missing Anthropic API key
### Attempt 5: Real Anthropic API Key (Simple Format)
```json
{
"anthropic": {
"apiKey": "sk-ant-api03-..."
}
}
```
**Result**: ❌ Same error
### Attempt 6: Provider:Profile Name Format
Based on GitHub issues showing profile names like "anthropic:claude-cli":
```json
{
"anthropic:default": {
"provider": "anthropic",
"apiKey": "sk-ant-api03-..."
}
}
```
**Result**: ❌ Same error
### Attempt 7: Multiple File Locations
Tried placing auth-profiles.json in:
- `/srv/openclaw/config/auth-profiles.json` (root level)
- `/srv/openclaw/config/agents/main/agent/auth-profiles.json` (agent level)
- Both locations simultaneously
**Result**: ❌ Same error regardless of location
### Attempt 8: Agent-Specific Config
Created `/srv/openclaw/config/agents/main/agent/config.json`:
```json
{
"provider": "openai",
"model": "llama3",
"authProfile": "anthropic",
"openai": {
"baseURL": "http://ollama:11434/v1"
}
}
```
**Result**: ❌ Same error
### Attempt 9: Ollama Launch Command
Tried using Ollama's built-in OpenClaw launcher:
```bash
docker exec ollama ollama launch openclaw --config --model llama3
```
**Result**: ✅ Command succeeded and said "Added llama3 to OpenClaw"
**But**: Gateway mode in Docker still couldn't use it
## Files Verified
All configurations were verified to be:
- ✅ Valid JSON (validated with `python3 -m json.tool`)
- ✅ Correct permissions (`chown 1000:1000`, owned by `node` user)
- ✅ In correct locations (verified with `docker exec` inside container)
- ✅ Properly mounted via Docker volumes
## Network Verification
Confirmed OpenClaw and Ollama connectivity:
```bash
docker exec openclaw curl -s http://ollama:11434/api/tags
# Successfully returned list of available models including llama3
```
Both containers on same network (`npm-network`): ✅
## Key Findings
### 1. Gateway Mode Default Behavior
The log message `agent model: anthropic/claude-opus-4-6` appears on every startup regardless of configuration, suggesting gateway mode with `--allow-unconfigured` flag has hardcoded Anthropic as default.
### 2. Ollama Integration Method
Official documentation shows `ollama launch openclaw` is intended for HOST installation, not containerized deployment. The containerized gateway mode may not support Ollama properly.
### 3. Auth Profile Format
GitHub issues show various profile formats:
- `"anthropic:claude-cli"` with `"mode": "oauth"`
- `"ollama:local"` with `"token": "..."`
- `"minimax-cn:default"` with `"provider": "minimax"`
None of these formats worked in our containerized setup.
## Documentation Links
- **OpenClaw GitHub**: https://github.com/openclaw/openclaw
- **Ollama Integration Docs**: https://docs.ollama.com/integrations/openclaw
- **OpenClaw + Ollama Setup Guide**: https://openclawai.net/blog/openclaw-ollama-setup
- **GitHub Discussion #2936**: https://github.com/openclaw/openclaw/discussions/2936 (Ollama usage)
- **GitHub Issue #3740**: https://github.com/openclaw/openclaw/issues/3740 (Ollama auth check)
- **GitHub Issue #1253**: https://github.com/openclaw/openclaw/issues/1253 (Auth profile format)
## Current Working Configuration
### docker-compose.yml
```yaml
services:
openclaw:
container_name: openclaw
image: ghcr.io/openclaw/openclaw:main
platform: linux/amd64
restart: unless-stopped
command: ["node", "openclaw.mjs", "gateway", "--allow-unconfigured", "--bind", "lan"]
environment:
- HOME=/home/node
- TERM=xterm-256color
- TZ=America/New_York
- PUID=1000
- PGID=1000
- OPENCLAW_GATEWAY_TOKEN=27d4e63adce6c8f7c5396e8ca3f9ec5e6ff590077247fb11da03a8684ee3c711
- OPENCLAW_GATEWAY_BIND=lan
- VIRTUAL_HOST=openclaw.kolpacksoftware.com
- VIRTUAL_PORT=18789
- LETSENCRYPT_HOST=openclaw.kolpacksoftware.com
volumes:
- /srv/openclaw/config:/home/node/.openclaw
- /srv/openclaw/workspace:/home/node/.openclaw/workspace
init: true
networks:
- npm-network
networks:
npm-network:
external: true
```
### Status
- ✅ Container runs without crashes
- ✅ Gateway accessible at https://openclaw.kolpacksoftware.com
- ✅ WebSocket connections work
- ✅ Device pairing works
- ❌ Cannot use any AI model (Ollama or Anthropic)
## Possible Causes
1. **Docker Image Issue**: The containerized version may have a bug or missing configuration
2. **Gateway Mode Limitation**: `--allow-unconfigured` flag may bypass auth-profiles.json entirely
3. **Version-Specific Bug**: This version (2026.2.6-3) may have a regression
4. **Missing Setup Step**: There may be an initialization step we're missing
5. **Auth System Design**: The auth system may be designed for host installation only
## Recommended Solutions to Try
### 1. Install on Host Instead of Docker
```bash
npm install -g openclaw@latest
openclaw onboard --install-daemon
ollama launch openclaw
```
This follows the official documentation more closely.
### 2. Try Different Docker Image Tag
```yaml
image: ghcr.io/openclaw/openclaw:2026.2.5 # or other specific version
```
### 3. Remove `--allow-unconfigured` Flag
May require running `openclaw onboard` inside container first (interactive, difficult in Docker).
### 4. File GitHub Issue
Report this configuration with:
- docker-compose.yml
- auth-profiles.json attempts
- Full error logs
- Ask maintainers for Docker-specific setup guide
### 5. Alternative AI Assistants
- **Open WebUI**: Excellent Ollama integration, easy Docker setup
- **LibreChat**: Multi-provider support including Ollama
- Both are well-documented and Docker-friendly
## Next Steps
Before revisiting:
1. Check OpenClaw GitHub for updates/fixes related to Docker deployment
2. Look for new documentation about auth-profiles.json format
3. Search for successful Docker deployment examples
4. Consider whether host installation is more appropriate for this use case
## Environment Details
- **Host OS**: Linux 6.8.0-94-generic (Ubuntu 24.04)
- **Docker Version**: 28.2.2
- **Architecture**: x86_64
- **Ollama Version**: Running in separate container on same network
- **Ollama Models**: llama3:latest, qwen3-coder, qwen3:8b, gemma3:4b, and others
- **Network**: npm-network (shared with nginx-proxy)
+24
View File
@@ -0,0 +1,24 @@
services:
openclaw:
container_name: openclaw
build: .
restart: unless-stopped
command: ["openclaw", "gateway", "--allow-unconfigured", "--bind", "lan"]
environment:
- HOME=/home/node
- TERM=xterm-256color
- TZ=America/New_York
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
# Enable Ollama provider (opt-in via env var)
- OLLAMA_API_KEY=ollama-local
- BRAVE_API_KEY=${BRAVE_API_KEY}
volumes:
- /srv/openclaw/config:/home/node/.openclaw
- /srv/openclaw/workspace:/home/node/.openclaw/workspace
init: true
networks:
- npm-network
networks:
npm-network:
external: true
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# Fix OpenClaw storage permissions
echo "Creating OpenClaw storage directories..."
sudo mkdir -p /srv/openclaw/config /srv/openclaw/workspace
sudo chown -R 1000:1000 /srv/openclaw
sudo chmod -R 755 /srv/openclaw
echo "Restarting OpenClaw container..."
docker restart openclaw
echo "Done! Check logs with: docker logs openclaw -f"
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
set -e
# Portainer Stack Setup Script for OpenClaw
# Following the git-linked migration plan
# Load credentials if available
if [ -f "../.credentials" ]; then
source ../.credentials
echo "Loaded credentials from .credentials file"
fi
# Configuration
SERVICE_NAME="openclaw"
COMPOSE_FILE="openclaw/docker-compose.yml"
REPO_URL="${GITEA_REPO_URL:-https://gitea.kolpacksoftware.com/homelab/docker-infrastructure.git}"
ENDPOINT_ID="${PORTAINER_ENDPOINT_ID:-2}"
# Check required environment variables
if [ -z "$PORTAINER_API_TOKEN" ]; then
echo "Error: PORTAINER_API_TOKEN not set"
echo "Get token from: Portainer UI → User Settings → Access Tokens"
echo "Then run: export PORTAINER_API_TOKEN='ptr_...'"
exit 1
fi
if [ -z "$GITEA_TOKEN" ]; then
echo "Error: GITEA_TOKEN not set"
echo "Get token from: Gitea → Settings → Applications → Generate New Token"
echo "Then run: export GITEA_TOKEN='...'"
exit 1
fi
if [ -z "$GITEA_USER" ]; then
echo "Error: GITEA_USER not set"
echo "Then run: export GITEA_USER='your-username'"
exit 1
fi
# Gateway token from .env file
OPENCLAW_TOKEN=$(grep OPENCLAW_GATEWAY_TOKEN .env | cut -d'=' -f2)
if [ -z "$OPENCLAW_TOKEN" ]; then
echo "Error: OPENCLAW_GATEWAY_TOKEN not found in .env file"
exit 1
fi
echo "=== Portainer Stack Setup for $SERVICE_NAME ==="
echo ""
# Step 1: Check for existing stack
echo "Step 1: Checking for existing stack..."
EXISTING_STACK=$(curl -s "https://localhost:9443/api/stacks" \
-H "X-API-Key: $PORTAINER_API_TOKEN" \
--insecure | grep -o "\"Id\":[0-9]*,\"Name\":\"$SERVICE_NAME\"" || true)
if [ -n "$EXISTING_STACK" ]; then
STACK_ID=$(echo "$EXISTING_STACK" | grep -o "[0-9]*")
echo "Found existing stack with ID: $STACK_ID"
read -p "Delete existing stack and recreate? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Deleting stack $STACK_ID..."
curl -X DELETE "https://localhost:9443/api/stacks/$STACK_ID?endpointId=$ENDPOINT_ID" \
-H "X-API-Key: $PORTAINER_API_TOKEN" \
--insecure
echo "Stack deleted"
sleep 2
else
echo "Aborted"
exit 0
fi
else
echo "No existing stack found"
fi
# Step 2: Create git-linked stack
echo ""
echo "Step 2: Creating git-linked stack..."
RESPONSE=$(curl -X POST "https://localhost:9443/api/stacks/create/standalone/repository?endpointId=$ENDPOINT_ID" \
-H "X-API-Key: $PORTAINER_API_TOKEN" \
-H "Content-Type: application/json" \
--insecure \
-d '{
"name": "'"$SERVICE_NAME"'",
"repositoryURL": "'"$REPO_URL"'",
"repositoryReferenceName": "refs/heads/main",
"composeFile": "'"$COMPOSE_FILE"'",
"repositoryAuthentication": true,
"repositoryUsername": "'"$GITEA_USER"'",
"repositoryPassword": "'"$GITEA_TOKEN"'",
"autoUpdate": {
"interval": "5m"
},
"env": [
{
"name": "OPENCLAW_GATEWAY_TOKEN",
"value": "'"$OPENCLAW_TOKEN"'"
}
]
}')
if echo "$RESPONSE" | grep -q '"Id"'; then
NEW_STACK_ID=$(echo "$RESPONSE" | grep -o '"Id":[0-9]*' | head -1 | cut -d':' -f2)
echo "✓ Stack created successfully with ID: $NEW_STACK_ID"
else
echo "✗ Failed to create stack"
echo "Response: $RESPONSE"
exit 1
fi
# Step 3: Verify
echo ""
echo "Step 3: Verifying deployment..."
sleep 5
if docker ps | grep -q "$SERVICE_NAME"; then
echo "✓ Container is running"
else
echo "⚠ Container not found yet, check Portainer UI"
fi
echo ""
echo "=== Setup Complete ==="
echo "Next steps:"
echo "1. Configure nginx-proxy-manager:"
echo " - Domain: openclaw.kolpacksoftware.com"
echo " - Forward to: openclaw:18789"
echo " - Add SSL certificate"
echo " - Set access list to private network"
echo ""
echo "2. Run onboarding wizard:"
echo " docker exec -it openclaw npx openclaw onboard"
echo ""
echo "3. Access UI at: https://openclaw.kolpacksoftware.com"
echo " Gateway token: $OPENCLAW_TOKEN"
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# One-time host setup for OpenClaw with Ollama
# Run this BEFORE deploying the container.
set -e
echo "=== OpenClaw + Ollama Setup ==="
echo ""
# Ensure config dirs exist
mkdir -p /srv/openclaw/config
mkdir -p /srv/openclaw/workspace
# Remove old misconfigured files from previous attempts
rm -f /srv/openclaw/config/config.json
rm -f /srv/openclaw/config/auth-profiles.json
rm -f /srv/openclaw/config/agents/main/agent/auth-profiles.json 2>/dev/null || true
# Write openclaw.json - the correct config file for provider setup
cat > /srv/openclaw/config/openclaw.json << 'EOF'
{
"models": {
"providers": {
"ollama": {
"baseUrl": "http://ollama:11434/v1",
"apiKey": "ollama-local",
"api": "openai-completions",
"models": [
{
"id": "qwen2.5:7b",
"name": "Qwen 2.5 7B",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 32768,
"maxTokens": 32768
},
{
"id": "qwen2.5-coder:7b",
"name": "Qwen 2.5 Coder 7B",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 32768,
"maxTokens": 32768
}
]
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "ollama/qwen2.5:7b"
}
}
}
}
EOF
# Fix ownership so the node user (uid 1000) inside the container can read/write
chown -R 1000:1000 /srv/openclaw
echo "✓ Created /srv/openclaw/config/openclaw.json"
echo "✓ Removed old conflicting config files"
echo "✓ Fixed permissions (uid 1000)"
echo ""
echo "=== Next Steps ==="
echo ""
echo "1. Build the image:"
echo " docker compose -f openclaw/docker-compose.yml build"
echo ""
echo "2. Start the container:"
echo " docker compose -f openclaw/docker-compose.yml up -d"
echo ""
echo "3. Check logs:"
echo " docker logs openclaw -f"
echo ""
echo "4. Verify Ollama connectivity:"
echo " docker exec openclaw curl -s http://ollama:11434/v1/models | head -c 200"
echo ""
echo "5. Redeploy Portainer stack after pushing to git."
-23
View File
@@ -1,23 +0,0 @@
version: "3.2"
services:
picoshare:
container_name: picoshare
image: mtlynch/picoshare
environment:
- PORT=4001
- PS_SHARED_SECRET=${SHARED_SECRET} # Change to any password
- PS_BEHIND_PROXY=true
ports:
- 4001:4001
networks:
- npm-network
command: -db /data/store.db
volumes:
- /srv/picoshare-data:/data
networks:
default:
npm-network:
external: true
+9 -3
View File
@@ -2,8 +2,6 @@ volumes:
nas_media: nas_media:
external: true external: true
name: nas_media name: nas_media
services: services:
plex: plex:
container_name: plex container_name: plex
@@ -21,10 +19,18 @@ services:
- 32414:32414/udp - 32414:32414/udp
environment: environment:
- TZ=America/New_York - TZ=America/New_York
- PLEX_CLAIM=claim-VCuJznuC2nxFyVgAcKec
- ADVERTISE_IP=http://73.108.236.191:32400/ - ADVERTISE_IP=http://73.108.236.191:32400/
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
hostname: Photon hostname: Photon
volumes: volumes:
- /srv/plex:/config - /srv/plex:/config
- /srv/plex-transcode:/transcode - /srv/plex-transcode:/transcode
- nas_media:/data - nas_media:/data
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
+14
View File
@@ -0,0 +1,14 @@
services:
pmtiles:
image: protomaps/go-pmtiles:latest
container_name: pmtiles
restart: unless-stopped
command: serve /data --port=8080 --cors=* --public-url=https://maps.kolpacksoftware.com/tiles/
volumes:
- /mnt/nas_library/pmtiles/data:/data
networks:
- npm-network
networks:
npm-network:
external: true
Executable
+309
View File
@@ -0,0 +1,309 @@
#!/usr/bin/env bash
# Portainer management script
# Usage:
# ./portainer.sh list
# ./portainer.sh redeploy <stack-name>
# ./portainer.sh deploy <stack-name> <compose-path>
# ./portainer.sh get-env <stack-name>
# ./portainer.sh set-env <stack-name> KEY=VALUE [KEY=VALUE ...]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CREDENTIALS_FILE="$SCRIPT_DIR/.credentials"
if [[ ! -f "$CREDENTIALS_FILE" ]]; then
echo "Error: credentials file not found at $CREDENTIALS_FILE" >&2
exit 1
fi
# shellcheck source=.credentials
source "$CREDENTIALS_FILE"
API="$PORTAINER_URL/api"
ENDPOINT_ID="$PORTAINER_ENDPOINT_ID"
AUTH_HEADER="X-API-Key: $PORTAINER_API_TOKEN"
# --------------------------------------------------------------------------
# Helpers
# --------------------------------------------------------------------------
api_get() {
curl -sk -H "$AUTH_HEADER" "$API/$1"
}
api_put() {
curl -sk -X PUT -H "$AUTH_HEADER" -H "Content-Type: application/json" \
-d "$2" "$API/$1"
}
api_post() {
curl -sk -X POST -H "$AUTH_HEADER" -H "Content-Type: application/json" \
-d "$2" "$API/$1"
}
get_stack_by_name() {
local name="$1"
api_get "stacks?filters=%7B%22EndpointID%22%3A${ENDPOINT_ID}%7D" \
| python3 -c "
import json, sys
stacks = json.load(sys.stdin)
matches = [s for s in stacks if s['Name'] == '$name']
if not matches:
sys.exit(1)
s = matches[0]
print(s['Id'])
"
}
get_stack_json_by_name() {
local name="$1"
api_get "stacks?filters=%7B%22EndpointID%22%3A${ENDPOINT_ID}%7D" \
| python3 -c "
import json, sys
stacks = json.load(sys.stdin)
matches = [s for s in stacks if s['Name'] == '$name']
if not matches:
sys.exit(1)
print(json.dumps(matches[0]))
"
}
# --------------------------------------------------------------------------
# Commands
# --------------------------------------------------------------------------
cmd_list() {
echo "Fetching stacks..."
api_get "stacks?filters=%7B%22EndpointID%22%3A${ENDPOINT_ID}%7D" \
| python3 -c "
import json, sys
stacks = json.load(sys.stdin)
stacks.sort(key=lambda s: s['Id'])
status_map = {1: 'active', 2: 'inactive'}
print(f'{'ID':<6} {'STATUS':<10} NAME')
print('-' * 40)
for s in stacks:
status = status_map.get(s['Status'], str(s['Status']))
print(f\"{s['Id']:<6} {status:<10} {s['Name']}\")
"
}
cmd_redeploy() {
local name="${1:-}"
if [[ -z "$name" ]]; then
echo "Usage: $0 redeploy <stack-name>" >&2
exit 1
fi
echo "Looking up stack '$name'..."
local stack_json
if ! stack_json=$(get_stack_json_by_name "$name"); then
echo "Error: stack '$name' not found" >&2
exit 1
fi
local stack_id
stack_id=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['Id'])" "$stack_json")
echo "Found stack ID: $stack_id"
# Preserve existing env vars — git/redeploy clears them if env is omitted
local payload
payload=$(python3 -c "
import json, sys
stack = json.loads(sys.argv[1])
env = stack.get('Env') or []
print(json.dumps({'pullImage': True, 'prune': False, 'env': env}))
" "$stack_json")
echo "Redeploying..."
local response
response=$(api_put "stacks/${stack_id}/git/redeploy?endpointId=${ENDPOINT_ID}" "$payload")
python3 -c "
import json, sys
d = json.loads(sys.argv[1])
if 'message' in d and 'Id' not in d:
print('Error:', d['message'])
sys.exit(1)
hash = d.get('GitConfig', {}).get('ConfigHash', 'unknown')[:10]
print(f'Done. ConfigHash: {hash}')
" "$response"
}
cmd_deploy() {
local name="${1:-}"
local compose_path="${2:-}"
if [[ -z "$name" || -z "$compose_path" ]]; then
echo "Usage: $0 deploy <stack-name> <compose-path> [KEY=VALUE ...]" >&2
echo " Example: $0 deploy myapp myapp/docker-compose.yml FOO=bar BAZ=qux" >&2
exit 1
fi
shift 2
local kvs=("$@")
echo "Creating stack '$name' from $compose_path..."
local payload
payload=$(python3 -c "
import json, sys
kvs = sys.argv[1:]
env_list = []
for kv in kvs:
eq = kv.index('=')
env_list.append({'name': kv[:eq], 'value': kv[eq+1:]})
print(json.dumps({
'name': '$name',
'repositoryURL': '$GITEA_REPO_URL',
'repositoryReferenceName': 'refs/heads/main',
'composeFile': '$compose_path',
'repositoryAuthentication': True,
'repositoryUsername': '$GITEA_USER',
'repositoryPassword': '$GITEA_TOKEN',
'env': env_list,
}))
" "${kvs[@]}")
local response
response=$(api_post "stacks/create/standalone/repository?endpointId=${ENDPOINT_ID}" "$payload")
python3 -c "
import json, sys
d = json.load(sys.stdin)
if 'message' in d and 'Id' not in d:
print('Error:', d['message'])
sys.exit(1)
print(f\"Done. Stack ID: {d['Id']}, Name: {d['Name']}\")
" <<< "$response"
}
cmd_get_env() {
local name="${1:-}"
if [[ -z "$name" ]]; then
echo "Usage: $0 get-env <stack-name>" >&2
exit 1
fi
echo "Looking up stack '$name'..."
local stack_json
if ! stack_json=$(get_stack_json_by_name "$name"); then
echo "Error: stack '$name' not found" >&2
exit 1
fi
python3 -c "
import json, sys
s = json.loads(sys.argv[1])
env = s.get('Env') or []
if not env:
print('(no env vars set)')
else:
for e in sorted(env, key=lambda x: x['name']):
print(f\"{e['name']}={e['value']}\")
" "$stack_json"
}
cmd_set_env() {
local name="${1:-}"
shift || true
if [[ -z "$name" || $# -eq 0 ]]; then
echo "Usage: $0 set-env <stack-name> KEY=VALUE [KEY=VALUE ...]" >&2
echo " Example: $0 set-env authelia SECRET_KEY=abc123 OTHER_KEY=xyz" >&2
exit 1
fi
echo "Looking up stack '$name'..."
local stack_json
if ! stack_json=$(get_stack_json_by_name "$name"); then
echo "Error: stack '$name' not found" >&2
exit 1
fi
local stack_id
stack_id=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['Id'])" "$stack_json")
echo "Found stack ID: $stack_id"
local kvs=("$@")
local payload
payload=$(python3 -c "
import json, sys
stack = json.loads(sys.argv[1])
new_kvs = sys.argv[2:]
# Merge env vars: preserve existing, override/add new
env_dict = {e['name']: e['value'] for e in (stack.get('Env') or [])}
for kv in new_kvs:
eq = kv.index('=')
env_dict[kv[:eq]] = kv[eq+1:]
env_list = [{'name': k, 'value': v} for k, v in env_dict.items()]
git = stack.get('GitConfig') or {}
if git:
# Use git/redeploy endpoint (pullImage:false = redeploy with existing images only)
p = {'pullImage': False, 'prune': False, 'env': env_list}
else:
print('Error: string-based stacks are not supported; update env vars via Portainer UI.', file=sys.stderr)
sys.exit(2)
print(json.dumps(p))
" "$stack_json" "${kvs[@]}")
echo "Updating env vars (redeploys with existing images)..."
local endpoint
local stack_json_check
stack_json_check=$(python3 -c "
import json, sys
stack = json.loads(sys.argv[1])
git = stack.get('GitConfig') or {}
print('git' if git else 'string')
" "$stack_json")
local response
if [[ "$stack_json_check" == "git" ]]; then
response=$(api_put "stacks/${stack_id}/git/redeploy?endpointId=${ENDPOINT_ID}" "$payload")
else
echo "Error: string-based stacks are not supported; update env vars via Portainer UI." >&2
exit 2
fi
python3 -c "
import json, sys
try:
d = json.loads(sys.argv[1])
except Exception:
print('Failed to parse response:', sys.argv[1][:300])
sys.exit(1)
if 'message' in d and 'Id' not in d:
print('Error:', d['message'])
if 'details' in d:
print('Details:', d['details'])
sys.exit(1)
updated = len(d.get('Env') or [])
print(f'Done. Stack has {updated} env var(s) set.')
" "$response"
}
# --------------------------------------------------------------------------
# Dispatch
# --------------------------------------------------------------------------
command="${1:-}"
case "$command" in
list) cmd_list ;;
redeploy) cmd_redeploy "${2:-}" ;;
deploy) cmd_deploy "${@:2}" ;;
get-env) cmd_get_env "${2:-}" ;;
set-env) cmd_set_env "${2:-}" "${@:3}" ;;
*)
echo "Usage: $0 <command> [args]"
echo ""
echo "Commands:"
echo " list List all stacks"
echo " redeploy <stack-name> Pull latest git commit and redeploy"
echo " deploy <stack-name> <path> [K=V ...] Create new git-linked stack with optional env vars"
echo " get-env <stack-name> Show env vars for a stack"
echo " set-env <stack-name> KEY=VAL [...] Set env vars (redeploys without new image pull)"
exit 1
;;
esac
+2
View File
@@ -4,6 +4,8 @@ services:
container_name: portainer container_name: portainer
restart: always restart: always
privileged: true privileged: true
labels:
- com.centurylinklabs.watchtower.monitor-only=true
volumes: volumes:
- /mnt/containers/portainer/container-data/data:/data:Z - /mnt/containers/portainer/container-data/data:/data:Z
- /var/run/docker.sock:/var/run/docker.sock:Z - /var/run/docker.sock:/var/run/docker.sock:Z
-11
View File
@@ -1,11 +0,0 @@
version: "3.7"
services:
app:
image: nymanjens/quizmaster:latest
stdin_open: true
tty: true
ports: ["9001:9000"]
volumes:
- /srv/quizmaster/quiz:/app/conf/quiz
- /srv/quizmaster/application.conf:/app/conf/application.conf
command: "bin/server"
-11
View File
@@ -1,11 +0,0 @@
version: "3.7"
services:
app:
image: nymanjens/quizmaster:latest
stdin_open: true
tty: true
ports: ["9001:9000"]
volumes:
- /srv/quizmaster/quiz:/app/conf/quiz
- /srv/quizmaster/application.conf:/app/conf/application.conf
command: "bin/server"
+22
View File
@@ -0,0 +1,22 @@
services:
rackpeek:
container_name: rackpeek
image: aptacode/rackpeek:latest
ports:
- "8081:8080"
volumes:
- /srv/rackpeek-config:/app/config
environment:
- VIRTUAL_HOST=rackpeek.kolpacksoftware.com
- VIRTUAL_PORT=8081
- LETSENCRYPT_HOST=rackpeek.kolpacksoftware.com
- PUID=1000
- PGID=1000
- TZ=America/New_York
user: "1000:1000"
restart: unless-stopped
networks:
default:
external:
name: npm-network
+59
View File
@@ -0,0 +1,59 @@
networks:
npm-network:
external: true
romm-internal:
services:
romm:
image: rommapp/romm:latest
container_name: romm
restart: unless-stopped
environment:
- TZ=America/New_York
- DB_HOST=romm-db
- DB_NAME=romm
- DB_USER=romm-user
- DB_PASSWD=${DB_PASSWD}
- ROMM_AUTH_SECRET_KEY=${ROMM_AUTH_SECRET_KEY}
# Metadata providers (all optional but recommended)
- SCREENSCRAPER_USER=${SCREENSCRAPER_USER}
- SCREENSCRAPER_PASSWORD=${SCREENSCRAPER_PASSWORD}
- RETROACHIEVEMENTS_API_KEY=
- STEAMGRIDDB_API_KEY=
- HASHEOUS_API_ENABLED=true
- IGDB_CLIENT_ID=${IGDB_CLIENT_ID}
- IGDB_CLIENT_SECRET=${IGDB_CLIENT_SECRET}
volumes:
- /mnt/nas_games:/romm/library # NAS: media/games → library root; roms/ subdir inside
- /srv/romm/assets:/romm/assets # Saves, states, screenshots
- /srv/romm/config:/romm/config # config.yml (optional)
- /srv/romm/resources:/romm/resources
- /srv/romm/redis:/redis-data
networks:
- npm-network
- romm-internal
depends_on:
romm-db:
condition: service_healthy
restart: true
romm-db:
image: mariadb:latest
container_name: romm-db
restart: unless-stopped
environment:
- MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
- MARIADB_DATABASE=romm
- MARIADB_USER=romm-user
- MARIADB_PASSWORD=${MARIADB_PASSWORD}
volumes:
- /srv/romm/db:/var/lib/mysql
networks:
- romm-internal
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 30s
start_interval: 10s
interval: 10s
timeout: 5s
retries: 5

Some files were not shown because too many files have changed in this diff Show More