What Is This
PhotoPrism is my home photo management service, running on .164 (Celeron J1900 / 3.8G RAM / 1.5G zram). It handles photo indexing, browsing, and video transcoding, and is accessed via photo.xiejun-server.top. The photo album itself is fine; the problem is that nginx-proxy-manager (80/443, the entry point for all websites), AdGuard (whole-home DNS), natfrp, memos, filebrowser, and reader are crowded around it—one small machine is bearing too many responsibilities.
Why Limits Are Needed
The two outages on 10-16 and 10-17 point to the same pattern: the containers running things had no memory limit. In the 10-17 incident, PhotoPrism peaked at 2.2GB (RSS 1.64G + 1.74G swapped out), and combined with the daily SQLite backup and two libx264 transcodes, it pushed the machine into mechanical-disk swap thrashing—load peak 79.15, iowait 84%–90%, 73 processes blocked in uninterruptible IO, and eventually AdGuard health checks timed out repeatedly and it was restarted, cutting whole-home DNS for 2–3 minutes. The subjective experience was “the server is stuck and websites won’t open,” but in reality local IO was saturated.
How It Runs Now
I added three layers of limits to PhotoPrism: container hard limits (1200m memory / 1600m memswap / 1.5 cores / 512 pids), cgroup v2 fine-grained limits (io.max read 64MB/s, write 32MB/s, memory.high 1000M), and a watchdog that runs every 20 seconds—when the whole machine is near death, it automatically runs docker stop photoprism, preferring the album to be temporarily unavailable rather than letting the reverse proxy and websites go down with it. I also moved swap from the mechanical disk to the SSD and raised the CPU weight of critical containers to 8x. Current status: running, resident memory about 171MB, PSI io full avg60 about 2.5%, measured throttled write 24MB/s.
Features
- 容器硬限制:1200m 内存、1600m memswap、1.5 核、512 pids,超限只杀自己
- cgroup v2 精细限制:机械盘读写限 64/32MB/s、IOPS 80/60,memory.high 1000M 提前回收
- 整机濒死自动保护:PSI io full avg60 > 50% 且负载 > 25 连续两次,自动停掉相册服务
- 换页分级:zram(100) > SSD swapfile(50) > NAS 4G(10),不再打满机械盘
- 关键容器 cpu_shares=2048、相册 128,CPU 争抢时关键服务约 8 倍优先
- 转码降规格:FFMPEG_SIZE 1280、BITRATE 20Mbit/s,压低 CPU 与内存峰值
Stack: PhotoPrism · Docker Compose · cgroups v2 · systemd timer · PSI · SQLite · nginx-proxy-manager
1. Set container hard limits
Edit /opt/photoprism/compose.yaml (first back up with cp compose.yaml compose.yaml.bak):
services:
photoprism:
mem_limit: 1200m
memswap_limit: 1600m # 最多 400M 换页
cpus: 1.5
cpu_shares: 128 # 关键容器设 2048
pids_limit: 512
environment:
PHOTOPRISM_FFMPEG_SIZE: "1280"
PHOTOPRISM_FFMPEG_BITRATE: "20"
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
Rebuild with docker compose up -d photoprism; only it itself restarts, and the reverse proxy and website are unaffected.
2. Pin IO and memory soft limits with cgroup v2
First confirm the block device number: lsblk -o NAME,MAJ:MIN (in the data, the HDD is 8:0). Write a photoprism-guard.sh, reapplied every 20 seconds by photoprism-guard.timer (it can also automatically reapply after container rebuild):
echo "8:0 rbps=67108864 wbps=33554432 riops=80 wiops=60" > /sys/fs/cgroup/.../io.max
echo "default 20" > /sys/fs/cgroup/.../io.weight # 默认 100
echo 1000M > /sys/fs/cgroup/.../memory.high # 硬限 1200M 兜底
3. Add a system-wide near-death watchdog
In the same timer, read full avg60 from /proc/pressure/io. If it is greater than 50% and the 1-minute load is greater than 25 for two consecutive times (about 40 seconds), run docker stop photoprism. Write the count to /run/photoprism-guard.bad, write the trigger time to /run/photoprism-guard.stopped, and view logs with journalctl -t photoprism-guard. Recover manually with docker start photoprism.
4. Make swapping prefer the SSD
Create a 4G swapfile on the SSD, add a line to /etc/fstab (back up fstab first), and configure it in three tiers: zram(pri=100) > SSD(pri=50) > NAS 4G(pri=10). Run systemctl daemon-reload to apply.
5. Raise priority of key containers
docker update --cpu-shares 2048 npm adguardhome natfrp-service home memos reader filebrowser
The rules are stored in the container configuration and persist after restart.
6. Verification
- Use
docker inspect photoprismto check Memory / NanoCpus / CpuShares / PidsLimit - Use
cat io.max,cat memory.highto confirm cgroup is in effect - Write 128MB inside the container to test speed; it should be significantly lower than the raw write speed of the HDD
- curl each service port one by one to confirm all green; then check
load, PSI, and available memory
7. Rollback
cp compose.yaml.bak-严格限制-20261017-2143 compose.yaml && docker compose up -d photoprism; for the watchdog, use systemctl disable --now photoprism-guard.timer (the script remains but takes no effect).