Compare commits
197
Commits
3c714c597f
..
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 | ||
|
|
22166f116a | ||
|
|
73d80ea2b2 | ||
|
|
d84e412532 | ||
|
|
57590e63b1 | ||
|
|
a76803a4ea | ||
|
|
9f1626b5c3 | ||
|
|
a2bb987ea5 | ||
|
|
7153f1cd5e | ||
|
|
9691e50075 | ||
|
|
6d7567acee | ||
|
|
ec7b34f47a | ||
|
|
752eca99da |
@@ -0,0 +1,3 @@
|
||||
.credentials
|
||||
**/.env
|
||||
**/users_database.yaml
|
||||
@@ -0,0 +1,81 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
This is a homelab Docker infrastructure repository containing ~30 self-hosted services. Each service has its own directory with a `docker-compose.yaml` (or `.yml`) file. All services use pre-built Docker images - there are no custom Dockerfiles.
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Start a service
|
||||
cd <service-name> && docker compose up -d
|
||||
|
||||
# Stop a service
|
||||
cd <service-name> && docker compose down
|
||||
|
||||
# View logs
|
||||
docker compose logs -f <service-name>
|
||||
|
||||
# Reload nginx proxy config after changing vhost.d or conf.d
|
||||
docker exec nginx-proxy nginx -s reload
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Network Architecture
|
||||
|
||||
All proxied services connect to `npm-network`, an external Docker network shared with the nginx reverse proxy. Services define this in their compose file:
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
npm-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
Some services have additional internal networks (e.g., `mqtt-network` for mosquitto).
|
||||
|
||||
### Reverse Proxy (nginx-proxy-acme)
|
||||
|
||||
The primary ingress is `nginx-proxy-acme/`, which auto-discovers Docker containers and issues Let's Encrypt certificates. To expose a service:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- VIRTUAL_HOST=myapp.kolpacksoftware.com
|
||||
- VIRTUAL_PORT=8080
|
||||
- LETSENCRYPT_HOST=myapp.kolpacksoftware.com
|
||||
```
|
||||
|
||||
**Access control:**
|
||||
- Private hosts: Include `vhost.d/private` which restricts to 192.168.1.0/24 and Docker networks
|
||||
- Public hosts: No vhost.d file (only default security headers apply)
|
||||
- To make a host private: `echo 'include /etc/nginx/vhost.d/private;' | sudo tee /srv/nginx-proxy-acme/vhost.d/<hostname>`
|
||||
|
||||
**Static backends** (non-Docker services) are configured in `conf.d/static-upstreams.conf` and require adding the domain to the `static-certs` container's environment variables.
|
||||
|
||||
### Storage Patterns
|
||||
|
||||
- **Bind mounts**: Most services use `/srv/<service-name>/` paths
|
||||
- **NFS mounts**: Some services (immich, calibre, backrest, filebrowser-colleen-hd) use NFS volumes pointing to 192.168.1.4 or 192.168.1.192
|
||||
|
||||
### Automation
|
||||
|
||||
- **watchtower**: Monitors and auto-updates container images every 5 minutes
|
||||
- **netdata**: Real-time monitoring with alerting at port 19999
|
||||
|
||||
## Key Configuration Locations
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `/srv/nginx-proxy-acme/vhost.d/` | Per-host nginx config (access control) |
|
||||
| `/srv/nginx-proxy-acme/conf.d/` | Global nginx config (WAF rules, static backends) |
|
||||
| `<service>/docker-compose.yaml` | Service definition |
|
||||
| `<service>/.env` | Service-specific environment (when present) |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Common patterns across services:
|
||||
- `PUID=1000`, `PGID=1000` for file permissions
|
||||
- `TZ=America/New_York` for timezone
|
||||
- `VIRTUAL_HOST`, `VIRTUAL_PORT`, `LETSENCRYPT_HOST` for proxy integration
|
||||
@@ -0,0 +1,123 @@
|
||||
# NAS Connection Strategies
|
||||
|
||||
Summary of how each service connects to the unRAID NAS (192.168.1.192 / 192.168.1.4).
|
||||
|
||||
---
|
||||
|
||||
## Strategy Overview
|
||||
|
||||
| Strategy | Services | Reliability | Notes |
|
||||
|----------|----------|-------------|-------|
|
||||
| Docker NFS named volume | Plex, qBittorrent, Audiobookshelf | ⚠️ Fragile | Soft mount; stale handles on drive spin-down |
|
||||
| systemd permanent mount + bind | Calibre (import), oCIS, Immich | ✅ Solid | Hard NFS; permanent; no idle cycling |
|
||||
| systemd automount + bind | Backrest, Filebrowser-Colleen-HD | ✅ Solid | CIFS; TimeoutIdleSec=0; never unmounts once triggered |
|
||||
| Local SSD + lsyncd sync | Calibre (library) | ✅ Best | SQLite never touches NFS; NAS copy is read-only replica |
|
||||
|
||||
---
|
||||
|
||||
## Per-Service Details
|
||||
|
||||
### Plex
|
||||
- **Strategy**: Docker NFS named volume (`nas_media`, external)
|
||||
- **NAS path**: `192.168.1.192:/mnt/user/media`
|
||||
- **Mount options**: `nfsvers=4,soft,nolock,timeo=14,rsize=8192,wsize=8192`
|
||||
- **Container path**: `/data`
|
||||
- **Risk**: `soft` + `timeo=14` means EIO after 14 retries × 0.1s = ~1.4s timeout. If unRAID drives spin down, Plex gets I/O errors. Works in practice because Plex opens/closes file handles per-request rather than holding them open.
|
||||
|
||||
### qBittorrent (all 3 instances: open, vpn, vpn2)
|
||||
- **Strategy**: Docker NFS named volume (`nas_media`, same as Plex)
|
||||
- **NAS path**: `192.168.1.192:/mnt/user/media`
|
||||
- **Container path**: `/data`
|
||||
- **Risk**: Same as Plex. Active downloads write continuously — more exposure to spin-down EIO than Plex.
|
||||
|
||||
### Calibre
|
||||
- **Strategy (library)**: Local SSD + lsyncd one-way sync to NAS
|
||||
- Local: `/srv/calibre/library` → container `/config/Calibre Library`
|
||||
- lsyncd syncs to `/mnt/nas_books/calibre/Calibre Library` within ~15 seconds
|
||||
- SQLite (`metadata.db`, `notes.db`) never touches NFS — eliminates `apsw.IOError`
|
||||
- **Strategy (import)**: systemd permanent NFS mount + Docker bind mount
|
||||
- Host: `/mnt/nas_books/calibre-import` (permanent NFS, no automount)
|
||||
- Container: `/calibre-import`
|
||||
- NAS path: `192.168.1.192:/mnt/user/media/books`
|
||||
- **Mount options**: `nfsvers=3,hard,rw,noatime`
|
||||
- **Why permanent (not automount)**: Docker bind mounts snapshot the mount reference at container start. If autofs cycles between restarts, the container gets a stale handle. Permanent mount avoids this entirely.
|
||||
- **Manual book add** (when auto-add fails):
|
||||
```bash
|
||||
docker stop calibre
|
||||
docker run --rm \
|
||||
-v /srv/calibre/library:/config/Calibre\ Library \
|
||||
-v /mnt/nas_books/calibre-import:/calibre-import \
|
||||
lscr.io/linuxserver/calibre:latest \
|
||||
sh -c "cd /opt/calibre && LD_LIBRARY_PATH=/opt/calibre ./calibredb add \
|
||||
--library-path '/config/Calibre Library' '/calibre-import/<book>' 2>&1"
|
||||
docker start calibre
|
||||
```
|
||||
|
||||
### Filebrowser-Colleen-HD
|
||||
- **Strategy**: systemd CIFS automount + Docker bind mount
|
||||
- **Host mount**: `/mnt/nas_backup` (CIFS `//192.168.1.192/backup`)
|
||||
- **Container path**: `/folder`
|
||||
- **Mount options**: `vers=3.0,uid=0,gid=0,noperm,noserverino,soft,TimeoutIdleSec=0`
|
||||
- **Why CIFS**: Container runs as root; CIFS with `uid=0,gid=0` maps all files to root
|
||||
- **TimeoutIdleSec=0**: Never auto-unmounts once triggered; safe for persistent container access
|
||||
|
||||
### Backrest (backup service)
|
||||
- **Strategy**: systemd CIFS automount + Docker bind mount (same mount as Filebrowser)
|
||||
- **Host mount**: `/mnt/nas_backup/nas-docker-data`
|
||||
- **Container path**: `/backup-export`
|
||||
- **Same CIFS mount as Filebrowser-Colleen-HD** (`mnt-nas_backup`)
|
||||
|
||||
### oCIS (ownCloud Infinite Scale)
|
||||
- **Strategy**: systemd permanent NFS mount + Docker bind mount
|
||||
- **Host mount**: `/mnt/nas_owncloud` (permanent NFS, no automount)
|
||||
- **Container path**: `/var/lib/ocis/storage/users`
|
||||
- **NAS path**: `192.168.1.192:/mnt/user/owncloud`
|
||||
- **Mount options**: `nfsvers=3,hard,rw,noatime`
|
||||
- **Local config/index**: `/srv/ocis/data` and `/srv/ocis/config` stay on local SSD
|
||||
|
||||
### Immich
|
||||
- **Strategy**: systemd permanent NFS mount + Docker bind mount
|
||||
- **Host mount**: `/mnt/nas_family` (NFS `192.168.1.192:/mnt/user/family`)
|
||||
- **Container path**: `/usr/src/app/upload` → `/mnt/nas_family/immich-library`
|
||||
- **Mount options**: `nfsvers=3,hard,rw,noatime`
|
||||
- **Why permanent (not automount)**: Drive doesn't spin down; permanent mount avoids any autofs cycling risk
|
||||
- **Database**: PostgreSQL on local SSD (`/var/lib/postgresql/data`) — correct, not on NFS
|
||||
|
||||
### Audiobookshelf
|
||||
- **Strategy**: Docker NFS named volume
|
||||
- **NAS path**: `192.168.1.192:/mnt/user/media/audiobooks`
|
||||
- **Container path**: `/audiobooks`
|
||||
- **Config/metadata**: Local SSD (`/srv/audiobookshelf/`) — correct
|
||||
|
||||
---
|
||||
|
||||
## systemd Mount Units
|
||||
|
||||
| Unit | Type | Share | Status |
|
||||
|------|------|-------|--------|
|
||||
| `mnt-nas_books.mount` | NFS permanent | `:/mnt/user/media/books` | enabled, always up |
|
||||
| `mnt-nas_owncloud.mount` | NFS permanent | `:/mnt/user/owncloud` | enabled, always up |
|
||||
| `mnt-nas_family.mount` | NFS permanent | `:/mnt/user/family` | enabled, always up |
|
||||
| `mnt-nas_library.mount` | CIFS automount | `//192.168.1.192/library` | triggered on access, TimeoutIdleSec=0 |
|
||||
| `mnt-nas_library.automount` | CIFS automount | — | enabled |
|
||||
| `mnt-nas_backup.mount` | CIFS automount | `//192.168.1.192/backup` | triggered on access, TimeoutIdleSec=0 |
|
||||
| `mnt-nas_backup.automount` | CIFS automount | — | enabled |
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & Gotchas
|
||||
|
||||
- **Docker NFS named volumes**: The `nas_media` volume uses `soft,nolock,timeo=14,nfsvers=4`. This is fragile — short timeout means EIO on spin-up. Plex/qBittorrent survive because they open/close handles per request. If either starts having I/O errors, migrate to the systemd permanent mount pattern.
|
||||
- **Docker bind mounts + automount = stale handles**: Docker snapshots the mount reference at container start. If autofs cycles (unmount + remount) between container restarts, the container's bind mount goes stale while the host sees the path fine. Fix: use permanent `.mount` (no `.automount`) for any share that Docker containers bind-mount.
|
||||
- **SQLite on NFS**: Never store SQLite databases (calibre `metadata.db`/`notes.db`, any app DB) on NFS. Even with `hard` mounts and NLM locking, transient NFS errors cause `SQLITE_IOERR`. Keep SQLite on local SSD; sync non-SQLite files to NAS if sharing is needed.
|
||||
- **CIFS vs NFS for file permissions**: CIFS enforces server-side ACLs based on the SMB user. Files created via NFS by uid=99 with mode 600 are inaccessible via CIFS. Use NFS when container needs uid-mapped access to NFS-created files.
|
||||
- **lsyncd for NAS sync**: inotify-based, syncs within ~15 seconds of any write. Config at `/etc/lsyncd/lsyncd.conf.lua`. Log at `/var/log/lsyncd.log`.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Migration (future)
|
||||
|
||||
Plex, qBittorrent, Immich, and Audiobookshelf all use the fragile Docker NFS named volume pattern. The safer pattern is systemd permanent mount + Docker bind mount (as used by calibre-import and oCIS). This would involve:
|
||||
1. Create `/mnt/nas_media` systemd permanent mount unit
|
||||
2. Update compose files to use bind mounts instead of the external `nas_media` volume
|
||||
3. `docker volume rm nas_media` after redeployment
|
||||
@@ -0,0 +1,23 @@
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: npm-network
|
||||
|
||||
services:
|
||||
audiobookshelf:
|
||||
image: ghcr.io/advplyr/audiobookshelf:latest
|
||||
container_name: audiobookshelf
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/New_York
|
||||
- VIRTUAL_HOST=audiobookshelf.kolpacksoftware.com
|
||||
- VIRTUAL_PORT=80
|
||||
- LETSENCRYPT_HOST=audiobookshelf.kolpacksoftware.com
|
||||
volumes:
|
||||
- /srv/audiobookshelf/config:/config
|
||||
- /srv/audiobookshelf/metadata:/metadata
|
||||
- /mnt/nas_audiobooks:/audiobooks
|
||||
ports:
|
||||
- 13378:80
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,26 @@
|
||||
# Authelia secrets — set all of these in Portainer stack environment variables
|
||||
# Generate random values with: openssl rand -hex 32
|
||||
|
||||
# Core secrets (safe as env vars — no $ signs in values)
|
||||
AUTHELIA_JWT_SECRET=
|
||||
AUTHELIA_SESSION_SECRET=
|
||||
AUTHELIA_STORAGE_ENCRYPTION_KEY=
|
||||
|
||||
# OIDC HMAC secret (safe as env var — hex string, no $ signs)
|
||||
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET=
|
||||
|
||||
# OIDC client secrets are NOT stored as env vars.
|
||||
# Bcrypt hashes contain $ characters which Portainer/shell interpolates, truncating them.
|
||||
# Instead, store hashes as files on the host:
|
||||
# /srv/authelia/config/secrets/oidc_open_webui <- bcrypt hash of open-webui client secret
|
||||
# /srv/authelia/config/secrets/oidc_linkding <- bcrypt hash of linkding client secret
|
||||
#
|
||||
# Write them with Python (not shell) to avoid $ interpolation:
|
||||
# docker run --rm -v /srv/authelia/config:/config python:3-alpine python3 -c "
|
||||
# open('/config/secrets/oidc_open_webui','w').write('<bcrypt-hash>')
|
||||
# open('/config/secrets/oidc_linkding','w').write('<bcrypt-hash>')
|
||||
# "
|
||||
# Generate a bcrypt hash:
|
||||
# docker run --rm authelia/authelia:4.38 authelia crypto hash generate bcrypt --password <plaintext>
|
||||
#
|
||||
# Note: the OIDC JWK private key is also host-managed inline in /srv/authelia/config/configuration.yml
|
||||
@@ -0,0 +1,179 @@
|
||||
server:
|
||||
address: 0.0.0.0:9091
|
||||
|
||||
log:
|
||||
level: info
|
||||
|
||||
totp:
|
||||
issuer: kolpacksoftware.com
|
||||
|
||||
webauthn:
|
||||
disable: true
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: /config/users_database.yaml
|
||||
password:
|
||||
algorithm: argon2id
|
||||
|
||||
access_control:
|
||||
default_policy: deny
|
||||
rules:
|
||||
- domain: auth.kolpacksoftware.com
|
||||
policy: bypass
|
||||
- domain: ultralytics.kolpacksoftware.com
|
||||
policy: one_factor
|
||||
- domain: "*.kolpacksoftware.com"
|
||||
policy: one_factor
|
||||
subject: "group:admins"
|
||||
|
||||
session:
|
||||
cookies:
|
||||
- domain: kolpacksoftware.com
|
||||
authelia_url: https://auth.kolpacksoftware.com
|
||||
default_redirection_url: https://kolpacksoftware.com
|
||||
name: authelia_session
|
||||
expiration: 1h
|
||||
inactivity: 5m
|
||||
redis:
|
||||
host: authelia-redis
|
||||
port: 6379
|
||||
|
||||
storage:
|
||||
local:
|
||||
path: /config/db.sqlite3
|
||||
|
||||
notifier:
|
||||
filesystem:
|
||||
filename: /config/notifications.txt
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
find_time: 2m
|
||||
ban_time: 5m
|
||||
|
||||
identity_providers:
|
||||
oidc:
|
||||
hmac_secret: ${AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET}
|
||||
cors:
|
||||
endpoints:
|
||||
- authorization
|
||||
- token
|
||||
- revocation
|
||||
- introspection
|
||||
- userinfo
|
||||
allowed_origins_from_client_redirect_uris: true
|
||||
jwks:
|
||||
- key_id: main
|
||||
algorithm: RS256
|
||||
use: sig
|
||||
# key is host-managed — never commit to git
|
||||
# Host copy inlines the PEM content as a YAML block scalar (key: |)
|
||||
# using Python to avoid shell $ interpolation of the PEM content.
|
||||
# Generate with: openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out /srv/authelia/config/oidc.key
|
||||
clients:
|
||||
- client_id: open-webui
|
||||
client_name: Open WebUI
|
||||
client_secret: '{{ secret "/config/secrets/oidc_open_webui" }}'
|
||||
public: false
|
||||
authorization_policy: one_factor
|
||||
token_endpoint_auth_method: client_secret_basic
|
||||
redirect_uris:
|
||||
- https://open-webui.kolpacksoftware.com/oauth/oidc/callback
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
userinfo_signed_response_alg: none
|
||||
|
||||
- client_id: immich
|
||||
client_name: Immich
|
||||
client_secret: '{{ secret "/config/secrets/oidc_immich" }}'
|
||||
public: false
|
||||
authorization_policy: one_factor
|
||||
token_endpoint_auth_method: client_secret_post
|
||||
redirect_uris:
|
||||
- https://immich.kolpacksoftware.com/auth/login
|
||||
- app.immich:///oauth-callback
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
userinfo_signed_response_alg: none
|
||||
|
||||
- client_id: linkding
|
||||
client_name: Linkding
|
||||
client_secret: '{{ secret "/config/secrets/oidc_linkding" }}'
|
||||
public: false
|
||||
authorization_policy: one_factor
|
||||
token_endpoint_auth_method: client_secret_post
|
||||
redirect_uris:
|
||||
- https://linkding.kolpacksoftware.com/oidc/callback/
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
- groups
|
||||
userinfo_signed_response_alg: none
|
||||
|
||||
- client_id: ocis
|
||||
client_name: ownCloud Infinite Scale
|
||||
public: true
|
||||
require_pkce: true
|
||||
pkce_challenge_method: S256
|
||||
authorization_policy: one_factor
|
||||
redirect_uris:
|
||||
- https://cloud.kolpacksoftware.com/
|
||||
- https://cloud.kolpacksoftware.com/oidc-callback.html
|
||||
- https://cloud.kolpacksoftware.com/oidc-silent-redirect.html
|
||||
- https://cloud.kolpacksoftware.com/apps/openidconnect/redirect
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
- groups
|
||||
- offline_access
|
||||
userinfo_signed_response_alg: none
|
||||
- client_id: mxd5OQDk6es5LzOzRvidJNfXLUZS2oN3oUFeXPP8LpPrhx3UroJFduGEYIBOxkY1
|
||||
client_name: ownCloud iOS
|
||||
client_secret: '{{ secret "/config/secrets/oidc_ocis_ios" }}'
|
||||
public: false
|
||||
require_pkce: true
|
||||
pkce_challenge_method: S256
|
||||
token_endpoint_auth_method: client_secret_basic
|
||||
authorization_policy: one_factor
|
||||
response_types:
|
||||
- code
|
||||
grant_types:
|
||||
- authorization_code
|
||||
- refresh_token
|
||||
redirect_uris:
|
||||
- oc://ios.owncloud.com
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
- offline_access
|
||||
userinfo_signed_response_alg: none
|
||||
|
||||
- client_id: xdXOt13JKxym1B1QcEncf2XDkLAexMBFwiT9j6EfhhHFJhs2KM9jbjTmf8JBXE69
|
||||
client_name: ownCloud Desktop
|
||||
client_secret: '{{ secret "/config/secrets/oidc_ocis_desktop" }}'
|
||||
public: false
|
||||
require_pkce: true
|
||||
pkce_challenge_method: S256
|
||||
token_endpoint_auth_method: client_secret_basic
|
||||
authorization_policy: one_factor
|
||||
response_types:
|
||||
- code
|
||||
grant_types:
|
||||
- authorization_code
|
||||
- refresh_token
|
||||
redirect_uris:
|
||||
- http://127.0.0.1
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
- offline_access
|
||||
userinfo_signed_response_alg: none
|
||||
@@ -0,0 +1,39 @@
|
||||
services:
|
||||
authelia:
|
||||
container_name: authelia
|
||||
image: authelia/authelia:4.38
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
volumes:
|
||||
- /srv/authelia/config:/config
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- AUTHELIA_JWT_SECRET=${AUTHELIA_JWT_SECRET}
|
||||
- AUTHELIA_SESSION_SECRET=${AUTHELIA_SESSION_SECRET}
|
||||
- AUTHELIA_STORAGE_ENCRYPTION_KEY=${AUTHELIA_STORAGE_ENCRYPTION_KEY}
|
||||
- AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET=${AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET}
|
||||
# OIDC client secrets are NOT passed as env vars — bcrypt hashes contain $ which
|
||||
# Portainer/shell interpolates. Stored as files in /srv/authelia/config/secrets/ instead.
|
||||
- X_AUTHELIA_CONFIG_FILTERS=template
|
||||
networks:
|
||||
- npm-network
|
||||
- authelia-internal
|
||||
depends_on:
|
||||
- authelia-redis
|
||||
|
||||
authelia-redis:
|
||||
container_name: authelia-redis
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: --save 60 1 --loglevel warning
|
||||
volumes:
|
||||
- /srv/authelia/redis:/data
|
||||
networks:
|
||||
- authelia-internal
|
||||
|
||||
networks:
|
||||
npm-network:
|
||||
external: true
|
||||
authelia-internal:
|
||||
driver: bridge
|
||||
@@ -1,95 +0,0 @@
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: nginx-network
|
||||
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
environment:
|
||||
POSTGRES_DB: ${PG_DB:-authentik}
|
||||
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
|
||||
POSTGRES_USER: ${PG_USER:-authentik}
|
||||
healthcheck:
|
||||
interval: 30s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
|
||||
timeout: 5s
|
||||
image: docker.io/library/postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
redis:
|
||||
command: --save 60 1 --loglevel warning
|
||||
healthcheck:
|
||||
interval: 30s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- redis-cli ping | grep PONG
|
||||
timeout: 3s
|
||||
image: docker.io/library/redis:alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
command: server
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.3}
|
||||
ports:
|
||||
- ${COMPOSE_PORT_HTTP:-9000}:9000
|
||||
- ${COMPOSE_PORT_HTTPS:-9443}:9443
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./media:/media
|
||||
- ./custom-templates:/templates
|
||||
worker:
|
||||
command: worker
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.3}
|
||||
restart: unless-stopped
|
||||
user: root
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./media:/media
|
||||
- ./certs:/certs
|
||||
- ./custom-templates:/templates
|
||||
volumes:
|
||||
database:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: 'none'
|
||||
o: 'bind'
|
||||
device: '/srv/authentik/database'
|
||||
redis:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: 'none'
|
||||
o: 'bind'
|
||||
device: '/srv/authentik/redis'
|
||||
@@ -0,0 +1,2 @@
|
||||
SMB_USERNAME=your_smb_username
|
||||
SMB_PASSWORD=your_smb_password
|
||||
@@ -1,13 +1,5 @@
|
||||
version: "3.8"
|
||||
|
||||
volumes:
|
||||
backup-mnt:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: "addr=192.168.1.192,rw,noatime,rsize=8192,wsize=8192,tcp,timeo=14,nolock,soft,nfsvers=4"
|
||||
device: " :/mnt/user/backup/nas-docker-data"
|
||||
|
||||
services:
|
||||
backrest:
|
||||
image: garethgeorge/backrest:latest
|
||||
@@ -20,8 +12,7 @@ services:
|
||||
- /srv/backrest/tmp:/tmp
|
||||
- /srv/backrest/rclone:/root/.config/rclone # Mount for rclone config (needed when using rclone remotes)
|
||||
- /srv:/srv-data # Mount local paths to backup
|
||||
#- /mnt/backup-export:/backup-export # NFS share backup destination
|
||||
- backup-mnt:/backup-export # NFS share backup destination
|
||||
- /mnt/nas_backup/nas-docker-data:/backup-export
|
||||
environment:
|
||||
- BACKREST_DATA=/data
|
||||
- BACKREST_CONFIG=/config/config.json
|
||||
@@ -30,4 +21,4 @@ services:
|
||||
- TZ=America/New_York
|
||||
ports:
|
||||
- "9898:9898"
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
|
||||
+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:
|
||||
default:
|
||||
external:
|
||||
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:
|
||||
calibre:
|
||||
image: lscr.io/linuxserver/calibre:latest
|
||||
container_name: calibre
|
||||
security_opt:
|
||||
- seccomp:unconfined #optional
|
||||
#security-opt:
|
||||
# - seccomp:unconfined
|
||||
- seccomp:unconfined
|
||||
environment:
|
||||
- PUID=1024
|
||||
- PGID=1024
|
||||
- PUID=99
|
||||
- PGID=100
|
||||
- TZ=America/New_York
|
||||
- PASSWORD= #optional
|
||||
- CLI_ARGS= #optional
|
||||
- VIRTUAL_HOST=calibre.kolpacksoftware.com
|
||||
- VIRTUAL_PORT=8080
|
||||
- LETSENCRYPT_HOST=calibre.kolpacksoftware.com
|
||||
volumes:
|
||||
- config:/config
|
||||
- import:/calibre-import
|
||||
- /srv/calibre/config:/config
|
||||
- /srv/calibre/library:/config/Calibre Library
|
||||
- /mnt/nas_books/calibre-import:/calibre-import
|
||||
ports:
|
||||
- 8090:8080
|
||||
- 8091:8081
|
||||
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/tsa-chapter-organizer-rms:/mnt/tsa-chapter-organizer-rms
|
||||
- /srv/glances:/mnt/glances
|
||||
- /srv/authelia/config:/mnt/authelia
|
||||
ports:
|
||||
- 8443:8443
|
||||
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
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
couchserver:
|
||||
image: couchdb
|
||||
container_name: couchdb
|
||||
restart: always
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
ports:
|
||||
- "127.0.0.1:5984:5984"
|
||||
environment:
|
||||
- COUCHDB_USER=admin
|
||||
- COUCHDB_PASSWORD=${ADMIN_PASSWORD}
|
||||
volumes:
|
||||
- /srv/couchdb-data:/opt/couchdb/data
|
||||
- /srv/couchdb-config/local.ini:/opt/couchdb/etc/local.ini
|
||||
networks:
|
||||
- npm-network
|
||||
|
||||
networks:
|
||||
default:
|
||||
npm-network:
|
||||
external: true
|
||||
@@ -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
|
||||
tty: true
|
||||
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=false
|
||||
environment:
|
||||
- APIKEY=s5P998CqUu5bqAukdS1dW57EfjgFlX
|
||||
- APIKEY=${APIKEY}
|
||||
- HOST=kolpacksoftware.com,rmstsa.org,popcyclical.com
|
||||
- INTERVAL=1h
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
SECRET_KEY_BASE=generate_with_openssl_rand_-hex_64
|
||||
@@ -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"
|
||||
TZ: "America/New_York"
|
||||
volumes:
|
||||
- backup-mnt:/folder
|
||||
- /mnt/nas_backup:/folder
|
||||
- /srv/filebrowser-colleen-hd:/home/filebrowser/data
|
||||
ports:
|
||||
- 3427:80
|
||||
@@ -14,15 +14,7 @@ services:
|
||||
- npm-network
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
backup-mnt:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: "addr=192.168.1.192,rw,nolock,soft"
|
||||
device: ":/mnt/user/backup/"
|
||||
|
||||
networks:
|
||||
default:
|
||||
npm-network:
|
||||
external: true
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Generate with: openssl rand -hex 16 | head -c 32
|
||||
APP_KEY=
|
||||
|
||||
APP_URL=https://firefly.kolpacksoftware.com
|
||||
|
||||
DB_PASSWORD=
|
||||
@@ -0,0 +1,65 @@
|
||||
# Firefly III Data Import Plan
|
||||
|
||||
## Bank Accounts
|
||||
|
||||
| Institution | Type | Export Format | Automation |
|
||||
|-------------|------|---------------|------------|
|
||||
| Capital One | Bank/Credit Card | CSV only (dropped OFX) | SimpleFIN |
|
||||
| Citibank | Credit Card | CSV + QFX/OFX Direct Connect | SimpleFIN or OFX |
|
||||
|
||||
## Phase 1: Manual Import via FIDI (Free)
|
||||
|
||||
**Deploy FIDI** as a companion container to Firefly III.
|
||||
|
||||
- Image: `fireflyiii/data-importer:latest`
|
||||
- Needs: `FIREFLY_III_URL`, `FIREFLY_III_ACCESS_TOKEN` (generate in Firefly UI under Profile → OAuth)
|
||||
- Port: 8383 (internal, no need to expose publicly)
|
||||
- Add to `firefly-iii/docker-compose.yaml` as a second service on the internal network
|
||||
|
||||
**Workflow:**
|
||||
1. Log into Capital One → Account → Download transactions → CSV
|
||||
2. Log into Citi → Account → Download transactions → QFX or CSV
|
||||
3. Go to FIDI UI → upload file → map columns → import
|
||||
|
||||
**Citi OFX Direct Connect** (optional, skips manual download):
|
||||
- Server: `https://www.citi.com/ofxdirect`
|
||||
- Requires Quicken-compatible credentials (may need to call Citi to enable)
|
||||
- FIDI supports OFX connections natively
|
||||
|
||||
## Phase 2: Automation via SimpleFIN (Optional, ~$1.50/mo)
|
||||
|
||||
SimpleFIN Bridge aggregates US bank data including Capital One and Citi.
|
||||
|
||||
**Setup:**
|
||||
1. Sign up at https://bridge.simplefin.org/
|
||||
2. Get a SimpleFIN token
|
||||
3. Deploy a bridge script (e.g. `simplefin-to-firefly` community tool) as a cron container
|
||||
4. Script polls SimpleFIN → pushes new transactions to Firefly III API
|
||||
|
||||
**Firefly III API base URL:** `https://firefly.kolpacksoftware.com/api/v1`
|
||||
**Auth:** Personal Access Token (Profile → OAuth → Personal Access Tokens)
|
||||
|
||||
## FIDI Compose Addition
|
||||
|
||||
Add to `firefly-iii/docker-compose.yaml`:
|
||||
|
||||
```yaml
|
||||
fidi:
|
||||
image: fireflyiii/data-importer:latest
|
||||
container_name: firefly-iii-fidi
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- app
|
||||
environment:
|
||||
- FIREFLY_III_URL=http://app:8080
|
||||
- VANITY_URL=https://firefly.kolpacksoftware.com
|
||||
- FIREFLY_III_ACCESS_TOKEN=${FIDI_ACCESS_TOKEN}
|
||||
- TZ=America/New_York
|
||||
ports:
|
||||
- 8383:8080
|
||||
networks:
|
||||
- internal
|
||||
```
|
||||
|
||||
- `FIDI_ACCESS_TOKEN`: Generate in Firefly III UI → Profile → OAuth → Personal Access Tokens
|
||||
- FIDI runs on internal network only — access via `http://<host-ip>:8383` or add to npm-network if you want a public URL
|
||||
@@ -0,0 +1,45 @@
|
||||
services:
|
||||
app:
|
||||
image: fireflyiii/core:latest
|
||||
container_name: firefly-iii
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- APP_KEY=${APP_KEY}
|
||||
- APP_URL=${APP_URL}
|
||||
- TRUSTED_PROXIES=**
|
||||
- DB_CONNECTION=pgsql
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_DATABASE=firefly
|
||||
- DB_USERNAME=firefly
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- TZ=America/New_York
|
||||
volumes:
|
||||
- /srv/firefly-iii/upload:/var/www/html/storage/upload
|
||||
ports:
|
||||
- 8282:8080
|
||||
networks:
|
||||
- default
|
||||
- internal
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: firefly-iii-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=firefly
|
||||
- POSTGRES_USER=firefly
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
volumes:
|
||||
- /srv/firefly-iii/db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- internal
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: npm-network
|
||||
internal:
|
||||
driver: bridge
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3"
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
@@ -9,16 +7,19 @@ services:
|
||||
server:
|
||||
image: docker.gitea.com/gitea:nightly
|
||||
container_name: gitea
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- VIRTUAL_HOST=gitea.kolpacksoftware.com
|
||||
- VIRTUAL_PORT=3000
|
||||
- LETSENCRYPT_HOST=gitea.kolpacksoftware.com
|
||||
restart: always
|
||||
# networks:
|
||||
# - gitea
|
||||
volumes:
|
||||
- /srv/gitea-data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- "222:22"
|
||||
- "222:22"
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
glances:
|
||||
container_name: glances
|
||||
image: 'nicolargo/glances:latest-full'
|
||||
image: 'nicolargo/glances:ubuntu-latest-full'
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
ports:
|
||||
- 61208-61209:61208-61209
|
||||
- 127.0.0.1:61208-61209:61208-61209
|
||||
environment:
|
||||
- TZ=${TZ}
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
- "GLANCES_OPT=-C /glances/conf/glances.conf -w"
|
||||
volumes:
|
||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||
- '/run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro'
|
||||
- /srv/glances/glances.conf:/glances/conf/glances.conf
|
||||
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:
|
||||
homebox:
|
||||
# image: ghcr.io/sysadminsmedia/homebox:latest
|
||||
image: ghcr.io/sysadminsmedia/homebox:latest-rootless
|
||||
container_name: homebox
|
||||
restart: always
|
||||
environment:
|
||||
- HBOX_LOG_LEVEL=info
|
||||
- HBOX_LOG_FORMAT=text
|
||||
- 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_LOG_LEVEL=info
|
||||
- HBOX_LOG_FORMAT=text
|
||||
- HBOX_WEB_MAX_FILE_UPLOAD=10
|
||||
- HBOX_OPTIONS_ALLOW_ANALYTICS=false
|
||||
- HBOX_AUTH_API_KEY_PEPPER=${HBOX_AUTH_API_KEY_PEPPER}
|
||||
- VIRTUAL_HOST=homebox.kolpacksoftware.com
|
||||
- VIRTUAL_PORT=7745
|
||||
- LETSENCRYPT_HOST=homebox.kolpacksoftware.com
|
||||
volumes:
|
||||
- /srv/homebox-data:/data/
|
||||
ports:
|
||||
@@ -18,4 +20,4 @@ services:
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: npm-network
|
||||
name: npm-network
|
||||
|
||||
+11
-11
@@ -16,9 +16,10 @@ services:
|
||||
# extends:
|
||||
# file: hwaccel.transcoding.yml
|
||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
volumes:
|
||||
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /mnt/nas_family/immich-library:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- stack.env
|
||||
@@ -42,6 +43,8 @@ services:
|
||||
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
|
||||
# file: hwaccel.ml.yml
|
||||
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
volumes:
|
||||
- model-cache:/cache
|
||||
env_file:
|
||||
@@ -53,13 +56,17 @@ services:
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=false
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=false
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
@@ -68,24 +75,17 @@ services:
|
||||
volumes:
|
||||
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
|
||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||
shm_size: 128mb
|
||||
healthcheck:
|
||||
test: >-
|
||||
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
||||
interval: 5m
|
||||
#start_interval: 30s
|
||||
start_period: 5m
|
||||
command: >-
|
||||
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
model-cache:
|
||||
library:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: "addr=192.168.1.192,rw,nolock,soft"
|
||||
device: ":/mnt/user/family/immich-library"
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=inboxzero
|
||||
- POSTGRES_DB=inboxzero
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U inboxzero']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- /srv/inbox-zero/postgres:/var/lib/postgresql/data
|
||||
networks:
|
||||
- inbox-zero-network
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /srv/inbox-zero/redis:/data
|
||||
networks:
|
||||
- inbox-zero-network
|
||||
|
||||
serverless-redis-http:
|
||||
image: hiett/serverless-redis-http:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SRH_MODE: env
|
||||
SRH_TOKEN: ${UPSTASH_REDIS_TOKEN}
|
||||
SRH_CONNECTION_STRING: "redis://redis:6379"
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
- inbox-zero-network
|
||||
|
||||
web:
|
||||
image: ghcr.io/elie222/inbox-zero:v1.4.12
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
environment:
|
||||
NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}
|
||||
NEXT_PUBLIC_BYPASS_PREMIUM_CHECKS: "true"
|
||||
NEXT_PUBLIC_EMAIL_SEND_ENABLED: "true"
|
||||
DATABASE_URL: postgresql://inboxzero:${POSTGRES_PASSWORD}@db:5432/inboxzero?schema=public
|
||||
DIRECT_URL: postgresql://inboxzero:${POSTGRES_PASSWORD}@db:5432/inboxzero?schema=public
|
||||
UPSTASH_REDIS_URL: http://serverless-redis-http:80
|
||||
UPSTASH_REDIS_TOKEN: ${UPSTASH_REDIS_TOKEN}
|
||||
REDIS_URL: redis://redis:6379
|
||||
INTERNAL_API_URL: http://web:3000
|
||||
AUTH_SECRET: ${AUTH_SECRET}
|
||||
EMAIL_ENCRYPT_SECRET: ${EMAIL_ENCRYPT_SECRET}
|
||||
EMAIL_ENCRYPT_SALT: ${EMAIL_ENCRYPT_SALT}
|
||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
|
||||
GOOGLE_PUBSUB_TOPIC_NAME: ${GOOGLE_PUBSUB_TOPIC_NAME}
|
||||
GOOGLE_PUBSUB_VERIFICATION_TOKEN: ${GOOGLE_PUBSUB_VERIFICATION_TOKEN}
|
||||
CRON_SECRET: ${CRON_SECRET}
|
||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||
API_KEY_SALT: ${API_KEY_SALT}
|
||||
HOSTNAME: "0.0.0.0"
|
||||
# Ollama AI provider
|
||||
DEFAULT_LLM_PROVIDER: ollama
|
||||
DEFAULT_LLM_MODEL: ${OLLAMA_MODEL}
|
||||
ECONOMY_LLM_PROVIDER: ollama
|
||||
ECONOMY_LLM_MODEL: ${OLLAMA_MODEL}
|
||||
OLLAMA_BASE_URL: http://ollama:11434/api
|
||||
networks:
|
||||
- inbox-zero-network
|
||||
- npm-network
|
||||
|
||||
cron:
|
||||
image: alpine:latest
|
||||
restart: unless-stopped
|
||||
command: >
|
||||
sh -c "
|
||||
apk add --no-cache curl &&
|
||||
(
|
||||
while true; do
|
||||
curl -s -X GET 'http://web:3000/api/cron/scheduled-actions' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
|
||||
sleep 900
|
||||
done
|
||||
) &
|
||||
(
|
||||
while true; do
|
||||
curl -s -X GET 'http://web:3000/api/cron/automation-jobs' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
|
||||
sleep 900
|
||||
done
|
||||
) &
|
||||
(
|
||||
while true; do
|
||||
curl -s -X GET 'http://web:3000/api/follow-up-reminders' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
|
||||
sleep 3600
|
||||
done
|
||||
) &
|
||||
(
|
||||
while true; do
|
||||
curl -s -X GET 'http://web:3000/api/resend/digest/all' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
|
||||
sleep 1800
|
||||
done
|
||||
) &
|
||||
(
|
||||
while true; do
|
||||
curl -s -X GET 'http://web:3000/api/meeting-briefs' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
|
||||
sleep 900
|
||||
done
|
||||
) &
|
||||
(
|
||||
while true; do
|
||||
curl -s -X GET 'http://web:3000/api/watch/all' -H \"Authorization: Bearer $${CRON_SECRET}\" || true
|
||||
sleep 21600
|
||||
done
|
||||
) &
|
||||
wait
|
||||
"
|
||||
environment:
|
||||
CRON_SECRET: ${CRON_SECRET}
|
||||
depends_on:
|
||||
- web
|
||||
networks:
|
||||
- inbox-zero-network
|
||||
|
||||
networks:
|
||||
inbox-zero-network:
|
||||
driver: bridge
|
||||
npm-network:
|
||||
external: true
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Gmail Filter Cleanup Script
|
||||
* Run from https://script.google.com
|
||||
*
|
||||
* SETUP REQUIRED:
|
||||
* Before running, enable the Gmail Advanced Service:
|
||||
* 1. Click "+" next to "Services" in the left sidebar
|
||||
* 2. Find "Gmail API" and add it
|
||||
*
|
||||
* Run dryRun() first to see what will be deleted.
|
||||
* Run deleteFilters() to actually delete them.
|
||||
*/
|
||||
|
||||
// Patterns to match against filter "from" criteria.
|
||||
// Any filter whose "from" contains one of these strings will be deleted.
|
||||
var DELETE_FROM_PATTERNS = [
|
||||
// Dead services
|
||||
'mozy.com',
|
||||
'ingdirect.com',
|
||||
'stumbleupon.com',
|
||||
'plaxo.com',
|
||||
'jott.com',
|
||||
'svpply.com',
|
||||
'alice@email.aliceupdates.com',
|
||||
'telltalegames.com',
|
||||
'gymboree.com',
|
||||
'landofnod.com',
|
||||
'evganews@evga.com',
|
||||
'thinkgeek.com',
|
||||
'recyclebank.com',
|
||||
'sharebuilder.com',
|
||||
'zulily.com',
|
||||
|
||||
// Political / campaign (no longer relevant)
|
||||
'barackobama.com',
|
||||
'berniesanders.com',
|
||||
'democrats.org',
|
||||
'rockthevote.com',
|
||||
'rockthevote',
|
||||
|
||||
// No longer relevant (uncertain ones user confirmed)
|
||||
'vacationsurvey@vargaresearch.com',
|
||||
'5h4rpd0g@gmail.com',
|
||||
'mdegan@jacsmechanical.com',
|
||||
'ispcm-conf.org',
|
||||
'tntechnology.org',
|
||||
'Jason Mechler',
|
||||
|
||||
// Duplicates / inbox-zero handles these via AI
|
||||
'nytimes@email.newyorktimes.com',
|
||||
'nytimes@e.newyorktimesinfo.com',
|
||||
'redcross-email@usa.redcross.org',
|
||||
'redcross-email@redcross.org',
|
||||
'redcross@theamericanredcross.org',
|
||||
'xfinity@info.xfinity.com',
|
||||
'xfinity@emails.xfinity.com',
|
||||
'kirkusreviews.com',
|
||||
'smarttoys@aol.com',
|
||||
'spotify@email.news.spotifymail.com',
|
||||
'no-reply@news.spotifymail.com',
|
||||
'newsletters@audible.com',
|
||||
'fightingback@messages.cancer.org',
|
||||
'@messages.cancer.org',
|
||||
|
||||
// Newsletter/marketing — inbox-zero AI handles these
|
||||
'eff.org',
|
||||
'rccetimes.com',
|
||||
'group-digests@linkedin.com',
|
||||
'info.knologyupclose.com',
|
||||
'newsletter@theorangepeel.net',
|
||||
'microsoft@e-mail.microsoft.com',
|
||||
'newsletters-no-reply@myfonts.com',
|
||||
'act@credoaction.com',
|
||||
'yourpowertip@powershell.com',
|
||||
'email@email.microsoftemail.com',
|
||||
'ramit@iwillteachyoutoberich.com',
|
||||
'mkt@manning.com',
|
||||
'deals@livingsocial.com',
|
||||
'idera.com',
|
||||
'newsletter@kirkusreviews.com',
|
||||
'email@earthfare-email.com',
|
||||
'gameinfo@email2.telltalegames.com',
|
||||
'no-reply@mail.goodreads.com',
|
||||
'angieslist@members.angieslist.com',
|
||||
'reply@e.taxact.com',
|
||||
'josabank@shop.josbank.com',
|
||||
'frenchpaperco@gmail.com',
|
||||
'csaimages@frenchpaper.com',
|
||||
'mailman@xoxide.com',
|
||||
'info@petsafe.net',
|
||||
'flor@news.flor.com',
|
||||
'info@launchtn.org',
|
||||
'alumni@tntech.edu',
|
||||
'info@rockthevote.com',
|
||||
'payscalemonitor@mail.payscale.com',
|
||||
'statefarm@e.statefarm.com',
|
||||
'mailer@svpply.com',
|
||||
'noreply@dontcrack.com',
|
||||
'info@paperandpolkadots.com',
|
||||
'dennis@adirondackguitar.com',
|
||||
'Yamaha_Corporation_of_America@mail.vresp.com',
|
||||
'carters.email@e.carters.com',
|
||||
'FTD@email.ftd.com',
|
||||
'info@email.glassdoor.com',
|
||||
'reply@glassdoor.com',
|
||||
'customerservice@1800petmeds.com',
|
||||
'news@crownclub.regmovies.com',
|
||||
'autonationspecials@autonation.chtah.com',
|
||||
'EddieBauerEmail@em.eddiebauer.com',
|
||||
'email@style.ballarddesigns.com',
|
||||
'barnesandnoble@m.bn.com',
|
||||
'sales@xoxide.com',
|
||||
'tripadvisor.com',
|
||||
'noreply@youtube.com',
|
||||
'donotreply@proguitarshop.com',
|
||||
'info@knoxvilleacademyofmusic.com',
|
||||
'artistupdates@reverbnation.com',
|
||||
'promotion@mwave.com',
|
||||
'deansbeans.com',
|
||||
'express@e.express.com',
|
||||
'alice@email.aliceupdates.com',
|
||||
'ispcm-conf.org',
|
||||
'no_reply@jetbrains.com',
|
||||
'noreply@roddenberry.com',
|
||||
'yourhondadealer@ahmdcms.com',
|
||||
'mdegan@jacsmechanical.com',
|
||||
];
|
||||
|
||||
// Subject patterns — delete filters matching these subjects
|
||||
// (only when combined with a dead/irrelevant from, handled separately below)
|
||||
// For now these are standalone subject-only filters to delete
|
||||
var DELETE_SUBJECT_PATTERNS = [
|
||||
'Abandonia Newsletter',
|
||||
];
|
||||
|
||||
// List patterns (mailing list ID in filter query)
|
||||
var DELETE_LIST_PATTERNS = [
|
||||
'oakridgemoms.googlegroups.com',
|
||||
'14-days.googlegroups.com',
|
||||
'120331169_84238_19051',
|
||||
'andrewbird.fanbridge.com',
|
||||
];
|
||||
|
||||
// Specific from+subject combos — delete if BOTH match
|
||||
var DELETE_FROM_SUBJECT_PAIRS = [
|
||||
{ from: 'linkedin.com', subject: 'endorsed' },
|
||||
{ from: 'updates@linkedin.com', subject: null }, // skip inbox only, no label
|
||||
{ from: 'connections@linkedin.com', subject: null }, // linkedin connection spam
|
||||
{ from: 'noreply@plaxo.com', subject: 'birthday' },
|
||||
{ from: 'confirm+ye22mger@facebookmail.com', subject: null }, // old specific fb filter
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function getFiltersToDelete_() {
|
||||
var response = Gmail.Users.Settings.Filters.list('me');
|
||||
var filters = response.filter || [];
|
||||
var toDelete = [];
|
||||
|
||||
filters.forEach(function(f) {
|
||||
var from = (f.criteria && f.criteria.from) ? f.criteria.from.toLowerCase() : '';
|
||||
var subject = (f.criteria && f.criteria.subject) ? f.criteria.subject.toLowerCase() : '';
|
||||
var query = (f.criteria && f.criteria.query) ? f.criteria.query.toLowerCase() : '';
|
||||
var shouldDelete = false;
|
||||
var reason = '';
|
||||
|
||||
// Check from patterns
|
||||
DELETE_FROM_PATTERNS.forEach(function(p) {
|
||||
if (from.indexOf(p.toLowerCase()) !== -1) {
|
||||
shouldDelete = true;
|
||||
reason = 'from matches: ' + p;
|
||||
}
|
||||
});
|
||||
|
||||
// Check subject patterns
|
||||
if (!shouldDelete) {
|
||||
DELETE_SUBJECT_PATTERNS.forEach(function(p) {
|
||||
if (subject.indexOf(p.toLowerCase()) !== -1) {
|
||||
shouldDelete = true;
|
||||
reason = 'subject matches: ' + p;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check list patterns in query
|
||||
if (!shouldDelete) {
|
||||
DELETE_LIST_PATTERNS.forEach(function(p) {
|
||||
if (query.indexOf(p.toLowerCase()) !== -1) {
|
||||
shouldDelete = true;
|
||||
reason = 'list matches: ' + p;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check from+subject pairs
|
||||
if (!shouldDelete) {
|
||||
DELETE_FROM_SUBJECT_PAIRS.forEach(function(pair) {
|
||||
var fromMatch = pair.from ? from.indexOf(pair.from.toLowerCase()) !== -1 : true;
|
||||
var subjectMatch = pair.subject ? subject.indexOf(pair.subject.toLowerCase()) !== -1 : true;
|
||||
if (fromMatch && subjectMatch && (pair.from || pair.subject)) {
|
||||
shouldDelete = true;
|
||||
reason = 'pair match: from=' + pair.from + ' subject=' + pair.subject;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldDelete) {
|
||||
toDelete.push({ id: f.id, criteria: f.criteria, action: f.action, reason: reason });
|
||||
}
|
||||
});
|
||||
|
||||
return { all: filters, toDelete: toDelete };
|
||||
}
|
||||
|
||||
function dryRun() {
|
||||
Logger.log('=== DRY RUN — no filters will be deleted ===');
|
||||
var result = getFiltersToDelete_();
|
||||
Logger.log('Total filters: ' + result.all.length);
|
||||
Logger.log('Filters that WOULD be deleted: ' + result.toDelete.length);
|
||||
Logger.log('');
|
||||
|
||||
result.toDelete.forEach(function(f) {
|
||||
Logger.log('DELETE [' + f.id + ']');
|
||||
Logger.log(' From: ' + (f.criteria.from || ''));
|
||||
Logger.log(' Subject: ' + (f.criteria.subject || ''));
|
||||
Logger.log(' Query: ' + (f.criteria.query || ''));
|
||||
Logger.log(' Reason: ' + f.reason);
|
||||
Logger.log('');
|
||||
});
|
||||
|
||||
Logger.log('=== Filters that will be KEPT ===');
|
||||
var kept = result.all.filter(function(f) {
|
||||
return !result.toDelete.some(function(d) { return d.id === f.id; });
|
||||
});
|
||||
kept.forEach(function(f) {
|
||||
Logger.log('KEEP [' + f.id + '] from=' + (f.criteria.from || '') +
|
||||
' subject=' + (f.criteria.subject || '') +
|
||||
' query=' + (f.criteria.query || ''));
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFilters() {
|
||||
Logger.log('=== Deleting filters ===');
|
||||
var result = getFiltersToDelete_();
|
||||
Logger.log('Deleting ' + result.toDelete.length + ' of ' + result.all.length + ' filters...');
|
||||
|
||||
var deleted = 0;
|
||||
result.toDelete.forEach(function(f) {
|
||||
try {
|
||||
Gmail.Users.Settings.Filters.remove('me', f.id);
|
||||
Logger.log('Deleted [' + f.id + '] from=' + (f.criteria.from || '') + ' (' + f.reason + ')');
|
||||
deleted++;
|
||||
Utilities.sleep(200);
|
||||
} catch(e) {
|
||||
Logger.log('ERROR deleting [' + f.id + ']: ' + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
Logger.log('Done. Deleted ' + deleted + ' filters.');
|
||||
Logger.log('Remaining: ' + (result.all.length - deleted) + ' filters.');
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Gmail Label Cleanup Script
|
||||
* Run from https://script.google.com
|
||||
*
|
||||
* What this does:
|
||||
* 1. Merges old labels into inbox-zero categories (relabels all threads)
|
||||
* 2. Archives + relabels cvlp threads to Newsletter
|
||||
* 3. Merges purchases into Receipt, deletes purchases
|
||||
* 4. Moves donations top-level, selling under business
|
||||
* 5. Deletes contract label (threads stay under business)
|
||||
* 6. Deletes empty/unused labels
|
||||
*
|
||||
* Safe to run multiple times - skips labels that don't exist.
|
||||
*/
|
||||
|
||||
// ─── STEP 1: Merge old labels into inbox-zero categories ─────────────────────
|
||||
|
||||
function mergeLabels() {
|
||||
var merges = [
|
||||
{ from: 'promo', to: 'Marketing' },
|
||||
{ from: 'mailing list', to: 'Newsletter' },
|
||||
{ from: 'twitter', to: 'Notification' },
|
||||
{ from: 'facebook', to: 'Notification' },
|
||||
{ from: 'tech', to: 'Newsletter' },
|
||||
{ from: 'To Reply', to: 'Awaiting Reply' },
|
||||
];
|
||||
|
||||
merges.forEach(function(m) {
|
||||
var fromLabel = GmailApp.getUserLabelByName(m.from);
|
||||
if (!fromLabel) {
|
||||
Logger.log('Skipping (not found): ' + m.from);
|
||||
return;
|
||||
}
|
||||
|
||||
var toLabel = GmailApp.getUserLabelByName(m.to);
|
||||
if (!toLabel) {
|
||||
Logger.log('Creating label: ' + m.to);
|
||||
toLabel = GmailApp.createLabel(m.to);
|
||||
}
|
||||
|
||||
Logger.log('Merging ' + m.from + ' -> ' + m.to);
|
||||
var threads = fromLabel.getThreads(0, 100);
|
||||
var total = 0;
|
||||
while (threads.length > 0) {
|
||||
toLabel.addToThreads(threads);
|
||||
fromLabel.removeFromThreads(threads);
|
||||
total += threads.length;
|
||||
Utilities.sleep(1000);
|
||||
threads = fromLabel.getThreads(0, 100);
|
||||
}
|
||||
Logger.log('Moved ' + total + ' threads: ' + m.from + ' -> ' + m.to);
|
||||
fromLabel.deleteLabel();
|
||||
Logger.log('Deleted label: ' + m.from);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── STEP 2: cvlp → Newsletter + archive ─────────────────────────────────────
|
||||
|
||||
function archiveCvlp() {
|
||||
var cvlpLabel = GmailApp.getUserLabelByName('cvlp');
|
||||
if (!cvlpLabel) {
|
||||
Logger.log('cvlp label not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
var newsletterLabel = GmailApp.getUserLabelByName('Newsletter');
|
||||
if (!newsletterLabel) {
|
||||
newsletterLabel = GmailApp.createLabel('Newsletter');
|
||||
}
|
||||
|
||||
Logger.log('Processing cvlp: relabeling to Newsletter and archiving...');
|
||||
var threads = cvlpLabel.getThreads(0, 100);
|
||||
var total = 0;
|
||||
while (threads.length > 0) {
|
||||
newsletterLabel.addToThreads(threads);
|
||||
cvlpLabel.removeFromThreads(threads);
|
||||
threads.forEach(function(t) { t.moveToArchive(); });
|
||||
total += threads.length;
|
||||
Utilities.sleep(1000);
|
||||
threads = cvlpLabel.getThreads(0, 100);
|
||||
}
|
||||
Logger.log('Archived ' + total + ' cvlp threads');
|
||||
cvlpLabel.deleteLabel();
|
||||
Logger.log('Deleted label: cvlp');
|
||||
}
|
||||
|
||||
// ─── STEP 3: Merge purchases into Receipt ────────────────────────────────────
|
||||
|
||||
function mergePurchasesIntoReceipt() {
|
||||
var purchasesLabel = GmailApp.getUserLabelByName('purchases');
|
||||
if (!purchasesLabel) {
|
||||
Logger.log('purchases label not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
var receiptLabel = GmailApp.getUserLabelByName('Receipt');
|
||||
if (!receiptLabel) {
|
||||
receiptLabel = GmailApp.createLabel('Receipt');
|
||||
}
|
||||
|
||||
Logger.log('Merging purchases -> Receipt...');
|
||||
var threads = purchasesLabel.getThreads(0, 100);
|
||||
var total = 0;
|
||||
while (threads.length > 0) {
|
||||
receiptLabel.addToThreads(threads);
|
||||
purchasesLabel.removeFromThreads(threads);
|
||||
total += threads.length;
|
||||
Utilities.sleep(1000);
|
||||
threads = purchasesLabel.getThreads(0, 100);
|
||||
}
|
||||
Logger.log('Moved ' + total + ' threads: purchases -> Receipt');
|
||||
purchasesLabel.deleteLabel();
|
||||
Logger.log('Deleted label: purchases');
|
||||
}
|
||||
|
||||
// ─── STEP 4: Restructure business nested labels ───────────────────────────────
|
||||
|
||||
function restructureBusinessLabels() {
|
||||
// Move donations top-level: rename business/donations -> donations
|
||||
var bizDonations = GmailApp.getUserLabelByName('business/donations');
|
||||
if (bizDonations) {
|
||||
var donationsLabel = GmailApp.getUserLabelByName('donations');
|
||||
if (!donationsLabel) {
|
||||
donationsLabel = GmailApp.createLabel('donations');
|
||||
}
|
||||
Logger.log('Moving business/donations -> donations (top-level)');
|
||||
var threads = bizDonations.getThreads(0, 100);
|
||||
var total = 0;
|
||||
while (threads.length > 0) {
|
||||
donationsLabel.addToThreads(threads);
|
||||
bizDonations.removeFromThreads(threads);
|
||||
total += threads.length;
|
||||
Utilities.sleep(1000);
|
||||
threads = bizDonations.getThreads(0, 100);
|
||||
}
|
||||
Logger.log('Moved ' + total + ' threads to top-level donations');
|
||||
bizDonations.deleteLabel();
|
||||
} else {
|
||||
Logger.log('business/donations not found - skipping');
|
||||
}
|
||||
|
||||
// Move selling under business: rename selling -> business/selling
|
||||
var sellingLabel = GmailApp.getUserLabelByName('selling');
|
||||
if (sellingLabel) {
|
||||
var bizSelling = GmailApp.getUserLabelByName('business/selling');
|
||||
if (!bizSelling) {
|
||||
bizSelling = GmailApp.createLabel('business/selling');
|
||||
}
|
||||
Logger.log('Moving selling -> business/selling');
|
||||
var threads = sellingLabel.getThreads(0, 100);
|
||||
var total = 0;
|
||||
while (threads.length > 0) {
|
||||
bizSelling.addToThreads(threads);
|
||||
sellingLabel.removeFromThreads(threads);
|
||||
total += threads.length;
|
||||
Utilities.sleep(1000);
|
||||
threads = sellingLabel.getThreads(0, 100);
|
||||
}
|
||||
Logger.log('Moved ' + total + ' threads to business/selling');
|
||||
sellingLabel.deleteLabel();
|
||||
}
|
||||
|
||||
// Delete contract label (threads already have business label)
|
||||
var contractLabel = GmailApp.getUserLabelByName('business/contract');
|
||||
if (!contractLabel) {
|
||||
contractLabel = GmailApp.getUserLabelByName('contract');
|
||||
}
|
||||
if (contractLabel) {
|
||||
Logger.log('Removing contract label from all threads...');
|
||||
var threads = contractLabel.getThreads(0, 100);
|
||||
var total = 0;
|
||||
while (threads.length > 0) {
|
||||
contractLabel.removeFromThreads(threads);
|
||||
total += threads.length;
|
||||
Utilities.sleep(1000);
|
||||
threads = contractLabel.getThreads(0, 100);
|
||||
}
|
||||
contractLabel.deleteLabel();
|
||||
Logger.log('Deleted contract label (' + total + ' threads unaffected)');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── STEP 5: Delete unused labels ────────────────────────────────────────────
|
||||
|
||||
function deleteUnusedLabels() {
|
||||
var toDelete = [
|
||||
'Notes',
|
||||
'[Mailbox]',
|
||||
'Later',
|
||||
'To Buy',
|
||||
'To Read',
|
||||
'To Watch',
|
||||
'Cold Email',
|
||||
'Oak Ridge Moms',
|
||||
'music',
|
||||
];
|
||||
|
||||
toDelete.forEach(function(name) {
|
||||
var label = GmailApp.getUserLabelByName(name);
|
||||
if (!label) {
|
||||
Logger.log('Not found (already gone?): ' + name);
|
||||
return;
|
||||
}
|
||||
var threads = label.getThreads(0, 100);
|
||||
var total = 0;
|
||||
while (threads.length > 0) {
|
||||
label.removeFromThreads(threads);
|
||||
total += threads.length;
|
||||
Utilities.sleep(500);
|
||||
threads = label.getThreads(0, 100);
|
||||
}
|
||||
if (total > 0) {
|
||||
Logger.log('Removed label from ' + total + ' threads: ' + name);
|
||||
}
|
||||
label.deleteLabel();
|
||||
Logger.log('Deleted: ' + name);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── RUN ALL ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function runAll() {
|
||||
Logger.log('=== Starting Gmail label cleanup ===');
|
||||
Logger.log('--- Step 1: Merging labels ---');
|
||||
mergeLabels();
|
||||
Logger.log('--- Step 2: Archiving cvlp ---');
|
||||
archiveCvlp();
|
||||
Logger.log('--- Step 3: Merging purchases into Receipt ---');
|
||||
mergePurchasesIntoReceipt();
|
||||
Logger.log('--- Step 4: Restructuring business labels ---');
|
||||
restructureBusinessLabels();
|
||||
Logger.log('--- Step 5: Deleting unused labels ---');
|
||||
deleteUnusedLabels();
|
||||
Logger.log('=== Done! ===');
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
kiwix:
|
||||
image: ghcr.io/kiwix/kiwix-serve:latest
|
||||
container_name: kiwix
|
||||
restart: unless-stopped
|
||||
command: '*.zim'
|
||||
ports:
|
||||
- "8085:8080"
|
||||
volumes:
|
||||
- /mnt/nas_library/kiwix/zim:/data
|
||||
networks:
|
||||
- npm-network
|
||||
|
||||
networks:
|
||||
npm-network:
|
||||
external: true
|
||||
@@ -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:
|
||||
linkding:
|
||||
container_name: "${LD_CONTAINER_NAME:-linkding}"
|
||||
container_name: linkding
|
||||
image: sissbruecker/linkding:latest
|
||||
ports:
|
||||
- "${LD_HOST_PORT:-9090}:9090"
|
||||
- "9090:9090"
|
||||
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
|
||||
|
||||
|
||||
networks:
|
||||
default:
|
||||
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:
|
||||
EULA: "true"
|
||||
SEED: 46942827301
|
||||
SERVER_NAME: "Ourcraft"
|
||||
SERVER_NAME: "Terraforma SMP"
|
||||
MEMORY: "4G"
|
||||
OPS: "Jimcognito"
|
||||
#TYPE: "FABRIC"
|
||||
|
||||
volumes:
|
||||
- /srv/minecraft-data:/data
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
creative:
|
||||
mem_limit: 6g
|
||||
# creative:
|
||||
# image: itzg/minecraft-server:latest
|
||||
# container_name: minecraft-creative
|
||||
# ports:
|
||||
# - "25566:25565"
|
||||
# environment:
|
||||
# EULA: "true"
|
||||
# SEED: 8486672581758651406
|
||||
# SERVER_NAME: "Creative Kolpacks"
|
||||
# volumes:
|
||||
# - /srv/minecraft-data-creative:/data
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
# restart: unless-stopped
|
||||
# mem_limit: 2g
|
||||
# battle:
|
||||
# image: itzg/minecraft-server:latest
|
||||
# container_name: minecraft-battle
|
||||
# ports:
|
||||
# - "25573:25565"
|
||||
# environment:
|
||||
# EULA: "true"
|
||||
# SEED: "9037888329883360986"
|
||||
# SERVER_NAME: "Battle Royale"
|
||||
# MODE: "survival"
|
||||
# PVP: "true"
|
||||
# OPS: "Jimcognito"
|
||||
# TYPE: "PAPER"
|
||||
# MODRINTH_PROJECTS: "ezcountdown,worldresetplugin,viaversion"
|
||||
# volumes:
|
||||
# - /srv/minecraft-battle:/data
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
# restart: unless-stopped
|
||||
# mem_limit: 2g
|
||||
|
||||
# immersive:
|
||||
# image: itzg/minecraft-server:latest
|
||||
# container_name: minecraft-immersive
|
||||
# ports:
|
||||
# - "55567:25565"
|
||||
# environment:
|
||||
# EULA: "true"
|
||||
# TYPE: "FABRIC"
|
||||
# VERSION: "1.20.4"
|
||||
# SERVER_NAME: "Immersive Portals"
|
||||
# SEED: "302814868814545"
|
||||
# MODE: "creative"
|
||||
# FORCE_GAMEMODE: "true"
|
||||
# ENABLE_COMMAND_BLOCK: "true"
|
||||
# ALLOW_FLIGHT: "true"
|
||||
# ENFORCE_SECURE_PROFILE: "false"
|
||||
# MEMORY: "3G"
|
||||
# OPS: "Jimcognito,OlympicJumperL"
|
||||
# MODRINTH_PROJECTS: "fabric-api,pehkui,immersiveportals,gravity-api-fork:v1.3.0-mc1.20.4"
|
||||
# MODRINTH_DOWNLOAD_DEPENDENCIES: "required"
|
||||
# ICON: "https://cdn.modrinth.com/data/zJpHMkdD/0b2fa76a843b96f8bba1eb9d41f18f9f8ed4ce18_96.webp"
|
||||
# volumes:
|
||||
# - /srv/minecraft-modded:/data
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
# restart: unless-stopped
|
||||
# mem_limit: 4g
|
||||
|
||||
empire-smp:
|
||||
image: itzg/minecraft-server:latest
|
||||
container_name: minecraft-creative
|
||||
container_name: minecraft-empire-smp
|
||||
ports:
|
||||
- "25566:25565"
|
||||
- "31415:25565"
|
||||
environment:
|
||||
EULA: "true"
|
||||
SEED: 8486672581758651406
|
||||
SERVER_NAME: "Creative Kolpacks"
|
||||
SEED: "-4690695632699825725"
|
||||
SERVER_NAME: "Empire SMP"
|
||||
MODE: "survival"
|
||||
PVP: "false"
|
||||
OPS: "Jimcognito"
|
||||
volumes:
|
||||
- /srv/minecraft-data-creative:/data
|
||||
- /srv/minecraft-empire-smp:/data
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
mem_limit: 2g
|
||||
|
||||
epic-bases:
|
||||
image: itzg/minecraft-server:latest
|
||||
container_name: minecraft-pvp0
|
||||
@@ -60,39 +129,42 @@ services:
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
hardcore:
|
||||
image: itzg/minecraft-server:latest
|
||||
container_name: minecraft-hardcore
|
||||
ports:
|
||||
- "25568:25565"
|
||||
environment:
|
||||
EULA: "true"
|
||||
SEED: "-3149142039438819319"
|
||||
SERVER_NAME: "Hardcore"
|
||||
OPS: "Jimcognito,MagicSpaceCat"
|
||||
TYPE: "FABRIC"
|
||||
MAX_WORLD_SIZE: 415
|
||||
HARDCORE: true
|
||||
PVP: false
|
||||
volumes:
|
||||
- /srv/minecraft-data-hardcore:/data
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
survival:
|
||||
image: itzg/minecraft-server:latest
|
||||
container_name: minecraft-survival-smp
|
||||
ports:
|
||||
- "25571:25565"
|
||||
environment:
|
||||
EULA: "true"
|
||||
SERVER_NAME: "Survival SMP"
|
||||
OPS: "Jimcognito,OlympicJumperL"
|
||||
volumes:
|
||||
- /srv/minecraft-data-survival-smp:/data
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
mem_limit: 2g
|
||||
# hardcore temporarily disabled to reduce swap pressure
|
||||
# hardcore:
|
||||
# image: itzg/minecraft-server:latest
|
||||
# container_name: minecraft-hardcore
|
||||
# ports:
|
||||
# - "25568:25565"
|
||||
# environment:
|
||||
# EULA: "true"
|
||||
# SEED: "-3149142039438819319"
|
||||
# SERVER_NAME: "Hardcore"
|
||||
# OPS: "Jimcognito,MagicSpaceCat"
|
||||
# TYPE: "FABRIC"
|
||||
# MAX_WORLD_SIZE: 415
|
||||
# HARDCORE: true
|
||||
# PVP: false
|
||||
# volumes:
|
||||
# - /srv/minecraft-data-hardcore:/data
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
# restart: unless-stopped
|
||||
# survival temporarily disabled to reduce swap pressure
|
||||
# survival:
|
||||
# image: itzg/minecraft-server:latest
|
||||
# container_name: minecraft-survival-smp
|
||||
# ports:
|
||||
# - "25571:25565"
|
||||
# environment:
|
||||
# EULA: "true"
|
||||
# SERVER_NAME: "Survival SMP"
|
||||
# OPS: "Jimcognito,OlympicJumperL"
|
||||
# volumes:
|
||||
# - /srv/minecraft-data-survival-smp:/data
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
# restart: unless-stopped
|
||||
# spells:
|
||||
# image: itzg/minecraft-server
|
||||
# container_name: minecraft-spells
|
||||
|
||||
@@ -28,7 +28,6 @@ services:
|
||||
|
||||
networks:
|
||||
mqtt-network:
|
||||
driver: bridge
|
||||
default:
|
||||
external: true
|
||||
npm-network:
|
||||
external: true
|
||||
@@ -0,0 +1,2 @@
|
||||
# Let's Encrypt notification email
|
||||
LETSENCRYPT_EMAIL=your-email@example.com
|
||||
@@ -0,0 +1,142 @@
|
||||
# nginx-proxy + acme-companion
|
||||
|
||||
Automated reverse proxy with Let's Encrypt SSL certificates.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create the data directories:
|
||||
```bash
|
||||
sudo mkdir -p /srv/nginx-proxy-acme/{certs,vhost.d,html,conf.d,acme}
|
||||
```
|
||||
|
||||
2. Copy configuration files:
|
||||
```bash
|
||||
sudo cp conf/conf.d/* /srv/nginx-proxy-acme/conf.d/
|
||||
sudo cp conf/vhost.d/* /srv/nginx-proxy-acme/vhost.d/
|
||||
```
|
||||
|
||||
3. Configure environment:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your email
|
||||
```
|
||||
|
||||
4. Start the proxy:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- **nginx-proxy**: Reverse proxy with auto-discovery of Docker containers
|
||||
- **acme-companion**: Automatic Let's Encrypt certificate management
|
||||
- **static-certs**: Dummy container that triggers cert issuance for non-container backends
|
||||
|
||||
## Security
|
||||
|
||||
All hosts receive these security features via `vhost.d/default`:
|
||||
- **HSTS**: Strict-Transport-Security header (1 year)
|
||||
- **Security headers**: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy
|
||||
- **Block exploits**: WAF rules against SQL injection, XSS, file injection, spam, bad user agents
|
||||
|
||||
## Access Control
|
||||
|
||||
### Private Hosts (IP restricted)
|
||||
|
||||
Private hosts include `vhost.d/private` which restricts access to:
|
||||
- 192.168.1.0/24 (local network)
|
||||
- 172.16.0.0/12 (Docker networks)
|
||||
|
||||
To make a new host private, create a vhost.d file:
|
||||
```bash
|
||||
echo 'include /etc/nginx/vhost.d/private;' | sudo tee /srv/nginx-proxy-acme/vhost.d/myapp.kolpacksoftware.com
|
||||
```
|
||||
|
||||
### Public Hosts
|
||||
|
||||
Public hosts have no vhost.d file (only `default` applies). They get security headers and block-exploits but no IP restrictions.
|
||||
|
||||
Current public hosts:
|
||||
- chd.kolpacksoftware.com
|
||||
- linkding.kolpacksoftware.com
|
||||
- organizer.rmstsa.org
|
||||
- ridge-resources.org
|
||||
- rmstsa.org / www.rmstsa.org
|
||||
- share.kolpacksoftware.com
|
||||
- vikunja.kolpacksoftware.com
|
||||
|
||||
## Adding a Proxied Service
|
||||
|
||||
Add these environment variables to any container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
myapp:
|
||||
image: myapp:latest
|
||||
environment:
|
||||
- VIRTUAL_HOST=myapp.kolpacksoftware.com
|
||||
- VIRTUAL_PORT=8080
|
||||
- LETSENCRYPT_HOST=myapp.kolpacksoftware.com
|
||||
networks:
|
||||
- npm-network
|
||||
|
||||
networks:
|
||||
npm-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
Then if private, add the vhost.d file as shown above.
|
||||
|
||||
## Static IP Backends
|
||||
|
||||
Services running on physical hosts or VMs (not Docker) are configured in `conf.d/static-upstreams.conf`:
|
||||
|
||||
| Domain | Backend |
|
||||
|--------|---------|
|
||||
| portainer.kolpacksoftware.com | 172.17.0.1:9443 |
|
||||
| btt-cb1.kolpacksoftware.com | 192.168.1.173:80 |
|
||||
| hats.kolpacksoftware.com | 192.168.1.66:9999 |
|
||||
| pve-nas.kolpacksoftware.com | 192.168.1.245:8006 |
|
||||
| unraid.kolpacksoftware.com | 192.168.1.192:80 |
|
||||
|
||||
All static backends are private (IP restricted).
|
||||
|
||||
To add a new static backend:
|
||||
1. Add server block to `conf.d/static-upstreams.conf`
|
||||
2. Add domain to `static-certs` container's VIRTUAL_HOST and LETSENCRYPT_HOST in docker-compose.yaml
|
||||
3. Reload: `docker exec nginx-proxy nginx -s reload`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
/srv/nginx-proxy-acme/
|
||||
├── acme/ # acme.sh state (auto-managed)
|
||||
├── certs/ # SSL certificates (auto-managed)
|
||||
├── conf.d/
|
||||
│ ├── block-exploits.conf # WAF rules
|
||||
│ └── static-upstreams.conf # Static IP backend server blocks
|
||||
├── html/ # ACME challenge files (auto-managed)
|
||||
└── vhost.d/
|
||||
├── default # Security headers + HSTS + block-exploits (all hosts)
|
||||
├── private # IP allowlist (private hosts include this)
|
||||
├── docker-registry.* # Special config (auth headers + private)
|
||||
└── <hostname> # Per-host includes (usually just 'include private')
|
||||
```
|
||||
|
||||
## Reload Config
|
||||
|
||||
After changing vhost.d or conf.d files:
|
||||
```bash
|
||||
docker exec nginx-proxy nginx -s reload
|
||||
```
|
||||
|
||||
## Multiple Domains
|
||||
|
||||
For multiple domains on one cert:
|
||||
```yaml
|
||||
environment:
|
||||
- VIRTUAL_HOST=example.com,www.example.com
|
||||
- LETSENCRYPT_HOST=example.com,www.example.com
|
||||
```
|
||||
|
||||
Create vhost.d entries for each domain if private.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Block SQL injections
|
||||
set $block_sql_injections 0;
|
||||
|
||||
if ($query_string ~ "union.*select.*\(") {
|
||||
set $block_sql_injections 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "union.*all.*select.*") {
|
||||
set $block_sql_injections 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "concat.*\(") {
|
||||
set $block_sql_injections 1;
|
||||
}
|
||||
|
||||
if ($block_sql_injections = 1) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# Block file injections
|
||||
set $block_file_injections 0;
|
||||
|
||||
if ($query_string ~ "[a-zA-Z0-9_]=http://") {
|
||||
set $block_file_injections 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") {
|
||||
set $block_file_injections 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") {
|
||||
set $block_file_injections 1;
|
||||
}
|
||||
|
||||
if ($block_file_injections = 1) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# Block common exploits
|
||||
set $block_common_exploits 0;
|
||||
|
||||
if ($query_string ~ "(<|%3C).*script.*(>|%3E)") {
|
||||
set $block_common_exploits 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") {
|
||||
set $block_common_exploits 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") {
|
||||
set $block_common_exploits 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "proc/self/environ") {
|
||||
set $block_common_exploits 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") {
|
||||
set $block_common_exploits 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "base64_(en|de)code\(.*\)") {
|
||||
set $block_common_exploits 1;
|
||||
}
|
||||
|
||||
if ($block_common_exploits = 1) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# Block spam
|
||||
set $block_spam 0;
|
||||
|
||||
if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") {
|
||||
set $block_spam 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") {
|
||||
set $block_spam 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") {
|
||||
set $block_spam 1;
|
||||
}
|
||||
|
||||
if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") {
|
||||
set $block_spam 1;
|
||||
}
|
||||
|
||||
if ($block_spam = 1) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# Block user agents
|
||||
set $block_user_agents 0;
|
||||
|
||||
if ($http_user_agent ~ "Indy Library") {
|
||||
set $block_user_agents 1;
|
||||
}
|
||||
|
||||
if ($http_user_agent ~ "libwww-perl") {
|
||||
set $block_user_agents 1;
|
||||
}
|
||||
|
||||
if ($http_user_agent ~ "GetRight") {
|
||||
set $block_user_agents 1;
|
||||
}
|
||||
|
||||
if ($http_user_agent ~ "GetWeb!") {
|
||||
set $block_user_agents 1;
|
||||
}
|
||||
|
||||
if ($http_user_agent ~ "Go!Zilla") {
|
||||
set $block_user_agents 1;
|
||||
}
|
||||
|
||||
if ($http_user_agent ~ "Download Demon") {
|
||||
set $block_user_agents 1;
|
||||
}
|
||||
|
||||
if ($http_user_agent ~ "Go-Ahead-Got-It") {
|
||||
set $block_user_agents 1;
|
||||
}
|
||||
|
||||
if ($http_user_agent ~ "TurnitinBot") {
|
||||
set $block_user_agents 1;
|
||||
}
|
||||
|
||||
if ($http_user_agent ~ "GrabNet") {
|
||||
set $block_user_agents 1;
|
||||
}
|
||||
|
||||
if ($block_user_agents = 1) {
|
||||
return 403;
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
# Static upstream configurations for non-container backends
|
||||
# These hosts forward to IP addresses instead of Docker containers
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# portainer.kolpacksoftware.com -> 172.17.0.1:9443 (HTTPS backend via Docker bridge)
|
||||
# ------------------------------------------------------------------------------
|
||||
upstream portainer-backend {
|
||||
server 172.17.0.1:9443;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name portainer.kolpacksoftware.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name portainer.kolpacksoftware.com;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/portainer.kolpacksoftware.com.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/portainer.kolpacksoftware.com.key;
|
||||
|
||||
include /etc/nginx/vhost.d/default;
|
||||
include /etc/nginx/vhost.d/private;
|
||||
|
||||
location / {
|
||||
proxy_pass https://portainer-backend;
|
||||
proxy_ssl_verify off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# btt-cb1.kolpacksoftware.com -> 192.168.1.173:80
|
||||
# ------------------------------------------------------------------------------
|
||||
upstream btt-cb1-backend {
|
||||
server 192.168.1.173:80;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name btt-cb1.kolpacksoftware.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name btt-cb1.kolpacksoftware.com;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/btt-cb1.kolpacksoftware.com.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/btt-cb1.kolpacksoftware.com.key;
|
||||
|
||||
include /etc/nginx/vhost.d/default;
|
||||
include /etc/nginx/vhost.d/private;
|
||||
|
||||
location / {
|
||||
proxy_pass http://btt-cb1-backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# hats.kolpacksoftware.com -> 192.168.1.66:9999
|
||||
# ------------------------------------------------------------------------------
|
||||
upstream hats-backend {
|
||||
server 192.168.1.66:9999;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name hats.kolpacksoftware.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name hats.kolpacksoftware.com;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/hats.kolpacksoftware.com.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/hats.kolpacksoftware.com.key;
|
||||
|
||||
include /etc/nginx/vhost.d/default;
|
||||
include /etc/nginx/vhost.d/private;
|
||||
|
||||
location / {
|
||||
proxy_pass http://hats-backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# pve-nas.kolpacksoftware.com -> 192.168.1.245:8006 (HTTPS backend - Proxmox)
|
||||
# ------------------------------------------------------------------------------
|
||||
upstream pve-nas-backend {
|
||||
server 192.168.1.245:8006;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name pve-nas.kolpacksoftware.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name pve-nas.kolpacksoftware.com;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/pve-nas.kolpacksoftware.com.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/pve-nas.kolpacksoftware.com.key;
|
||||
|
||||
include /etc/nginx/vhost.d/default;
|
||||
include /etc/nginx/vhost.d/private;
|
||||
|
||||
# Proxmox needs larger buffers
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
|
||||
location / {
|
||||
proxy_pass https://pve-nas-backend;
|
||||
proxy_ssl_verify off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# unraid.kolpacksoftware.com -> 192.168.1.192:80
|
||||
# ------------------------------------------------------------------------------
|
||||
upstream unraid-backend {
|
||||
server 192.168.1.192:80;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name unraid.kolpacksoftware.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name unraid.kolpacksoftware.com;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/unraid.kolpacksoftware.com.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/unraid.kolpacksoftware.com.key;
|
||||
|
||||
include /etc/nginx/vhost.d/default;
|
||||
include /etc/nginx/vhost.d/private;
|
||||
|
||||
location / {
|
||||
proxy_pass http://unraid-backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1,12 @@
|
||||
# Default configuration applied to all virtual hosts
|
||||
# Security headers and WAF rules - applied to ALL hosts (public and private)
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Include block-exploits rules
|
||||
include /etc/nginx/conf.d/block-exploits.conf;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1,6 @@
|
||||
# Docker registry needs Authorization header passthrough
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
proxy_pass_header Authorization;
|
||||
|
||||
# Private access control
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1,7 @@
|
||||
# Access control for private hosts
|
||||
# Symlink this file to restrict access to local networks only
|
||||
|
||||
allow 192.168.1.0/24;
|
||||
allow 172.16.0.0/12;
|
||||
# allow 162.10.130.22; # Netskope - uncomment if needed for corporate VPN
|
||||
deny all;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1 @@
|
||||
include /etc/nginx/vhost.d/private;
|
||||
@@ -0,0 +1,48 @@
|
||||
services:
|
||||
nginx-proxy:
|
||||
image: nginxproxy/nginx-proxy:1.6
|
||||
container_name: nginx-proxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
volumes:
|
||||
- /srv/nginx-proxy-acme/certs:/etc/nginx/certs:ro
|
||||
- /srv/nginx-proxy-acme/vhost.d:/etc/nginx/vhost.d
|
||||
- /srv/nginx-proxy-acme/html:/usr/share/nginx/html
|
||||
- /srv/nginx-proxy-acme/conf.d/static-upstreams.conf:/etc/nginx/conf.d/static-upstreams.conf:ro
|
||||
- /srv/nginx-proxy-acme/conf.d/block-exploits.conf:/etc/nginx/conf.d/block-exploits.conf:ro
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
environment:
|
||||
- TRUST_DOWNSTREAM_PROXY=false
|
||||
|
||||
acme-companion:
|
||||
image: nginxproxy/acme-companion:2.4
|
||||
container_name: acme-companion
|
||||
restart: unless-stopped
|
||||
volumes_from:
|
||||
- nginx-proxy
|
||||
volumes:
|
||||
- /srv/nginx-proxy-acme/certs:/etc/nginx/certs:rw
|
||||
- /srv/nginx-proxy-acme/acme:/etc/acme.sh
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- DEFAULT_EMAIL=${LETSENCRYPT_EMAIL}
|
||||
depends_on:
|
||||
- nginx-proxy
|
||||
|
||||
# Dummy container to trigger certificate issuance for static IP backends
|
||||
# This container does nothing but hold env vars for acme-companion to detect
|
||||
static-certs:
|
||||
image: alpine:3.19
|
||||
container_name: static-certs
|
||||
restart: unless-stopped
|
||||
command: ["sleep", "infinity"]
|
||||
environment:
|
||||
- VIRTUAL_HOST=portainer.kolpacksoftware.com,btt-cb1.kolpacksoftware.com,hats.kolpacksoftware.com,pve-nas.kolpacksoftware.com,unraid.kolpacksoftware.com
|
||||
- LETSENCRYPT_HOST=portainer.kolpacksoftware.com,btt-cb1.kolpacksoftware.com,hats.kolpacksoftware.com,pve-nas.kolpacksoftware.com,unraid.kolpacksoftware.com
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: npm-network
|
||||
external: true
|
||||
@@ -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:
|
||||
external: true
|
||||
name: nas_media
|
||||
|
||||
|
||||
services:
|
||||
plex:
|
||||
container_name: plex
|
||||
@@ -21,10 +19,18 @@ services:
|
||||
- 32414:32414/udp
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PLEX_CLAIM=claim-VCuJznuC2nxFyVgAcKec
|
||||
- ADVERTISE_IP=http://73.108.236.191:32400/
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
|
||||
hostname: Photon
|
||||
volumes:
|
||||
- /srv/plex:/config
|
||||
- /srv/plex-transcode:/transcode
|
||||
- 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
|
||||
restart: always
|
||||
privileged: true
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.monitor-only=true
|
||||
volumes:
|
||||
- /mnt/containers/portainer/container-data/data:/data:Z
|
||||
- /var/run/docker.sock:/var/run/docker.sock:Z
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
app:
|
||||
image: nymanjens/quizmaster:latest
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports: ["9001:9000"]
|
||||
volumes:
|
||||
- /srv/quizmaster/quiz:/app/conf/quiz
|
||||
- /srv/quizmaster/application.conf:/app/conf/application.conf
|
||||
command: "bin/server"
|
||||
@@ -1,11 +0,0 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
app:
|
||||
image: nymanjens/quizmaster:latest
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports: ["9001:9000"]
|
||||
volumes:
|
||||
- /srv/quizmaster/quiz:/app/conf/quiz
|
||||
- /srv/quizmaster/application.conf:/app/conf/application.conf
|
||||
command: "bin/server"
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
rackpeek:
|
||||
container_name: rackpeek
|
||||
image: aptacode/rackpeek:latest
|
||||
ports:
|
||||
- "8081:8080"
|
||||
volumes:
|
||||
- /srv/rackpeek-config:/app/config
|
||||
environment:
|
||||
- VIRTUAL_HOST=rackpeek.kolpacksoftware.com
|
||||
- VIRTUAL_PORT=8081
|
||||
- LETSENCRYPT_HOST=rackpeek.kolpacksoftware.com
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/New_York
|
||||
user: "1000:1000"
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: npm-network
|
||||
@@ -0,0 +1,59 @@
|
||||
networks:
|
||||
npm-network:
|
||||
external: true
|
||||
romm-internal:
|
||||
|
||||
services:
|
||||
romm:
|
||||
image: rommapp/romm:latest
|
||||
container_name: romm
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- DB_HOST=romm-db
|
||||
- DB_NAME=romm
|
||||
- DB_USER=romm-user
|
||||
- DB_PASSWD=${DB_PASSWD}
|
||||
- ROMM_AUTH_SECRET_KEY=${ROMM_AUTH_SECRET_KEY}
|
||||
# Metadata providers (all optional but recommended)
|
||||
- SCREENSCRAPER_USER=${SCREENSCRAPER_USER}
|
||||
- SCREENSCRAPER_PASSWORD=${SCREENSCRAPER_PASSWORD}
|
||||
- RETROACHIEVEMENTS_API_KEY=
|
||||
- STEAMGRIDDB_API_KEY=
|
||||
- HASHEOUS_API_ENABLED=true
|
||||
- IGDB_CLIENT_ID=${IGDB_CLIENT_ID}
|
||||
- IGDB_CLIENT_SECRET=${IGDB_CLIENT_SECRET}
|
||||
volumes:
|
||||
- /mnt/nas_games:/romm/library # NAS: media/games → library root; roms/ subdir inside
|
||||
- /srv/romm/assets:/romm/assets # Saves, states, screenshots
|
||||
- /srv/romm/config:/romm/config # config.yml (optional)
|
||||
- /srv/romm/resources:/romm/resources
|
||||
- /srv/romm/redis:/redis-data
|
||||
networks:
|
||||
- npm-network
|
||||
- romm-internal
|
||||
depends_on:
|
||||
romm-db:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
|
||||
romm-db:
|
||||
image: mariadb:latest
|
||||
container_name: romm-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
|
||||
- MARIADB_DATABASE=romm
|
||||
- MARIADB_USER=romm-user
|
||||
- MARIADB_PASSWORD=${MARIADB_PASSWORD}
|
||||
volumes:
|
||||
- /srv/romm/db:/var/lib/mysql
|
||||
networks:
|
||||
- romm-internal
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
start_period: 30s
|
||||
start_interval: 10s
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user