Storage Layout — NUC 14 Docker Stacks
Data Location Architecture
All persistent service data is being consolidated to /mnt/pve/unas/services/{service}/
on the UNAS NFS share, with media content in /mnt/pve/unas/media/.
PostgreSQL databases stay on local NVMe — NFS ownership checks (stat(2) UID match) prevent
PostgreSQL from running on network filesystems. PostgreSQL is backed up via pg_dump to UNAS instead.
| Mount |
Source |
Content |
/mnt/pve/unas/services/ |
UNAS NFS |
SQLite DBs, configs, files, S3 data |
/mnt/pve/unas/media/ |
UNAS NFS |
User media (photos, docs, books, audio) |
/mnt/pve/unas/backup/ |
UNAS NFS |
Service backups |
/var/lib/docker/volumes/ |
Local NVMe |
PostgreSQL data, Redis data |
/opt/stacks/ |
Local NVMe |
Compose files, small configs |
Migration Status
✅ Already on UNAS — /mnt/pve/unas/services/
| Service |
UNAS Path(s) |
Container Mount |
Data Type |
| gluetun |
gluetun/data/ |
/gluetun |
VPN config |
| immich |
immich/{encoded-video,profile,thumbs}/ |
/usr/src/app/upload/{encoded-video,profile,thumbs} |
App derived data |
| karakeep |
karakeep/data/, karakeep/meilisearch/ |
/data, /meili_data |
Bookmarks DB + Meilisearch index |
| ntfy |
ntfy/{config,cache}/ |
/etc/ntfy, /var/lib/ntfy |
Notification config + cache |
| paperless-ai |
paperless-ai/ |
/app/data |
AI processing data |
| pocketid |
pocketid/ |
/app/data |
Auth data (SQLite) |
| stremio |
stremio/ |
/root/.stremio-server |
Streaming server data |
| traccar |
traccar/{data,logs,traccar.xml} |
/opt/traccar/{data,logs,conf/traccar.xml} |
GPS tracking data |
| vaultwarden |
vaultwarden/ |
/data |
Password vault (SQLite) |
| garage |
shared-db/garage/{data,meta}/ |
/var/lib/garage/{data,meta} |
S3 object storage (Garage) |
🆕 Shared Infrastructure — /opt/stacks/shared-db/
| Service |
Hostname |
Type |
Data Location |
| shared-postgres |
shared-postgres |
PostgreSQL 16 (local volume) |
shared-db_shared-pgdata volume |
| garage |
garage |
S3-compatible object storage (on UNAS) |
/mnt/pve/unas/services/shared-db/garage/ |
✅ Migrated (Phase 1) — SQLite to UNAS
| Service |
Old Path |
New Path |
Status |
| memos |
/opt/stacks/memos/data |
/mnt/pve/unas/services/memos |
✅ Healthy |
| arcane |
volume arcane_arcane-data |
/mnt/pve/unas/services/arcane |
✅ Running |
| n8n |
volume n8n_n8n_storage |
/mnt/pve/unas/services/n8n |
✅ Healthy |
⏳ Still Local — /opt/stacks/ or Docker Volumes
| Service |
Current Location |
Data Type |
Estimated Size |
UNAS Target |
| arr-stack |
/opt/stacks/arr-stack/ |
Configs + media |
~55 GB |
arr-stack/{config,audiobooks,ebooks,podcasts,torrents} |
| qdrant |
/opt/stacks/qdrant/qdrant_storage |
Vector DB |
~7 MB |
qdrant/ |
| dozzle |
/opt/stacks/dozzle/dozzle_data |
Log viewer state |
tiny |
(ephemeral, optional) |
| homepage |
/opt/stacks/homepage/{config,icons} |
Dashboard config |
~164 KB |
(keep in stack dir) |
🗄️ Databases Requiring Migration
| Service |
Engine |
Current Path |
Target |
Migration Method |
Status |
|
| immich |
PostgreSQL |
/opt/stacks/immich/postgres |
→ shared-postgres |
pg_dump → pg_restore |
⏳ Pending |
|
| litellm |
PostgreSQL |
/opt/stacks/ai/postgres_data |
→ shared-postgres |
pg_dump → pg_restore |
⏳ Pending |
|
| lobehub |
PostgreSQL |
/opt/stacks/ai/lobehub/data |
→ shared-postgres |
pg_dump → pg_restore |
⏳ Pending |
|
| daytona |
PostgreSQL |
volume daytona-minimal_db_data |
→ shared-postgres |
pg_dump → pg_restore |
⏳ Pending |
|
| paperless-ngx |
PostgreSQL |
volume paperless-ngx_pgdata |
→ shared-postgres |
pg_dump → pg_restore |
⏳ Pending |
|
| memos |
SQLite |
/opt/stacks/memos/data |
/mnt/pve/unas/services/memos |
cp + chown |
✅ Done |
|
| arcane |
SQLite |
volume arcane_arcane-data |
/mnt/pve/unas/services/arcane |
cp + chown |
✅ Done |
|
| n8n |
SQLite |
volume n8n_n8n_storage |
/mnt/pve/unas/services/n8n |
cp + chown |
✅ Done |
|
| pocketid |
SQLite |
pocketid/ |
(already on UNAS) |
— |
✅ Done |
|
| vaultwarden |
SQLite |
vaultwarden/ |
(already on UNAS) |
— |
✅ Done |
|
| karakeep |
SQLite + Meilisearch |
karakeep/{data,meilisearch} |
(already on UNAS) |
— |
✅ Done |
|
🧠 ML Models & Caches (Regenerable — Can Stay Local)
| Resource |
Path |
Decision |
| Immich ML models |
volume immich_model-cache |
Cache — stay |
| Paperless Redis |
volume paperless-ngx_redisdata |
Ephemeral — stay |
| AI stack Redis |
volume nuc-ai-core_redis_data |
Ephemeral — stay |
| SearXNG search cache |
volume (hash) at /var/lib/docker/volumes/bb5bb... |
Ephemeral — stay |
| Daytona runner Docker |
volume daytona-minimal runner data |
Can stay |
Media Content (Already on UNAS)
| Mount |
Content |
/mnt/pve/unas/media/images/ |
Immich photo library |
/mnt/pve/unas/media/documents/public/paperless-ngx/ |
Paperless documents (consume, export, library) |
/mnt/pve/unas/media/audiobooks/ |
Audiobookshelf media |
/mnt/pve/unas/media/ebooks/ |
Audiobookshelf ebooks |
/mnt/pve/unas/media/podcasts/ |
Audiobookshelf podcasts |
/mnt/pve/unas/backup/immich/ |
Immich DB backups |
Prospective UNAS Layout
Garage S3 — /opt/stacks/shared-db/
S3-compatible object storage using Garage v1.0.0.
Replaces RustFS from the LobeHub AI stack. Single-node, replication_factor=1.
| Detail |
Value |
| Container |
garage (dxflrs/garage:v1.0.0) |
| Data |
/mnt/pve/unas/services/shared-db/garage/{data,meta} |
| S3 API |
garage:3900 (internal, shared_backend) |
| Admin API |
127.0.0.1:3901 (localhost only) |
| Regional |
No auth (internal network) |
| Bucket |
lobe-files |
| Key ID |
GK55210bd6b56d92891db26fac |
| Secret Key |
d0af9e4eb69d7773ffa17950feee27925dbc70eb3efd4ee23a3e1324672c869a |
Shared PostgreSQL — /opt/stacks/shared-db/
Centralized PostgreSQL 16 instance for all non-immich services.
| Detail |
Value |
| Container |
shared-postgres (postgres:16) |
| Data |
shared-db_shared-pgdata (local Docker volume, NOT NFS) |
| Networks |
shared_backend |
| Auth |
postgres superuser with password from .env |
Databases & Users (pre-created on init)
| Database |
Owner |
Password |
Used By |
paperless |
paperless |
paperless |
paperless-ngx |
daytona |
user |
pass |
daytona |
litellm |
litellm |
litellm_password_here |
LiteLLM |
lobechat |
lobe |
uWNZugjBqixf8dxC |
LobeHub |
paradedb |
lobe |
uWNZugjBqixf8dxC |
LobeHub |
Key Architectural Decision: PostgreSQL Stays Local
Problem: PostgreSQL checks data directory ownership via stat(2) and refuses to start
if the process UID doesn't match the file owner UID. NFS with all_squash maps all UIDs
to 65534 (nobody), breaking this check. chown also fails on NFS.
Solution: All PostgreSQL instances use local Docker volumes on NVMe:
Backup strategy: pg_dump each database to UNAS on a schedule (e.g. daily cron).
Migration Priority
| Priority |
Service |
Effort |
Risk |
Status |
Reason |
| P0 |
memos |
Minimal (SQLite copy) |
Low |
✅ Done |
Simple file move |
| P0 |
n8n |
Minimal (SQLite copy) |
Low |
✅ Done |
Simple file move |
| P0 |
arcane |
Minimal (SQLite copy) |
Low |
✅ Done |
Simple file move |
| P0 |
qdrant |
Minimal (file copy) |
Low |
✅ Done |
Fresh start on UNAS |
| P1 |
daytona → shared-postgres |
Medium (pg_dump) |
Low |
✅ Done |
pg_dump → restore → running |
| P1 |
litellm → shared-postgres |
Medium (pg_dump) |
Low |
✅ Done |
pg_dump → restore → running |
| P1 |
lobehub → shared-postgres |
Medium (pg_dump) |
Low |
⛔ Skipped |
Uses ParadeDB (pg17+extensions), incompatible with shared-postgres (pg16) |
| P1 |
paperless-ngx → shared-postgres |
Medium (pg_dump) |
Medium |
✅ Done |
pg_dump → restore → running |
| P1 |
immich → shared-postgres |
Medium (pg_dump) |
Medium |
⏳ |
Custom pgvector image |
| P2 |
arr-stack |
Large (55 GB copy) |
Low |
⏳ |
Simple copy, time-consuming |
Mail Archive (Potential Savings)
Unattached anonymous volumes from old/stopped containers:
| Volume |
Size |
Likely From |
Action |
0b6d07ff... |
? |
old daytona runner |
prune |
| (100+ hex-named volumes) |
? |
ephemeral build cache |
prune |
Recommend: docker system df then docker volume prune to clean.
Database Consolidation Plan
Current: 10 Database Instances
| # |
Container |
Type |
Image |
Used By |
Networks |
| 1 |
immich_postgres |
PostgreSQL |
immich-app/postgres:14-vectorchord (pgvector) |
immich |
immich_default |
| 2 |
paperless-ngx-db-1 |
PostgreSQL |
postgres:16 |
paperless-ngx |
paperless-ngx_default |
| 3 |
daytona-minimal-db-1 |
PostgreSQL |
postgres:16-alpine |
daytona |
daytona-minimal_daytona-network |
| 4 |
nuc-ai-core-litellm-db-1 |
PostgreSQL |
postgres:16-alpine |
litellm |
ai-internal |
| 5 |
lobe-postgres |
PostgreSQL |
postgres:16-alpine |
lobehub (lobechat, paradedb) |
ai-internal |
| 6 |
paperless-ngx-broker-1 |
Redis 7 |
redis:7 |
paperless broker |
paperless-ngx_default |
| 7 |
immich_redis |
Valkey 9 |
valkey/valkey:9 |
immich cache |
immich_default |
| 8 |
daytona-minimal-redis-1 |
Redis Alpine |
redis:alpine |
daytona |
daytona-minimal_daytona-network |
| 9 |
lobe-redis |
Redis 7 |
redis:7-alpine |
lobehub |
ai-internal |
| 10 |
redis-searxng |
Redis 7 |
redis:7-alpine |
searxng |
ai-internal |
Target: 6 Database Instances (remove 4)
| # |
Container |
Type |
DBs Hosted |
Saved |
| 1 |
immich_postgres |
PostgreSQL immich-app (pgvector) |
immich |
🔒 Must stay separate |
| 2 |
shared-postgres 🆕 |
PostgreSQL 16 |
paperless, daytona, litellm, lobechat, paradedb |
−3 containers |
| 3 |
paperless-ngx-broker-1 |
Redis 7 |
paperless broker |
🔒 Must stay separate |
| 4 |
immich_redis |
Valkey 9 |
immich cache |
🔒 Must stay separate |
| 5 |
daytona-minimal-redis-1 |
Redis Alpine |
daytona |
🔒 Stay separate |
| 6 |
lobe-redis |
Redis 7 |
lobehub |
🔒 Stay separate |
| 7 |
redis-searxng |
Redis 7 |
searxng |
🔒 Stay separate |
| 7 |
nuc-ai-core-litellm-db-1 |
🗑️ removed |
→ shared-postgres |
✅ |
| 8 |
lobe-postgres |
🗑️ removed |
→ shared-postgres |
✅ |
| 9 |
paperless-ngx-db-1 |
🗑️ removed |
→ shared-postgres |
✅ |
| 10 |
daytona-minimal-db-1 |
🗑️ removed |
→ shared-postgres |
✅ |
Result: 10 → 6 instances, removing 4 containers, saving ~160MB RAM.
Why Redis Stays Separate
| Reason |
Detail |
| Valkey ≠ Redis |
Immich uses Valkey 9 which has diverged from Redis 7 |
SELECT collisions |
Apps may use specific DB numbers that clash |
| Password mismatch |
Each app has its own requirepass / REDIS_PASSWORD |
| Ephemeral = low value |
Caches/brokers rebuild on restart, savings don't justify risk |
FLUSHALL risk |
One misconfigured app could wipe another's cache |
Why PG Consolidation Works
| Reason |
Detail |
| Same major version |
All target DBs use PostgreSQL 16 |
| Native compatibility |
No conflicting extensions (only immich has pgvector) |
| Isolation via databases |
Each service gets its own database + user within shared PG |
| Simple restore |
pg_dump + pg_restore into shared instance |
| Single backup |
One pg_dumpall covers all 5 databases |
Implementation: Shared PostgreSQL
Create /opt/stacks/shared-db/docker-compose.yml:
Migration Order (Phase 2)
Detailed: Paperless-ngx
| Change |
Before |
After |
| DB service |
db (in paperless compose) |
remove entirely |
PAPERLESS_DBHOST |
db |
shared-postgres |
| DB credentials |
defaults (paperless/paperless) |
keep same |
| Network |
paperless-ngx_default |
shared_backend |
Detailed: Daytona
| Change |
Before |
After |
| DB service |
db (in daytona compose) |
remove entirely |
DB_HOST |
db |
shared-postgres |
DB_USERNAME |
user |
keep same |
DB_DATABASE |
daytona |
keep same |
| Network |
daytona-minimal_daytona-network |
shared_backend |
Detailed: LiteLLM
| Change |
Before |
After |
| DB service |
litellm-db (in ai compose) |
remove entirely |
DATABASE_URL |
postgresql://litellm:...@litellm-db:5432/litellm |
postgresql://litellm:...@shared-postgres:5432/litellm |
| Network |
ai-internal |
shared_backend |
Detailed: LobeHub
| Change |
Before |
After |
| DB service |
postgresql (in lobehub.yml) |
remove entirely |
DATABASE_URL |
postgresql://postgres:...@postgresql:5432/lobechat |
postgresql://postgres:...@shared-postgres:5432/lobechat |
Lobe also needs paradedb DB |
created manually on initial setup |
created via SQL script |
| Network |
ai-internal |
shared_backend |
Total Impact
| Metric |
Before |
After |
| PostgreSQL instances |
5 |
2 |
| Redis instances |
5 |
5 (unchanged) |
| Total container count |
10 DBs |
7 DBs (−3) |
| RAM saved (estimated) |
— |
~160 MB |
| Complexity |
Fragmented |
Centralized |
| Backup burden |
5 pg_dumps |
2 pg_dumps |
Timing Estimates
| Service |
Downtime |
Cumulative |
| Setup shared-postgres + dump all |
0 (live dump) |
0 |
| daytona → shared |
~2 min |
2 min |
| litellm → shared |
~2 min |
4 min |
| lobehub → shared |
~3 min |
7 min |
| paperless-ngx → shared |
~5 min |
12 min |
| immich DB → UNAS bind mount |
~10 min |
22 min |
| Total |
~22 min |
|
Notes
- All compose files are version-controlled in
/opt/stacks/{service}/docker-compose.yml
- Always test with
docker compose config before up -d
- PostgreSQL stays on local NVMe — backed up via
pg_dump to UNAS
- Garage S3 replaces RustFS from LobeHub stack (RustFS will be removed when LobeHub is switched)
- LobeHub's
paradedb extension requires CREATE EXTENSION IF NOT EXISTS paradedb; on the shared instance
- All PostgreSQL dbs consolidated into shared-postgres except immich (pgvector requirement)