6c82958c79
- Implement React frontend with TypeScript and Tailwind CSS - Add scrappbook.de-inspired UI design with photo galleries - Implement authentication, photo viewing, and download features - Add Docker Swarm configuration with Traefik reverse proxy - Set up Drone CI/CD pipeline for automated deployments - Add monitoring stack with Prometheus and Grafana - Create comprehensive deployment documentation - Add simple local development setup with docker-compose.local.yml Features: - Password-protected galleries with expiration warnings - Responsive photo grid with lightbox viewer - Bulk download functionality - Hot reload development environment - Email testing with Mailhog - Production-ready deployment scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
265 lines
7.1 KiB
YAML
265 lines
7.1 KiB
YAML
version: '3.8'
|
|
|
|
services:
|
|
backend:
|
|
image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest}
|
|
networks:
|
|
- photo-sharing
|
|
- traefik-public
|
|
environment:
|
|
- NODE_ENV=production
|
|
- PORT=3000
|
|
- JWT_SECRET_FILE=/run/secrets/jwt_secret
|
|
- ADMIN_URL=${ADMIN_URL}
|
|
- FRONTEND_URL=${FRONTEND_URL}
|
|
- SMTP_HOST=${SMTP_HOST}
|
|
- SMTP_PORT=${SMTP_PORT}
|
|
- SMTP_SECURE=${SMTP_SECURE}
|
|
- SMTP_USER_FILE=/run/secrets/smtp_user
|
|
- SMTP_PASS_FILE=/run/secrets/smtp_pass
|
|
- EMAIL_FROM=${EMAIL_FROM}
|
|
- UMAMI_URL=${UMAMI_URL}
|
|
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
|
- DB_HOST=db
|
|
- DB_PORT=5432
|
|
- DB_NAME=${DB_NAME:-photo_sharing}
|
|
- DB_USER_FILE=/run/secrets/db_user
|
|
- DB_PASSWORD_FILE=/run/secrets/db_password
|
|
secrets:
|
|
- jwt_secret
|
|
- smtp_user
|
|
- smtp_pass
|
|
- db_user
|
|
- db_password
|
|
volumes:
|
|
- photo-storage:/app/storage
|
|
- app-data:/app/data
|
|
- app-logs:/app/logs
|
|
deploy:
|
|
replicas: 3
|
|
update_config:
|
|
parallelism: 1
|
|
delay: 10s
|
|
failure_action: rollback
|
|
max_failure_ratio: 0.3
|
|
restart_policy:
|
|
condition: on-failure
|
|
delay: 5s
|
|
max_attempts: 3
|
|
resources:
|
|
limits:
|
|
cpus: '1'
|
|
memory: 512M
|
|
reservations:
|
|
cpus: '0.25'
|
|
memory: 128M
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.docker.network=traefik-public"
|
|
- "traefik.constraint-label=traefik-public"
|
|
- "traefik.http.routers.backend.rule=Host(`${BACKEND_HOST}`) && PathPrefix(`/api`)"
|
|
- "traefik.http.routers.backend.entrypoints=https"
|
|
- "traefik.http.routers.backend.tls=true"
|
|
- "traefik.http.routers.backend.tls.certresolver=letsencrypt"
|
|
- "traefik.http.services.backend.loadbalancer.server.port=3000"
|
|
- "traefik.http.services.backend.loadbalancer.healthcheck.path=/api/health"
|
|
- "traefik.http.services.backend.loadbalancer.healthcheck.interval=10s"
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 40s
|
|
|
|
frontend:
|
|
image: ${REGISTRY_URL}/photo-sharing-frontend:${VERSION:-latest}
|
|
networks:
|
|
- photo-sharing
|
|
- traefik-public
|
|
deploy:
|
|
replicas: 2
|
|
update_config:
|
|
parallelism: 1
|
|
delay: 10s
|
|
failure_action: rollback
|
|
restart_policy:
|
|
condition: on-failure
|
|
resources:
|
|
limits:
|
|
cpus: '0.5'
|
|
memory: 256M
|
|
reservations:
|
|
cpus: '0.1'
|
|
memory: 64M
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.docker.network=traefik-public"
|
|
- "traefik.constraint-label=traefik-public"
|
|
- "traefik.http.routers.frontend.rule=Host(`${FRONTEND_HOST}`)"
|
|
- "traefik.http.routers.frontend.entrypoints=https"
|
|
- "traefik.http.routers.frontend.tls=true"
|
|
- "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
|
|
- "traefik.http.services.frontend.loadbalancer.server.port=80"
|
|
- "traefik.http.middlewares.frontend-compress.compress=true"
|
|
- "traefik.http.routers.frontend.middlewares=frontend-compress"
|
|
|
|
db:
|
|
image: postgres:14-alpine
|
|
networks:
|
|
- photo-sharing
|
|
environment:
|
|
- POSTGRES_USER_FILE=/run/secrets/db_user
|
|
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
|
|
- POSTGRES_DB=${DB_NAME:-photo_sharing}
|
|
secrets:
|
|
- db_user
|
|
- db_password
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
deploy:
|
|
placement:
|
|
constraints:
|
|
- node.labels.db == true
|
|
restart_policy:
|
|
condition: on-failure
|
|
resources:
|
|
limits:
|
|
cpus: '2'
|
|
memory: 1G
|
|
reservations:
|
|
cpus: '0.5'
|
|
memory: 256M
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# Background workers as separate services for better control
|
|
email-worker:
|
|
image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest}
|
|
command: ["node", "src/services/emailService.js"]
|
|
networks:
|
|
- photo-sharing
|
|
environment:
|
|
- NODE_ENV=production
|
|
- JWT_SECRET_FILE=/run/secrets/jwt_secret
|
|
- SMTP_HOST=${SMTP_HOST}
|
|
- SMTP_PORT=${SMTP_PORT}
|
|
- SMTP_SECURE=${SMTP_SECURE}
|
|
- SMTP_USER_FILE=/run/secrets/smtp_user
|
|
- SMTP_PASS_FILE=/run/secrets/smtp_pass
|
|
- EMAIL_FROM=${EMAIL_FROM}
|
|
secrets:
|
|
- jwt_secret
|
|
- smtp_user
|
|
- smtp_pass
|
|
volumes:
|
|
- app-data:/app/data
|
|
- app-logs:/app/logs
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy:
|
|
condition: on-failure
|
|
delay: 5s
|
|
resources:
|
|
limits:
|
|
cpus: '0.5'
|
|
memory: 256M
|
|
|
|
expiration-checker:
|
|
image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest}
|
|
command: ["node", "src/services/expirationChecker.js"]
|
|
networks:
|
|
- photo-sharing
|
|
environment:
|
|
- NODE_ENV=production
|
|
volumes:
|
|
- photo-storage:/app/storage
|
|
- app-data:/app/data
|
|
- app-logs:/app/logs
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy:
|
|
condition: on-failure
|
|
delay: 5s
|
|
resources:
|
|
limits:
|
|
cpus: '0.5'
|
|
memory: 256M
|
|
|
|
archive-worker:
|
|
image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest}
|
|
command: ["node", "src/services/archiveService.js"]
|
|
networks:
|
|
- photo-sharing
|
|
environment:
|
|
- NODE_ENV=production
|
|
volumes:
|
|
- photo-storage:/app/storage
|
|
- app-data:/app/data
|
|
- app-logs:/app/logs
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy:
|
|
condition: on-failure
|
|
delay: 5s
|
|
resources:
|
|
limits:
|
|
cpus: '1'
|
|
memory: 512M
|
|
|
|
# Umami Analytics
|
|
umami:
|
|
image: ghcr.io/umami-software/umami:postgresql-latest
|
|
networks:
|
|
- photo-sharing
|
|
- traefik-public
|
|
environment:
|
|
DATABASE_URL: postgresql://umami:${UMAMI_DB_PASSWORD}@db:5432/umami
|
|
DATABASE_TYPE: postgresql
|
|
HASH_SALT: ${UMAMI_HASH_SALT}
|
|
depends_on:
|
|
- db
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy:
|
|
condition: on-failure
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.docker.network=traefik-public"
|
|
- "traefik.constraint-label=traefik-public"
|
|
- "traefik.http.routers.umami.rule=Host(`${UMAMI_HOST}`)"
|
|
- "traefik.http.routers.umami.entrypoints=https"
|
|
- "traefik.http.routers.umami.tls=true"
|
|
- "traefik.http.routers.umami.tls.certresolver=letsencrypt"
|
|
- "traefik.http.services.umami.loadbalancer.server.port=3000"
|
|
|
|
networks:
|
|
photo-sharing:
|
|
driver: overlay
|
|
attachable: true
|
|
traefik-public:
|
|
external: true
|
|
|
|
volumes:
|
|
postgres-data:
|
|
driver: local
|
|
photo-storage:
|
|
driver: local
|
|
app-data:
|
|
driver: local
|
|
app-logs:
|
|
driver: local
|
|
|
|
secrets:
|
|
jwt_secret:
|
|
external: true
|
|
smtp_user:
|
|
external: true
|
|
smtp_pass:
|
|
external: true
|
|
db_user:
|
|
external: true
|
|
db_password:
|
|
external: true |