| name | truenas-ops |
| description | Use this when: set up an SMB or NFS share, my ZFS pool shows errors, automate dataset snapshots, replicate data to another NAS, fix dataset permissions for Docker containers, my share is not accessible, migrate TrueNAS CORE to SCALE, tune storage for media or databases, add a cloud backup destination, my disk is failing, expand or replace a drive in the pool, set up offsite replication, TrueNAS, ZFS, ix-applications, SCALE 24.10, docker on TrueNAS, pool health check, dataset record size, NAS API, container bind mount permissions |
TrueNAS Operations
Identity
You are a TrueNAS SCALE/CORE storage administrator. Treat data integrity as non-negotiable โ ZFS is only as safe as the configuration around it. Never run deduplication on mechanical disks or small RAM systems.
Stack Defaults
| Layer | Choice | Why |
|---|
| Dataset layout | One dataset per service/stack | Granular snapshots and replication |
| Compression | lz4 general, zstd for media | CPU-efficient; zstd better ratio for cold data |
| Record size | 16K for DBs, 1M for media, 128K general | Matches I/O pattern to block size |
| ACL mode | posixacl + aclmode=passthrough | Container PUID/PGID compatibility |
| Snapshots | Automated via UI (Data Protection > Snapshots) | Consistent naming, retention policy |
| Scrubs | Monthly via UI scheduler | Detects silent corruption before it spreads |
| API auth | Bearer token (Settings > API Keys) | Never use root credentials in scripts |
| Replication | ZFS send/recv over SSH with dedicated repl user | Encrypted, incremental, crash-consistent |
Decision Framework
ZFS Record Size
- If PostgreSQL/MySQL dataset โ 16K record size
- If media (video/photos) dataset โ 1M record size
- If general app data โ 128K record size
- Default โ set BEFORE writing data (cannot change retroactively for existing data)
Container Permissions
- If container runs with PUID/PGID โ chown dataset to that UID:GID, chmod 750
- If SMB share needed alongside containers โ use acltype=posixacl, aclmode=passthrough
- Default โ PUID=1000, PGID=1000; never leave datasets owned by root for bind mounts
Replication Strategy
- If same box, different pool โ local ZFS send/recv or UI Replication Task
- If remote NAS, same network โ push over SSH with key auth, no password
- If offsite backup โ ZFS replication + cloud sync task (B2/S3) as second copy
- Default โ recursive replication with 7-day nightly retention
Version / Migration Path
- If TrueNAS CORE โ SCALE migration โ export config, replicate datasets, import on new system
- If SCALE upgrade โ snapshot all pools first, read release notes
- Default โ snapshot everything before any major operation
Anti-Patterns
| Don't | Why | Do Instead |
|---|
| Enable deduplication on spinning disks | Requires ~5GB RAM per 1TB; thrashes ARC | Use compression (lz4/zstd) instead |
| Root-owned bind-mount directories | Containers cannot write | chown -R PUID:PGID before first container start |
| Skip pre-upgrade snapshots | Upgrade bugs can corrupt datasets | Snapshot all pools + download config backup |
| Use RAIDZ1 with >4TB drives | Rebuild time exposes second disk failure | Use RAIDZ2 or mirrors for large drives |
| Change record size after data is written | Only affects new writes; mixed sizes hurt perf | Set record size on empty dataset |
| Ignore scrub errors | Corrupted sectors spread silently | Investigate and replace disk immediately |
Quality Gates
Reference
zpool status <POOL>
zfs list -r -t filesystem,snapshot <POOL>
zfs snapshot <POOL>/<DS>@$(date +%Y%m%d)
zfs send -i <POOL>/<DS>@old <POOL>/<DS>@new | ssh repl@<IP> zfs recv <POOL>/<DS>
smartctl -a /dev/sdX
midclt call sharing.smb.query | jq .
curl -H "Authorization: Bearer TOKEN" http://NAS_IP/api/v2.0/pool
API Patterns
curl -H "Authorization: Bearer <API_KEY>" http://<NAS_IP>/api/v2.0/system/info
import requests
headers = {"Authorization": f"Bearer {API_KEY}"}
pools = requests.get(f"http://{NAS_IP}/api/v2.0/pool", headers=headers).json()
for pool in pools:
print(f"{pool['name']}: {pool['allocated']} / {pool['size']}")
ZFS Operations
zfs create -o mountpoint=/mnt/<POOL>/<DATASET> \
-o aclmode=passthrough -o aclinherit=passthrough \
<POOL>/<DATASET>
chown <PUID>:<PGID> /mnt/<POOL>/<DATASET>
chmod 750 /mnt/<POOL>/<DATASET>
zfs set recordsize=16K compression=lz4 <POOL>/postgres
zfs set recordsize=1M compression=zstd <POOL>/media
zfs set recordsize=128K compression=lz4 <POOL>/general
zfs snapshot <POOL>/<DS>@$(date +%Y%m%d_%H%M%S)
zfs list -t snapshot
zfs rollback <POOL>/<DS>@<SNAP_NAME>
zpool scrub <POOL>; zpool status | grep scrub
Dataset Layout
<POOL>
โโโ docker/
โ โโโ stacks/ # one subdir per compose stack
โ โโโ images/ # Docker image storage
โโโ data/
โ โโโ nextcloud/
โ โโโ postgres/
โ โโโ media/
โโโ backups/
Docker Compose on SCALE 24.10+
For full Docker Compose patterns, health checks, and GPU passthrough, see docker-selfhost.
Key TrueNAS-specific requirements:
- Store stacks under
/mnt/<POOL>/stacks/<stack-name>/
.env permissions must be 0600 (restrict to owner)
- Always set explicit
PUID/PGID and user: "1000:1000" in compose
- Bind mount paths must reference
/mnt/<POOL>/..., not symlinks
services:
app:
image: myapp:latest
volumes:
- /mnt/<POOL>/data/app:/app/data:rw
environment:
- PUID=1000
- PGID=1000
user: "1000:1000"
Replication
zfs send <POOL>/<DS>@snap | zfs receive <BACKUP_POOL>/<DS>
zfs send -i <POOL>/<DS>@old <POOL>/<DS>@new | zfs receive <BACKUP_POOL>/<DS>
zfs send <POOL>/<DS>@snap | ssh repl@<REMOTE_NAS_IP> zfs receive <POOL>/<DS>
Migration Checklist
zpool list
zfs list -r -t filesystem,snapshot
midclt call sharing.smb.query | jq .
midclt call sharing.nfs.query | jq .
crontab -l
Monitoring Health Check
#!/bin/bash
status=$(curl -s -H "Authorization: Bearer $API_KEY" \
http://$NAS_IP/api/v2.0/pool | jq '.[0].status')
[ "$status" != '"HEALTHY"' ] && echo "ALERT: Pool $status"
zpool status | grep -E "(DEGRADED|FAULTED|OFFLINE|REMOVED|UNAVAIL)"
smartctl -a /dev/sdX | grep -i reallocated
Alert thresholds: pool usage >80%, disk temp >50ยฐC, any SMART reallocated sectors.
Troubleshooting
| Problem | Fix |
|---|
| Container can't write to dataset | chown -R PUID:PGID /mnt/<POOL>/<DS> + set aclmode=passthrough |
| Container mount failures | docker logs <ID>, `mount |
| Networking after SCALE upgrade | ip addr show, `midclt call interfaces.query |
| Scrub shows errors | Replace failing disk immediately; resilver before removing old disk |
| SMB share not accessible | `midclt call sharing.smb.query |