Synology DSM install
The deployment topology is just one app container + postgres + redis. web, media, and the notifications worker run as three processes inside a single image (
app) and expose only port 3000. The browser reaches media viaPUBLIC_URL/media/*, which Next proxies tolocalhost:3001inside the container. No separate reverse-proxy container is needed.
Requirements
- DSM 7.2+
- The Container Manager package
- A shared folder
docker(or a name of your choice) - amd64 Synology (the image is currently
linux/amd64only — ARM models are unsupported until anarm64image is added)
1. Prepare folders
Creating these under /volume1/docker/bebe-moment in File Station ahead of time avoids permission tangles.
/volume1/docker/bebe-moment/
data/ # photo originals + derivatives
pg/ # Postgres data
redis/ # queue (transient)
backups/ # backup bundles
2. Create the project
- Container Manager → Project → Create
- Name:
bebe-moment, Path:/volume1/docker/bebe-moment - Source: "Create docker-compose.yml" → paste the content below
services:
app:
image: ghcr.io/svrforum/bebe-moment/app:latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://bebe:${POSTGRES_PASSWORD}@postgres:5432/bebe
DATABASE_URL_WEB: postgres://bebe_web:${BEBE_WEB_DB_PASSWORD}@postgres:5432/bebe
DATABASE_URL_MEDIA: postgres://bebe_media:${BEBE_MEDIA_DB_PASSWORD}@postgres:5432/bebe
REDIS_URL: redis://redis:6379
SECRET_KEY: ${SECRET_KEY}
PUBLIC_URL: ${PUBLIC_URL}
# Quiet hours / digest / memory pushes use container-local time -> set the family timezone.
TZ: ${TZ:-Asia/Seoul}
# web -> media in the same container (runtime) + Next /media rewrite (build). Single container, so localhost.
MEDIA_INTERNAL_URL: http://localhost:3001
# Usually leave unset so it falls back to PUBLIC_URL (same-origin /media/*). Do NOT put :3001 etc.
NEXT_PUBLIC_MEDIA_BASE_URL: ${PUBLIC_URL}
MEDIA_PUBLIC_BASE_URL: ${PUBLIC_URL}
MEDIA_SERVICE_TOKEN: ${MEDIA_SERVICE_TOKEN}
MEDIA_JWT_SECRET: ${MEDIA_JWT_SECRET}
STORAGE_MODE: local
STORAGE_PATH: /data
BACKUP_DIR: /backups
# DSM admin: uid 1026 / gid users(100). For a different user, SSH and run `id <user>`.
PUID: ${PUID:-1026}
PGID: ${PGID:-100}
ADMIN_USER_EMAIL: ${ADMIN_USER_EMAIL:-}
LOG_LEVEL: ${LOG_LEVEL:-info}
BEBE_WEB_DB_PASSWORD: ${BEBE_WEB_DB_PASSWORD}
BEBE_MEDIA_DB_PASSWORD: ${BEBE_MEDIA_DB_PASSWORD}
# Face recognition (opt-in) ML sidecar. If you don't run the ml container, it's never called.
FACE_ML_URL: ${FACE_ML_URL:-http://ml:8000}
volumes:
- /volume1/docker/bebe-moment/data:/data
- /volume1/docker/bebe-moment/backups:/backups
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
postgres:
# Includes pgvector (vector search for face embeddings).
image: pgvector/pgvector:pg17
environment:
POSTGRES_DB: bebe
POSTGRES_USER: bebe
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- /volume1/docker/bebe-moment/pg:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bebe"]
interval: 10s
timeout: 5s
retries: 5
redis:
# Valkey (BSD fork of Redis). The service name stays redis.
image: valkey/valkey:9-alpine
volumes:
- /volume1/docker/bebe-moment/redis:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
This is the same structure as the standard compose, just with Synology absolute paths and PUID/PGID set. Resource limits and the face-recognition
mlservice can be copied over from the standard compose (see "Face recognition" below).
3. Environment variables (env tab)
Enter these in the Environment (.env) tab of the project create screen. Secrets must go here (runtime env) — they are not baked into the image.
SECRET_KEY=<openssl rand -hex 32>
POSTGRES_PASSWORD=<a strong string>
PUBLIC_URL=https://bebe.mydomain.synology.me
ADMIN_USER_EMAIL=<admin email (optional)>
TZ=Asia/Seoul
# Service-to-service auth/signing (32+ bytes each) — generate: openssl rand -hex 32
MEDIA_SERVICE_TOKEN=<random hex>
MEDIA_JWT_SECRET=<random hex>
# DB role split (web=public schema, media=media schema). The entrypoint creates the roles with these passwords.
BEBE_WEB_DB_PASSWORD=<a strong string>
BEBE_MEDIA_DB_PASSWORD=<a strong string>
⚠️ Do not put
:3001(or any unexposed port) intoMEDIA_PUBLIC_BASE_URL/NEXT_PUBLIC_MEDIA_BASE_URL. It points at a port that isn't exposed and photos won't load. Unset (or equal toPUBLIC_URL) is correct.
4. Start & first login
After build/start:
- Open
PUBLIC_URL(or the NAShttp://<NAS-IP>:3000) → sign up as the first user → onboarding (family + baby). - The first user becomes the admin (owner). Everyone after joins by invite link only.
- The admin area is
/admin, or Settings → Admin.
5. TLS / public exposure — DSM reverse proxy
Since there's no separate proxy inside, you only need DSM's reverse proxy for TLS termination.
- DSM → Control Panel → Login Portal → Advanced → Reverse Proxy → Create
- Source:
bebe.mydomain.synology.me, 443, HTTPS - Destination:
localhost, 3000, HTTP
- Source:
- Under Custom Header, enable WebSocket (
Create > WebSocket) and set timeout ≥ 600s (so large tus uploads and SSE aren't dropped). - Set
PUBLIC_URLto this public domain (https://…). Switching to https flips the session cookie to the__Secure-prefix, so a one-time re-login may be required. - Issue a certificate via Let's Encrypt in DSM → Security → Certificate and apply it to this reverse proxy.
6. Face recognition (opt-in)
Off by default. To enable it, add the ml service block from the standard compose (profile faces, image ghcr.io/svrforum/bebe-moment/ml) to the compose above, keep FACE_ML_URL=http://ml:8000, then turn on face recognition in Admin → Features. The model pack is downloaded on first start and cached in the volume (amd64 CPU inference, ~3GB RAM recommended).
7. Backups — Hyper Backup
The built-in app backup (Settings → Admin → Backup, with full/incremental/remote S3) is recommended, but you can also back these up at the NAS level:
/volume1/docker/bebe-moment/data— photo originals + derivatives/volume1/docker/bebe-moment/pg— metadata DB/volume1/docker/bebe-moment/backups— app backup bundles
redis holds transient queue state, so it doesn't need backing up.
8. Updating
Container Manager → project bebe-moment → Image → Pull → Rebuild. On startup prisma migrate deploy runs automatically to bring the schema up to date.
Troubleshooting
- Photo page returns 500 / log shows
MEDIA_SERVICE_TOKEN env required— the env tab is missingMEDIA_SERVICE_TOKEN/MEDIA_JWT_SECRET./api/healthdoesn't touch media and stays 200, so verify with a real photo page. - Page loads but thumbnails are broken — you put an unexposed port like
:3001intoMEDIA_PUBLIC_BASE_URL/NEXT_PUBLIC_MEDIA_BASE_URL. Leave it unset (PUBLIC_URL fallback). - pg16 → pg17 upgrade — the data directory is incompatible across major versions. Either
pg_dump(old) → restore (pg17), then clearpg/and start, or use the app's backup/restore.