Compare commits
164
Commits
f46d493ca2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15640da229 | ||
|
|
7468df2b8d | ||
|
|
f665865762 | ||
|
|
2f1a988ac3 | ||
|
|
362643a064 | ||
|
|
eac385cc70 | ||
|
|
586b48457b | ||
|
|
4febd0c303 | ||
|
|
ac6946ccb5 | ||
|
|
6a2d24b6ea | ||
|
|
69d6640d54 | ||
|
|
089f7eafa9 | ||
|
|
b9a050f8ab | ||
|
|
cbbcb76fcf | ||
|
|
cfefcbfe1b | ||
|
|
8f3e7614fa | ||
|
|
82cc6117fb | ||
|
|
8087d19d7f | ||
|
|
8871c8354b | ||
|
|
1ac9f06e29 | ||
|
|
cb17443ab6 | ||
|
|
98d64139fe | ||
|
|
ca170af736 | ||
|
|
a925d61e83 | ||
|
|
2c90e4218b | ||
|
|
ba8f1f7668 | ||
|
|
c1c86e3ab2 | ||
|
|
26c89ebb39 | ||
|
|
d4a23d0d6f | ||
|
|
5353df05fb | ||
|
|
76e02bf26d | ||
|
|
c58ffb1b68 | ||
|
|
03d4dae8b5 | ||
|
|
9dee75e2a0 | ||
|
|
b2d7f82c53 | ||
|
|
f1a9346861 | ||
|
|
a483c2e7fc | ||
|
|
cd232d93ac | ||
|
|
ef1d136a3e | ||
|
|
662b558d21 | ||
|
|
d7d20cd080 | ||
|
|
0b4571205a | ||
|
|
97ff83a246 | ||
|
|
0fe632e1b5 | ||
|
|
aa686d0827 | ||
|
|
597ed26f89 | ||
|
|
9d084f3f19 | ||
|
|
adcaa39e41 | ||
|
|
964c8bedf3 | ||
|
|
35a6715f01 | ||
|
|
2925305ded | ||
|
|
a92d4d21d5 | ||
|
|
06646b5dee | ||
|
|
1bb41f5cc9 | ||
|
|
3c6e1588e7 | ||
|
|
d50e696162 | ||
|
|
9bd4b70960 | ||
|
|
1e13be7739 | ||
|
|
b7b9dc91b4 | ||
|
|
a08c7ffbcb | ||
|
|
b615c503cd | ||
|
|
460afd7221 | ||
|
|
9a9d0562ae | ||
|
|
aee517463d | ||
|
|
eb5f9117c8 | ||
|
|
ebd813afcc | ||
|
|
d186aec6e5 | ||
|
|
e2050fee96 | ||
|
|
860b7772cc | ||
|
|
0cd293e763 | ||
|
|
ec56e1f68e | ||
|
|
9d5f5cec43 | ||
|
|
650be93503 | ||
|
|
6ad7e74c39 | ||
|
|
4f1c422d4d | ||
|
|
f03033b176 | ||
|
|
294bbde259 | ||
|
|
dab1267e90 | ||
|
|
fc886e89e0 | ||
|
|
827675d847 | ||
|
|
1c8547d5da | ||
|
|
44c773c943 | ||
|
|
3417fdcfaa | ||
|
|
dce25c6ed4 | ||
|
|
acfde6f040 | ||
|
|
10d008636c | ||
|
|
66664a5925 | ||
|
|
bfb71d67d2 | ||
|
|
696398a863 | ||
|
|
e6d4b0a349 | ||
|
|
39f6ca8530 | ||
|
|
907d214b5c | ||
|
|
6e62d9ba2f | ||
|
|
a6f2d4c4b2 | ||
|
|
196613f684 | ||
|
|
5e91f0c68b | ||
|
|
0ce030275b | ||
|
|
645d908ca5 | ||
|
|
94819639dd | ||
|
|
e923fe0655 | ||
|
|
fecb4831ad | ||
|
|
c4157023f6 | ||
|
|
38d5edc37f | ||
|
|
d0037cf4cd | ||
|
|
4c838e1989 | ||
|
|
3ec74c1f69 | ||
|
|
4ec70062ce | ||
|
|
877d916c15 | ||
|
|
4293022561 | ||
|
|
9f98a4081e | ||
|
|
717ba151e5 | ||
|
|
7ef6d248da | ||
|
|
aa9560569f | ||
|
|
697818845d | ||
|
|
eebb5d11d0 | ||
|
|
3eeaa8ff6d | ||
|
|
0aae4324a8 | ||
|
|
1944589989 | ||
|
|
03bf4bc05c | ||
|
|
2ab1cccdc8 | ||
|
|
0b38b19d20 | ||
|
|
18b202909c | ||
|
|
e9bf43469c | ||
|
|
bb41473360 | ||
|
|
03a2ce647b | ||
|
|
c84175640f | ||
|
|
8523f597da | ||
|
|
2544694586 | ||
|
|
5f704441fb | ||
|
|
3474043bb9 | ||
|
|
9a7b00780c | ||
|
|
ab465c0234 | ||
|
|
54f3c37e7c | ||
|
|
9fbd6f6900 | ||
|
|
d7f7f19fbd | ||
|
|
eaaf15339d | ||
|
|
cba9531a71 | ||
|
|
9f0d0249cd | ||
|
|
57150c9e5c | ||
|
|
058247465a | ||
|
|
e846a9c1cb | ||
|
|
23498377fc | ||
|
|
752de89955 | ||
|
|
eb56dd2513 | ||
|
|
15901a6ff7 | ||
|
|
0b34690bc5 | ||
|
|
a712132eef | ||
|
|
68f97de1ea | ||
|
|
7ab8533910 | ||
|
|
2bf8895740 | ||
|
|
24b6e247a6 | ||
|
|
2d91aab062 | ||
|
|
f0902944e0 | ||
|
|
c7733147b3 | ||
|
|
716718504d | ||
|
|
8209caecff | ||
|
|
68e3f673e8 | ||
|
|
d66fc86287 | ||
|
|
c2a25c2185 | ||
|
|
7c4393828d | ||
|
|
a6c4302c00 | ||
|
|
f926ff733c | ||
|
|
aefe76478a | ||
|
|
3a990fc686 |
@@ -0,0 +1,3 @@
|
||||
.credentials
|
||||
**/.env
|
||||
**/users_database.yaml
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -0,0 +1,2 @@
|
||||
SMB_USERNAME=your_smb_username
|
||||
SMB_PASSWORD=your_smb_password
|
||||
@@ -1,13 +1,5 @@
|
||||
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:
|
||||
backrest:
|
||||
image: garethgeorge/backrest:latest
|
||||
@@ -20,8 +12,7 @@ services:
|
||||
- /srv/backrest/tmp:/tmp
|
||||
- /srv/backrest/rclone:/root/.config/rclone # Mount for rclone config (needed when using rclone remotes)
|
||||
- /srv:/srv-data # Mount local paths to backup
|
||||
#- /mnt/backup-export:/backup-export # NFS share backup destination
|
||||
- backup-mnt:/backup-export # NFS share backup destination
|
||||
- /mnt/nas_backup/nas-docker-data:/backup-export
|
||||
environment:
|
||||
- BACKREST_DATA=/data
|
||||
- BACKREST_CONFIG=/config/config.json
|
||||
@@ -30,4 +21,4 @@ services:
|
||||
- TZ=America/New_York
|
||||
ports:
|
||||
- "9898:9898"
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
# 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:
|
||||
default:
|
||||
external:
|
||||
name: npm-network
|
||||
|
||||
volumes:
|
||||
config:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: "addr=192.168.1.192,rw,nolock,soft"
|
||||
device: ":/mnt/user/media/books/calibre"
|
||||
import:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: "addr=192.168.1.192,rw,nolock,soft"
|
||||
device: ":/mnt/user/media/books/calibre-import"
|
||||
|
||||
services:
|
||||
calibre:
|
||||
image: lscr.io/linuxserver/calibre:latest
|
||||
@@ -33,8 +23,9 @@ services:
|
||||
- VIRTUAL_PORT=8080
|
||||
- LETSENCRYPT_HOST=calibre.kolpacksoftware.com
|
||||
volumes:
|
||||
- config:/config
|
||||
- import:/calibre-import
|
||||
- /srv/calibre/config:/config
|
||||
- /srv/calibre/library:/config/Calibre Library
|
||||
- /mnt/nas_books/calibre-import:/calibre-import
|
||||
ports:
|
||||
- 8090:8080
|
||||
- 8091:8081
|
||||
|
||||
@@ -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
|
||||
@@ -27,6 +27,7 @@ services:
|
||||
- /srv/nginx-proxy/data:/mnt/nginx-proxy
|
||||
- /srv/tsa-chapter-organizer-rms:/mnt/tsa-chapter-organizer-rms
|
||||
- /srv/glances:/mnt/glances
|
||||
- /srv/authelia/config:/mnt/authelia
|
||||
ports:
|
||||
- 8443:8443
|
||||
restart: unless-stopped
|
||||
@@ -3,13 +3,16 @@ services:
|
||||
image: couchdb
|
||||
container_name: couchdb
|
||||
restart: always
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
ports:
|
||||
- "5984:5984"
|
||||
- "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
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: npm-network
|
||||
|
||||
services:
|
||||
dashy:
|
||||
image: lissy93/dashy
|
||||
container_name: dashy
|
||||
volumes:
|
||||
- /srv/dashy:/app/user-data
|
||||
ports:
|
||||
- 9000:8080
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- VIRTUAL_HOST=dashy.kolpacksoftware.com
|
||||
- VIRTUAL_PORT=8080
|
||||
- LETSENCRYPT_HOST=dashy.kolpacksoftware.com
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ['CMD', 'node', '/app/services/healthcheck']
|
||||
interval: 1m30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
@@ -0,0 +1 @@
|
||||
APIKEY=your_dnsexit_api_key
|
||||
@@ -8,7 +8,9 @@ services:
|
||||
stdin_open: true
|
||||
tty: true
|
||||
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=false
|
||||
environment:
|
||||
- APIKEY=s5P998CqUu5bqAukdS1dW57EfjgFlX
|
||||
- APIKEY=${APIKEY}
|
||||
- HOST=kolpacksoftware.com,rmstsa.org,popcyclical.com
|
||||
- INTERVAL=1h
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
SECRET_KEY_BASE=generate_with_openssl_rand_-hex_64
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
environment:
|
||||
- DOCKER_REGISTRY_URL=https://docker-registry.kolpacksoftware.com/
|
||||
- PUBLIC_REGISTRY_URL=https://docker-registry.kolpacksoftware.com/
|
||||
- SECRET_KEY_BASE=0c11bc7a755901fcbb5ba0ef5e6ede0911452e9c944d4ab0d8eb3ee1cf8ff7dd4f8fee82615415f5dc665763c6b18b3b8aee6655f44388bc27b27624f218bf86
|
||||
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
|
||||
- ENABLE_DELETE_IMAGES=true
|
||||
- VIRTUAL_HOST=docker-registry-ui.kolpacksoftware.com
|
||||
- VIRTUAL_PORT=8080
|
||||
@@ -40,4 +40,4 @@ services:
|
||||
- /srv/registry/data:/data
|
||||
- /srv/registry/registry.password:/auth/registry.password
|
||||
ports:
|
||||
- 5000
|
||||
- 127.0.0.1:5000:5000
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
SMB_USERNAME=your_smb_username
|
||||
SMB_PASSWORD=your_smb_password
|
||||
@@ -6,7 +6,7 @@ services:
|
||||
FILEBROWSER_CONFIG: "data/config.yaml"
|
||||
TZ: "America/New_York"
|
||||
volumes:
|
||||
- backup-mnt:/folder
|
||||
- /mnt/nas_backup:/folder
|
||||
- /srv/filebrowser-colleen-hd:/home/filebrowser/data
|
||||
ports:
|
||||
- 3427:80
|
||||
@@ -14,15 +14,7 @@ services:
|
||||
- npm-network
|
||||
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:
|
||||
default:
|
||||
npm-network:
|
||||
external: true
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Generate with: openssl rand -hex 16 | head -c 32
|
||||
APP_KEY=
|
||||
|
||||
APP_URL=https://firefly.kolpacksoftware.com
|
||||
|
||||
DB_PASSWORD=
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -7,6 +7,8 @@ services:
|
||||
server:
|
||||
image: docker.gitea.com/gitea:nightly
|
||||
container_name: gitea
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
|
||||
@@ -3,9 +3,8 @@ services:
|
||||
container_name: glances
|
||||
image: 'nicolargo/glances:ubuntu-latest-full'
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
ports:
|
||||
- 61208-61209:61208-61209
|
||||
- 127.0.0.1:61208-61209:61208-61209
|
||||
environment:
|
||||
- TZ=${TZ}
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
|
||||
@@ -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/)
|
||||
@@ -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
|
||||
@@ -8,6 +8,7 @@ services:
|
||||
- HBOX_LOG_FORMAT=text
|
||||
- HBOX_WEB_MAX_FILE_UPLOAD=10
|
||||
- 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
|
||||
|
||||
+11
-11
@@ -16,9 +16,10 @@ services:
|
||||
# extends:
|
||||
# file: hwaccel.transcoding.yml
|
||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
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
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /mnt/nas_family/immich-library:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- stack.env
|
||||
@@ -42,6 +43,8 @@ services:
|
||||
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
|
||||
# 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
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
volumes:
|
||||
- model-cache:/cache
|
||||
env_file:
|
||||
@@ -53,13 +56,17 @@ services:
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=false
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
database:
|
||||
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:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
@@ -68,24 +75,17 @@ services:
|
||||
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
|
||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||
shm_size: 128mb
|
||||
healthcheck:
|
||||
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
|
||||
interval: 5m
|
||||
#start_interval: 30s
|
||||
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
|
||||
|
||||
volumes:
|
||||
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:
|
||||
default:
|
||||
|
||||
@@ -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
|
||||
@@ -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.');
|
||||
}
|
||||
@@ -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! ===');
|
||||
}
|
||||
@@ -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
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
image: ghcr.io/everywall/ladder:latest
|
||||
container_name: ladder
|
||||
#build: .
|
||||
#restart: always
|
||||
restart: unless-stopped
|
||||
#command: sh -c ./ladder
|
||||
environment:
|
||||
- PORT=8080
|
||||
|
||||
@@ -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"
|
||||
@@ -10,6 +10,15 @@ services:
|
||||
- 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
|
||||
|
||||
networks:
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
@@ -20,30 +20,99 @@ services:
|
||||
environment:
|
||||
EULA: "true"
|
||||
SEED: 46942827301
|
||||
SERVER_NAME: "Ourcraft"
|
||||
SERVER_NAME: "Terraforma SMP"
|
||||
MEMORY: "4G"
|
||||
OPS: "Jimcognito"
|
||||
#TYPE: "FABRIC"
|
||||
|
||||
volumes:
|
||||
- /srv/minecraft-data:/data
|
||||
stdin_open: true
|
||||
tty: true
|
||||
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
|
||||
container_name: minecraft-creative
|
||||
container_name: minecraft-empire-smp
|
||||
ports:
|
||||
- "25566:25565"
|
||||
- "31415:25565"
|
||||
environment:
|
||||
EULA: "true"
|
||||
SEED: 8486672581758651406
|
||||
SERVER_NAME: "Creative Kolpacks"
|
||||
SEED: "-4690695632699825725"
|
||||
SERVER_NAME: "Empire SMP"
|
||||
MODE: "survival"
|
||||
PVP: "false"
|
||||
OPS: "Jimcognito"
|
||||
volumes:
|
||||
- /srv/minecraft-data-creative:/data
|
||||
- /srv/minecraft-empire-smp:/data
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
mem_limit: 2g
|
||||
|
||||
epic-bases:
|
||||
image: itzg/minecraft-server:latest
|
||||
container_name: minecraft-pvp0
|
||||
@@ -60,39 +129,42 @@ services:
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
hardcore:
|
||||
image: itzg/minecraft-server:latest
|
||||
container_name: minecraft-hardcore
|
||||
ports:
|
||||
- "25568:25565"
|
||||
environment:
|
||||
EULA: "true"
|
||||
SEED: "-3149142039438819319"
|
||||
SERVER_NAME: "Hardcore"
|
||||
OPS: "Jimcognito,MagicSpaceCat"
|
||||
TYPE: "FABRIC"
|
||||
MAX_WORLD_SIZE: 415
|
||||
HARDCORE: true
|
||||
PVP: false
|
||||
volumes:
|
||||
- /srv/minecraft-data-hardcore:/data
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
survival:
|
||||
image: itzg/minecraft-server:latest
|
||||
container_name: minecraft-survival-smp
|
||||
ports:
|
||||
- "25571:25565"
|
||||
environment:
|
||||
EULA: "true"
|
||||
SERVER_NAME: "Survival SMP"
|
||||
OPS: "Jimcognito,OlympicJumperL"
|
||||
volumes:
|
||||
- /srv/minecraft-data-survival-smp:/data
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
mem_limit: 2g
|
||||
# hardcore temporarily disabled to reduce swap pressure
|
||||
# hardcore:
|
||||
# image: itzg/minecraft-server:latest
|
||||
# container_name: minecraft-hardcore
|
||||
# ports:
|
||||
# - "25568:25565"
|
||||
# environment:
|
||||
# EULA: "true"
|
||||
# SEED: "-3149142039438819319"
|
||||
# SERVER_NAME: "Hardcore"
|
||||
# OPS: "Jimcognito,MagicSpaceCat"
|
||||
# TYPE: "FABRIC"
|
||||
# MAX_WORLD_SIZE: 415
|
||||
# HARDCORE: true
|
||||
# PVP: false
|
||||
# volumes:
|
||||
# - /srv/minecraft-data-hardcore:/data
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
# restart: unless-stopped
|
||||
# survival temporarily disabled to reduce swap pressure
|
||||
# survival:
|
||||
# image: itzg/minecraft-server:latest
|
||||
# container_name: minecraft-survival-smp
|
||||
# ports:
|
||||
# - "25571:25565"
|
||||
# environment:
|
||||
# EULA: "true"
|
||||
# SERVER_NAME: "Survival SMP"
|
||||
# OPS: "Jimcognito,OlympicJumperL"
|
||||
# volumes:
|
||||
# - /srv/minecraft-data-survival-smp:/data
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
# restart: unless-stopped
|
||||
# spells:
|
||||
# image: itzg/minecraft-server
|
||||
# container_name: minecraft-spells
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -1 +0,0 @@
|
||||
public
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
# Open WebUI OIDC — must match AUTHELIA_OIDC_CLIENT_SECRET_OPEN_WEBUI in Authelia stack
|
||||
AUTHELIA_OIDC_CLIENT_SECRET_OPEN_WEBUI=
|
||||
@@ -4,11 +4,12 @@ services:
|
||||
image: ollama/ollama:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 11434:11434
|
||||
- 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
|
||||
@@ -25,6 +26,14 @@ services:
|
||||
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:
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
# Generate a random token for gateway authentication
|
||||
# You can use: openssl rand -hex 32
|
||||
OPENCLAW_GATEWAY_TOKEN=your-gateway-token-here
|
||||
|
||||
# Your Anthropic API key (get from console.anthropic.com)
|
||||
ANTHROPIC_API_KEY=sk-ant-xxxxx
|
||||
# OpenClaw Gateway Token (auto-generated or set manually)
|
||||
OPENCLAW_GATEWAY_TOKEN=your_gateway_token
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -1,17 +1,17 @@
|
||||
services:
|
||||
openclaw:
|
||||
container_name: openclaw
|
||||
image: fourplayers/openclaw:latest
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 18789:18789
|
||||
- 18790:18790
|
||||
command: ["openclaw", "gateway", "--allow-unconfigured", "--bind", "lan"]
|
||||
environment:
|
||||
- HOME=/home/node
|
||||
- TERM=xterm-256color
|
||||
- TZ=America/New_York
|
||||
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
# 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
|
||||
|
||||
Executable
+12
@@ -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"
|
||||
Executable
+136
@@ -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"
|
||||
Executable
+81
@@ -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."
|
||||
@@ -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
|
||||
@@ -19,7 +19,6 @@ services:
|
||||
- 32414:32414/udp
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PLEX_CLAIM=claim-VCuJznuC2nxFyVgAcKec
|
||||
- ADVERTISE_IP=http://73.108.236.191:32400/
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -4,6 +4,8 @@ services:
|
||||
container_name: portainer
|
||||
restart: always
|
||||
privileged: true
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
volumes:
|
||||
- /mnt/containers/portainer/container-data/data:/data:Z
|
||||
- /var/run/docker.sock:/var/run/docker.sock:Z
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+576
@@ -0,0 +1,576 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
NO_INTRO_BASE="/mnt/nas_games/[No-Intro] PropeR 1G1R Collection (2024)/ROMs"
|
||||
FBNEO_ARCADE="/mnt/nas_games/FBNeo 1.0.0.2 ROMs (split)/arcade"
|
||||
ROMS_DEST="/mnt/nas_games/roms"
|
||||
|
||||
extracted=0
|
||||
skipped=0
|
||||
not_found=0
|
||||
|
||||
copy_arcade() {
|
||||
local src_dir="$1"
|
||||
local dest_dir="$2"
|
||||
local rom="$3"
|
||||
|
||||
if [[ -f "$dest_dir/$rom" ]]; then
|
||||
echo " [SKIP] $rom"
|
||||
(( skipped++ )) || true; return 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$src_dir/$rom" ]]; then
|
||||
echo " [MISSING] $rom"
|
||||
(( not_found++ )) || true; return 0
|
||||
fi
|
||||
|
||||
echo " [COPY] $rom"
|
||||
cp "$src_dir/$rom" "$dest_dir/$rom"
|
||||
(( extracted++ )) || true
|
||||
}
|
||||
|
||||
extract_game() {
|
||||
local platform_zip="$1"
|
||||
local inner_prefix="$2"
|
||||
local dest_dir="$3"
|
||||
local game_name="$4"
|
||||
|
||||
if [[ -f "$dest_dir/$game_name" ]]; then
|
||||
echo " [SKIP] $game_name"
|
||||
(( skipped++ )) || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! unzip -Z1 "$platform_zip" "${inner_prefix}${game_name}" &>/dev/null; then
|
||||
echo " [MISSING] $game_name"
|
||||
(( not_found++ )) || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " [EXTRACT] $game_name"
|
||||
unzip -j -n "$platform_zip" "${inner_prefix}${game_name}" -d "$dest_dir"
|
||||
(( extracted++ )) || true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Atari 2600
|
||||
# ---------------------------------------------------------------------------
|
||||
ATARI_ZIP="$NO_INTRO_BASE/Atari - 2600.zip"
|
||||
ATARI_PREFIX="Atari - 2600/"
|
||||
ATARI_DEST="$ROMS_DEST/atari2600"
|
||||
|
||||
if [[ ! -f "$ATARI_ZIP" ]]; then
|
||||
echo "[WARN] Atari 2600 zip not found: $ATARI_ZIP"
|
||||
else
|
||||
echo "==> Atari 2600 -> $ATARI_DEST"
|
||||
mkdir -p "$ATARI_DEST"
|
||||
|
||||
ATARI_GAMES=(
|
||||
"Adventure (USA).zip"
|
||||
"Asteroids (Japan, USA) (En).zip"
|
||||
"Atlantis (USA).zip"
|
||||
"Berzerk (Japan, USA) (En).zip"
|
||||
"Boxing (USA).zip"
|
||||
"Breakout ~ Breakaway IV (Japan, USA) (En).zip"
|
||||
"Centipede (Japan, USA) (En).zip"
|
||||
"Chopper Command (USA).zip"
|
||||
"Combat ~ Tank-Plus (USA).zip"
|
||||
"Demon Attack (USA) (Rev 1).zip"
|
||||
"Donkey Kong (USA).zip"
|
||||
"Enduro (USA).zip"
|
||||
"Frogger (USA).zip"
|
||||
"H.E.R.O. (USA).zip"
|
||||
"Haunted House (USA).zip"
|
||||
"Kaboom! (USA).zip"
|
||||
"Missile Command (USA).zip"
|
||||
"Moon Patrol (USA).zip"
|
||||
"Pac-Man (USA).zip"
|
||||
"Phoenix (USA).zip"
|
||||
"Pitfall! - Pitfall Harry's Jungle Adventure (USA).zip"
|
||||
"Pitfall II - Lost Caverns (USA).zip"
|
||||
"River Raid (USA).zip"
|
||||
"Space Invaders (USA).zip"
|
||||
"Yars' Revenge (USA).zip"
|
||||
)
|
||||
|
||||
for game in "${ATARI_GAMES[@]}"; do
|
||||
extract_game "$ATARI_ZIP" "$ATARI_PREFIX" "$ATARI_DEST" "$game"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NES
|
||||
# ---------------------------------------------------------------------------
|
||||
NES_ZIP="$NO_INTRO_BASE/Nintendo - Nintendo Entertainment System (Headerless).zip"
|
||||
NES_PREFIX="Nintendo - Nintendo Entertainment System (Headerless)/"
|
||||
NES_DEST="$ROMS_DEST/nes"
|
||||
|
||||
if [[ ! -f "$NES_ZIP" ]]; then
|
||||
echo "[WARN] NES zip not found: $NES_ZIP"
|
||||
else
|
||||
echo "==> NES -> $NES_DEST"
|
||||
mkdir -p "$NES_DEST"
|
||||
|
||||
NES_GAMES=(
|
||||
"Balloon Fight (USA).zip"
|
||||
"Battletoads (USA).zip"
|
||||
"Bionic Commando (USA).zip"
|
||||
"Blaster Master (USA).zip"
|
||||
"Castlevania (USA) (Rev 1).zip"
|
||||
"Chip 'n Dale - Rescue Rangers (USA).zip"
|
||||
"Contra (USA).zip"
|
||||
"Double Dragon (USA).zip"
|
||||
"Double Dragon II - The Revenge (USA) (Rev 1).zip"
|
||||
"Dr. Mario (Japan, USA) (En) (Rev 1).zip"
|
||||
"Duck Hunt (World).zip"
|
||||
"DuckTales (USA).zip"
|
||||
"Excitebike (Japan, USA) (En).zip"
|
||||
"Final Fantasy (USA).zip"
|
||||
"Gradius (USA).zip"
|
||||
"Ice Climber (USA, Europe, Korea) (En).zip"
|
||||
"Kid Icarus (USA, Europe) (Rev 1).zip"
|
||||
"Kirby's Adventure (USA) (Rev 1).zip"
|
||||
"Legend of Zelda, The (USA) (Rev 1).zip"
|
||||
"Mega Man 2 (USA).zip"
|
||||
"Mega Man 3 (USA).zip"
|
||||
"Metroid (USA).zip"
|
||||
"Mike Tyson's Punch-Out!! (Japan, USA) (En) (Rev 1).zip"
|
||||
"Ninja Gaiden (USA).zip"
|
||||
"River City Ransom (USA).zip"
|
||||
"Super Mario Bros. (World).zip"
|
||||
"Super Mario Bros. 3 (USA) (Rev 1).zip"
|
||||
"Teenage Mutant Ninja Turtles II - The Arcade Game (USA).zip"
|
||||
)
|
||||
|
||||
for game in "${NES_GAMES[@]}"; do
|
||||
extract_game "$NES_ZIP" "$NES_PREFIX" "$NES_DEST" "$game"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Genesis (Mega Drive)
|
||||
# ---------------------------------------------------------------------------
|
||||
GENESIS_ZIP="$NO_INTRO_BASE/Sega - Mega Drive - Genesis.zip"
|
||||
GENESIS_PREFIX="Sega - Mega Drive - Genesis/"
|
||||
GENESIS_DEST="$ROMS_DEST/genesis"
|
||||
|
||||
if [[ ! -f "$GENESIS_ZIP" ]]; then
|
||||
echo "[WARN] Genesis zip not found: $GENESIS_ZIP"
|
||||
else
|
||||
echo "==> Genesis -> $GENESIS_DEST"
|
||||
mkdir -p "$GENESIS_DEST"
|
||||
|
||||
GENESIS_GAMES=(
|
||||
"Aladdin (USA).zip"
|
||||
"Altered Beast (Japan, USA) (En) (Rev A).zip"
|
||||
"Beyond Oasis (USA).zip"
|
||||
"Castle of Illusion Starring Mickey Mouse (USA, Europe).zip"
|
||||
"Castlevania - Bloodlines (USA).zip"
|
||||
"Comix Zone (USA).zip"
|
||||
"Contra - Hard Corps (USA, Korea) (En).zip"
|
||||
"Earthworm Jim (USA).zip"
|
||||
"Ecco the Dolphin (USA, Europe, Korea) (En).zip"
|
||||
"Golden Axe (World) (Rev A).zip"
|
||||
"Golden Axe II (World).zip"
|
||||
"Gunstar Heroes (USA).zip"
|
||||
"Lion King, The (World).zip"
|
||||
"Mortal Kombat (World).zip"
|
||||
"Mortal Kombat II (World).zip"
|
||||
"NBA Jam - Tournament Edition (World).zip"
|
||||
"Phantasy Star II (USA, Europe) (Rev A).zip"
|
||||
"QuackShot Starring Donald Duck (World) (Rev A).zip"
|
||||
"Ristar (USA, Europe).zip"
|
||||
"Road Rash II (USA, Europe) (RR206).zip"
|
||||
"Rocket Knight Adventures (USA).zip"
|
||||
"Shining Force (USA).zip"
|
||||
"Sonic The Hedgehog (Japan, Europe, Korea) (En).zip"
|
||||
"Sonic The Hedgehog 2 (World) (Rev B).zip"
|
||||
"Sonic The Hedgehog 3 (USA).zip"
|
||||
"Streets of Rage (World) (Rev A).zip"
|
||||
"Streets of Rage 2 (USA).zip"
|
||||
"ToeJam & Earl (USA, Europe, Korea) (En).zip"
|
||||
"Vectorman (USA, Europe).zip"
|
||||
"Virtua Racing (USA).zip"
|
||||
)
|
||||
|
||||
for game in "${GENESIS_GAMES[@]}"; do
|
||||
extract_game "$GENESIS_ZIP" "$GENESIS_PREFIX" "$GENESIS_DEST" "$game"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SNES
|
||||
# ---------------------------------------------------------------------------
|
||||
SNES_ZIP="$NO_INTRO_BASE/Nintendo - Super Nintendo Entertainment System.zip"
|
||||
SNES_PREFIX="Nintendo - Super Nintendo Entertainment System/"
|
||||
SNES_DEST="$ROMS_DEST/snes"
|
||||
|
||||
if [[ ! -f "$SNES_ZIP" ]]; then
|
||||
echo "[WARN] SNES zip not found: $SNES_ZIP"
|
||||
else
|
||||
echo "==> SNES -> $SNES_DEST"
|
||||
mkdir -p "$SNES_DEST"
|
||||
|
||||
SNES_GAMES=(
|
||||
"ActRaiser (USA).zip"
|
||||
"Breath of Fire II (USA).zip"
|
||||
"Chrono Trigger (USA).zip"
|
||||
"Contra III - The Alien Wars (USA).zip"
|
||||
"Donkey Kong Country (USA) (Rev 2).zip"
|
||||
"Donkey Kong Country 2 - Diddy's Kong Quest (USA) (En,Fr) (Rev 1).zip"
|
||||
"EarthBound (USA).zip"
|
||||
"F-Zero (USA).zip"
|
||||
"Final Fantasy III (USA) (Rev 1).zip"
|
||||
"Illusion of Gaia (USA).zip"
|
||||
"Kirby Super Star (USA).zip"
|
||||
"Legend of Zelda, The - A Link to the Past (USA).zip"
|
||||
"Mega Man X (USA) (Rev 1).zip"
|
||||
"Mega Man X2 (USA).zip"
|
||||
"Pilotwings (USA).zip"
|
||||
"Secret of Mana (USA).zip"
|
||||
"Soul Blazer (USA).zip"
|
||||
"Star Fox (USA) (Rev 2).zip"
|
||||
"Street Fighter II Turbo (USA) (Rev 1).zip"
|
||||
"Super Bomberman 2 (USA).zip"
|
||||
"Super Castlevania IV (USA).zip"
|
||||
"Super Mario All-Stars + Super Mario World (USA).zip"
|
||||
"Super Mario Kart (USA).zip"
|
||||
"Super Mario RPG - Legend of the Seven Stars (USA).zip"
|
||||
"Super Mario World 2 - Yoshi's Island (USA) (Rev 1).zip"
|
||||
"Super Metroid (Japan, USA) (En,Ja).zip"
|
||||
"Super Punch-Out!! (USA).zip"
|
||||
)
|
||||
|
||||
for game in "${SNES_GAMES[@]}"; do
|
||||
extract_game "$SNES_ZIP" "$SNES_PREFIX" "$SNES_DEST" "$game"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Game Boy
|
||||
# ---------------------------------------------------------------------------
|
||||
GB_ZIP="$NO_INTRO_BASE/Nintendo - Game Boy.zip"
|
||||
GB_PREFIX="Nintendo - Game Boy/"
|
||||
GB_DEST="$ROMS_DEST/gb"
|
||||
|
||||
if [[ ! -f "$GB_ZIP" ]]; then
|
||||
echo "[WARN] Game Boy zip not found: $GB_ZIP"
|
||||
else
|
||||
echo "==> Game Boy -> $GB_DEST"
|
||||
mkdir -p "$GB_DEST"
|
||||
|
||||
GB_GAMES=(
|
||||
"Alleyway (World).zip"
|
||||
"Balloon Kid (USA, Europe).zip"
|
||||
"Castlevania - The Adventure (USA).zip"
|
||||
"Castlevania II - Belmont's Revenge (USA, Europe).zip"
|
||||
"Castlevania Legends (USA, Europe) (SGB Enhanced).zip"
|
||||
"Contra - The Alien Wars (USA) (SGB Enhanced).zip"
|
||||
"Donkey Kong Land 2 (USA, Europe) (SGB Enhanced).zip"
|
||||
"Dr. Mario (World) (Rev 1).zip"
|
||||
"Final Fantasy Adventure (USA).zip"
|
||||
"Final Fantasy Legend II (USA).zip"
|
||||
"Final Fantasy Legend III (USA).zip"
|
||||
"Game & Watch Gallery (USA) (Rev 1) (SGB Enhanced).zip"
|
||||
"Gargoyle's Quest (USA, Europe).zip"
|
||||
"Kid Dracula (USA, Europe).zip"
|
||||
"Kirby's Dream Land (USA, Europe).zip"
|
||||
"Kirby's Dream Land 2 (USA, Europe) (SGB Enhanced).zip"
|
||||
"Kirby's Pinball Land (USA, Europe).zip"
|
||||
"Legend of Zelda, The - Link's Awakening (USA, Europe) (Rev 2).zip"
|
||||
"Mario's Picross (USA, Europe) (SGB Enhanced).zip"
|
||||
"Mega Man II (USA).zip"
|
||||
"Mega Man III (USA).zip"
|
||||
"Mega Man IV (USA).zip"
|
||||
"Mega Man V (USA) (SGB Enhanced).zip"
|
||||
"Metroid II - Return of Samus (World).zip"
|
||||
"Pokemon - Blue Version (USA, Europe) (SGB Enhanced).zip"
|
||||
"Pokemon - Red Version (USA, Europe) (SGB Enhanced).zip"
|
||||
"Pokemon - Yellow Version - Special Pikachu Edition (USA, Europe) (CGB+SGB Enhanced).zip"
|
||||
"Super Mario Land (World) (Rev 1).zip"
|
||||
"Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev 2).zip"
|
||||
"Tetris (World) (Rev 1).zip"
|
||||
"Wario Land - Super Mario Land 3 (World).zip"
|
||||
"Wave Race (USA, Europe).zip"
|
||||
)
|
||||
|
||||
for game in "${GB_GAMES[@]}"; do
|
||||
extract_game "$GB_ZIP" "$GB_PREFIX" "$GB_DEST" "$game"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Game Boy Advance
|
||||
# ---------------------------------------------------------------------------
|
||||
GBA_ZIP="$NO_INTRO_BASE/Nintendo - Game Boy Advance.zip"
|
||||
GBA_PREFIX="Nintendo - Game Boy Advance/"
|
||||
GBA_DEST="$ROMS_DEST/gba"
|
||||
|
||||
if [[ ! -f "$GBA_ZIP" ]]; then
|
||||
echo "[WARN] GBA zip not found: $GBA_ZIP"
|
||||
else
|
||||
echo "==> Game Boy Advance -> $GBA_DEST"
|
||||
mkdir -p "$GBA_DEST"
|
||||
|
||||
GBA_GAMES=(
|
||||
"Advance Wars (USA) (Rev 1).zip"
|
||||
"Advance Wars 2 - Black Hole Rising (USA).zip"
|
||||
"Castlevania - Aria of Sorrow (USA).zip"
|
||||
"Classic NES Series - Castlevania (USA).zip"
|
||||
"Classic NES Series - Dr. Mario (USA, Europe).zip"
|
||||
"Classic NES Series - Metroid (USA, Europe).zip"
|
||||
"Classic NES Series - The Legend of Zelda (USA, Europe).zip"
|
||||
"Dragon Ball Z - Supersonic Warriors (USA).zip"
|
||||
"Final Fantasy I & II - Dawn of Souls (USA).zip"
|
||||
"Final Fantasy IV Advance (USA).zip"
|
||||
"Final Fantasy V Advance (USA).zip"
|
||||
"Final Fantasy VI Advance (USA).zip"
|
||||
"Fire Emblem (USA, Australia).zip"
|
||||
"Fire Emblem - The Sacred Stones (USA, Australia).zip"
|
||||
"Golden Sun (USA, Europe).zip"
|
||||
"Golden Sun - The Lost Age (USA, Europe).zip"
|
||||
"Kirby & The Amazing Mirror (USA).zip"
|
||||
"Kirby - Nightmare in Dream Land (USA).zip"
|
||||
"Legend of Zelda, The - A Link to the Past & Four Swords (USA).zip"
|
||||
"Legend of Zelda, The - The Minish Cap (USA).zip"
|
||||
"Mario & Luigi - Superstar Saga (USA).zip"
|
||||
"Mario Golf - Advance Tour (USA).zip"
|
||||
"Mario vs. Donkey Kong (USA, Australia).zip"
|
||||
"Metroid Fusion (USA).zip"
|
||||
"Metroid - Zero Mission (USA).zip"
|
||||
"Mother 3 (Japan).zip"
|
||||
"Pokemon - Ruby Version (USA, Europe) (Rev 2).zip"
|
||||
"Pokemon - Sapphire Version (USA, Europe) (Rev 2).zip"
|
||||
"Sonic Advance (USA) (En,Ja).zip"
|
||||
"Sonic Advance 2 (USA) (En,Ja,Fr,De,Es,It).zip"
|
||||
"Sonic Advance 3 (USA) (En,Ja,Fr,De,Es,It).zip"
|
||||
"Super Mario Advance (USA, Europe).zip"
|
||||
"Super Mario Advance 2 - Super Mario World (USA, Australia).zip"
|
||||
"Super Mario Advance 3 - Yoshi's Island (USA).zip"
|
||||
"Wario Land 4 (USA, Europe).zip"
|
||||
"WarioWare - Twisted! (USA, Australia).zip"
|
||||
)
|
||||
|
||||
for game in "${GBA_GAMES[@]}"; do
|
||||
extract_game "$GBA_ZIP" "$GBA_PREFIX" "$GBA_DEST" "$game"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Amiga
|
||||
# NOTE: Amiga emulation requires Kickstart ROM firmware (not included in
|
||||
# No-Intro). Place kickstart .rom files in /romm/library/bios/ in the
|
||||
# RomM container. Source from Amiga Forever (Cloanto) or real hardware.
|
||||
# ---------------------------------------------------------------------------
|
||||
AMIGA_ZIP="$NO_INTRO_BASE/Commodore - Amiga.zip"
|
||||
AMIGA_PREFIX="Commodore - Amiga/"
|
||||
AMIGA_DEST="$ROMS_DEST/amiga"
|
||||
|
||||
if [[ ! -f "$AMIGA_ZIP" ]]; then
|
||||
echo "[WARN] Amiga zip not found: $AMIGA_ZIP"
|
||||
else
|
||||
echo "==> Amiga -> $AMIGA_DEST"
|
||||
mkdir -p "$AMIGA_DEST"
|
||||
|
||||
AMIGA_GAMES=(
|
||||
"Alien Breed (Europe).zip"
|
||||
"Alien Breed - Special Edition 92 (Europe).zip"
|
||||
"Alien Breed II - The Horror Continues (Europe).zip"
|
||||
"All New World of Lemmings (Europe) (AGA).zip"
|
||||
"Another World (Europe).zip"
|
||||
"Body Blows (Europe) (v2.0).zip"
|
||||
"Chaos Engine, The (Europe).zip"
|
||||
"Civilization (Europe) (v855e.01) (AGA).zip"
|
||||
"Elite (Europe) (v2.0).zip"
|
||||
"Frontier - Elite II (Europe) (2.9.1992).zip"
|
||||
"Gods (Europe).zip"
|
||||
"James Pond - Underwater Agent (Europe).zip"
|
||||
"James Pond II - Codename RoboCod (Europe) (Budget - Kixx).zip"
|
||||
"Lemmings (USA).zip"
|
||||
"Lemmings 2 - The Tribes (USA).zip"
|
||||
"Lotus Esprit Turbo Challenge (Europe).zip"
|
||||
"Monkey Island 2 - LeChuck's Revenge (Europe).zip"
|
||||
"Oh No! More Lemmings (USA) (Addon).zip"
|
||||
"Oscar (Europe).zip"
|
||||
"Pinball Dreams (Europe).zip"
|
||||
"Pinball Illusions (Europe) (AGA).zip"
|
||||
"Populous (USA).zip"
|
||||
"Populous II - Trials of the Olympian Gods (Europe).zip"
|
||||
"Rick Dangerous (Europe) (Amiga + PC) (Budget - Kixx).zip"
|
||||
"Rick Dangerous 2 (Europe).zip"
|
||||
"Secret of Monkey Island, The (Europe) (v1.2).zip"
|
||||
"Sensible Soccer - European Champions (Europe) (En,Fr,De,It) (v1.1).zip"
|
||||
"Sensible World of Soccer '96-'97 (Europe).zip"
|
||||
"Shadow of the Beast (Europe).zip"
|
||||
"Shadow of the Beast II (Europe).zip"
|
||||
"Shadow of the Beast III (USA).zip"
|
||||
"Speedball (USA) (v1.05).zip"
|
||||
"Speedball 2 - Brutal Deluxe (USA).zip"
|
||||
"Superfrog (Europe).zip"
|
||||
"Syndicate (Europe) (En,Fr,It).zip"
|
||||
"Theme Park (Europe) (En,Fr,De,It).zip"
|
||||
"Turrican (USA).zip"
|
||||
"Turrican II - The Final Fight (Europe).zip"
|
||||
"Turrican 3 (Europe).zip"
|
||||
"Worms (Europe) (En,Fr,De).zip"
|
||||
"Zool 2 (Europe) (AGA).zip"
|
||||
)
|
||||
|
||||
for game in "${AMIGA_GAMES[@]}"; do
|
||||
extract_game "$AMIGA_ZIP" "$AMIGA_PREFIX" "$AMIGA_DEST" "$game"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Arcade (FBNeo)
|
||||
# ---------------------------------------------------------------------------
|
||||
ARCADE_DEST="$ROMS_DEST/arcade"
|
||||
|
||||
if [[ ! -d "$FBNEO_ARCADE" ]]; then
|
||||
echo "[WARN] FBNeo arcade directory not found: $FBNEO_ARCADE"
|
||||
else
|
||||
echo "==> Arcade (FBNeo) -> $ARCADE_DEST"
|
||||
mkdir -p "$ARCADE_DEST"
|
||||
|
||||
ARCADE_ROMS=(
|
||||
# Pac-Man / Namco classics
|
||||
"pacman.zip"
|
||||
"mspacman.zip"
|
||||
"galaga.zip"
|
||||
"galaxian.zip"
|
||||
"digdug.zip"
|
||||
"btime.zip"
|
||||
"pengo.zip"
|
||||
# Nintendo classics
|
||||
"dkong.zip"
|
||||
"dkongjr.zip"
|
||||
"popeye.zip"
|
||||
"punchout.zip"
|
||||
"spnchout.zip"
|
||||
# Early arcade classics
|
||||
"invaders.zip"
|
||||
"centiped.zip"
|
||||
"frogger.zip"
|
||||
"qbert.zip"
|
||||
"zaxxon.zip"
|
||||
"arkanoid.zip"
|
||||
# Sega
|
||||
"outrun.zip"
|
||||
"aburner2.zip"
|
||||
# Shooters / Shmups
|
||||
"1942.zip"
|
||||
"1943.zip"
|
||||
"19xx.zip"
|
||||
"gradius.zip"
|
||||
"gradius2.zip"
|
||||
"gradius3.zip"
|
||||
"raiden.zip"
|
||||
"raiden2.zip"
|
||||
"contra.zip"
|
||||
# Capcom beat-em-ups
|
||||
"ffight.zip"
|
||||
"knights.zip"
|
||||
"kod.zip"
|
||||
"wof.zip"
|
||||
"dino.zip"
|
||||
"avsp.zip"
|
||||
"ddsom.zip"
|
||||
"ddtod.zip"
|
||||
"batcir.zip"
|
||||
"msword.zip"
|
||||
"strider.zip"
|
||||
# Konami beat-em-ups
|
||||
"tmnt.zip"
|
||||
"tmnt2.zip"
|
||||
"simpsons.zip"
|
||||
"xmen.zip"
|
||||
"captaven.zip"
|
||||
"punisher.zip"
|
||||
"aliens.zip"
|
||||
"ssriders.zip"
|
||||
# Double Dragon / Technos
|
||||
"ddragon.zip"
|
||||
"ddragon2.zip"
|
||||
"ddragon3.zip"
|
||||
# Platform / action
|
||||
"ghouls.zip"
|
||||
"pang.zip"
|
||||
"spang.zip"
|
||||
"bublbobl.zip"
|
||||
"pbobble.zip"
|
||||
"rbisland.zip"
|
||||
"snowbros.zip"
|
||||
"slammast.zip"
|
||||
"gauntlet.zip"
|
||||
"gaunt2.zip"
|
||||
# Street Fighter
|
||||
"sf2.zip"
|
||||
"sf2ce.zip"
|
||||
"sf2hf.zip"
|
||||
"ssf2t.zip"
|
||||
"sfa.zip"
|
||||
"sfa2.zip"
|
||||
"sfa3.zip"
|
||||
"sfiii3.zip"
|
||||
# Mortal Kombat
|
||||
"mk.zip"
|
||||
"mk2.zip"
|
||||
"mk3.zip"
|
||||
# Marvel / Capcom crossovers
|
||||
"xmvsf.zip"
|
||||
"msh.zip"
|
||||
"mvsc.zip"
|
||||
"nwarr.zip"
|
||||
# King of Fighters
|
||||
"kof94.zip"
|
||||
"kof95.zip"
|
||||
"kof96.zip"
|
||||
"kof97.zip"
|
||||
"kof98.zip"
|
||||
"kof99.zip"
|
||||
"kof2000.zip"
|
||||
"kof2001.zip"
|
||||
"kof2002.zip"
|
||||
"kof2003.zip"
|
||||
# Fatal Fury / Garou
|
||||
"fatfury1.zip"
|
||||
"fatfury2.zip"
|
||||
"fatfursp.zip"
|
||||
"fatfury3.zip"
|
||||
"rbff1.zip"
|
||||
"rbff2.zip"
|
||||
"garou.zip"
|
||||
# Samurai Shodown
|
||||
"samsho.zip"
|
||||
"samsho2.zip"
|
||||
"samsho3.zip"
|
||||
"samsho4.zip"
|
||||
"samsho5.zip"
|
||||
# Metal Slug
|
||||
"mslug.zip"
|
||||
"mslug2.zip"
|
||||
"mslug3.zip"
|
||||
"mslug4.zip"
|
||||
"mslug5.zip"
|
||||
"mslugx.zip"
|
||||
# Neo Geo misc
|
||||
"blazstar.zip"
|
||||
"wjammers.zip"
|
||||
"turfmast.zip"
|
||||
"lastbld2.zip"
|
||||
"rotd.zip"
|
||||
"neobombe.zip"
|
||||
"kabukikl.zip"
|
||||
"matrim.zip"
|
||||
)
|
||||
|
||||
for rom in "${ARCADE_ROMS[@]}"; do
|
||||
copy_arcade "$FBNEO_ARCADE" "$ARCADE_DEST" "$rom"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "Done. extracted=$extracted skipped=$skipped not_found=$not_found"
|
||||
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
spoolman:
|
||||
container_name: spoolman
|
||||
image: ghcr.io/donkie/spoolman:latest
|
||||
ports:
|
||||
- "7912:8000"
|
||||
volumes:
|
||||
- /srv/spoolman/data:/home/app/.local/share/spoolman
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: npm-network
|
||||
@@ -8,6 +8,7 @@ services:
|
||||
environment:
|
||||
- FORGE_ARGS=--xformers --api
|
||||
- AUTO_UPDATE=false
|
||||
- WEB_ENABLE_AUTH=false
|
||||
- TZ=America/New_York
|
||||
# Optional - for gated models
|
||||
# - HF_TOKEN=your_token
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# Synchronet BBS - Legend of the Red Dragon
|
||||
|
||||
This is a Synchronet BBS instance configured to host Legend of the Red Dragon (LORD) and other classic BBS door games.
|
||||
|
||||
## Features
|
||||
|
||||
- **Web-based terminal access** via fTelnet (your friends can play in a browser!)
|
||||
- **Classic telnet access** on port 23
|
||||
- **SSH access** on port 2222
|
||||
- **LORD (Legend of the Red Dragon)** - JavaScript port included with Synchronet
|
||||
- Additional door games available through Synchronet's xtrn system
|
||||
|
||||
## Initial Setup
|
||||
|
||||
### 1. First Run - Initialize Configuration
|
||||
|
||||
```bash
|
||||
cd /home/poprhythm/docker-infrastructure/synchronet
|
||||
docker compose up -d
|
||||
# Wait a few seconds for initialization
|
||||
docker compose down
|
||||
sudo chmod -R a+rwX /srv/synchronet
|
||||
```
|
||||
|
||||
### 2. Run Configuration Program
|
||||
|
||||
Configure your BBS name, sysop info, and enable LORD:
|
||||
|
||||
```bash
|
||||
docker exec -it synchronet scfg
|
||||
```
|
||||
|
||||
In the configuration menu:
|
||||
- Navigate to **System** → Set your BBS name and sysop information
|
||||
- Navigate to **External Programs** → **Online Programs (Doors)** → **Available Online Programs**
|
||||
- Look for LORD or add it if not already configured
|
||||
- Save and exit (ESC to go back, then save when prompted)
|
||||
|
||||
### 3. Enable LORD Door Game
|
||||
|
||||
LORD comes with Synchronet as a JavaScript implementation. To enable it:
|
||||
|
||||
```bash
|
||||
# Connect to the container
|
||||
docker exec -it synchronet bash
|
||||
|
||||
# Run the install script for LORD
|
||||
cd /sbbs
|
||||
./exec/jsexec /sbbs/xtrn/install-xtrn.js lord
|
||||
|
||||
# Exit the container
|
||||
exit
|
||||
```
|
||||
|
||||
Alternatively, you can manually configure LORD through scfg:
|
||||
- **External Programs** → **Online Programs** → **Main** section
|
||||
- Add new program with:
|
||||
- **Name**: Legend of the Red Dragon
|
||||
- **Internal Code**: LORD
|
||||
- **Start-up Directory**: ../xtrn/lord
|
||||
- **Command Line**: ?lord.js
|
||||
|
||||
### 4. Restart the BBS
|
||||
|
||||
```bash
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
## Access Methods
|
||||
|
||||
### Web Browser (Recommended!)
|
||||
|
||||
The BBS includes a **web-based terminal** (ttyd) with:
|
||||
- Full ANSI color support
|
||||
- 56k modem speed simulation for nostalgia
|
||||
- Adjustable font size (Ctrl+/Ctrl-/Ctrl+0)
|
||||
- Works on any device with a browser
|
||||
|
||||
Access at: **https://bbs.popcyclical.com** (or your configured domain)
|
||||
|
||||
### Direct Telnet
|
||||
|
||||
```bash
|
||||
telnet bbs.popcyclical.com 23
|
||||
```
|
||||
|
||||
### SSH
|
||||
|
||||
```bash
|
||||
ssh -p 2222 new@bbs.popcyclical.com
|
||||
```
|
||||
|
||||
## nginx-proxy-manager Configuration
|
||||
|
||||
Configure in NPM web interface:
|
||||
|
||||
**Proxy Host for Web Terminal**:
|
||||
- Domain: `bbs.popcyclical.com` (or your choice)
|
||||
- Forward Hostname/IP: `synchronet-web-terminal`
|
||||
- Forward Port: `7681`
|
||||
- Enable **WebSocket Support** ✓
|
||||
- Enable SSL certificate
|
||||
|
||||
## Customization
|
||||
|
||||
### ORMG Branding
|
||||
|
||||
Custom ANSI art files are included in `customization/` directory:
|
||||
|
||||
- **answer.ans** - Custom welcome screen with "ORMG" branding instead of "Synchronet"
|
||||
- **synch.ans.original** - Backup of original Synchronet ANSI art
|
||||
|
||||
To apply the ORMG branding:
|
||||
```bash
|
||||
cp customization/answer.ans /srv/synchronet/ctrl/
|
||||
docker restart synchronet
|
||||
```
|
||||
|
||||
To revert to original:
|
||||
```bash
|
||||
rm /srv/synchronet/ctrl/answer.ans
|
||||
docker restart synchronet
|
||||
```
|
||||
|
||||
## File Locations
|
||||
|
||||
- **Config**: `/srv/synchronet/ctrl/`
|
||||
- **Door games**: `/srv/synchronet/xtrn/`
|
||||
- **Web files**: `/srv/synchronet/web/`
|
||||
- **User data**: `/srv/synchronet/data/`
|
||||
- **Custom ANSI**: `/srv/synchronet/ctrl/answer.ans` (welcome screen)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check logs
|
||||
|
||||
```bash
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
### Access container shell
|
||||
|
||||
```bash
|
||||
docker exec -it synchronet bash
|
||||
```
|
||||
|
||||
### Reset permissions
|
||||
|
||||
```bash
|
||||
sudo chmod -R a+rwX /srv/synchronet
|
||||
```
|
||||
|
||||
### LORD not appearing
|
||||
|
||||
1. Check that LORD is enabled in scfg under External Programs
|
||||
2. Verify the command line is set correctly
|
||||
3. Check `/srv/synchronet/xtrn/lord/` exists and has the game files
|
||||
|
||||
## Additional Door Games
|
||||
|
||||
Synchronet includes many door games and can run DOS doors through DOSEMU. Popular ones included:
|
||||
- **LORD** (Legend of the Red Dragon)
|
||||
- **TradeWars 2002**
|
||||
- **BRE (Barren Realms Elite)**
|
||||
- **Usurper**
|
||||
|
||||
Install additional doors through scfg or the install-xtrn.js script.
|
||||
|
||||
## Sources & Documentation
|
||||
|
||||
- [Synchronet Docker GitHub](https://github.com/bbs-io/synchronet-docker)
|
||||
- [Synchronet Wiki - Door Installation](http://wiki.synchro.net/howto:door:index)
|
||||
- [Synchronet Wiki - LORD Setup](http://wiki.synchro.net/howto:door:lord)
|
||||
- [fTelnet Web Terminal](https://www.ftelnet.ca/)
|
||||
- [LORD Online](https://lord.stabs.org/)
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Synchronet BBS Initial Setup Script
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Synchronet BBS Setup ==="
|
||||
echo ""
|
||||
|
||||
# Check if container is running
|
||||
if ! docker ps | grep -q synchronet; then
|
||||
echo "Error: Synchronet container is not running!"
|
||||
echo "Please deploy the stack in Portainer first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 1: Setting permissions on data directory..."
|
||||
sudo chmod -R a+rwX /srv/synchronet
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Waiting for initial configuration files to be created..."
|
||||
sleep 5
|
||||
|
||||
echo ""
|
||||
echo "Step 3: Running Synchronet configuration program (scfg)..."
|
||||
echo "Important tasks to complete in scfg:"
|
||||
echo " 1. System → Set your BBS name and sysop information"
|
||||
echo " 2. Networks → Configure any network settings"
|
||||
echo " 3. External Programs → Verify door games are available"
|
||||
echo ""
|
||||
echo "Press ENTER to launch scfg..."
|
||||
read
|
||||
|
||||
docker exec -it synchronet scfg
|
||||
|
||||
echo ""
|
||||
echo "Step 4: Installing/Enabling LORD door game..."
|
||||
docker exec -it synchronet bash -c "cd /sbbs && ./exec/jsexec /sbbs/xtrn/install-xtrn.js lord || echo 'LORD may already be installed'"
|
||||
|
||||
echo ""
|
||||
echo "Step 5: Restarting Synchronet to apply changes..."
|
||||
docker restart synchronet
|
||||
|
||||
echo ""
|
||||
echo "=== Setup Complete! ==="
|
||||
echo ""
|
||||
echo "Access your BBS:"
|
||||
echo " Web Interface: http://$(hostname -I | awk '{print $1}'):8023"
|
||||
echo " Telnet: telnet $(hostname -I | awk '{print $1}') 23"
|
||||
echo " SSH: ssh -p 2222 new@$(hostname -I | awk '{print $1}')"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Configure nginx-proxy-manager for web access (see README.md)"
|
||||
echo " 2. Test LORD by connecting and selecting External Programs"
|
||||
echo " 3. Invite your friends!"
|
||||
@@ -0,0 +1,21 @@
|
||||
[0m
|
||||
[28C[1;30m [0m[19C[34m�[1m�[0m [34m�
|
||||
[37m[13C[1m [33m [0m[13C[34m�[37m [34m�ܰ[44;30m�[40;37m[11C[34m [1m��[0m[9C[1;30mgj/��
|
||||
[0m[6C[1mormg [0m [1;30m��[0m [1;30m�[0m [34m�[37m[5C[34m ߲���[37m [34m�� [1m�[0;34m�
|
||||
[37m[7C[34m�[37m [34m�[37m ���[1m���[0m [34m�����[1;44m� �[0;34m����[44;30m�[40;34m�� �[1;44m��[0;34m���[1m�[46m�[0;34m [37m�[1;30m�[0m�[1;47m�����[0m� [34m�[37m [34m����
|
||||
[37m [1;34m�[0;34m�[37m �[1m�[47m�[40m��[47m��۱[40m�[0m [34m�[1;44m��[40m���[0;34m�[37m ��� [34m�[1;44m�[0;34m��[37m ��� [34m�[1;44m�[0;34m�[44;32m�[46;34m�[1;40m��[0;34m�[36m [37m [1;47;30m�[37m����[40m�[0m ������ [1;34m��
|
||||
[0m [1;34m �[0;34m�[37m [1m�[47m�������۲[0m� [1;34m�[46m�[47m�[46m�[40m�[0m [1;47m����[40m�[0m [1;34m�[44m�[0;34m�[37m�[1;47m���۲[0m� [46;34m�[1;40m�[0m [1;30m�[0m�[1;47m�����[0m��[1;47m��۲[40m�[0m �[1;47m�۰[0m [34m��[1m���[0;34m�
|
||||
[37m [46;30m�[40;34m [1;30m�[47;37m�����[0m� �[1;47m��[0m�� [1;34m��[0;34m [1m�[0m [1;47m���[40m�[0m�[1;34m�[46m�[0m [1;30m�[0m��[1;47m����[40m�[0m �[1;47m���������[0m [1;47;30m�[37m���[40m�[0m �[1;47m�۲[0m�[34m�[1;44m�[40m�[47m��[40m�[0;34m��[37m [34m�
|
||||
[1m [0;34m��[1;44m�[0;34m�[37m [1;30m�[0m�[1;47m�����[0m� [1;30m�[0m���[1m�[30m�[34m�[0m [1;47m���[40m��[0m�[36m�[37m [34m�[37m �[1;47m�۲�[0m [1m�[47m��������[40m�[0m� [1;30m�[47;37m��۱[0m���[1;47m��[40m���[0m�[1m�[0m�� � [1;34m�[0;44;30m�[0m
|
||||
[34m �[1;44m��[0;34m [1;44m�[40m�[0;46;34m�[40m��[37m ��[1;47m���[0m� [1;47m����[0m� [1;30m�[47;37m���[40m��[47m���[40m�[0m�[34m [1;37m [47m��� [40;30m�[47;37m����[0m� [1m�[47m���[0m [34m� [1;37m�[47m�������۲�[0m [34m�[1m�[0;36m�[1;34m�[0;34m��[1m�[0;34m�
|
||||
�[44;36m�[46;34m�[1;40m����[0m ��� [34m�[37m ��[1;47m�[0m�[1m�[47m����[0m��[1;47m�۲[0m�[1;47m�������[0m�[1;47m���[0m�[1;30m�[47;37m��۱[0m [34m �[1;30m�[0m�[1m��[30m [0m �[1;47m���[0m� �[1;47m���[0m� [34m�[1m��[46m�[0m
|
||||
[34m �[1m�[46m߲[40m�[0m�[47;30m�[40;37m�[1m�[0m����[1;47m [40m��[47m [0m�[1;47;30m�[40;37m�[47m�����۱[0m [1;47m����[0m [1m��[47m�۲�[0m� [1;47;30m�[37m���[0m�� [34m��[37m ܱ[1;30m�[0m �[1;47m��[0m�[34m��[37m�[1;47m���[0m� [1;44;36m�[40;34m��
|
||||
[0m[5C[1;34m [0;34m [37m�[47;30m�[40;37m���[1;47m۲��� [0m���[1;47;30m�[40m�[0m �[1;47m���[0m���[1;47m��[0m�� [1m��[47m۱�[0m� �[1;47m [40m��[47m �[0m�[1;47m�[0m�۲[1m�[0;34m�[37m�[1;47m�[0m�[1m�[34m�[0;44;36m�[40;37m�[1;47m���[0m�[1m�[0;36m��
|
||||
[37m [36m�[46;30m�[40;37m [1m�[47m���[40m�[0m����� [1;34m��[0;34m��[37m �[1;47m���[0m [1m���[47m���[40m�[0;34m�[1m�[0m [1m�[47m��[40m�[0m� [34m�[37m ����[47;30m�[40;37m�[1m��[0m ��[1m�[0;34m [44;36m�[40;37m [1m�[0m��[47;30m�[1;40;37m�[0m [1;36m�
|
||||
[0m[6C[36m�[37m [36m�[37m [36m��[1m�[0;36m���[1m�[44m�[0;44;36m�[40;34m��[37m�[1;47;33m [0m�[1;47;33m�[40m�[30m�[0m�[47;30m�[40;37m [34m�[37m [1m�[0m [1;36m�[46;34m�[40m��[0m [1m�����[0m [34m� ��� [37m [34m [1;37m�[0m�[1m�[0m [1;34m�[0m ���[1m��
|
||||
[0m[13C[34m [37m [34m�[37m [34m�[44;30m�[40;37m [1m�[47m�[33m�۲�[40;37m���[33m�[0m [34m ��[44;36m �߰[40;34m�[1m��[0m[5C� �[5C[34m �
|
||||
[37m[26C[1;33m����[0m[12C[34m �[1mݰ[0;44;30m�[40;37m [1;30m [33mdoor[0m games
|
||||
[25C[1;33m��[37m�[0m[12C[34m�[37m [34m�
|
||||
[37m[24C[1;36m�[37m�[33m��
|
||||
[0m[25C[1;33m��
|
||||
[0m[25C[1;33m�
|
||||
@@ -0,0 +1,21 @@
|
||||
[0m
|
||||
[28C[1;30m [0m[19C[34m�[1m�[0m [34m�
|
||||
[37m[13C[1m [33m [0m[13C[34m�[37m [34m�ܰ[44;30m�[40;37m[11C[34m [1m��[0m[9C[1;30mgj/��
|
||||
[0m[6C[1msynchronet[0m [1;30m��[0m [1;30m�[0m [34m�[37m[5C[34m ߲���[37m [34m�� [1m�[0;34m�
|
||||
[37m[7C[34m�[37m [34m�[37m ���[1m���[0m [34m�����[1;44m� �[0;34m����[44;30m�[40;34m�� �[1;44m��[0;34m���[1m�[46m�[0;34m [37m�[1;30m�[0m�[1;47m�����[0m� [34m�[37m [34m����
|
||||
[37m [1;34m�[0;34m�[37m �[1m�[47m�[40m��[47m��۱[40m�[0m [34m�[1;44m��[40m���[0;34m�[37m ��� [34m�[1;44m�[0;34m��[37m ��� [34m�[1;44m�[0;34m�[44;32m�[46;34m�[1;40m��[0;34m�[36m [37m [1;47;30m�[37m����[40m�[0m ������ [1;34m��
|
||||
[0m [1;34m �[0;34m�[37m [1m�[47m�������۲[0m� [1;34m�[46m�[47m�[46m�[40m�[0m [1;47m����[40m�[0m [1;34m�[44m�[0;34m�[37m�[1;47m���۲[0m� [46;34m�[1;40m�[0m [1;30m�[0m�[1;47m�����[0m��[1;47m��۲[40m�[0m �[1;47m�۰[0m [34m��[1m���[0;34m�
|
||||
[37m [46;30m�[40;34m [1;30m�[47;37m�����[0m� �[1;47m��[0m�� [1;34m��[0;34m [1m�[0m [1;47m���[40m�[0m�[1;34m�[46m�[0m [1;30m�[0m��[1;47m����[40m�[0m �[1;47m���������[0m [1;47;30m�[37m���[40m�[0m �[1;47m�۲[0m�[34m�[1;44m�[40m�[47m��[40m�[0;34m��[37m [34m�
|
||||
[1m [0;34m��[1;44m�[0;34m�[37m [1;30m�[0m�[1;47m�����[0m� [1;30m�[0m���[1m�[30m�[34m�[0m [1;47m���[40m��[0m�[36m�[37m [34m�[37m �[1;47m�۲�[0m [1m�[47m��������[40m�[0m� [1;30m�[47;37m��۱[0m���[1;47m��[40m���[0m�[1m�[0m�� � [1;34m�[0;44;30m�[0m
|
||||
[34m �[1;44m��[0;34m [1;44m�[40m�[0;46;34m�[40m��[37m ��[1;47m���[0m� [1;47m����[0m� [1;30m�[47;37m���[40m��[47m���[40m�[0m�[34m [1;37m [47m��� [40;30m�[47;37m����[0m� [1m�[47m���[0m [34m� [1;37m�[47m�������۲�[0m [34m�[1m�[0;36m�[1;34m�[0;34m��[1m�[0;34m�
|
||||
�[44;36m�[46;34m�[1;40m����[0m ��� [34m�[37m ��[1;47m�[0m�[1m�[47m����[0m��[1;47m�۲[0m�[1;47m�������[0m�[1;47m���[0m�[1;30m�[47;37m��۱[0m [34m �[1;30m�[0m�[1m��[30m [0m �[1;47m���[0m� �[1;47m���[0m� [34m�[1m��[46m�[0m
|
||||
[34m �[1m�[46m߲[40m�[0m�[47;30m�[40;37m�[1m�[0m����[1;47m [40m��[47m [0m�[1;47;30m�[40;37m�[47m�����۱[0m [1;47m����[0m [1m��[47m�۲�[0m� [1;47;30m�[37m���[0m�� [34m��[37m ܱ[1;30m�[0m �[1;47m��[0m�[34m��[37m�[1;47m���[0m� [1;44;36m�[40;34m��
|
||||
[0m[5C[1;34m [0;34m [37m�[47;30m�[40;37m���[1;47m۲��� [0m���[1;47;30m�[40m�[0m �[1;47m���[0m���[1;47m��[0m�� [1m��[47m۱�[0m� �[1;47m [40m��[47m �[0m�[1;47m�[0m�۲[1m�[0;34m�[37m�[1;47m�[0m�[1m�[34m�[0;44;36m�[40;37m�[1;47m���[0m�[1m�[0;36m��
|
||||
[37m [36m�[46;30m�[40;37m [1m�[47m���[40m�[0m����� [1;34m��[0;34m��[37m �[1;47m���[0m [1m���[47m���[40m�[0;34m�[1m�[0m [1m�[47m��[40m�[0m� [34m�[37m ����[47;30m�[40;37m�[1m��[0m ��[1m�[0;34m [44;36m�[40;37m [1m�[0m��[47;30m�[1;40;37m�[0m [1;36m�
|
||||
[0m[6C[36m�[37m [36m�[37m [36m��[1m�[0;36m���[1m�[44m�[0;44;36m�[40;34m��[37m�[1;47;33m [0m�[1;47;33m�[40m�[30m�[0m�[47;30m�[40;37m [34m�[37m [1m�[0m [1;36m�[46;34m�[40m��[0m [1m�����[0m [34m� ��� [37m [34m [1;37m�[0m�[1m�[0m [1;34m�[0m ���[1m��
|
||||
[0m[13C[34m [37m [34m�[37m [34m�[44;30m�[40;37m [1m�[47m�[33m�۲�[40;37m���[33m�[0m [34m ��[44;36m �߰[40;34m�[1m��[0m[5C� �[5C[34m �
|
||||
[37m[26C[1;33m����[0m[12C[34m �[1mݰ[0;44;30m�[40;37m [1;30m [33mbbs[0m software
|
||||
[25C[1;33m��[37m�[0m[12C[34m�[37m [34m�
|
||||
[37m[24C[1;36m�[37m�[33m��
|
||||
[0m[25C[1;33m��
|
||||
[0m[25C[1;33m�
|
||||
@@ -0,0 +1,43 @@
|
||||
services:
|
||||
synchronet:
|
||||
image: bbsio/synchronet:3.19c
|
||||
container_name: synchronet
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- npm-network
|
||||
ports:
|
||||
# Web interfaces
|
||||
- "8023:80" # HTTP web interface
|
||||
- "8444:443" # HTTPS web interface
|
||||
# Terminal access
|
||||
- "23:23" # Telnet
|
||||
- "2222:22" # SSH (using 2222 to avoid conflict with host SSH)
|
||||
# Web-based terminal (fTelnet)
|
||||
- "1123:1123" # WebSocket terminal (ws)
|
||||
- "11235:11235" # WebSocket terminal (wss)
|
||||
# Optional: Classic BBS protocols
|
||||
- "21:21" # FTP
|
||||
- "6667:6667" # IRC
|
||||
volumes:
|
||||
- /srv/synchronet:/sbbs-data
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- SBBSCTRL=/sbbs-data/ctrl
|
||||
|
||||
ttyd:
|
||||
image: tsl0922/ttyd:alpine
|
||||
container_name: synchronet-web-terminal
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- npm-network
|
||||
ports:
|
||||
- "7681:7681"
|
||||
environment:
|
||||
- TERM=ansi
|
||||
command: sh -c "apk add --no-cache busybox-extras pv && ttyd -W -p 7681 -t fontSize=18 -t 'titleFixed=ORMG BBS - Legend of the Red Dragon' sh -c 'telnet synchronet 23 | pv -q -L 7000'"
|
||||
depends_on:
|
||||
- synchronet
|
||||
|
||||
networks:
|
||||
npm-network:
|
||||
external: true
|
||||
@@ -0,0 +1,55 @@
|
||||
# qBittorrent Web UI Instance Labels
|
||||
|
||||
The `open` and `vpn` instances are proxied via nginx-proxy-manager and have colored
|
||||
badges injected into the Web UI to distinguish them at a glance.
|
||||
|
||||
## How it works
|
||||
|
||||
nginx's `sub_filter` module replaces `</head>` in the HTML response with a `<style>`
|
||||
block that adds a fixed-position badge in the top-right corner of the page.
|
||||
|
||||
**open** (qbto.kolpacksoftware.com) — blue badge
|
||||
**vpn** (qbtv.kolpacksoftware.com) — green badge
|
||||
|
||||
## NPM Advanced tab config
|
||||
|
||||
For each proxy host, the following is set in the **Advanced** tab:
|
||||
|
||||
**open:**
|
||||
```nginx
|
||||
sub_filter '</head>' '<style>body::after{content:"OPEN";position:fixed;top:5px;right:5px;background:#1565c0;color:#fff;padding:3px 10px;border-radius:3px;font-size:13px;font-weight:bold;z-index:99999;font-family:monospace;pointer-events:none;}</style></head>';
|
||||
sub_filter_once on;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
```
|
||||
|
||||
**vpn:**
|
||||
```nginx
|
||||
sub_filter '</head>' '<style>body::after{content:"VPN";position:fixed;top:5px;right:5px;background:#2e7d32;color:#fff;padding:3px 10px;border-radius:3px;font-size:13px;font-weight:bold;z-index:99999;font-family:monospace;pointer-events:none;}</style></head>';
|
||||
sub_filter_once on;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
```
|
||||
|
||||
## Important: manual fix required after saving in NPM UI
|
||||
|
||||
NPM regenerates the nginx conf file whenever you save a proxy host in the UI,
|
||||
which overwrites a necessary fix. After saving either proxy host, re-run:
|
||||
|
||||
```bash
|
||||
# For open (proxy host 4):
|
||||
sudo sed -i 's| include conf.d/include/proxy.conf;| proxy_set_header Accept-Encoding "";\n include conf.d/include/proxy.conf;|' /srv/nginx-proxy/data/nginx/proxy_host/4.conf
|
||||
|
||||
# For vpn (proxy host 28):
|
||||
sudo sed -i 's| include conf.d/include/proxy.conf;| proxy_set_header Accept-Encoding "";\n include conf.d/include/proxy.conf;|' /srv/nginx-proxy/data/nginx/proxy_host/28.conf
|
||||
|
||||
docker exec nginx-proxy nginx -s reload
|
||||
```
|
||||
|
||||
### Why is this needed?
|
||||
|
||||
`proxy_set_header Accept-Encoding ""` must be inside the `location /` block for
|
||||
nginx to actually strip the header before forwarding to qBittorrent. When set at
|
||||
the server block level (via NPM's Advanced tab), it is silently ignored because
|
||||
the location block defines its own `proxy_set_header` directives via `proxy.conf`.
|
||||
|
||||
Without this fix, qBittorrent returns gzip-compressed responses and `sub_filter`
|
||||
cannot process them, so the badge never appears.
|
||||
@@ -16,6 +16,7 @@ services:
|
||||
volumes:
|
||||
- /srv/qbittorrent_open-config:/config
|
||||
- nas_media:/data
|
||||
- /mnt/nas_library:/library
|
||||
qbittorrent_vpn:
|
||||
container_name: qbittorrent_vpn
|
||||
image: ghcr.io/hotio/qbittorrent:release-5.1.2
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
services:
|
||||
ultralytics:
|
||||
image: ultralytics/ultralytics:latest
|
||||
container_name: ultralytics
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- >-
|
||||
pip install 'streamlit>=1.29.0' -q --root-user-action=ignore &&
|
||||
sed -i 's/cv2.destroyAllWindows()/pass/' /ultralytics/ultralytics/solutions/streamlit_inference.py &&
|
||||
streamlit run /ultralytics/ultralytics/solutions/streamlit_inference.py
|
||||
--server.headless true --server.address 0.0.0.0 --server.port 8501
|
||||
-- /data/models/yolo11x_leaf.pt
|
||||
ports:
|
||||
- "127.0.0.1:8501:8501"
|
||||
volumes:
|
||||
- /srv/ultralytics/data:/data
|
||||
- /srv/ultralytics/runs:/root/runs
|
||||
- pip_cache:/root/.cache/pip
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- STREAMLIT_SERVER_ADDRESS=0.0.0.0
|
||||
- STREAMLIT_SERVER_PORT=8501
|
||||
- STREAMLIT_SERVER_HEADLESS=true
|
||||
networks:
|
||||
- npm-network
|
||||
|
||||
networks:
|
||||
npm-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
pip_cache:
|
||||
@@ -0,0 +1,256 @@
|
||||
# Uptime Kuma API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
Uptime Kuma provides a Socket.IO-based API for programmatic monitor management. The REST API does **not** support monitor CRUD operations - you must use the Socket.IO API via the `uptime-kuma-api` Python package.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Python Package
|
||||
|
||||
```bash
|
||||
pip3 install uptime-kuma-api
|
||||
```
|
||||
|
||||
**Requirements:** Python 3.7+
|
||||
|
||||
### 2. Create API Credentials
|
||||
|
||||
#### Option A: API Key (Recommended)
|
||||
|
||||
1. Access Uptime Kuma web UI: https://uptime.kolpacksoftware.com
|
||||
2. Navigate to **Settings → API Keys**
|
||||
3. Click **Generate** to create a new API key
|
||||
4. Set a descriptive name (e.g., "CLI Management")
|
||||
5. Set expiry (or "Never expire")
|
||||
6. **Copy the key immediately** (shown only once)
|
||||
7. Store securely in environment variable:
|
||||
|
||||
```bash
|
||||
export UPTIME_KUMA_API_KEY="uk1_your_api_key_here"
|
||||
```
|
||||
|
||||
**Important:** Once the first API key is created, basic authentication is permanently disabled for supported endpoints.
|
||||
|
||||
#### Option B: Username/Password
|
||||
|
||||
Use existing Uptime Kuma login credentials:
|
||||
|
||||
```bash
|
||||
export UPTIME_KUMA_USERNAME="your_username"
|
||||
export UPTIME_KUMA_PASSWORD="your_password"
|
||||
```
|
||||
|
||||
### 3. Make Script Executable
|
||||
|
||||
```bash
|
||||
chmod +x /home/poprhythm/docker-infrastructure/uptime-kuma/manage_monitors.py
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### List All Monitors
|
||||
|
||||
```bash
|
||||
./manage_monitors.py list
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
ID Name Type URL
|
||||
------------------------------------------------------------------------------------------------
|
||||
1 Example Service http https://example.com
|
||||
2 Database Health tcp 192.168.1.100:5432
|
||||
3 Gateway Ping ping 192.168.1.1
|
||||
```
|
||||
|
||||
### Add a Monitor
|
||||
|
||||
**HTTP/HTTPS Monitor:**
|
||||
```bash
|
||||
./manage_monitors.py add --name "My Service" --url "https://myservice.com" --type http
|
||||
```
|
||||
|
||||
**TCP Port Monitor:**
|
||||
```bash
|
||||
./manage_monitors.py add --name "PostgreSQL" --url "192.168.1.100:5432" --type tcp
|
||||
```
|
||||
|
||||
**Ping Monitor:**
|
||||
```bash
|
||||
./manage_monitors.py add --name "Router" --url "192.168.1.1" --type ping
|
||||
```
|
||||
|
||||
**Custom Interval (default is 60 seconds):**
|
||||
```bash
|
||||
./manage_monitors.py add --name "Critical Service" --url "https://critical.com" --interval 30
|
||||
```
|
||||
|
||||
### Update a Monitor
|
||||
|
||||
```bash
|
||||
# Update name
|
||||
./manage_monitors.py update --id 123 --name "New Name"
|
||||
|
||||
# Update URL
|
||||
./manage_monitors.py update --id 123 --url "https://newurl.com"
|
||||
|
||||
# Update interval
|
||||
./manage_monitors.py update --id 123 --interval 300
|
||||
|
||||
# Update multiple fields
|
||||
./manage_monitors.py update --id 123 --name "Updated" --url "https://updated.com" --interval 120
|
||||
```
|
||||
|
||||
### Delete a Monitor
|
||||
|
||||
```bash
|
||||
./manage_monitors.py delete --id 123
|
||||
```
|
||||
|
||||
## Monitor Types
|
||||
|
||||
| Type | Description | URL Format |
|
||||
|------|-------------|------------|
|
||||
| `http` | HTTP/HTTPS endpoint | `https://example.com` or `http://example.com` |
|
||||
| `tcp` | TCP port check | `hostname:port` or `ip:port` |
|
||||
| `ping` | ICMP ping | `hostname` or `ip` |
|
||||
| `dns` | DNS resolution | `example.com` |
|
||||
| `docker` | Docker container | Container name or ID |
|
||||
|
||||
## Authentication
|
||||
|
||||
The script checks for credentials in the following order:
|
||||
|
||||
1. `UPTIME_KUMA_API_KEY` environment variable (preferred)
|
||||
2. `UPTIME_KUMA_USERNAME` and `UPTIME_KUMA_PASSWORD` environment variables
|
||||
|
||||
To persist credentials, add to your shell profile (`~/.bashrc` or `~/.zshrc`):
|
||||
|
||||
```bash
|
||||
export UPTIME_KUMA_API_KEY="uk1_your_api_key_here"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No credentials provided" Error
|
||||
|
||||
**Problem:** Script cannot find API credentials.
|
||||
|
||||
**Solution:** Set environment variables before running:
|
||||
```bash
|
||||
export UPTIME_KUMA_API_KEY="uk1_your_key"
|
||||
./manage_monitors.py list
|
||||
```
|
||||
|
||||
### Connection Refused
|
||||
|
||||
**Problem:** Cannot connect to Uptime Kuma instance.
|
||||
|
||||
**Solution:**
|
||||
1. Verify service is running: `docker ps | grep uptime-kuma`
|
||||
2. Check URL in script matches web UI access
|
||||
3. Ensure SSL certificate is valid (or use `verify=False` in script for self-signed)
|
||||
|
||||
### "Monitor not found" Error
|
||||
|
||||
**Problem:** Monitor ID doesn't exist.
|
||||
|
||||
**Solution:** Run `./manage_monitors.py list` to see all valid monitor IDs.
|
||||
|
||||
### Import Error: uptime_kuma_api
|
||||
|
||||
**Problem:** Python package not installed.
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
pip3 install uptime-kuma-api
|
||||
# Or with sudo if needed:
|
||||
sudo pip3 install uptime-kuma-api
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Using as a Python Module
|
||||
|
||||
```python
|
||||
from uptime_kuma_api import UptimeKumaApi, MonitorType
|
||||
|
||||
# Connect
|
||||
api = UptimeKumaApi("https://uptime.kolpacksoftware.com")
|
||||
api.login_by_token("uk1_your_api_key")
|
||||
|
||||
# Get all monitors
|
||||
monitors = api.get_monitors()
|
||||
|
||||
# Add monitor
|
||||
api.add_monitor(
|
||||
type=MonitorType.HTTP,
|
||||
name="My Service",
|
||||
url="https://example.com",
|
||||
interval=60
|
||||
)
|
||||
|
||||
# Edit monitor
|
||||
monitor = api.get_monitor(1)
|
||||
monitor['name'] = "Updated Name"
|
||||
api.edit_monitor(1, **monitor)
|
||||
|
||||
# Delete monitor
|
||||
api.delete_monitor(1)
|
||||
|
||||
# Always disconnect
|
||||
api.disconnect()
|
||||
```
|
||||
|
||||
## Tag Management
|
||||
|
||||
Tags help organize monitors by category. See [TAG_MANAGEMENT.md](TAG_MANAGEMENT.md) for comprehensive tag documentation.
|
||||
|
||||
### Quick Tag Commands
|
||||
|
||||
**Tag a new monitor:**
|
||||
```bash
|
||||
# After creating a monitor, tag it quickly
|
||||
./tag_monitor.sh MONITOR_ID TAG_ID [TAG_ID...]
|
||||
|
||||
# Example: Tag monitor 15 as application + web
|
||||
./tag_monitor.sh 15 3 4
|
||||
```
|
||||
|
||||
**View all tags:**
|
||||
```bash
|
||||
./check_tags.py
|
||||
```
|
||||
|
||||
**Common Tag IDs:**
|
||||
- 1=hardware, 2=monitoring, 3=application, 4=web, 5=database
|
||||
- 6=media, 7=infrastructure, 8=storage, 9=virtualization, 10=vm
|
||||
|
||||
**Full workflow for adding a tagged monitor:**
|
||||
```bash
|
||||
# 1. Add monitor and note the ID
|
||||
./manage_monitors.py add --name "New Service" --url "https://example.com"
|
||||
./manage_monitors.py list | grep "New Service"
|
||||
|
||||
# 2. Tag it
|
||||
./tag_monitor.sh 16 3 4 # application + web
|
||||
|
||||
# 3. Verify
|
||||
./check_tags.py | grep "New Service"
|
||||
```
|
||||
|
||||
For detailed tagging strategies and workflows, see **[TAG_MANAGEMENT.md](TAG_MANAGEMENT.md)**.
|
||||
|
||||
## References
|
||||
|
||||
- [Uptime Kuma API Keys Documentation](https://github.com/louislam/uptime-kuma/wiki/API-Keys)
|
||||
- [Uptime Kuma API Documentation](https://github.com/louislam/uptime-kuma/wiki/API-Documentation)
|
||||
- [uptime-kuma-api Python Package](https://github.com/lucasheld/uptime-kuma-api)
|
||||
- [uptime-kuma-api Documentation](https://uptime-kuma-api.readthedocs.io/)
|
||||
|
||||
## Web UI Access
|
||||
|
||||
- **URL:** https://uptime.kolpacksoftware.com
|
||||
- **Settings:** Click gear icon → API Keys
|
||||
- **Monitor Management:** Dashboard → Add/Edit monitors via UI
|
||||
@@ -0,0 +1,206 @@
|
||||
# Uptime Kuma Management
|
||||
|
||||
Complete documentation for managing Uptime Kuma monitors and tags via CLI and API.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Prerequisites
|
||||
```bash
|
||||
# Set credentials (add to ~/.bashrc for persistence)
|
||||
export UPTIME_KUMA_USERNAME="poprhythm"
|
||||
export UPTIME_KUMA_PASSWORD="your_password"
|
||||
```
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# List all monitors
|
||||
./manage_monitors.py list
|
||||
|
||||
# Add a monitor
|
||||
./manage_monitors.py add --name "Service" --url "https://example.com"
|
||||
|
||||
# Update a monitor
|
||||
./manage_monitors.py update --id 123 --interval 120
|
||||
|
||||
# Delete a monitor
|
||||
./manage_monitors.py delete --id 123
|
||||
|
||||
# Tag a monitor
|
||||
./tag_monitor.sh 123 3 4 # Tag as application + web
|
||||
|
||||
# View all tags
|
||||
./check_tags.py
|
||||
```
|
||||
|
||||
## Documentation Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| **[SETUP.md](SETUP.md)** | Initial setup and installation |
|
||||
| **[API.md](API.md)** | Complete API usage and examples |
|
||||
| **[TAG_MANAGEMENT.md](TAG_MANAGEMENT.md)** | Comprehensive tag management guide |
|
||||
| **[TAG_SUMMARY.md](TAG_SUMMARY.md)** | Current tag schema and assignments |
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `manage_monitors.py` | Main CLI for monitor CRUD operations |
|
||||
| `tag_monitor.sh` | Quick script to tag monitors |
|
||||
| `check_tags.py` | View current tag assignments |
|
||||
| `add_tags.py` | Bulk tag assignment (requires editing) |
|
||||
| `fix_missing_tags.py` | Add specific tags to specific monitors |
|
||||
|
||||
## Complete Workflow: Adding a New Monitor
|
||||
|
||||
### Step 1: Add the Monitor
|
||||
```bash
|
||||
./manage_monitors.py add \
|
||||
--name "New Service" \
|
||||
--url "https://newservice.com" \
|
||||
--type http \
|
||||
--interval 60
|
||||
```
|
||||
|
||||
### Step 2: Get Monitor ID
|
||||
```bash
|
||||
./manage_monitors.py list | grep "New Service"
|
||||
# Output: 16 New Service http https://newservice.com
|
||||
```
|
||||
|
||||
### Step 3: Tag the Monitor
|
||||
```bash
|
||||
# Choose appropriate tags based on service type:
|
||||
# - Web service: 3 (application) + 4 (web)
|
||||
# - Database: 3 (application) + 5 (database)
|
||||
# - Storage: 7 (infrastructure) + 8 (storage)
|
||||
# - VM: 7 (infrastructure) + 10 (vm)
|
||||
|
||||
./tag_monitor.sh 16 3 4
|
||||
```
|
||||
|
||||
### Step 4: Verify
|
||||
```bash
|
||||
./check_tags.py | grep "New Service"
|
||||
# Should show the monitor with its tags
|
||||
```
|
||||
|
||||
## Tag Reference
|
||||
|
||||
| ID | Tag | Use For |
|
||||
|----|-----|---------|
|
||||
| 1 | hardware | Physical hardware monitoring |
|
||||
| 2 | monitoring | Monitoring systems |
|
||||
| 3 | application | Software applications |
|
||||
| 4 | web | Web services |
|
||||
| 5 | database | Database systems |
|
||||
| 6 | media | Media servers |
|
||||
| 7 | infrastructure | Core infrastructure |
|
||||
| 8 | storage | Storage systems |
|
||||
| 9 | virtualization | Virtualization platforms |
|
||||
| 10 | vm | Virtual machines |
|
||||
|
||||
## Monitor Types
|
||||
|
||||
| Type | Example URL | Use For |
|
||||
|------|-------------|---------|
|
||||
| `http` | `https://example.com` | HTTP/HTTPS endpoints |
|
||||
| `tcp` | `hostname:port` | TCP port checks |
|
||||
| `ping` | `192.168.1.1` | ICMP ping |
|
||||
| `dns` | `example.com` | DNS resolution |
|
||||
| `docker` | Container name | Docker container health |
|
||||
|
||||
## Common Tagging Patterns
|
||||
|
||||
```bash
|
||||
# Web application
|
||||
./manage_monitors.py add --name "GitLab" --url "https://gitlab.example.com"
|
||||
./tag_monitor.sh NEW_ID 3 4 # application + web
|
||||
|
||||
# Database
|
||||
./manage_monitors.py add --name "PostgreSQL" --url "db.example.com:5432" --type tcp
|
||||
./tag_monitor.sh NEW_ID 3 5 # application + database
|
||||
|
||||
# Storage/NAS
|
||||
./manage_monitors.py add --name "Storage" --url "nas.example.com" --type ping
|
||||
./tag_monitor.sh NEW_ID 7 8 # infrastructure + storage
|
||||
|
||||
# Virtual Machine
|
||||
./manage_monitors.py add --name "VM Host" --url "vm.example.com" --type ping
|
||||
./tag_monitor.sh NEW_ID 7 10 # infrastructure + vm
|
||||
|
||||
# Hardware monitoring
|
||||
./manage_monitors.py add --name "Server Fan" --url "..." --type mqtt
|
||||
./tag_monitor.sh NEW_ID 1 2 # hardware + monitoring
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Issues
|
||||
```bash
|
||||
# Check credentials are set
|
||||
echo $UPTIME_KUMA_USERNAME
|
||||
echo $UPTIME_KUMA_PASSWORD
|
||||
|
||||
# If not set, export them
|
||||
export UPTIME_KUMA_USERNAME="poprhythm"
|
||||
export UPTIME_KUMA_PASSWORD="your_password"
|
||||
```
|
||||
|
||||
### Monitor Not Found
|
||||
```bash
|
||||
# List all monitors to verify ID
|
||||
./manage_monitors.py list
|
||||
```
|
||||
|
||||
### Tag Already Exists
|
||||
This is normal if you try to add a tag that's already assigned. It can be ignored.
|
||||
|
||||
### SSL Certificate Warnings
|
||||
These warnings are expected for self-signed certificates and can be ignored. The scripts use `ssl_verify=False` to handle this.
|
||||
|
||||
## Web UI
|
||||
|
||||
Access the web interface at: **https://uptime.kolpacksoftware.com**
|
||||
|
||||
- Filter by tags
|
||||
- View status pages
|
||||
- Manage notifications
|
||||
- Configure settings
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always tag new monitors** immediately after creation
|
||||
2. **Use 2-3 tags** per monitor for better organization
|
||||
3. **Document custom tags** if you add new tag types
|
||||
4. **Run audits** periodically with `check_tags.py`
|
||||
5. **Set credentials** persistently in your shell profile
|
||||
6. **Use descriptive names** for monitors (include service purpose)
|
||||
7. **Group related services** using consistent tagging
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Setup issues**: See [SETUP.md](SETUP.md)
|
||||
- **API questions**: See [API.md](API.md)
|
||||
- **Tag management**: See [TAG_MANAGEMENT.md](TAG_MANAGEMENT.md)
|
||||
- **Current tags**: See [TAG_SUMMARY.md](TAG_SUMMARY.md)
|
||||
- **Uptime Kuma docs**: https://github.com/louislam/uptime-kuma/wiki
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Regular Tasks
|
||||
|
||||
**Weekly:**
|
||||
- Review untagged monitors: `./check_tags.py | grep "No tags"`
|
||||
- Verify critical monitors are up
|
||||
|
||||
**Monthly:**
|
||||
- Review and update monitor intervals
|
||||
- Clean up old/unused monitors
|
||||
- Verify tag schema still makes sense
|
||||
|
||||
**As Needed:**
|
||||
- Update documentation when adding new tag types
|
||||
- Adjust check intervals based on service criticality
|
||||
- Review and optimize notification rules
|
||||
@@ -0,0 +1,189 @@
|
||||
# Uptime Kuma API Setup Guide
|
||||
|
||||
## Prerequisites Installation
|
||||
|
||||
The management script requires Python 3.7+ and the `uptime-kuma-api` package.
|
||||
|
||||
### Step 1: Install pip (if not already installed)
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install python3-pip -y
|
||||
```
|
||||
|
||||
### Step 2: Install uptime-kuma-api Package
|
||||
|
||||
```bash
|
||||
pip3 install uptime-kuma-api
|
||||
# Or if pip3 is not in PATH:
|
||||
python3 -m pip install uptime-kuma-api
|
||||
```
|
||||
|
||||
**Verify installation:**
|
||||
```bash
|
||||
python3 -c "import uptime_kuma_api; print('Package installed successfully')"
|
||||
```
|
||||
|
||||
## Authentication Setup
|
||||
|
||||
You must configure API credentials before using the management script.
|
||||
|
||||
### Option A: API Key (Recommended)
|
||||
|
||||
1. **Access Uptime Kuma web UI:**
|
||||
- URL: https://uptime.kolpacksoftware.com
|
||||
- Log in with your credentials
|
||||
|
||||
2. **Generate API Key:**
|
||||
- Click the gear icon (Settings)
|
||||
- Navigate to **API Keys** section
|
||||
- Click **Generate**
|
||||
- Set a descriptive name (e.g., "CLI Management")
|
||||
- Set expiry (or "Never expire")
|
||||
- **IMPORTANT:** Copy the key immediately - it's only shown once!
|
||||
|
||||
3. **Configure Environment Variable:**
|
||||
|
||||
Add to `~/.bashrc` or `~/.zshrc`:
|
||||
```bash
|
||||
export UPTIME_KUMA_API_KEY="uk1_your_api_key_here"
|
||||
```
|
||||
|
||||
Or set temporarily:
|
||||
```bash
|
||||
export UPTIME_KUMA_API_KEY="uk1_your_api_key_here"
|
||||
```
|
||||
|
||||
Apply changes:
|
||||
```bash
|
||||
source ~/.bashrc # or source ~/.zshrc
|
||||
```
|
||||
|
||||
### Option B: Username/Password
|
||||
|
||||
```bash
|
||||
export UPTIME_KUMA_USERNAME="your_username"
|
||||
export UPTIME_KUMA_PASSWORD="your_password"
|
||||
```
|
||||
|
||||
**Note:** Once you create the first API key, username/password authentication is permanently disabled for API endpoints. API keys are more secure and recommended.
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Check Script is Executable
|
||||
|
||||
```bash
|
||||
cd /home/poprhythm/docker-infrastructure
|
||||
ls -l uptime-kuma/manage_monitors.py
|
||||
# Should show -rwxr-xr-x (executable permissions)
|
||||
```
|
||||
|
||||
### Test 2: List Existing Monitors
|
||||
|
||||
```bash
|
||||
cd /home/poprhythm/docker-infrastructure
|
||||
./uptime-kuma/manage_monitors.py list
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
ID Name Type URL
|
||||
------------------------------------------------------------------------------------------------
|
||||
1 Example Service http https://example.com
|
||||
...
|
||||
```
|
||||
|
||||
### Test 3: Add a Test Monitor
|
||||
|
||||
```bash
|
||||
./uptime-kuma/manage_monitors.py add --name "Test Monitor" --url "https://httpbin.org/status/200" --type http --interval 300
|
||||
```
|
||||
|
||||
### Test 4: Delete the Test Monitor
|
||||
|
||||
```bash
|
||||
# Find the ID from the list command
|
||||
./uptime-kuma/manage_monitors.py list
|
||||
|
||||
# Delete it (replace 999 with actual ID)
|
||||
./uptime-kuma/manage_monitors.py delete --id 999
|
||||
```
|
||||
|
||||
### Test 5: Verify in Web UI
|
||||
|
||||
1. Access https://uptime.kolpacksoftware.com
|
||||
2. Check that the test monitor was created and deleted
|
||||
3. Verify existing monitors match the CLI list output
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No credentials provided" Error
|
||||
|
||||
**Cause:** Environment variables not set.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Check current environment
|
||||
echo $UPTIME_KUMA_API_KEY
|
||||
|
||||
# If empty, set it:
|
||||
export UPTIME_KUMA_API_KEY="uk1_your_key"
|
||||
```
|
||||
|
||||
### "ModuleNotFoundError: No module named 'uptime_kuma_api'"
|
||||
|
||||
**Cause:** Package not installed or installed for wrong Python version.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Install for your Python 3
|
||||
python3 -m pip install --user uptime-kuma-api
|
||||
|
||||
# Or system-wide (requires sudo)
|
||||
sudo python3 -m pip install uptime-kuma-api
|
||||
```
|
||||
|
||||
### Connection Errors
|
||||
|
||||
**Cause:** Uptime Kuma service not running or URL incorrect.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Check service is running
|
||||
docker ps | grep uptime-kuma
|
||||
|
||||
# Test web UI access
|
||||
curl -k https://uptime.kolpacksoftware.com
|
||||
|
||||
# If needed, start the service
|
||||
cd /home/poprhythm/docker-infrastructure/uptime-kuma
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### SSL Certificate Errors
|
||||
|
||||
**Cause:** Self-signed certificate or certificate validation issues.
|
||||
|
||||
**Fix:** If using self-signed certificates, you may need to modify the script to disable SSL verification (not recommended for production).
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Install prerequisites (`python3-pip`, `uptime-kuma-api`)
|
||||
2. ✅ Create API key in web UI
|
||||
3. ✅ Set environment variable
|
||||
4. ✅ Test with `./manage_monitors.py list`
|
||||
5. 📖 Read [API.md](API.md) for full usage documentation
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Set credentials (add to ~/.bashrc for persistence)
|
||||
export UPTIME_KUMA_API_KEY="uk1_your_api_key"
|
||||
|
||||
# Common commands
|
||||
cd /home/poprhythm/docker-infrastructure
|
||||
./uptime-kuma/manage_monitors.py list
|
||||
./uptime-kuma/manage_monitors.py add --name "Service" --url "https://example.com"
|
||||
./uptime-kuma/manage_monitors.py update --id 123 --interval 120
|
||||
./uptime-kuma/manage_monitors.py delete --id 123
|
||||
```
|
||||
@@ -0,0 +1,264 @@
|
||||
# Tag Management Guide
|
||||
|
||||
This guide covers managing tags and tag assignments in Uptime Kuma.
|
||||
|
||||
## Overview
|
||||
|
||||
Tags help organize and categorize monitors. Each monitor can have multiple tags, and you can filter monitors by tags in the web UI.
|
||||
|
||||
## Current Tag Schema
|
||||
|
||||
### Service Categories
|
||||
|
||||
| Tag Name | Color | Use For |
|
||||
|----------|-------|---------|
|
||||
| `hardware` | 🔴 Red | Physical hardware (fans, sensors) |
|
||||
| `monitoring` | 🟠 Orange | Monitoring systems and health checks |
|
||||
| `application` | 🔵 Blue | Software applications and services |
|
||||
| `web` | 🔷 Cyan | Web services and HTTP endpoints |
|
||||
| `database` | 🟣 Purple | Database systems |
|
||||
| `media` | 🌸 Pink | Media servers (Plex, etc.) |
|
||||
| `infrastructure` | 🟢 Green | Core infrastructure components |
|
||||
| `storage` | 🟢 Light Green | Storage systems (NAS, file servers) |
|
||||
| `virtualization` | 🟦 Teal | Virtualization platforms (Proxmox) |
|
||||
| `vm` | 🔹 Blue Grey | Virtual machines |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Adding a New Monitor with Tags
|
||||
|
||||
**Step 1: Add the monitor**
|
||||
```bash
|
||||
export UPTIME_KUMA_USERNAME="your_username"
|
||||
export UPTIME_KUMA_PASSWORD="your_password"
|
||||
|
||||
./manage_monitors.py add --name "New Service" --url "https://newservice.com" --type http
|
||||
```
|
||||
|
||||
**Step 2: Note the monitor ID from the output or list**
|
||||
```bash
|
||||
./manage_monitors.py list
|
||||
# Note the ID of your new monitor (e.g., 15)
|
||||
```
|
||||
|
||||
**Step 3: Add tags using Python API**
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
|
||||
api = UptimeKumaApi("https://uptime.kolpacksoftware.com", ssl_verify=False)
|
||||
api.login("username", "password")
|
||||
|
||||
# Add tags to monitor
|
||||
api.add_monitor_tag(tag_id=3, monitor_id=15, value='') # application
|
||||
api.add_monitor_tag(tag_id=4, monitor_id=15, value='') # web
|
||||
|
||||
api.disconnect()
|
||||
```
|
||||
|
||||
### Quick Tag Script
|
||||
|
||||
Create a simple script to tag a new monitor:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# tag_monitor.sh - Quick script to tag a monitor
|
||||
# Usage: ./tag_monitor.sh MONITOR_ID TAG_ID [TAG_ID...]
|
||||
|
||||
MONITOR_ID=$1
|
||||
shift
|
||||
|
||||
python3 << EOF
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
import os
|
||||
|
||||
api = UptimeKumaApi("https://uptime.kolpacksoftware.com", ssl_verify=False)
|
||||
api.login(os.getenv('UPTIME_KUMA_USERNAME'), os.getenv('UPTIME_KUMA_PASSWORD'))
|
||||
|
||||
for tag_id in [${@}]:
|
||||
try:
|
||||
api.add_monitor_tag(tag_id=tag_id, monitor_id=${MONITOR_ID}, value='')
|
||||
print(f"✓ Added tag {tag_id} to monitor ${MONITOR_ID}")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed: {e}")
|
||||
|
||||
api.disconnect()
|
||||
EOF
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Tag monitor 15 as application + web
|
||||
export UPTIME_KUMA_USERNAME="poprhythm"
|
||||
export UPTIME_KUMA_PASSWORD="your_password"
|
||||
./tag_monitor.sh 15 3 4
|
||||
```
|
||||
|
||||
### Viewing Tags on a Monitor
|
||||
|
||||
```bash
|
||||
./check_tags.py
|
||||
```
|
||||
|
||||
Or check a specific monitor:
|
||||
```python
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
|
||||
api = UptimeKumaApi("https://uptime.kolpacksoftware.com", ssl_verify=False)
|
||||
api.login("username", "password")
|
||||
|
||||
monitor = api.get_monitor(15)
|
||||
print(f"Tags: {monitor.get('tags', [])}")
|
||||
|
||||
api.disconnect()
|
||||
```
|
||||
|
||||
### Removing a Tag from a Monitor
|
||||
|
||||
```python
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
|
||||
api = UptimeKumaApi("https://uptime.kolpacksoftware.com", ssl_verify=False)
|
||||
api.login("username", "password")
|
||||
|
||||
# Remove tag_id 3 from monitor 15
|
||||
api.delete_monitor_tag(tag_id=3, monitor_id=15, value='')
|
||||
|
||||
api.disconnect()
|
||||
```
|
||||
|
||||
### Adding New Tag Types
|
||||
|
||||
If you need to create a new tag:
|
||||
|
||||
```python
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
|
||||
api = UptimeKumaApi("https://uptime.kolpacksoftware.com", ssl_verify=False)
|
||||
api.login("username", "password")
|
||||
|
||||
# Create a new tag
|
||||
result = api.add_tag(name="production", color="#FF5722")
|
||||
print(f"Created tag with ID: {result['id']}")
|
||||
|
||||
api.disconnect()
|
||||
```
|
||||
|
||||
## Tag Assignment Patterns
|
||||
|
||||
### By Service Type
|
||||
- **Web Services**: `application` + `web`
|
||||
- **Databases**: `application` + `database`
|
||||
- **Media Servers**: `application` + `media`
|
||||
- **Storage Systems**: `infrastructure` + `storage`
|
||||
- **Virtual Machines**: `infrastructure` + `vm`
|
||||
- **Hardware Monitoring**: `hardware` + `monitoring`
|
||||
- **Virtualization Platforms**: `infrastructure` + `virtualization`
|
||||
|
||||
### By Environment (if needed)
|
||||
You can create additional tags for environments:
|
||||
```python
|
||||
api.add_tag(name="production", color="#4CAF50")
|
||||
api.add_tag(name="staging", color="#FFC107")
|
||||
api.add_tag(name="development", color="#2196F3")
|
||||
```
|
||||
|
||||
### By Priority (if needed)
|
||||
```python
|
||||
api.add_tag(name="critical", color="#F44336")
|
||||
api.add_tag(name="high", color="#FF9800")
|
||||
api.add_tag(name="normal", color="#4CAF50")
|
||||
api.add_tag(name="low", color="#9E9E9E")
|
||||
```
|
||||
|
||||
## Complete Example: Adding a New Service
|
||||
|
||||
```bash
|
||||
# 1. Add the monitor
|
||||
./manage_monitors.py add \
|
||||
--name "GitLab" \
|
||||
--url "https://gitlab.example.com" \
|
||||
--type http \
|
||||
--interval 60
|
||||
|
||||
# 2. List to get the ID
|
||||
./manage_monitors.py list | grep GitLab
|
||||
# Output: 16 GitLab http https://gitlab.example.com
|
||||
|
||||
# 3. Tag it (using Python one-liner)
|
||||
python3 -c "
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
import os
|
||||
api = UptimeKumaApi('https://uptime.kolpacksoftware.com', ssl_verify=False)
|
||||
api.login(os.getenv('UPTIME_KUMA_USERNAME'), os.getenv('UPTIME_KUMA_PASSWORD'))
|
||||
api.add_monitor_tag(tag_id=3, monitor_id=16, value='') # application
|
||||
api.add_monitor_tag(tag_id=4, monitor_id=16, value='') # web
|
||||
api.disconnect()
|
||||
print('Tagged successfully!')
|
||||
"
|
||||
|
||||
# 4. Verify
|
||||
./check_tags.py | grep GitLab
|
||||
```
|
||||
|
||||
## Tag Reference Quick Lookup
|
||||
|
||||
| Tag ID | Tag Name | Common Use |
|
||||
|--------|----------|------------|
|
||||
| 1 | hardware | Server fans, sensors |
|
||||
| 2 | monitoring | Health checks |
|
||||
| 3 | application | Any software service |
|
||||
| 4 | web | HTTP/HTTPS services |
|
||||
| 5 | database | PostgreSQL, MySQL, CouchDB, etc. |
|
||||
| 6 | media | Plex, Jellyfin, etc. |
|
||||
| 7 | infrastructure | Core systems |
|
||||
| 8 | storage | NAS, file servers |
|
||||
| 9 | virtualization | Proxmox, ESXi |
|
||||
| 10 | vm | Virtual machine instances |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Consistent Naming**: Use lowercase for tag names
|
||||
2. **Multiple Tags**: Apply 2-3 relevant tags per monitor for better organization
|
||||
3. **Color Coding**: Use similar colors for related tags (all infrastructure tags are shades of green)
|
||||
4. **Document Custom Tags**: If you add custom tags, document them in this file
|
||||
5. **Tag on Creation**: Always tag new monitors immediately after creation
|
||||
6. **Regular Audits**: Run `check_tags.py` periodically to find untagged monitors
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Tag already exists" Error
|
||||
This means the tag is already assigned to the monitor. You can safely ignore this.
|
||||
|
||||
### "Monitor not found"
|
||||
Double-check the monitor ID with `./manage_monitors.py list`.
|
||||
|
||||
### Session Timeout
|
||||
If you're tagging many monitors, you may need to reconnect. The API session can timeout after several minutes of inactivity.
|
||||
|
||||
### View All Tags
|
||||
```python
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
api = UptimeKumaApi("https://uptime.kolpacksoftware.com", ssl_verify=False)
|
||||
api.login("username", "password")
|
||||
tags = api.get_tags()
|
||||
for tag in tags:
|
||||
print(f"ID {tag['id']}: {tag['name']} ({tag['color']})")
|
||||
api.disconnect()
|
||||
```
|
||||
|
||||
## Scripts Available
|
||||
|
||||
- `check_tags.py` - View all monitor tag assignments
|
||||
- `add_tags.py` - Bulk tag assignment (requires editing for new monitors)
|
||||
- `fix_missing_tags.py` - Add specific tags to specific monitors
|
||||
- `TAG_SUMMARY.md` - Current tag schema documentation
|
||||
|
||||
## Web UI Management
|
||||
|
||||
For one-off changes, the web UI is often easier:
|
||||
1. Go to https://uptime.kolpacksoftware.com
|
||||
2. Click on a monitor to edit it
|
||||
3. Scroll to the "Tags" section
|
||||
4. Add/remove tags as needed
|
||||
5. Click "Save"
|
||||
@@ -0,0 +1,61 @@
|
||||
# Uptime Kuma Monitor Tags Summary
|
||||
|
||||
## All Tags Created
|
||||
|
||||
| Tag ID | Tag Name | Color | Category |
|
||||
|--------|----------|-------|----------|
|
||||
| 1 | hardware | 🔴 Red | Physical hardware monitoring |
|
||||
| 2 | monitoring | 🟠 Orange | Monitoring systems |
|
||||
| 3 | application | 🔵 Blue | Software applications |
|
||||
| 4 | web | 🔷 Cyan | Web services |
|
||||
| 5 | database | 🟣 Purple | Database systems |
|
||||
| 6 | media | 🌸 Pink | Media servers |
|
||||
| 7 | infrastructure | 🟢 Green | Core infrastructure |
|
||||
| 8 | storage | 🟢 Light Green | Storage systems |
|
||||
| 9 | virtualization | 🟦 Teal | Virtualization platforms |
|
||||
| 10 | vm | 🔹 Blue Grey | Virtual machines |
|
||||
|
||||
## Monitor Tag Assignments
|
||||
|
||||
### Hardware/Monitoring (4 monitors)
|
||||
| ID | Monitor Name | Tags |
|
||||
|----|--------------|------|
|
||||
| 1 | Server Fan Online | `hardware` `monitoring` |
|
||||
| 2 | Server Front Fan Stalled | `hardware` `monitoring` |
|
||||
| 3 | Server Back Fan Stalled | `hardware` `monitoring` |
|
||||
| 4 | Server Fan | `hardware` `monitoring` |
|
||||
|
||||
### Applications (3 monitors)
|
||||
| ID | Monitor Name | Tags |
|
||||
|----|--------------|------|
|
||||
| 5 | TSA Chapter Organizer RMS | `application` `web` |
|
||||
| 6 | couchdb | `application` `database` |
|
||||
| 7 | Plex | `application` `media` |
|
||||
|
||||
### Infrastructure (5 monitors)
|
||||
| ID | Monitor Name | Tags |
|
||||
|----|--------------|------|
|
||||
| 8 | unraid | `infrastructure` `storage` |
|
||||
| 10 | pallas VM | `infrastructure` `vm` |
|
||||
| 11 | NAS | `infrastructure` `storage` |
|
||||
| 13 | Proxmox | `infrastructure` `virtualization` |
|
||||
| 14 | dev-win10 VM | `infrastructure` `vm` |
|
||||
|
||||
## Using Tags in Uptime Kuma
|
||||
|
||||
1. **Filter by Tag**: In the web UI, click on a tag to filter monitors
|
||||
2. **View All Tags**: Navigate to Settings → Tags to manage
|
||||
3. **Add/Edit Tags**: Click on a monitor → Edit → Tags section
|
||||
4. **Status Pages**: Create status pages filtered by specific tags
|
||||
|
||||
## Scripts Available
|
||||
|
||||
- `add_tags.py` - Create tags and assign them to monitors
|
||||
- `check_tags.py` - Verify current tag assignments
|
||||
- `fix_missing_tags.py` - Add missing tags to specific monitors
|
||||
|
||||
## Web UI
|
||||
|
||||
Access Uptime Kuma at: https://uptime.kolpacksoftware.com
|
||||
|
||||
All tags are now visible and active in the web interface!
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Add service category tags to Uptime Kuma monitors
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
|
||||
# Configuration
|
||||
UPTIME_KUMA_URL = "https://uptime.kolpacksoftware.com"
|
||||
USERNAME = os.getenv('UPTIME_KUMA_USERNAME')
|
||||
PASSWORD = os.getenv('UPTIME_KUMA_PASSWORD')
|
||||
|
||||
# Service category mappings
|
||||
MONITOR_TAGS = {
|
||||
# Hardware/Monitoring
|
||||
1: ['hardware', 'monitoring'], # Server Fan Online
|
||||
2: ['hardware', 'monitoring'], # Server Front Fan Stalled
|
||||
3: ['hardware', 'monitoring'], # Server Back Fan Stalled
|
||||
4: ['hardware', 'monitoring'], # Server Fan (group)
|
||||
|
||||
# Applications
|
||||
5: ['application', 'web'], # TSA Chapter Organizer RMS
|
||||
6: ['database', 'application'], # couchdb
|
||||
7: ['media', 'application'], # Plex
|
||||
|
||||
# Infrastructure
|
||||
8: ['infrastructure', 'storage'], # unraid
|
||||
13: ['infrastructure', 'virtualization'], # Proxmox
|
||||
|
||||
# Network/Hosts
|
||||
10: ['infrastructure', 'vm'], # pallas VM
|
||||
11: ['infrastructure', 'storage'], # NAS
|
||||
14: ['infrastructure', 'vm'], # dev-win10 VM
|
||||
}
|
||||
|
||||
def connect():
|
||||
"""Connect to Uptime Kuma API"""
|
||||
api = UptimeKumaApi(UPTIME_KUMA_URL, ssl_verify=False)
|
||||
|
||||
if USERNAME and PASSWORD:
|
||||
api.login(USERNAME, PASSWORD)
|
||||
else:
|
||||
print("Error: Set UPTIME_KUMA_USERNAME and UPTIME_KUMA_PASSWORD")
|
||||
sys.exit(1)
|
||||
|
||||
return api
|
||||
|
||||
def ensure_tags_exist(api):
|
||||
"""Create all needed tags upfront"""
|
||||
tag_colors = {
|
||||
'hardware': '#F44336', # Red
|
||||
'monitoring': '#FF9800', # Orange
|
||||
'application': '#2196F3', # Blue
|
||||
'web': '#00BCD4', # Cyan
|
||||
'database': '#9C27B0', # Purple
|
||||
'media': '#E91E63', # Pink
|
||||
'infrastructure': '#4CAF50', # Green
|
||||
'storage': '#8BC34A', # Light Green
|
||||
'virtualization': '#009688', # Teal
|
||||
'vm': '#607D8B', # Blue Grey
|
||||
}
|
||||
|
||||
existing_tags = api.get_tags()
|
||||
# Tags might have 'id' or 'tag_id'
|
||||
existing_tag_names = {tag['name']: (tag.get('tag_id') or tag.get('id')) for tag in existing_tags}
|
||||
tag_map = {}
|
||||
|
||||
print("Ensuring all tags exist...")
|
||||
for tag_name, color in tag_colors.items():
|
||||
if tag_name in existing_tag_names:
|
||||
tag_map[tag_name] = existing_tag_names[tag_name]
|
||||
print(f" ✓ Tag '{tag_name}' already exists (ID: {tag_map[tag_name]})")
|
||||
else:
|
||||
result = api.add_tag(name=tag_name, color=color)
|
||||
# The result might be the tag dict itself or contain it
|
||||
if isinstance(result, dict):
|
||||
tag_id = result.get('tag_id') or result.get('id')
|
||||
else:
|
||||
tag_id = result
|
||||
tag_map[tag_name] = tag_id
|
||||
print(f" + Created tag '{tag_name}' (ID: {tag_map[tag_name]})")
|
||||
|
||||
return tag_map
|
||||
|
||||
def add_tags_to_monitor(api, monitor_id, tag_names, tag_map):
|
||||
"""Add tags to a monitor using add_monitor_tag method"""
|
||||
# Get monitor details for name
|
||||
monitor = api.get_monitor(monitor_id)
|
||||
|
||||
# Add each tag using the dedicated method
|
||||
for tag_name in tag_names:
|
||||
if tag_name in tag_map:
|
||||
tag_id = tag_map[tag_name]
|
||||
try:
|
||||
api.add_monitor_tag(tag_id=tag_id, monitor_id=monitor_id, value='')
|
||||
except Exception as e:
|
||||
# Tag might already be assigned, that's okay
|
||||
if 'already exists' not in str(e).lower():
|
||||
raise
|
||||
|
||||
print(f" ✓ Monitor {monitor_id} ({monitor['name']}): Added tags {tag_names}")
|
||||
|
||||
def main():
|
||||
api = connect()
|
||||
|
||||
try:
|
||||
# First, ensure all tags exist
|
||||
tag_map = ensure_tags_exist(api)
|
||||
|
||||
print("\nAdding tags to monitors...\n")
|
||||
|
||||
for monitor_id, tags in MONITOR_TAGS.items():
|
||||
try:
|
||||
add_tags_to_monitor(api, monitor_id, tags, tag_map)
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed to tag monitor {monitor_id}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print("\n✅ Tag assignment complete!")
|
||||
|
||||
finally:
|
||||
api.disconnect()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check current tag assignments on monitors
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
|
||||
UPTIME_KUMA_URL = "https://uptime.kolpacksoftware.com"
|
||||
USERNAME = os.getenv('UPTIME_KUMA_USERNAME')
|
||||
PASSWORD = os.getenv('UPTIME_KUMA_PASSWORD')
|
||||
|
||||
def connect():
|
||||
api = UptimeKumaApi(UPTIME_KUMA_URL, ssl_verify=False)
|
||||
if USERNAME and PASSWORD:
|
||||
api.login(USERNAME, PASSWORD)
|
||||
else:
|
||||
print("Error: Set UPTIME_KUMA_USERNAME and UPTIME_KUMA_PASSWORD")
|
||||
sys.exit(1)
|
||||
return api
|
||||
|
||||
def main():
|
||||
api = connect()
|
||||
|
||||
try:
|
||||
monitors = api.get_monitors()
|
||||
|
||||
print("Current Monitor Tags:\n")
|
||||
for monitor in monitors:
|
||||
tags = monitor.get('tags', [])
|
||||
tag_names = [f"ID:{tag.get('tag_id', tag.get('id'))}" for tag in tags] if tags else ['No tags']
|
||||
print(f"Monitor {monitor['id']} ({monitor['name']}): {', '.join(tag_names)}")
|
||||
|
||||
print("\n\nAvailable Tags:\n")
|
||||
all_tags = api.get_tags()
|
||||
for tag in all_tags:
|
||||
tag_id = tag.get('tag_id') or tag.get('id')
|
||||
print(f" ID {tag_id}: {tag['name']} ({tag.get('color', 'no color')})")
|
||||
|
||||
finally:
|
||||
api.disconnect()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,11 +1,11 @@
|
||||
services:
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:1
|
||||
image: louislam/uptime-kuma:2
|
||||
container_name: uptime-kuma
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /srv/uptime-kuma/data:/app/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- VIRTUAL_HOST=uptime.kolpacksoftware.com
|
||||
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fix missing tags on specific monitors"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
|
||||
UPTIME_KUMA_URL = "https://uptime.kolpacksoftware.com"
|
||||
USERNAME = os.getenv('UPTIME_KUMA_USERNAME')
|
||||
PASSWORD = os.getenv('UPTIME_KUMA_PASSWORD')
|
||||
|
||||
# Missing tags to add
|
||||
MISSING_TAGS = {
|
||||
1: [2], # Server Fan Online: add monitoring
|
||||
2: [2], # Server Front Fan Stalled: add monitoring
|
||||
11: [7, 8], # NAS: add infrastructure, storage
|
||||
14: [7, 10], # dev-win10 VM: add infrastructure, vm
|
||||
}
|
||||
|
||||
def connect():
|
||||
api = UptimeKumaApi(UPTIME_KUMA_URL, ssl_verify=False)
|
||||
if USERNAME and PASSWORD:
|
||||
api.login(USERNAME, PASSWORD)
|
||||
else:
|
||||
print("Error: Set credentials")
|
||||
sys.exit(1)
|
||||
return api
|
||||
|
||||
def main():
|
||||
api = connect()
|
||||
|
||||
try:
|
||||
print("Adding missing tags...\n")
|
||||
|
||||
for monitor_id, tag_ids in MISSING_TAGS.items():
|
||||
monitor = api.get_monitor(monitor_id)
|
||||
print(f"Monitor {monitor_id} ({monitor['name']}):")
|
||||
|
||||
for tag_id in tag_ids:
|
||||
try:
|
||||
api.add_monitor_tag(tag_id=tag_id, monitor_id=monitor_id, value='')
|
||||
print(f" ✓ Added tag ID {tag_id}")
|
||||
except Exception as e:
|
||||
if 'already' in str(e).lower():
|
||||
print(f" - Tag ID {tag_id} already exists")
|
||||
else:
|
||||
print(f" ✗ Failed to add tag ID {tag_id}: {e}")
|
||||
|
||||
print("\n✅ Complete!")
|
||||
|
||||
finally:
|
||||
api.disconnect()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Uptime Kuma Monitor Management Script
|
||||
Usage examples:
|
||||
./manage_monitors.py list
|
||||
./manage_monitors.py add --name "My Service" --url "https://example.com" --type http
|
||||
./manage_monitors.py delete --id 123
|
||||
./manage_monitors.py update --id 123 --name "Updated Name"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
from uptime_kuma_api import UptimeKumaApi, MonitorType
|
||||
|
||||
# Configuration
|
||||
UPTIME_KUMA_URL = "https://uptime.kolpacksoftware.com"
|
||||
# Set one of these (or use environment variables):
|
||||
API_KEY = os.getenv('UPTIME_KUMA_API_KEY') # Set via UPTIME_KUMA_API_KEY env var
|
||||
USERNAME = os.getenv('UPTIME_KUMA_USERNAME') # Or use UPTIME_KUMA_USERNAME
|
||||
PASSWORD = os.getenv('UPTIME_KUMA_PASSWORD') # And UPTIME_KUMA_PASSWORD
|
||||
|
||||
def connect():
|
||||
"""Connect to Uptime Kuma API"""
|
||||
# Disable SSL verification for self-signed certificates
|
||||
api = UptimeKumaApi(UPTIME_KUMA_URL, ssl_verify=False)
|
||||
|
||||
if USERNAME and PASSWORD:
|
||||
api.login(USERNAME, PASSWORD)
|
||||
elif API_KEY:
|
||||
api.login_by_token(API_KEY)
|
||||
else:
|
||||
print("Error: No credentials provided. Set UPTIME_KUMA_API_KEY or UPTIME_KUMA_USERNAME/PASSWORD environment variables.")
|
||||
sys.exit(1)
|
||||
|
||||
return api
|
||||
|
||||
def list_monitors(api):
|
||||
"""List all monitors"""
|
||||
monitors = api.get_monitors()
|
||||
if not monitors:
|
||||
print("No monitors found")
|
||||
return
|
||||
|
||||
print(f"{'ID':<6} {'Name':<30} {'Type':<10} {'URL/Hostname':<50}")
|
||||
print("-" * 96)
|
||||
for monitor in monitors:
|
||||
url = monitor.get('url') or monitor.get('hostname', 'N/A')
|
||||
if url is None:
|
||||
url = 'N/A'
|
||||
print(f"{monitor['id']:<6} {monitor['name']:<30} {str(monitor['type']):<10} {url:<50}")
|
||||
|
||||
def add_monitor(api, name, url, monitor_type='http', interval=60, ignore_tls=False):
|
||||
"""Add a new monitor"""
|
||||
type_map = {
|
||||
'http': MonitorType.HTTP,
|
||||
'https': MonitorType.HTTP,
|
||||
'tcp': MonitorType.PORT,
|
||||
'ping': MonitorType.PING,
|
||||
'dns': MonitorType.DNS,
|
||||
'docker': MonitorType.DOCKER,
|
||||
}
|
||||
|
||||
monitor_type_value = type_map.get(monitor_type.lower(), MonitorType.HTTP)
|
||||
|
||||
# Different monitor types use different parameters
|
||||
if monitor_type.lower() == 'ping':
|
||||
result = api.add_monitor(
|
||||
type=monitor_type_value,
|
||||
name=name,
|
||||
hostname=url,
|
||||
interval=interval
|
||||
)
|
||||
elif monitor_type.lower() == 'tcp':
|
||||
hostname, port = url.rsplit(':', 1)
|
||||
result = api.add_monitor(
|
||||
type=monitor_type_value,
|
||||
name=name,
|
||||
hostname=hostname,
|
||||
port=int(port),
|
||||
interval=interval
|
||||
)
|
||||
else:
|
||||
kwargs = {
|
||||
'type': monitor_type_value,
|
||||
'name': name,
|
||||
'url': url,
|
||||
'interval': interval
|
||||
}
|
||||
# Add SSL ignore for HTTPS URLs
|
||||
if ignore_tls or (url.startswith('https://') and monitor_type.lower() in ['http', 'https']):
|
||||
kwargs['ignoreTls'] = True
|
||||
result = api.add_monitor(**kwargs)
|
||||
print(f"Monitor added successfully: ID {result['monitorID']}")
|
||||
|
||||
def delete_monitor(api, monitor_id):
|
||||
"""Delete a monitor"""
|
||||
api.delete_monitor(monitor_id)
|
||||
print(f"Monitor {monitor_id} deleted successfully")
|
||||
|
||||
def update_monitor(api, monitor_id, **kwargs):
|
||||
"""Update a monitor"""
|
||||
# Get existing monitor
|
||||
monitor = api.get_monitor(monitor_id)
|
||||
|
||||
# Update fields
|
||||
for key, value in kwargs.items():
|
||||
if value is not None:
|
||||
monitor[key] = value
|
||||
|
||||
api.edit_monitor(monitor_id, **monitor)
|
||||
print(f"Monitor {monitor_id} updated successfully")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Manage Uptime Kuma monitors')
|
||||
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
||||
|
||||
# List command
|
||||
subparsers.add_parser('list', help='List all monitors')
|
||||
|
||||
# Add command
|
||||
add_parser = subparsers.add_parser('add', help='Add a monitor')
|
||||
add_parser.add_argument('--name', required=True, help='Monitor name')
|
||||
add_parser.add_argument('--url', required=True, help='URL to monitor')
|
||||
add_parser.add_argument('--type', default='http', help='Monitor type (http, tcp, ping, dns, docker)')
|
||||
add_parser.add_argument('--interval', type=int, default=60, help='Check interval in seconds')
|
||||
add_parser.add_argument('--ignore-tls', action='store_true', help='Ignore TLS/SSL certificate errors')
|
||||
|
||||
# Delete command
|
||||
delete_parser = subparsers.add_parser('delete', help='Delete a monitor')
|
||||
delete_parser.add_argument('--id', type=int, required=True, help='Monitor ID')
|
||||
|
||||
# Update command
|
||||
update_parser = subparsers.add_parser('update', help='Update a monitor')
|
||||
update_parser.add_argument('--id', type=int, required=True, help='Monitor ID')
|
||||
update_parser.add_argument('--name', help='New name')
|
||||
update_parser.add_argument('--url', help='New URL')
|
||||
update_parser.add_argument('--interval', type=int, help='New interval')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Connect to API
|
||||
api = connect()
|
||||
|
||||
try:
|
||||
if args.command == 'list':
|
||||
list_monitors(api)
|
||||
elif args.command == 'add':
|
||||
ignore_tls = getattr(args, 'ignore_tls', False)
|
||||
add_monitor(api, args.name, args.url, args.type, args.interval, ignore_tls)
|
||||
elif args.command == 'delete':
|
||||
delete_monitor(api, args.id)
|
||||
elif args.command == 'update':
|
||||
update_monitor(api, args.id, name=args.name, url=args.url, interval=args.interval)
|
||||
finally:
|
||||
api.disconnect()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user