Bebe MomentBebe Moment

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 via PUBLIC_URL/media/*, which Next proxies to localhost:3001 inside 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/amd64 only — ARM models are unsupported until an arm64 image 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

  1. Container Manager → Project → Create
  2. Name: bebe-moment, Path: /volume1/docker/bebe-moment
  3. 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 ml service 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) into MEDIA_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 to PUBLIC_URL) is correct.

4. Start & first login

After build/start:

  • Open PUBLIC_URL (or the NAS http://<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
  • Under Custom Header, enable WebSocket (Create > WebSocket) and set timeout ≥ 600s (so large tus uploads and SSE aren't dropped).
  • Set PUBLIC_URL to 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-momentImage → PullRebuild. 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 missing MEDIA_SERVICE_TOKEN / MEDIA_JWT_SECRET. /api/health doesn'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 :3001 into MEDIA_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 clear pg/ and start, or use the app's backup/restore.