Compare commits
185
Commits
22166f116a
..
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 | ||
|
|
f46d493ca2 | ||
|
|
dab1385ca7 | ||
|
|
038767ef76 | ||
|
|
4d089966d8 | ||
|
|
da970d1ed5 | ||
|
|
8a5be5989f | ||
|
|
da7a0295e3 | ||
|
|
201349a6fd | ||
|
|
8ab2ff68a1 | ||
|
|
8bab95d2ef | ||
|
|
94b5587831 | ||
|
|
8e878cec0d | ||
|
|
42bf219965 | ||
|
|
947f5fd348 | ||
|
|
3426193bc2 | ||
|
|
fda2a9b8d8 | ||
|
|
edf45229dc | ||
|
|
b721e0108c | ||
|
|
e0c8ce3d8a | ||
|
|
fddda863de | ||
|
|
4a0c703fc4 |
@@ -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"
|
version: "3.8"
|
||||||
|
|
||||||
volumes:
|
|
||||||
backup-mnt:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
o: "addr=192.168.1.192,rw,noatime,rsize=8192,wsize=8192,tcp,timeo=14,nolock,soft,nfsvers=4"
|
|
||||||
device: " :/mnt/user/backup/nas-docker-data"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backrest:
|
backrest:
|
||||||
image: garethgeorge/backrest:latest
|
image: garethgeorge/backrest:latest
|
||||||
@@ -20,8 +12,7 @@ services:
|
|||||||
- /srv/backrest/tmp:/tmp
|
- /srv/backrest/tmp:/tmp
|
||||||
- /srv/backrest/rclone:/root/.config/rclone # Mount for rclone config (needed when using rclone remotes)
|
- /srv/backrest/rclone:/root/.config/rclone # Mount for rclone config (needed when using rclone remotes)
|
||||||
- /srv:/srv-data # Mount local paths to backup
|
- /srv:/srv-data # Mount local paths to backup
|
||||||
#- /mnt/backup-export:/backup-export # NFS share backup destination
|
- /mnt/nas_backup/nas-docker-data:/backup-export
|
||||||
- backup-mnt:/backup-export # NFS share backup destination
|
|
||||||
environment:
|
environment:
|
||||||
- BACKREST_DATA=/data
|
- BACKREST_DATA=/data
|
||||||
- BACKREST_CONFIG=/config/config.json
|
- BACKREST_CONFIG=/config/config.json
|
||||||
@@ -30,4 +21,4 @@ services:
|
|||||||
- TZ=America/New_York
|
- TZ=America/New_York
|
||||||
ports:
|
ports:
|
||||||
- "9898:9898"
|
- "9898:9898"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
+15
-24
@@ -1,41 +1,32 @@
|
|||||||
|
# https://thebloody.cloud/posts/Installing-Web-Calibre-in-Docker/
|
||||||
|
# SQLite databases (metadata.db, notes.db) live on local SSD to avoid NFS IOError.
|
||||||
|
# lsyncd syncs /srv/calibre/library → /mnt/nas_books/calibre/Calibre Library
|
||||||
|
# within ~15s so other devices see an up-to-date read-only replica on the NAS.
|
||||||
|
# calibre-import stays on NFS as the drop folder for new books.
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
external:
|
external:
|
||||||
name: npm-network
|
name: npm-network
|
||||||
|
|
||||||
volumes:
|
|
||||||
config:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
o: "addr=192.168.1.4,rw,nolock,soft"
|
|
||||||
device: ":/srv/books/calibre"
|
|
||||||
import:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
o: "addr=192.168.1.4,rw,nolock,soft"
|
|
||||||
device: ":/srv/books/calibre-import"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
calibre:
|
calibre:
|
||||||
image: lscr.io/linuxserver/calibre:latest
|
image: lscr.io/linuxserver/calibre:latest
|
||||||
container_name: calibre
|
container_name: calibre
|
||||||
security_opt:
|
security_opt:
|
||||||
- seccomp:unconfined #optional
|
- seccomp:unconfined
|
||||||
#security-opt:
|
|
||||||
# - seccomp:unconfined
|
|
||||||
environment:
|
environment:
|
||||||
- PUID=1024
|
- PUID=99
|
||||||
- PGID=1024
|
- PGID=100
|
||||||
- TZ=America/New_York
|
- TZ=America/New_York
|
||||||
- PASSWORD= #optional
|
- VIRTUAL_HOST=calibre.kolpacksoftware.com
|
||||||
- CLI_ARGS= #optional
|
- VIRTUAL_PORT=8080
|
||||||
|
- LETSENCRYPT_HOST=calibre.kolpacksoftware.com
|
||||||
volumes:
|
volumes:
|
||||||
- config:/config
|
- /srv/calibre/config:/config
|
||||||
- import:/calibre-import
|
- /srv/calibre/library:/config/Calibre Library
|
||||||
|
- /mnt/nas_books/calibre-import:/calibre-import
|
||||||
ports:
|
ports:
|
||||||
- 8090:8080
|
- 8090:8080
|
||||||
- 8091:8081
|
- 8091:8081
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -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/nginx-proxy/data:/mnt/nginx-proxy
|
||||||
- /srv/tsa-chapter-organizer-rms:/mnt/tsa-chapter-organizer-rms
|
- /srv/tsa-chapter-organizer-rms:/mnt/tsa-chapter-organizer-rms
|
||||||
- /srv/glances:/mnt/glances
|
- /srv/glances:/mnt/glances
|
||||||
|
- /srv/authelia/config:/mnt/authelia
|
||||||
ports:
|
ports:
|
||||||
- 8443:8443
|
- 8443:8443
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
comfyui:
|
||||||
|
container_name: comfyui
|
||||||
|
image: ghcr.io/ai-dock/comfyui:latest-cuda
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 8188:8188
|
||||||
|
environment:
|
||||||
|
- COMFYUI_ARGS=
|
||||||
|
- AUTO_UPDATE=false
|
||||||
|
- TZ=America/New_York
|
||||||
|
# Optional - for gated models
|
||||||
|
# - HF_TOKEN=your_token
|
||||||
|
# - CIVITAI_TOKEN=your_token
|
||||||
|
volumes:
|
||||||
|
- /srv/comfyui/workspace:/workspace
|
||||||
|
runtime: nvidia
|
||||||
|
networks:
|
||||||
|
- npm-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm-network:
|
||||||
|
external: true
|
||||||
@@ -3,13 +3,16 @@ services:
|
|||||||
image: couchdb
|
image: couchdb
|
||||||
container_name: couchdb
|
container_name: couchdb
|
||||||
restart: always
|
restart: always
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.monitor-only=true
|
||||||
ports:
|
ports:
|
||||||
- "5984:5984"
|
- "127.0.0.1:5984:5984"
|
||||||
environment:
|
environment:
|
||||||
- COUCHDB_USER=admin
|
- COUCHDB_USER=admin
|
||||||
- COUCHDB_PASSWORD=${ADMIN_PASSWORD}
|
- COUCHDB_PASSWORD=${ADMIN_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/couchdb-data:/opt/couchdb/data
|
- /srv/couchdb-data:/opt/couchdb/data
|
||||||
|
- /srv/couchdb-config/local.ini:/opt/couchdb/etc/local.ini
|
||||||
networks:
|
networks:
|
||||||
- npm-network
|
- npm-network
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
networks:
|
|
||||||
default:
|
|
||||||
external:
|
|
||||||
name: npm-network
|
|
||||||
|
|
||||||
services:
|
|
||||||
dashy:
|
|
||||||
# To build from source, replace 'image: lissy93/dashy' with 'build: .'
|
|
||||||
# build: .
|
|
||||||
image: lissy93/dashy
|
|
||||||
container_name: dashy
|
|
||||||
# Pass in your config file below, by specifying the path on your host machine
|
|
||||||
volumes:
|
|
||||||
- /srv/dashy:/app/user-data
|
|
||||||
ports:
|
|
||||||
- 9000:8080
|
|
||||||
# Set any environmental variables
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
# Specify your user ID and group ID. You can find this by running `id -u` and `id -g`
|
|
||||||
# - UID=1000
|
|
||||||
# - GID=1000
|
|
||||||
# Specify restart policy
|
|
||||||
restart: unless-stopped
|
|
||||||
# Configure healthchecks
|
|
||||||
healthcheck:
|
|
||||||
test: ['CMD', 'node', '/app/services/healthcheck']
|
|
||||||
interval: 1m30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
APIKEY=your_dnsexit_api_key
|
||||||
@@ -8,7 +8,9 @@ services:
|
|||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.enable=false
|
||||||
environment:
|
environment:
|
||||||
- APIKEY=s5P998CqUu5bqAukdS1dW57EfjgFlX
|
- APIKEY=${APIKEY}
|
||||||
- HOST=kolpacksoftware.com,rmstsa.org,popcyclical.com
|
- HOST=kolpacksoftware.com,rmstsa.org,popcyclical.com
|
||||||
- INTERVAL=1h
|
- INTERVAL=1h
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
SECRET_KEY_BASE=generate_with_openssl_rand_-hex_64
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external:
|
||||||
|
name: npm-network
|
||||||
|
|
||||||
|
services:
|
||||||
|
registry-browser:
|
||||||
|
container_name: registry-browser
|
||||||
|
image: klausmeyer/docker-registry-browser:latest
|
||||||
|
ports:
|
||||||
|
- 5080:8080
|
||||||
|
environment:
|
||||||
|
- DOCKER_REGISTRY_URL=https://docker-registry.kolpacksoftware.com/
|
||||||
|
- PUBLIC_REGISTRY_URL=https://docker-registry.kolpacksoftware.com/
|
||||||
|
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
|
||||||
|
- ENABLE_DELETE_IMAGES=true
|
||||||
|
- VIRTUAL_HOST=docker-registry-ui.kolpacksoftware.com
|
||||||
|
- VIRTUAL_PORT=8080
|
||||||
|
- LETSENCRYPT_HOST=docker-registry-ui.kolpacksoftware.com
|
||||||
|
|
||||||
|
registry-server:
|
||||||
|
image: registry:latest
|
||||||
|
container_name: registry-server
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin=[https://docker-registry.kolpacksoftware.com/]
|
||||||
|
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods=[HEAD,GET,OPTIONS,DELETE]
|
||||||
|
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials=[true]
|
||||||
|
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers=[Authorization,Accept,Cache-Control]
|
||||||
|
- REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers=[Docker-Content-Digest]
|
||||||
|
- REGISTRY_STORAGE_DELETE_ENABLED=true
|
||||||
|
- REGISTRY_AUTH=htpasswd
|
||||||
|
- REGISTRY_AUTH_HTPASSWD_REALM=KSC Docker Registry
|
||||||
|
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/registry.password
|
||||||
|
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/data
|
||||||
|
- VIRTUAL_HOST=docker-registry.kolpacksoftware.com
|
||||||
|
- VIRTUAL_PORT=5000
|
||||||
|
- LETSENCRYPT_HOST=docker-registry.kolpacksoftware.com
|
||||||
|
volumes:
|
||||||
|
- /srv/registry/data:/data
|
||||||
|
- /srv/registry/registry.password:/auth/registry.password
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:5000:5000
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
SMB_USERNAME=your_smb_username
|
||||||
|
SMB_PASSWORD=your_smb_password
|
||||||
@@ -6,7 +6,7 @@ services:
|
|||||||
FILEBROWSER_CONFIG: "data/config.yaml"
|
FILEBROWSER_CONFIG: "data/config.yaml"
|
||||||
TZ: "America/New_York"
|
TZ: "America/New_York"
|
||||||
volumes:
|
volumes:
|
||||||
- backup-mnt:/folder
|
- /mnt/nas_backup:/folder
|
||||||
- /srv/filebrowser-colleen-hd:/home/filebrowser/data
|
- /srv/filebrowser-colleen-hd:/home/filebrowser/data
|
||||||
ports:
|
ports:
|
||||||
- 3427:80
|
- 3427:80
|
||||||
@@ -14,15 +14,7 @@ services:
|
|||||||
- npm-network
|
- npm-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
|
||||||
backup-mnt:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
o: "addr=192.168.1.192,rw,nolock,soft"
|
|
||||||
device: ":/mnt/user/backup/"
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
npm-network:
|
npm-network:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
external:
|
external:
|
||||||
@@ -9,16 +7,19 @@ services:
|
|||||||
server:
|
server:
|
||||||
image: docker.gitea.com/gitea:nightly
|
image: docker.gitea.com/gitea:nightly
|
||||||
container_name: gitea
|
container_name: gitea
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.monitor-only=true
|
||||||
environment:
|
environment:
|
||||||
- USER_UID=1000
|
- USER_UID=1000
|
||||||
- USER_GID=1000
|
- USER_GID=1000
|
||||||
|
- VIRTUAL_HOST=gitea.kolpacksoftware.com
|
||||||
|
- VIRTUAL_PORT=3000
|
||||||
|
- LETSENCRYPT_HOST=gitea.kolpacksoftware.com
|
||||||
restart: always
|
restart: always
|
||||||
# networks:
|
|
||||||
# - gitea
|
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/gitea-data:/data
|
- /srv/gitea-data:/data
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
ports:
|
ports:
|
||||||
- "3001:3000"
|
- "3001:3000"
|
||||||
- "222:22"
|
- "222:22"
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
glances:
|
glances:
|
||||||
container_name: glances
|
container_name: glances
|
||||||
image: 'nicolargo/glances:latest-full'
|
image: 'nicolargo/glances:ubuntu-latest-full'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
privileged: true
|
|
||||||
ports:
|
ports:
|
||||||
- 61208-61209:61208-61209
|
- 127.0.0.1:61208-61209:61208-61209
|
||||||
environment:
|
environment:
|
||||||
- TZ=${TZ}
|
- TZ=${TZ}
|
||||||
|
- NVIDIA_VISIBLE_DEVICES=all
|
||||||
|
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||||
- "GLANCES_OPT=-C /glances/conf/glances.conf -w"
|
- "GLANCES_OPT=-C /glances/conf/glances.conf -w"
|
||||||
volumes:
|
volumes:
|
||||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||||
- '/run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro'
|
- '/run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro'
|
||||||
- /srv/glances/glances.conf:/glances/conf/glances.conf
|
- /srv/glances/glances.conf:/glances/conf/glances.conf
|
||||||
pid: host
|
pid: host
|
||||||
|
runtime: nvidia
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
homebox:
|
homebox:
|
||||||
# image: ghcr.io/sysadminsmedia/homebox:latest
|
|
||||||
image: ghcr.io/sysadminsmedia/homebox:latest-rootless
|
image: ghcr.io/sysadminsmedia/homebox:latest-rootless
|
||||||
container_name: homebox
|
container_name: homebox
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- HBOX_LOG_LEVEL=info
|
- HBOX_LOG_LEVEL=info
|
||||||
- HBOX_LOG_FORMAT=text
|
- HBOX_LOG_FORMAT=text
|
||||||
- HBOX_WEB_MAX_FILE_UPLOAD=10
|
- HBOX_WEB_MAX_FILE_UPLOAD=10
|
||||||
# Please consider allowing analytics to help us improve Homebox (basic computer information, no personal data)
|
- HBOX_OPTIONS_ALLOW_ANALYTICS=false
|
||||||
- HBOX_OPTIONS_ALLOW_ANALYTICS=false
|
- HBOX_AUTH_API_KEY_PEPPER=${HBOX_AUTH_API_KEY_PEPPER}
|
||||||
|
- VIRTUAL_HOST=homebox.kolpacksoftware.com
|
||||||
|
- VIRTUAL_PORT=7745
|
||||||
|
- LETSENCRYPT_HOST=homebox.kolpacksoftware.com
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/homebox-data:/data/
|
- /srv/homebox-data:/data/
|
||||||
ports:
|
ports:
|
||||||
@@ -18,4 +20,4 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
external:
|
external:
|
||||||
name: npm-network
|
name: npm-network
|
||||||
|
|||||||
+11
-11
@@ -16,9 +16,10 @@ services:
|
|||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.monitor-only=true
|
||||||
volumes:
|
volumes:
|
||||||
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
|
- /mnt/nas_family/immich-library:/usr/src/app/upload
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- stack.env
|
- stack.env
|
||||||
@@ -42,6 +43,8 @@ services:
|
|||||||
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
|
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
|
||||||
# file: hwaccel.ml.yml
|
# file: hwaccel.ml.yml
|
||||||
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.monitor-only=true
|
||||||
volumes:
|
volumes:
|
||||||
- model-cache:/cache
|
- model-cache:/cache
|
||||||
env_file:
|
env_file:
|
||||||
@@ -53,13 +56,17 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.enable=false
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.enable=false
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
@@ -68,24 +75,17 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
|
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
|
||||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||||
|
shm_size: 128mb
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: >-
|
test: >-
|
||||||
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
||||||
interval: 5m
|
interval: 5m
|
||||||
#start_interval: 30s
|
#start_interval: 30s
|
||||||
start_period: 5m
|
start_period: 5m
|
||||||
command: >-
|
|
||||||
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
library:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
o: "addr=192.168.1.192,rw,nolock,soft"
|
|
||||||
device: ":/mnt/user/family/immich-library"
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
ladder:
|
||||||
|
image: ghcr.io/everywall/ladder:latest
|
||||||
|
container_name: ladder
|
||||||
|
#build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
#command: sh -c ./ladder
|
||||||
|
environment:
|
||||||
|
- PORT=8080
|
||||||
|
- RULESET=https://raw.githubusercontent.com/everywall/ladder-rules/main/ruleset.yaml
|
||||||
|
#- RULESET=/app/ruleset.yaml
|
||||||
|
#- ALLOWED_DOMAINS=example.com,example.org
|
||||||
|
#- ALLOWED_DOMAINS_RULESET=false
|
||||||
|
#- EXPOSE_RULESET=true
|
||||||
|
#- PREFORK=false
|
||||||
|
#- DISABLE_FORM=false
|
||||||
|
#- FORM_PATH=/app/form.html
|
||||||
|
#- X_FORWARDED_FOR=66.249.66.1
|
||||||
|
#- USER_AGENT=Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
|
||||||
|
#- USERPASS=foo:bar
|
||||||
|
#- LOG_URLS=true
|
||||||
|
#- GODEBUG=netdns=go
|
||||||
|
ports:
|
||||||
|
- "8910:8080"
|
||||||
|
volumes:
|
||||||
|
- /srv/ladder/ruleset.yaml:/app/ruleset.yaml
|
||||||
|
- /srv/ladder/handlers/form.html:/app/form.html
|
||||||
@@ -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"
|
|
||||||
@@ -1,15 +1,27 @@
|
|||||||
services:
|
services:
|
||||||
linkding:
|
linkding:
|
||||||
container_name: "${LD_CONTAINER_NAME:-linkding}"
|
container_name: linkding
|
||||||
image: sissbruecker/linkding:latest
|
image: sissbruecker/linkding:latest
|
||||||
ports:
|
ports:
|
||||||
- "${LD_HOST_PORT:-9090}:9090"
|
- "9090:9090"
|
||||||
volumes:
|
volumes:
|
||||||
- "${LD_HOST_DATA_DIR:-./data}:/etc/linkding/data"
|
- /srv/linkding-data:/etc/linkding/data
|
||||||
|
environment:
|
||||||
|
- VIRTUAL_HOST=linkding.kolpacksoftware.com
|
||||||
|
- VIRTUAL_PORT=9090
|
||||||
|
- LETSENCRYPT_HOST=linkding.kolpacksoftware.com
|
||||||
|
# Authelia OIDC (LD_ENABLE_OIDC uses LD_ prefix; the rest do not)
|
||||||
|
- LD_ENABLE_OIDC=True
|
||||||
|
- OIDC_OP_AUTHORIZATION_ENDPOINT=https://auth.kolpacksoftware.com/api/oidc/authorization
|
||||||
|
- OIDC_OP_TOKEN_ENDPOINT=https://auth.kolpacksoftware.com/api/oidc/token
|
||||||
|
- OIDC_OP_USER_ENDPOINT=https://auth.kolpacksoftware.com/api/oidc/userinfo
|
||||||
|
- OIDC_OP_JWKS_ENDPOINT=https://auth.kolpacksoftware.com/jwks.json
|
||||||
|
- OIDC_RP_CLIENT_ID=linkding
|
||||||
|
- OIDC_RP_CLIENT_SECRET=${LINKDING_OIDC_CLIENT_SECRET}
|
||||||
|
- OIDC_RP_SIGN_ALGO=RS256
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
external:
|
external:
|
||||||
name: npm-network
|
name: npm-network
|
||||||
|
|||||||
@@ -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:
|
environment:
|
||||||
EULA: "true"
|
EULA: "true"
|
||||||
SEED: 46942827301
|
SEED: 46942827301
|
||||||
SERVER_NAME: "Ourcraft"
|
SERVER_NAME: "Terraforma SMP"
|
||||||
MEMORY: "4G"
|
MEMORY: "4G"
|
||||||
OPS: "Jimcognito"
|
OPS: "Jimcognito"
|
||||||
#TYPE: "FABRIC"
|
#TYPE: "FABRIC"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/minecraft-data:/data
|
- /srv/minecraft-data:/data
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
creative:
|
mem_limit: 6g
|
||||||
|
# creative:
|
||||||
|
# image: itzg/minecraft-server:latest
|
||||||
|
# container_name: minecraft-creative
|
||||||
|
# ports:
|
||||||
|
# - "25566:25565"
|
||||||
|
# environment:
|
||||||
|
# EULA: "true"
|
||||||
|
# SEED: 8486672581758651406
|
||||||
|
# SERVER_NAME: "Creative Kolpacks"
|
||||||
|
# volumes:
|
||||||
|
# - /srv/minecraft-data-creative:/data
|
||||||
|
# stdin_open: true
|
||||||
|
# tty: true
|
||||||
|
# restart: unless-stopped
|
||||||
|
# mem_limit: 2g
|
||||||
|
# battle:
|
||||||
|
# image: itzg/minecraft-server:latest
|
||||||
|
# container_name: minecraft-battle
|
||||||
|
# ports:
|
||||||
|
# - "25573:25565"
|
||||||
|
# environment:
|
||||||
|
# EULA: "true"
|
||||||
|
# SEED: "9037888329883360986"
|
||||||
|
# SERVER_NAME: "Battle Royale"
|
||||||
|
# MODE: "survival"
|
||||||
|
# PVP: "true"
|
||||||
|
# OPS: "Jimcognito"
|
||||||
|
# TYPE: "PAPER"
|
||||||
|
# MODRINTH_PROJECTS: "ezcountdown,worldresetplugin,viaversion"
|
||||||
|
# volumes:
|
||||||
|
# - /srv/minecraft-battle:/data
|
||||||
|
# stdin_open: true
|
||||||
|
# tty: true
|
||||||
|
# restart: unless-stopped
|
||||||
|
# mem_limit: 2g
|
||||||
|
|
||||||
|
# immersive:
|
||||||
|
# image: itzg/minecraft-server:latest
|
||||||
|
# container_name: minecraft-immersive
|
||||||
|
# ports:
|
||||||
|
# - "55567:25565"
|
||||||
|
# environment:
|
||||||
|
# EULA: "true"
|
||||||
|
# TYPE: "FABRIC"
|
||||||
|
# VERSION: "1.20.4"
|
||||||
|
# SERVER_NAME: "Immersive Portals"
|
||||||
|
# SEED: "302814868814545"
|
||||||
|
# MODE: "creative"
|
||||||
|
# FORCE_GAMEMODE: "true"
|
||||||
|
# ENABLE_COMMAND_BLOCK: "true"
|
||||||
|
# ALLOW_FLIGHT: "true"
|
||||||
|
# ENFORCE_SECURE_PROFILE: "false"
|
||||||
|
# MEMORY: "3G"
|
||||||
|
# OPS: "Jimcognito,OlympicJumperL"
|
||||||
|
# MODRINTH_PROJECTS: "fabric-api,pehkui,immersiveportals,gravity-api-fork:v1.3.0-mc1.20.4"
|
||||||
|
# MODRINTH_DOWNLOAD_DEPENDENCIES: "required"
|
||||||
|
# ICON: "https://cdn.modrinth.com/data/zJpHMkdD/0b2fa76a843b96f8bba1eb9d41f18f9f8ed4ce18_96.webp"
|
||||||
|
# volumes:
|
||||||
|
# - /srv/minecraft-modded:/data
|
||||||
|
# stdin_open: true
|
||||||
|
# tty: true
|
||||||
|
# restart: unless-stopped
|
||||||
|
# mem_limit: 4g
|
||||||
|
|
||||||
|
empire-smp:
|
||||||
image: itzg/minecraft-server:latest
|
image: itzg/minecraft-server:latest
|
||||||
container_name: minecraft-creative
|
container_name: minecraft-empire-smp
|
||||||
ports:
|
ports:
|
||||||
- "25566:25565"
|
- "31415:25565"
|
||||||
environment:
|
environment:
|
||||||
EULA: "true"
|
EULA: "true"
|
||||||
SEED: 8486672581758651406
|
SEED: "-4690695632699825725"
|
||||||
SERVER_NAME: "Creative Kolpacks"
|
SERVER_NAME: "Empire SMP"
|
||||||
|
MODE: "survival"
|
||||||
|
PVP: "false"
|
||||||
|
OPS: "Jimcognito"
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/minecraft-data-creative:/data
|
- /srv/minecraft-empire-smp:/data
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
mem_limit: 2g
|
||||||
|
|
||||||
epic-bases:
|
epic-bases:
|
||||||
image: itzg/minecraft-server:latest
|
image: itzg/minecraft-server:latest
|
||||||
container_name: minecraft-pvp0
|
container_name: minecraft-pvp0
|
||||||
@@ -60,39 +129,42 @@ services:
|
|||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
hardcore:
|
mem_limit: 2g
|
||||||
image: itzg/minecraft-server:latest
|
# hardcore temporarily disabled to reduce swap pressure
|
||||||
container_name: minecraft-hardcore
|
# hardcore:
|
||||||
ports:
|
# image: itzg/minecraft-server:latest
|
||||||
- "25568:25565"
|
# container_name: minecraft-hardcore
|
||||||
environment:
|
# ports:
|
||||||
EULA: "true"
|
# - "25568:25565"
|
||||||
SEED: "-3149142039438819319"
|
# environment:
|
||||||
SERVER_NAME: "Hardcore"
|
# EULA: "true"
|
||||||
OPS: "Jimcognito,MagicSpaceCat"
|
# SEED: "-3149142039438819319"
|
||||||
TYPE: "FABRIC"
|
# SERVER_NAME: "Hardcore"
|
||||||
MAX_WORLD_SIZE: 415
|
# OPS: "Jimcognito,MagicSpaceCat"
|
||||||
HARDCORE: true
|
# TYPE: "FABRIC"
|
||||||
PVP: false
|
# MAX_WORLD_SIZE: 415
|
||||||
volumes:
|
# HARDCORE: true
|
||||||
- /srv/minecraft-data-hardcore:/data
|
# PVP: false
|
||||||
stdin_open: true
|
# volumes:
|
||||||
tty: true
|
# - /srv/minecraft-data-hardcore:/data
|
||||||
restart: unless-stopped
|
# stdin_open: true
|
||||||
survival:
|
# tty: true
|
||||||
image: itzg/minecraft-server:latest
|
# restart: unless-stopped
|
||||||
container_name: minecraft-survival-smp
|
# survival temporarily disabled to reduce swap pressure
|
||||||
ports:
|
# survival:
|
||||||
- "25571:25565"
|
# image: itzg/minecraft-server:latest
|
||||||
environment:
|
# container_name: minecraft-survival-smp
|
||||||
EULA: "true"
|
# ports:
|
||||||
SERVER_NAME: "Survival SMP"
|
# - "25571:25565"
|
||||||
OPS: "Jimcognito,OlympicJumperL"
|
# environment:
|
||||||
volumes:
|
# EULA: "true"
|
||||||
- /srv/minecraft-data-survival-smp:/data
|
# SERVER_NAME: "Survival SMP"
|
||||||
stdin_open: true
|
# OPS: "Jimcognito,OlympicJumperL"
|
||||||
tty: true
|
# volumes:
|
||||||
restart: unless-stopped
|
# - /srv/minecraft-data-survival-smp:/data
|
||||||
|
# stdin_open: true
|
||||||
|
# tty: true
|
||||||
|
# restart: unless-stopped
|
||||||
# spells:
|
# spells:
|
||||||
# image: itzg/minecraft-server
|
# image: itzg/minecraft-server
|
||||||
# container_name: minecraft-spells
|
# container_name: minecraft-spells
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ services:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
mqtt-network:
|
mqtt-network:
|
||||||
driver: bridge
|
external: true
|
||||||
default:
|
|
||||||
npm-network:
|
npm-network:
|
||||||
external: true
|
external: true
|
||||||
@@ -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=
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
services:
|
||||||
|
ollama:
|
||||||
|
container_name: ollama
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:11434:11434
|
||||||
|
environment:
|
||||||
|
- NVIDIA_VISIBLE_DEVICES=all
|
||||||
|
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||||
|
- TZ=America/New_York
|
||||||
|
- OLLAMA_HOST=0.0.0.0
|
||||||
|
volumes:
|
||||||
|
- /srv/ollama:/root/.ollama
|
||||||
|
runtime: nvidia
|
||||||
|
networks:
|
||||||
|
- npm-network
|
||||||
|
|
||||||
|
# Optional: Web UI for Ollama
|
||||||
|
open-webui:
|
||||||
|
container_name: open-webui
|
||||||
|
image: ghcr.io/open-webui/open-webui:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 3000:8080
|
||||||
|
environment:
|
||||||
|
- OLLAMA_BASE_URL=http://ollama:11434
|
||||||
|
- TZ=America/New_York
|
||||||
|
- ENABLE_OAUTH_SIGNUP=true
|
||||||
|
- OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true
|
||||||
|
- OAUTH_PROVIDER_NAME=Authelia
|
||||||
|
- OPENID_PROVIDER_URL=https://auth.kolpacksoftware.com/.well-known/openid-configuration
|
||||||
|
- OAUTH_CLIENT_ID=open-webui
|
||||||
|
- OAUTH_CLIENT_SECRET=${AUTHELIA_OIDC_CLIENT_SECRET_OPEN_WEBUI}
|
||||||
|
- OAUTH_TOKEN_ENDPOINT_AUTH_METHOD=client_secret_post
|
||||||
|
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
|
||||||
|
volumes:
|
||||||
|
- /srv/open-webui:/app/backend/data
|
||||||
|
depends_on:
|
||||||
|
- ollama
|
||||||
|
networks:
|
||||||
|
- npm-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm-network:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
services:
|
||||||
|
openclaw:
|
||||||
|
container_name: openclaw
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["openclaw", "gateway", "--allow-unconfigured", "--bind", "lan"]
|
||||||
|
environment:
|
||||||
|
- HOME=/home/node
|
||||||
|
- TERM=xterm-256color
|
||||||
|
- TZ=America/New_York
|
||||||
|
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
|
||||||
|
# Enable Ollama provider (opt-in via env var)
|
||||||
|
- OLLAMA_API_KEY=ollama-local
|
||||||
|
- BRAVE_API_KEY=${BRAVE_API_KEY}
|
||||||
|
volumes:
|
||||||
|
- /srv/openclaw/config:/home/node/.openclaw
|
||||||
|
- /srv/openclaw/workspace:/home/node/.openclaw/workspace
|
||||||
|
init: true
|
||||||
|
networks:
|
||||||
|
- npm-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm-network:
|
||||||
|
external: true
|
||||||
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
|
|
||||||
@@ -2,8 +2,6 @@ volumes:
|
|||||||
nas_media:
|
nas_media:
|
||||||
external: true
|
external: true
|
||||||
name: nas_media
|
name: nas_media
|
||||||
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
plex:
|
plex:
|
||||||
container_name: plex
|
container_name: plex
|
||||||
@@ -21,10 +19,18 @@ services:
|
|||||||
- 32414:32414/udp
|
- 32414:32414/udp
|
||||||
environment:
|
environment:
|
||||||
- TZ=America/New_York
|
- TZ=America/New_York
|
||||||
- PLEX_CLAIM=claim-VCuJznuC2nxFyVgAcKec
|
|
||||||
- ADVERTISE_IP=http://73.108.236.191:32400/
|
- ADVERTISE_IP=http://73.108.236.191:32400/
|
||||||
|
- NVIDIA_VISIBLE_DEVICES=all
|
||||||
|
- NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
|
||||||
hostname: Photon
|
hostname: Photon
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/plex:/config
|
- /srv/plex:/config
|
||||||
- /srv/plex-transcode:/transcode
|
- /srv/plex-transcode:/transcode
|
||||||
- nas_media:/data
|
- nas_media:/data
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
@@ -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
|
container_name: portainer
|
||||||
restart: always
|
restart: always
|
||||||
privileged: true
|
privileged: true
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.monitor-only=true
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/containers/portainer/container-data/data:/data:Z
|
- /mnt/containers/portainer/container-data/data:/data:Z
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:Z
|
- /var/run/docker.sock:/var/run/docker.sock:Z
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
stable-diffusion:
|
||||||
|
container_name: stable-diffusion
|
||||||
|
image: ghcr.io/ai-dock/stable-diffusion-webui-forge:latest-cuda
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 7860:7860
|
||||||
|
environment:
|
||||||
|
- FORGE_ARGS=--xformers --api
|
||||||
|
- AUTO_UPDATE=false
|
||||||
|
- WEB_ENABLE_AUTH=false
|
||||||
|
- TZ=America/New_York
|
||||||
|
# Optional - for gated models
|
||||||
|
# - HF_TOKEN=your_token
|
||||||
|
# - CIVITAI_TOKEN=your_token
|
||||||
|
volumes:
|
||||||
|
- /srv/stable-diffusion/workspace:/workspace
|
||||||
|
# Optional: custom provisioning script
|
||||||
|
# - /srv/stable-diffusion/provisioning.sh:/opt/ai-dock/bin/provisioning.sh
|
||||||
|
runtime: nvidia
|
||||||
|
networks:
|
||||||
|
- npm-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm-network:
|
||||||
|
external: true
|
||||||
@@ -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:
|
volumes:
|
||||||
- /srv/qbittorrent_open-config:/config
|
- /srv/qbittorrent_open-config:/config
|
||||||
- nas_media:/data
|
- nas_media:/data
|
||||||
|
- /mnt/nas_library:/library
|
||||||
qbittorrent_vpn:
|
qbittorrent_vpn:
|
||||||
container_name: qbittorrent_vpn
|
container_name: qbittorrent_vpn
|
||||||
image: ghcr.io/hotio/qbittorrent:release-5.1.2
|
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,10 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
uptime-kuma:
|
uptime-kuma:
|
||||||
image: louislam/uptime-kuma:1
|
image: louislam/uptime-kuma:2
|
||||||
container_name: uptime-kuma
|
container_name: uptime-kuma
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/uptime-kuma/data:/app/data
|
- /srv/uptime-kuma/data:/app/data
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
environment:
|
environment:
|
||||||
- TZ=America/New_York
|
- TZ=America/New_York
|
||||||
- VIRTUAL_HOST=uptime.kolpacksoftware.com
|
- VIRTUAL_HOST=uptime.kolpacksoftware.com
|
||||||
@@ -12,7 +13,10 @@ services:
|
|||||||
- LETSENCRYPT_HOST=uptime.kolpacksoftware.com
|
- LETSENCRYPT_HOST=uptime.kolpacksoftware.com
|
||||||
networks:
|
networks:
|
||||||
- npm-network
|
- npm-network
|
||||||
|
- mqtt-network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
npm-network:
|
npm-network:
|
||||||
external: true
|
external: true
|
||||||
|
mqtt-network:
|
||||||
|
external: true
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user