diff --git a/.gitea/workflows/mirror-to-github.yml b/.gitea/workflows/mirror-to-github.yml index 0eadc20..d4d259b 100644 --- a/.gitea/workflows/mirror-to-github.yml +++ b/.gitea/workflows/mirror-to-github.yml @@ -33,6 +33,7 @@ jobs: rm -rf .drone* || true rm -rf photo-sharing-prd.md || true rm -rf CLAUDE.md || true + rm -rf storage/ || true diff --git a/.gitignore b/.gitignore index d4c0231..ad31952 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ yarn-error.log* .env.test.local .env.production.local +# Docker override file +docker-compose.override.yml + # Security - Never commit credentials ADMIN_CREDENTIALS.txt ADMIN_PASSWORD_RESET.txt diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md index 4db1c94..bc115b7 100644 --- a/DEPLOYMENT_GUIDE.md +++ b/DEPLOYMENT_GUIDE.md @@ -38,6 +38,13 @@ For local development, use `docker-compose.dev.yml` which includes Mailhog for e docker-compose -f docker-compose.dev.yml up -d ``` +### Production Customization +For local production customizations, copy `docker-compose.override.yml.example` to `docker-compose.override.yml`: +```bash +cp docker-compose.override.yml.example docker-compose.override.yml +# Edit docker-compose.override.yml with your customizations +``` + ## 🔐 Security Requirements ### Critical: JWT Secret Setup @@ -90,10 +97,10 @@ mkdir -p storage/events/active storage/events/archived storage/thumbnails storag mkdir -p data logs certbot/conf certbot/www # 6. Deploy -docker-compose -f docker-compose.prod.yml up -d +docker-compose up -d # 7. Check logs -docker-compose -f docker-compose.prod.yml logs -f +docker-compose logs -f ``` ## 📦 Deployment Methods @@ -165,13 +172,13 @@ services: chmod -R 755 storage data logs # Build images -docker-compose -f docker-compose.prod.yml build +docker-compose build # Start services -docker-compose -f docker-compose.prod.yml up -d +docker-compose up -d # Run database migrations -docker-compose -f docker-compose.prod.yml exec backend npm run migrate +docker-compose exec backend npm run migrate # Admin credentials will be displayed and saved to ADMIN_CREDENTIALS.txt ``` @@ -523,7 +530,7 @@ When you run migrations for the first time, an admin account is automatically cr ```bash # Docker -docker-compose -f docker-compose.prod.yml exec backend npm run migrate +docker-compose exec backend npm run migrate # PM2/Manual cd backend && npm run migrate @@ -555,7 +562,7 @@ Password: SwiftEagle3847! ```bash # Docker -docker-compose -f docker-compose.prod.yml exec backend node scripts/reset-admin-password.js +docker-compose exec backend node scripts/reset-admin-password.js # PM2/Manual cd backend && node scripts/reset-admin-password.js @@ -567,7 +574,7 @@ cd backend && node scripts/reset-admin-password.js ```bash # Initial certificate -docker-compose -f docker-compose.prod.yml run --rm certbot certonly \ +docker-compose run --rm certbot certonly \ --webroot --webroot-path=/var/www/certbot \ -d your-domain.com -d www.your-domain.com @@ -612,7 +619,7 @@ BACKUP_DIR="./backups/$DATE" mkdir -p $BACKUP_DIR # Database backup -docker-compose -f docker-compose.prod.yml exec -T db \ +docker-compose exec -T db \ pg_dump -U picpeak picpeak > $BACKUP_DIR/database.sql # Files backup @@ -634,8 +641,8 @@ The application includes a built-in backup service. Configure via Admin Panel: ```bash # Docker method git pull -docker-compose -f docker-compose.prod.yml build -docker-compose -f docker-compose.prod.yml up -d +docker-compose build +docker-compose up -d # PM2 method git pull @@ -652,17 +659,17 @@ pm2 restart picpeak curl https://your-domain.com/api/health # Database connection -docker-compose -f docker-compose.prod.yml exec backend \ +docker-compose exec backend \ psql -U picpeak -d picpeak -c "SELECT 1" # Service status -docker-compose -f docker-compose.prod.yml ps +docker-compose ps ``` #### Logs ```bash # Docker logs -docker-compose -f docker-compose.prod.yml logs -f +docker-compose logs -f # PM2 logs pm2 logs picpeak @@ -720,13 +727,13 @@ chmod -R 755 storage ```bash # Check all services -docker-compose -f docker-compose.prod.yml ps +docker-compose ps # Backend shell access -docker-compose -f docker-compose.prod.yml exec backend sh +docker-compose exec backend sh # Database access -docker-compose -f docker-compose.prod.yml exec db psql -U picpeak +docker-compose exec db psql -U picpeak # Test API curl -I http://localhost:3001/api/health diff --git a/backend/migrations/core/029_add_backup_service_tables.js b/backend/migrations/core/029_add_backup_service_tables.js index 10d697d..40ceaa8 100644 --- a/backend/migrations/core/029_add_backup_service_tables.js +++ b/backend/migrations/core/029_add_backup_service_tables.js @@ -1,4 +1,4 @@ -const { db } = require('../src/database/db'); +const { db } = require('../../src/database/db'); async function up() { console.log('Adding backup service tables and settings...'); diff --git a/backend/migrations/core/030_add_database_backup_tables.js b/backend/migrations/core/030_add_database_backup_tables.js index cc99100..d6aff30 100644 --- a/backend/migrations/core/030_add_database_backup_tables.js +++ b/backend/migrations/core/030_add_database_backup_tables.js @@ -1,4 +1,4 @@ -const { db } = require('../src/database/db'); +const { db } = require('../../src/database/db'); async function up() { console.log('Adding database backup tables and settings...'); diff --git a/backend/migrations/core/031_add_backup_manifest_columns.js b/backend/migrations/core/031_add_backup_manifest_columns.js index 5f075c4..0d32c19 100644 --- a/backend/migrations/core/031_add_backup_manifest_columns.js +++ b/backend/migrations/core/031_add_backup_manifest_columns.js @@ -1,5 +1,5 @@ -const { db } = require('../src/database/db'); -const logger = require('../src/utils/logger'); +const { db } = require('../../src/database/db'); +const logger = require('../../src/utils/logger'); async function up() { console.log('Adding backup manifest columns...'); diff --git a/backend/migrations/core/034_add_version_to_backups.js b/backend/migrations/core/034_add_version_to_backups.js index ef8155e..a6fe91b 100644 --- a/backend/migrations/core/034_add_version_to_backups.js +++ b/backend/migrations/core/034_add_version_to_backups.js @@ -1,4 +1,4 @@ -const { db } = require('../src/database/db'); +const { db } = require('../../src/database/db'); async function up() { console.log('Adding version tracking to backup tables...'); diff --git a/backend/migrations/core/035_enhance_backup_system.js b/backend/migrations/core/035_enhance_backup_system.js index 5ab4080..37df3c7 100644 --- a/backend/migrations/core/035_enhance_backup_system.js +++ b/backend/migrations/core/035_enhance_backup_system.js @@ -1,4 +1,4 @@ -const { db } = require('../src/database/db'); +const { db } = require('../../src/database/db'); async function up() { console.log('Enhancing backup system...'); diff --git a/backend/migrations/core/init.js b/backend/migrations/core/init.js index 162bc28..6cab3cb 100644 --- a/backend/migrations/core/init.js +++ b/backend/migrations/core/init.js @@ -1,6 +1,6 @@ const bcrypt = require('bcrypt'); -const { db, initializeDatabase } = require('../src/database/db'); -const { generateReadablePassword } = require('../src/utils/passwordGenerator'); +const { db, initializeDatabase } = require('../../src/database/db'); +const { generateReadablePassword } = require('../../src/utils/passwordGenerator'); const fs = require('fs').promises; const path = require('path'); diff --git a/backend/migrations/legacy/004_add_categories_and_cms.js b/backend/migrations/legacy/004_add_categories_and_cms.js index e77a8d5..297e8e5 100644 --- a/backend/migrations/legacy/004_add_categories_and_cms.js +++ b/backend/migrations/legacy/004_add_categories_and_cms.js @@ -1,4 +1,4 @@ -const { db } = require('../src/database/db'); +const { db } = require('../../src/database/db'); async function up() { console.log('Adding photo categories and CMS tables...'); diff --git a/backend/migrations/legacy/014_add_host_name_to_events_duplicate.js b/backend/migrations/legacy/014_add_host_name_to_events_duplicate.js index 259eda2..220105b 100644 --- a/backend/migrations/legacy/014_add_host_name_to_events_duplicate.js +++ b/backend/migrations/legacy/014_add_host_name_to_events_duplicate.js @@ -1,4 +1,4 @@ -const { db } = require('../src/database/db'); +const { db } = require('../../src/database/db'); async function up() { // Check if host_name column already exists diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example new file mode 100644 index 0000000..8a795a2 --- /dev/null +++ b/docker-compose.override.yml.example @@ -0,0 +1,51 @@ +# docker-compose.override.yml.example +# +# Copy this file to docker-compose.override.yml for local production customizations +# docker-compose.override.yml is git-ignored and will be automatically loaded by Docker Compose +# +# Example customizations: + +version: '3.8' + +services: + # Example: Expose backend port for debugging + # backend: + # ports: + # - "3001:3001" + + # Example: Expose database port for local tools + # db: + # ports: + # - "5432:5432" + + # Example: Custom nginx ports + # nginx: + # ports: + # - "8080:80" + # - "8443:443" + + # Example: Enable Umami web interface + # umami: + # ports: + # - "3000:3000" + + # Example: Use different storage paths + # backend: + # volumes: + # - /mnt/photos:/app/storage + # - /mnt/data:/app/data + + # Example: Development-like setup with code mounting + # backend: + # volumes: + # - ./backend:/app + # - /app/node_modules + # command: npm run dev + + # Example: Add Mailhog for email testing + # mailhog: + # image: mailhog/mailhog:latest + # ports: + # - "1025:1025" + # - "8025:8025" + # restart: unless-stopped \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index f5beeec..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,121 +0,0 @@ -# docker-compose.prod.yml - Production configuration -version: '3.8' - -services: - backend: - image: picpeak-backend:latest - build: - context: ./backend - dockerfile: Dockerfile - restart: unless-stopped - depends_on: - - db - environment: - - NODE_ENV=production - - PORT=3001 - - JWT_SECRET=${JWT_SECRET} - - ADMIN_URL=${ADMIN_URL} - - FRONTEND_URL=${FRONTEND_URL} - - BACKEND_URL=${BACKEND_URL} - # Database - - DATABASE_CLIENT=pg - - DB_HOST=db - - DB_PORT=5432 - - DB_USER=${DB_USER:-picpeak} - - DB_PASSWORD=${DB_PASSWORD} - - DB_NAME=${DB_NAME:-picpeak} - # Email - - SMTP_HOST=${SMTP_HOST} - - SMTP_PORT=${SMTP_PORT} - - SMTP_SECURE=${SMTP_SECURE} - - SMTP_USER=${SMTP_USER} - - SMTP_PASS=${SMTP_PASS} - - EMAIL_FROM=${EMAIL_FROM} - # Analytics - - UMAMI_URL=${UMAMI_URL} - - UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID} - # Storage paths - - STORAGE_PATH=/app/storage - - EVENTS_PATH=/app/storage/events - - ARCHIVE_PATH=/app/storage/events/archived - volumes: - - ./storage:/app/storage - - ./data:/app/data - - ./logs:/app/logs - networks: - - picpeak - - frontend: - image: picpeak-frontend:latest - build: - context: ./frontend - dockerfile: Dockerfile - args: - - VITE_API_URL=/api - restart: unless-stopped - depends_on: - - backend - networks: - - picpeak - - nginx: - image: nginx:alpine - restart: unless-stopped - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf - - ./nginx/sites-enabled:/etc/nginx/sites-enabled - - ./certbot/conf:/etc/letsencrypt - - ./certbot/www:/var/www/certbot - depends_on: - - frontend - - backend - networks: - - picpeak - command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" - - certbot: - image: certbot/certbot - restart: unless-stopped - volumes: - - ./certbot/conf:/etc/letsencrypt - - ./certbot/www:/var/www/certbot - entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" - - db: - image: postgres:14-alpine - restart: unless-stopped - environment: - - POSTGRES_USER=${DB_USER:-picpeak} - - POSTGRES_PASSWORD=${DB_PASSWORD} - - POSTGRES_DB=${DB_NAME:-picpeak} - # Allow connections from any host with password authentication - - POSTGRES_HOST_AUTH_METHOD=scram-sha-256 - - POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=trust - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - picpeak - # Allow connections without SSL requirement from Docker network - command: postgres -c ssl=off - - umami: - image: ghcr.io/umami-software/umami:postgresql-latest - restart: unless-stopped - environment: - DATABASE_URL: postgresql://${DB_USER:-picpeak}:${DB_PASSWORD}@db:5432/umami - DATABASE_TYPE: postgresql - HASH_SALT: ${UMAMI_HASH_SALT} - depends_on: - - db - networks: - - picpeak - -networks: - picpeak: - driver: bridge - -volumes: - postgres_data: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 062c993..848cd7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,93 +1,125 @@ +# docker-compose.yml - Production configuration +# +# For development, use: docker-compose -f docker-compose.dev.yml up -d +# For local customizations, create docker-compose.override.yml (see docker-compose.override.yml.example) +# version: '3.8' services: - # PostgreSQL Database - db: - image: postgres:15-alpine - container_name: picpeak-postgres - environment: - POSTGRES_DB: ${DB_NAME:-picpeak} - POSTGRES_USER: ${DB_USER:-picpeak} - POSTGRES_PASSWORD: ${DB_PASSWORD:-picpeak} - volumes: - - postgres_data:/var/lib/postgresql/data - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak}"] - interval: 10s - timeout: 5s - retries: 5 - - # Backend API backend: - build: ./backend - container_name: picpeak-backend + image: picpeak-backend:latest + build: + context: ./backend + dockerfile: Dockerfile + restart: unless-stopped depends_on: - db: - condition: service_healthy + - db environment: - NODE_ENV: production - PORT: 3001 - DATABASE_CLIENT: pg - DB_HOST: db - DB_PORT: 5432 - DB_NAME: ${DB_NAME:-picpeak} - DB_USER: ${DB_USER:-picpeak} - DB_PASSWORD: ${DB_PASSWORD:-picpeak} - env_file: - - .env + - NODE_ENV=production + - PORT=3001 + - JWT_SECRET=${JWT_SECRET} + - ADMIN_URL=${ADMIN_URL} + - FRONTEND_URL=${FRONTEND_URL} + - BACKEND_URL=${BACKEND_URL} + # Database + - DATABASE_CLIENT=pg + - DB_HOST=db + - DB_PORT=5432 + - DB_USER=${DB_USER:-picpeak} + - DB_PASSWORD=${DB_PASSWORD} + - DB_NAME=${DB_NAME:-picpeak} + # Email + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_SECURE=${SMTP_SECURE} + - SMTP_USER=${SMTP_USER} + - SMTP_PASS=${SMTP_PASS} + - EMAIL_FROM=${EMAIL_FROM} + # Analytics + - UMAMI_URL=${UMAMI_URL} + - UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID} + # Storage paths + - STORAGE_PATH=/app/storage + - EVENTS_PATH=/app/storage/events + - ARCHIVE_PATH=/app/storage/events/archived volumes: - ./storage:/app/storage - ./data:/app/data - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"] - interval: 30s - timeout: 10s - retries: 3 + - ./logs:/app/logs + networks: + - picpeak - # Frontend frontend: - build: + image: picpeak-frontend:latest + build: context: ./frontend + dockerfile: Dockerfile args: - VITE_API_URL: ${VITE_API_URL:-/api} - VITE_UMAMI_URL: ${VITE_UMAMI_URL} - VITE_UMAMI_WEBSITE_ID: ${VITE_UMAMI_WEBSITE_ID} - container_name: picpeak-frontend + - VITE_API_URL=/api + restart: unless-stopped depends_on: - backend - restart: unless-stopped + networks: + - picpeak - # Nginx Reverse Proxy nginx: image: nginx:alpine - container_name: picpeak-nginx + restart: unless-stopped ports: - "80:80" - "443:443" volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - ./nginx/sites-enabled:/etc/nginx/sites-enabled - ./certbot/conf:/etc/letsencrypt - ./certbot/www:/var/www/certbot depends_on: - frontend - backend - restart: unless-stopped + networks: + - picpeak command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" - # Certbot for SSL certbot: image: certbot/certbot - container_name: picpeak-certbot + restart: unless-stopped volumes: - ./certbot/conf:/etc/letsencrypt - ./certbot/www:/var/www/certbot entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" -volumes: - postgres_data: + db: + image: postgres:15-alpine + restart: unless-stopped + environment: + - POSTGRES_USER=${DB_USER:-picpeak} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME:-picpeak} + # Allow connections from any host with password authentication + - POSTGRES_HOST_AUTH_METHOD=scram-sha-256 + - POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=trust + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - picpeak + # Allow connections without SSL requirement from Docker network + command: postgres -c ssl=off + + umami: + image: ghcr.io/umami-software/umami:postgresql-latest + restart: unless-stopped + environment: + DATABASE_URL: postgresql://${DB_USER:-picpeak}:${DB_PASSWORD}@db:5432/umami + DATABASE_TYPE: postgresql + HASH_SALT: ${UMAMI_HASH_SALT} + depends_on: + - db + networks: + - picpeak networks: - default: - name: picpeak-network \ No newline at end of file + picpeak: + driver: bridge + +volumes: + postgres_data: \ No newline at end of file