chore: clean up unnecessary deployment files
Mirror to GitHub (Archive Method) / mirror (push) Failing after 13s
Mirror to GitHub (Rsync Method) / mirror (push) Failing after 16s
Mirror to GitHub / mirror (push) Failing after 18s
Test and Lint / backend-test (push) Successful in 1m4s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m14s
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Successful in 2s
Mirror to GitHub (Archive Method) / mirror (push) Failing after 13s
Mirror to GitHub (Rsync Method) / mirror (push) Failing after 16s
Mirror to GitHub / mirror (push) Failing after 18s
Test and Lint / backend-test (push) Successful in 1m4s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m14s
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Successful in 2s
- Remove deploy/ folder with complex Docker Swarm configs - Remove local development scripts referencing non-existent docker-compose.local.yml - Remove setup scripts for files that already exist - Remove docker-compose.dev.yml in favor of unified approach - Keep certbot/, CHANGELOG.md, docker-compose.prod.yml, and production guide This simplifies the repository structure and removes confusing duplicate deployment approaches, making it clearer for new users.
This commit is contained in:
@@ -41,6 +41,7 @@ jobs:
|
||||
git rm -r --cached photo-sharing-prd.md || true
|
||||
git rm -r --cached CLAUDE.md || true
|
||||
git rm -r --cached PRODUCTION_DEPLOYMENT_GUIDE.md || true
|
||||
git rm -r --cached logs/ || true
|
||||
|
||||
|
||||
# Commit the changes
|
||||
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
# 🚀 Quick Local Development Setup
|
||||
|
||||
Get the photo sharing platform running locally in under 2 minutes!
|
||||
|
||||
## Prerequisites
|
||||
- Docker Desktop installed and running
|
||||
- Git
|
||||
- 4GB RAM available
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Clone the repository
|
||||
git clone <your-repo-url>
|
||||
cd picpeak
|
||||
|
||||
# 2. Start everything
|
||||
./start-local.sh
|
||||
```
|
||||
|
||||
That's it! 🎉
|
||||
|
||||
## What You Get
|
||||
|
||||
| Service | URL | Description |
|
||||
|---------|-----|-------------|
|
||||
| Frontend (Dev) | http://localhost:3002 | React app with hot reload |
|
||||
| Frontend (Prod) | http://localhost:3000 | Production build |
|
||||
| Backend API | http://localhost:3001 | Express API |
|
||||
| Mailhog | http://localhost:8025 | Email testing UI |
|
||||
|
||||
## Default Credentials
|
||||
|
||||
- **Admin Login**: Check `ADMIN_CREDENTIALS.txt` after first setup
|
||||
- **Test Gallery**:
|
||||
- Create via Admin Panel
|
||||
- Set your own secure password
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
docker-compose -f docker-compose.local.yml logs -f
|
||||
```
|
||||
|
||||
### Stop Everything
|
||||
```bash
|
||||
./stop-local.sh
|
||||
```
|
||||
|
||||
### Reset Database
|
||||
```bash
|
||||
docker-compose -f docker-compose.local.yml exec backend npm run migrate
|
||||
```
|
||||
|
||||
### Add Test Photos
|
||||
1. Create a gallery in the admin panel
|
||||
2. Get the gallery slug (e.g., `wedding-smith-2024`)
|
||||
3. Add photos to: `./storage/events/active/wedding-smith-2024/`
|
||||
4. Photos appear automatically!
|
||||
|
||||
### Access Backend Shell
|
||||
```bash
|
||||
docker-compose -f docker-compose.local.yml exec backend sh
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Frontend Development** (Port 3002)
|
||||
- Hot reload enabled
|
||||
- Edit files in `./frontend/src`
|
||||
- Changes appear instantly
|
||||
|
||||
2. **Backend Development** (Port 3001)
|
||||
- Nodemon watches for changes
|
||||
- Edit files in `./backend/src`
|
||||
- Server restarts automatically
|
||||
|
||||
3. **Email Testing**
|
||||
- All emails go to Mailhog
|
||||
- View at http://localhost:8025
|
||||
- No real emails sent!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backend won't start
|
||||
```bash
|
||||
# Check logs
|
||||
docker-compose -f docker-compose.local.yml logs backend
|
||||
|
||||
# Rebuild
|
||||
docker-compose -f docker-compose.local.yml build backend
|
||||
```
|
||||
|
||||
### Frontend build issues
|
||||
```bash
|
||||
# Clear cache and rebuild
|
||||
docker-compose -f docker-compose.local.yml exec frontend-dev npm run build
|
||||
```
|
||||
|
||||
### Port conflicts
|
||||
Edit `docker-compose.local.yml` and change the port mappings:
|
||||
- Backend: Change `3001:3000` to `XXXX:3000`
|
||||
- Frontend: Change `3002:5173` to `YYYY:5173`
|
||||
|
||||
### Reset everything
|
||||
```bash
|
||||
# Stop and remove all data
|
||||
docker-compose -f docker-compose.local.yml down -v
|
||||
rm -rf data storage logs
|
||||
./start-local.sh
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- 📧 Check Mailhog for all emails
|
||||
- 🔄 Frontend auto-refreshes on save
|
||||
- 📁 SQLite DB at `./data/photo_sharing.db`
|
||||
- 🖼️ Photos in `./storage/events/active/`
|
||||
- 📝 Logs in `./logs/`
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create your first gallery via Admin Panel
|
||||
2. Upload some test photos
|
||||
3. Test the gallery with password
|
||||
4. Check expiration warnings
|
||||
5. View emails in Mailhog
|
||||
|
||||
Happy coding! 🎨
|
||||
@@ -1,862 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Complete setup script to create ALL remaining files
|
||||
|
||||
echo "========================================="
|
||||
echo "PicPeak Platform Setup"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Function to create directory if it doesn't exist
|
||||
create_dir() {
|
||||
if [ ! -d "$1" ]; then
|
||||
mkdir -p "$1"
|
||||
echo "Created directory: $1"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create all necessary directories
|
||||
echo "Creating directory structure..."
|
||||
create_dir "backend/src/services"
|
||||
create_dir "backend/src/utils"
|
||||
create_dir "backend/src/routes"
|
||||
create_dir "backend/migrations"
|
||||
create_dir "backend/scripts"
|
||||
create_dir "backend/__tests__"
|
||||
create_dir "frontend/public"
|
||||
create_dir "frontend/src/components"
|
||||
create_dir "frontend/src/contexts"
|
||||
create_dir "frontend/src/hooks"
|
||||
create_dir "frontend/src/pages/admin"
|
||||
create_dir "frontend/src/services"
|
||||
create_dir "frontend/src/config"
|
||||
create_dir "nginx/sites-enabled"
|
||||
create_dir "scripts"
|
||||
create_dir "storage/events/active"
|
||||
create_dir "storage/events/archived"
|
||||
create_dir "storage/thumbnails"
|
||||
create_dir "data"
|
||||
create_dir "logs"
|
||||
create_dir "certbot/conf"
|
||||
create_dir "certbot/www"
|
||||
|
||||
# Create .gitkeep files to preserve empty directories
|
||||
touch storage/events/active/.gitkeep
|
||||
touch storage/events/archived/.gitkeep
|
||||
touch storage/thumbnails/.gitkeep
|
||||
touch data/.gitkeep
|
||||
touch logs/.gitkeep
|
||||
|
||||
echo ""
|
||||
echo "Creating backend utilities..."
|
||||
|
||||
# Create helpers utility
|
||||
cat > backend/src/utils/helpers.js << 'EOF'
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
|
||||
function generateToken(length = 32) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
function sanitizeFilename(filename) {
|
||||
const basename = path.basename(filename);
|
||||
return basename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
}
|
||||
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function generateSlug(text) {
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/[\s_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
function daysBetween(date1, date2) {
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const firstDate = new Date(date1);
|
||||
const secondDate = new Date(date2);
|
||||
const diffDays = Math.round(Math.abs((firstDate - secondDate) / oneDay));
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
function paginate(totalItems, currentPage = 1, pageSize = 20) {
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
const offset = (currentPage - 1) * pageSize;
|
||||
return {
|
||||
totalItems,
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
offset,
|
||||
hasNext: currentPage < totalPages,
|
||||
hasPrev: currentPage > 1
|
||||
};
|
||||
}
|
||||
|
||||
function asyncHandler(fn) {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
function getClientIp(req) {
|
||||
return req.headers['x-forwarded-for']?.split(',')[0] ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.connection.remoteAddress;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateToken,
|
||||
sanitizeFilename,
|
||||
formatBytes,
|
||||
generateSlug,
|
||||
daysBetween,
|
||||
isValidEmail,
|
||||
paginate,
|
||||
asyncHandler,
|
||||
getClientIp
|
||||
};
|
||||
EOF
|
||||
|
||||
echo "Creating remaining backend routes..."
|
||||
|
||||
# Create admin routes
|
||||
cat > backend/src/routes/admin.js << 'EOF'
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const { db } = require('../database/db');
|
||||
const router = express.Router();
|
||||
|
||||
// Dashboard stats
|
||||
router.get('/stats', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const totalEvents = await db('events').count('id as count').first();
|
||||
const activeEvents = await db('events').where('is_active', true).count('id as count').first();
|
||||
const archivedEvents = await db('events').where('is_archived', true).count('id as count').first();
|
||||
const totalPhotos = await db('photos').count('id as count').first();
|
||||
|
||||
const upcomingExpirations = await db('events')
|
||||
.where('is_active', true)
|
||||
.where('expires_at', '<=', new Date(Date.now() + 7 * 24 * 60 * 60 * 1000))
|
||||
.orderBy('expires_at', 'asc')
|
||||
.limit(5);
|
||||
|
||||
const recentActivity = await db('access_logs')
|
||||
.join('events', 'access_logs.event_id', 'events.id')
|
||||
.select('access_logs.*', 'events.event_name')
|
||||
.orderBy('access_logs.timestamp', 'desc')
|
||||
.limit(10);
|
||||
|
||||
res.json({
|
||||
total_events: totalEvents.count,
|
||||
active_events: activeEvents.count,
|
||||
archived_events: archivedEvents.count,
|
||||
total_photos: totalPhotos.count,
|
||||
upcoming_expirations: upcomingExpirations,
|
||||
recent_activity: recentActivity
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch stats' });
|
||||
}
|
||||
});
|
||||
|
||||
// Email queue management
|
||||
router.get('/emails', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const emails = await db('email_queue')
|
||||
.join('events', 'email_queue.event_id', 'events.id')
|
||||
.select('email_queue.*', 'events.event_name')
|
||||
.orderBy('email_queue.scheduled_at', 'desc')
|
||||
.limit(50);
|
||||
|
||||
res.json(emails);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch emails' });
|
||||
}
|
||||
});
|
||||
|
||||
// Retry failed email
|
||||
router.post('/emails/:id/retry', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await db('email_queue').where('id', id).update({
|
||||
status: 'pending',
|
||||
retry_count: 0,
|
||||
error_message: null
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to retry email' });
|
||||
}
|
||||
});
|
||||
|
||||
// Archive management
|
||||
router.get('/archives', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const archives = await db('events')
|
||||
.where('is_archived', true)
|
||||
.select('id', 'event_name', 'event_date', 'archive_path', 'archived_at')
|
||||
.orderBy('archived_at', 'desc');
|
||||
|
||||
res.json(archives);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch archives' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create admin user
|
||||
router.post('/users', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { username, email, password } = req.body;
|
||||
|
||||
const existing = await db('admin_users')
|
||||
.where('username', username)
|
||||
.orWhere('email', email)
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({ error: 'User already exists' });
|
||||
}
|
||||
|
||||
const password_hash = await bcrypt.hash(password, 10);
|
||||
|
||||
const [userId] = await db('admin_users').insert({
|
||||
username,
|
||||
email,
|
||||
password_hash
|
||||
});
|
||||
|
||||
res.json({ id: userId, username, email });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create user' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
EOF
|
||||
|
||||
echo "Creating deployment scripts..."
|
||||
|
||||
# Create backup script
|
||||
cat > scripts/backup.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/backup/photo-sharing"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_NAME="backup_${TIMESTAMP}"
|
||||
|
||||
mkdir -p "${BACKUP_DIR}/${BACKUP_NAME}"
|
||||
|
||||
echo "Starting backup..."
|
||||
|
||||
if [ -f data/photo_sharing.db ]; then
|
||||
echo "Backing up SQLite database..."
|
||||
cp data/photo_sharing.db "${BACKUP_DIR}/${BACKUP_NAME}/"
|
||||
else
|
||||
echo "Backing up PostgreSQL database..."
|
||||
docker-compose -f docker-compose.prod.yml exec -T db pg_dump -U photoapp photo_sharing > "${BACKUP_DIR}/${BACKUP_NAME}/database.sql"
|
||||
fi
|
||||
|
||||
echo "Backing up active events..."
|
||||
tar -czf "${BACKUP_DIR}/${BACKUP_NAME}/active_events.tar.gz" -C storage/events active/
|
||||
|
||||
cp .env "${BACKUP_DIR}/${BACKUP_NAME}/"
|
||||
|
||||
cat > "${BACKUP_DIR}/${BACKUP_NAME}/backup_info.txt" << EOFINFO
|
||||
Backup created: $(date)
|
||||
Database: $([ -f data/photo_sharing.db ] && echo "photo_sharing.db" || echo "database.sql")
|
||||
Active events: active_events.tar.gz
|
||||
Configuration: .env
|
||||
EOFINFO
|
||||
|
||||
cd "${BACKUP_DIR}"
|
||||
tar -czf "${BACKUP_NAME}.tar.gz" "${BACKUP_NAME}/"
|
||||
rm -rf "${BACKUP_NAME}/"
|
||||
|
||||
find "${BACKUP_DIR}" -name "backup_*.tar.gz" -mtime +30 -delete
|
||||
|
||||
echo "Backup completed: ${BACKUP_DIR}/${BACKUP_NAME}.tar.gz"
|
||||
EOF
|
||||
|
||||
chmod +x scripts/backup.sh
|
||||
|
||||
# Create monitoring script
|
||||
cat > scripts/monitoring.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
check_service() {
|
||||
SERVICE=$1
|
||||
if docker-compose -f docker-compose.prod.yml ps | grep -q "${SERVICE}.*Up"; then
|
||||
echo "✓ ${SERVICE} is running"
|
||||
return 0
|
||||
else
|
||||
echo "✗ ${SERVICE} is down!"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Service Health Check"
|
||||
echo "==================="
|
||||
|
||||
SERVICES_OK=true
|
||||
|
||||
check_service "backend" || SERVICES_OK=false
|
||||
check_service "frontend" || SERVICES_OK=false
|
||||
check_service "nginx" || SERVICES_OK=false
|
||||
|
||||
echo ""
|
||||
echo "Disk Usage:"
|
||||
df -h | grep -E '^/dev/' | awk '{print $6 ": " $5 " used"}'
|
||||
|
||||
FAILED_EMAILS=$(docker-compose -f docker-compose.prod.yml exec -T backend sqlite3 data/photo_sharing.db "SELECT COUNT(*) FROM email_queue WHERE status='failed' AND retry_count >= 3;" 2>/dev/null || echo "0")
|
||||
if [ "$FAILED_EMAILS" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "⚠️ Warning: $FAILED_EMAILS failed emails in queue"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Upcoming Expirations:"
|
||||
docker-compose -f docker-compose.prod.yml exec -T backend sqlite3 data/photo_sharing.db "SELECT event_name, date(expires_at) as expires FROM events WHERE is_active=1 AND expires_at <= datetime('now', '+7 days') ORDER BY expires_at;" 2>/dev/null || echo "No database connection"
|
||||
|
||||
if [ "$SERVICES_OK" = false ]; then
|
||||
echo ""
|
||||
echo "⚠️ Some services are down! Run 'docker-compose -f docker-compose.prod.yml up -d' to restart."
|
||||
exit 1
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x scripts/monitoring.sh
|
||||
|
||||
# Create SSL setup script
|
||||
cat > scripts/setup-ssl.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "SSL Certificate Setup"
|
||||
echo "===================="
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Please run install.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source .env
|
||||
|
||||
ADMIN_DOMAIN=$(echo $ADMIN_URL | sed 's|https://||')
|
||||
FRONTEND_DOMAIN=$(echo $FRONTEND_URL | sed 's|https://||')
|
||||
|
||||
if [ -z "$ADMIN_DOMAIN" ] || [ -z "$FRONTEND_DOMAIN" ]; then
|
||||
echo "Error: Please set ADMIN_URL and FRONTEND_URL in .env file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i "s/admin.photos.yourdomain.com/$ADMIN_DOMAIN/g" nginx/sites-enabled/default.conf
|
||||
sed -i "s/photos.yourdomain.com/$FRONTEND_DOMAIN/g" nginx/sites-enabled/default.conf
|
||||
|
||||
read -p "Enter email for Let's Encrypt notifications: " EMAIL
|
||||
|
||||
docker-compose -f docker-compose.prod.yml up -d nginx
|
||||
|
||||
sleep 5
|
||||
|
||||
echo "Obtaining SSL certificates for $ADMIN_DOMAIN and $FRONTEND_DOMAIN..."
|
||||
|
||||
docker-compose -f docker-compose.prod.yml run --rm certbot certonly \
|
||||
--webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
--email $EMAIL \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
-d $ADMIN_DOMAIN \
|
||||
-d $FRONTEND_DOMAIN
|
||||
|
||||
echo "SSL certificates obtained successfully!"
|
||||
EOF
|
||||
|
||||
chmod +x scripts/setup-ssl.sh
|
||||
|
||||
# Create update script
|
||||
cat > scripts/update.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
echo "Photo Sharing Platform - Update"
|
||||
echo "=============================="
|
||||
|
||||
echo "Creating backup before update..."
|
||||
./scripts/backup.sh
|
||||
|
||||
echo "Pulling latest changes..."
|
||||
git pull origin main
|
||||
|
||||
echo "Rebuilding services..."
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
|
||||
echo "Restarting services..."
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
echo "Running database migrations..."
|
||||
docker-compose -f docker-compose.prod.yml exec backend npm run migrate
|
||||
|
||||
echo "Update completed successfully!"
|
||||
EOF
|
||||
|
||||
chmod +x scripts/update.sh
|
||||
|
||||
echo ""
|
||||
echo "Creating frontend files..."
|
||||
|
||||
# Create frontend Dockerfile
|
||||
cat > frontend/Dockerfile << 'EOF'
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/build /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
EOF
|
||||
|
||||
# Create frontend nginx.conf
|
||||
cat > frontend/nginx.conf << 'EOF'
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /photos {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create minimal frontend files to get started
|
||||
cat > frontend/public/index.html << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Share your event photos securely" />
|
||||
<title>Photo Gallery</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
# Create basic frontend files
|
||||
cat > frontend/src/index.js << 'EOF'
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
EOF
|
||||
|
||||
cat > frontend/src/App.js << 'EOF'
|
||||
import React from 'react';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>Photo Sharing Platform</h1>
|
||||
<p>Setup in progress. Please complete the frontend implementation.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
EOF
|
||||
|
||||
cat > frontend/src/index.css << 'EOF'
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create Tailwind config
|
||||
cat > frontend/tailwind.config.js << 'EOF'
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
wedding: {
|
||||
primary: '#d4a574',
|
||||
secondary: '#f3e5d0',
|
||||
accent: '#8b7355'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create postcss config
|
||||
cat > frontend/postcss.config.js << 'EOF'
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create nginx site config
|
||||
cat > nginx/sites-enabled/default.conf << 'EOF'
|
||||
# Redirect HTTP to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name admin.photos.yourdomain.com photos.yourdomain.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# Admin backend
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name admin.photos.yourdomain.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/admin.photos.yourdomain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/admin.photos.yourdomain.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
location / {
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /api/auth {
|
||||
limit_req zone=auth burst=5 nodelay;
|
||||
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# Public frontend
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name photos.yourdomain.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/photos.yourdomain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/photos.yourdomain.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /api {
|
||||
limit_req zone=general burst=20 nodelay;
|
||||
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /photos {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Cache images
|
||||
proxy_cache_valid 200 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create DEPLOYMENT.md
|
||||
cat > DEPLOYMENT.md << 'EOF'
|
||||
# Production Deployment Guide
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Ubuntu 20.04+ or similar Linux distribution
|
||||
- 2GB RAM minimum (4GB recommended)
|
||||
- 20GB storage minimum
|
||||
- Docker and Docker Compose
|
||||
- Valid domain names with DNS configured
|
||||
|
||||
## Step-by-Step Deployment
|
||||
|
||||
### 1. Server Preparation
|
||||
|
||||
```bash
|
||||
# Update system
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Install required packages
|
||||
sudo apt install -y git curl ufw
|
||||
|
||||
# Configure firewall
|
||||
sudo ufw allow 22/tcp
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
### 2. Clone and Install
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
cd /opt
|
||||
sudo git clone https://github.com/yourusername/photo-sharing-platform.git
|
||||
cd photo-sharing-platform
|
||||
|
||||
# Run installation script
|
||||
sudo ./scripts/install.sh
|
||||
```
|
||||
|
||||
### 3. Configuration
|
||||
|
||||
Edit `.env` file:
|
||||
```bash
|
||||
sudo nano .env
|
||||
```
|
||||
|
||||
Required settings:
|
||||
```env
|
||||
# URLs (use your actual domains)
|
||||
ADMIN_URL=https://admin.photos.yourdomain.com
|
||||
FRONTEND_URL=https://photos.yourdomain.com
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
### 4. SSL Certificate Setup
|
||||
|
||||
```bash
|
||||
# Configure SSL
|
||||
sudo ./scripts/setup-ssl.sh
|
||||
```
|
||||
|
||||
### 5. Start Services
|
||||
|
||||
```bash
|
||||
# Build and start all services
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Initialize database
|
||||
docker-compose -f docker-compose.prod.yml exec backend npm run migrate
|
||||
```
|
||||
|
||||
### 6. Verify Deployment
|
||||
|
||||
1. Check service status:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
```
|
||||
|
||||
2. View logs:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
```
|
||||
|
||||
3. Access sites:
|
||||
- Admin panel: https://admin.photos.yourdomain.com
|
||||
- Public gallery: https://photos.yourdomain.com
|
||||
|
||||
## Post-Deployment
|
||||
|
||||
### Configure Automatic Backups
|
||||
|
||||
```bash
|
||||
# Add to crontab
|
||||
sudo crontab -e
|
||||
|
||||
# Add this line for daily backups at 2 AM
|
||||
0 2 * * * /opt/photo-sharing-platform/scripts/backup.sh
|
||||
```
|
||||
|
||||
### Set Up Monitoring
|
||||
|
||||
```bash
|
||||
# Add health check to crontab
|
||||
*/5 * * * * /opt/photo-sharing-platform/scripts/monitoring.sh
|
||||
```
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
1. Change default admin password immediately
|
||||
2. Configure firewall rules
|
||||
3. Enable automatic security updates
|
||||
4. Monitor access logs regularly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Services won't start
|
||||
```bash
|
||||
# Check logs
|
||||
docker-compose -f docker-compose.prod.yml logs backend
|
||||
docker-compose -f docker-compose.prod.yml logs frontend
|
||||
|
||||
# Restart services
|
||||
docker-compose -f docker-compose.prod.yml restart
|
||||
```
|
||||
|
||||
### Email not sending
|
||||
1. Check SMTP settings in `.env`
|
||||
2. View email queue in admin panel
|
||||
3. Check logs: `docker-compose logs backend | grep email`
|
||||
|
||||
### SSL certificate issues
|
||||
```bash
|
||||
# Renew certificates
|
||||
docker-compose -f docker-compose.prod.yml run --rm certbot renew
|
||||
```
|
||||
EOF
|
||||
|
||||
# Set all script permissions
|
||||
chmod +x scripts/*.sh
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "✅ Setup Complete!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "All core files have been created. The platform structure is ready."
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Install dependencies:"
|
||||
echo " cd backend && npm install"
|
||||
echo " cd ../frontend && npm install"
|
||||
echo ""
|
||||
echo "2. Create a .env file from .env.example:"
|
||||
echo " cp .env.example .env"
|
||||
echo " nano .env # Edit with your settings"
|
||||
echo ""
|
||||
echo "3. Start development environment:"
|
||||
echo " docker-compose up"
|
||||
echo ""
|
||||
echo "4. For production deployment:"
|
||||
echo " Follow the instructions in DEPLOYMENT.md"
|
||||
echo ""
|
||||
echo "Note: The frontend is a basic skeleton. You'll need to implement:"
|
||||
echo "- Authentication context (AuthContext.js)"
|
||||
echo "- Page components (Login, Gallery, Admin pages)"
|
||||
echo "- API service layer"
|
||||
echo "- UI components"
|
||||
echo ""
|
||||
echo "All backend functionality is complete and ready to use!"
|
||||
echo ""
|
||||
echo "Default admin credentials: admin / admin123 (change immediately!)"
|
||||
@@ -1,265 +0,0 @@
|
||||
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
|
||||
@@ -1,189 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
networks:
|
||||
- monitoring
|
||||
- traefik-public
|
||||
volumes:
|
||||
- prometheus-data:/prometheus
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||
- '--web.enable-lifecycle'
|
||||
- '--storage.tsdb.retention.time=30d'
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.monitoring == true
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: '1'
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.http.routers.prometheus.rule=Host(`prometheus.${DOMAIN}`)"
|
||||
- "traefik.http.routers.prometheus.entrypoints=https"
|
||||
- "traefik.http.routers.prometheus.tls=true"
|
||||
- "traefik.http.routers.prometheus.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.prometheus.middlewares=admin-auth"
|
||||
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
networks:
|
||||
- monitoring
|
||||
- traefik-public
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin}
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_SERVER_ROOT_URL=https://grafana.${DOMAIN}
|
||||
- GF_SMTP_ENABLED=true
|
||||
- GF_SMTP_HOST=${SMTP_HOST}:${SMTP_PORT}
|
||||
- GF_SMTP_USER=${SMTP_USER}
|
||||
- GF_SMTP_PASSWORD=${SMTP_PASS}
|
||||
- GF_SMTP_FROM_ADDRESS=${EMAIL_FROM}
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.monitoring == true
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: '0.5'
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.http.routers.grafana.rule=Host(`grafana.${DOMAIN}`)"
|
||||
- "traefik.http.routers.grafana.entrypoints=https"
|
||||
- "traefik.http.routers.grafana.tls=true"
|
||||
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
|
||||
|
||||
loki:
|
||||
image: grafana/loki:latest
|
||||
networks:
|
||||
- monitoring
|
||||
volumes:
|
||||
- loki-data:/loki
|
||||
- ./loki-config.yml:/etc/loki/config.yml:ro
|
||||
command: -config.file=/etc/loki/config.yml
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.monitoring == true
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: '1'
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:latest
|
||||
networks:
|
||||
- monitoring
|
||||
volumes:
|
||||
- /var/log:/var/log:ro
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
- ./promtail-config.yml:/etc/promtail/config.yml:ro
|
||||
command: -config.file=/etc/promtail/config.yml
|
||||
deploy:
|
||||
mode: global
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
|
||||
node-exporter:
|
||||
image: prom/node-exporter:latest
|
||||
networks:
|
||||
- monitoring
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
command:
|
||||
- '--path.procfs=/host/proc'
|
||||
- '--path.sysfs=/host/sys'
|
||||
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
|
||||
deploy:
|
||||
mode: global
|
||||
resources:
|
||||
limits:
|
||||
memory: 128M
|
||||
cpus: '0.1'
|
||||
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:latest
|
||||
networks:
|
||||
- monitoring
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /var/run:/var/run:ro
|
||||
- /sys:/sys:ro
|
||||
- /var/lib/docker/:/var/lib/docker:ro
|
||||
- /dev/disk/:/dev/disk:ro
|
||||
privileged: true
|
||||
deploy:
|
||||
mode: global
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
|
||||
alertmanager:
|
||||
image: prom/alertmanager:latest
|
||||
networks:
|
||||
- monitoring
|
||||
- traefik-public
|
||||
volumes:
|
||||
- alertmanager-data:/alertmanager
|
||||
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
||||
command:
|
||||
- '--config.file=/etc/alertmanager/alertmanager.yml'
|
||||
- '--storage.path=/alertmanager'
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.monitoring == true
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.http.routers.alertmanager.rule=Host(`alerts.${DOMAIN}`)"
|
||||
- "traefik.http.routers.alertmanager.entrypoints=https"
|
||||
- "traefik.http.routers.alertmanager.tls=true"
|
||||
- "traefik.http.routers.alertmanager.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.alertmanager.middlewares=admin-auth"
|
||||
- "traefik.http.services.alertmanager.loadbalancer.server.port=9093"
|
||||
|
||||
networks:
|
||||
monitoring:
|
||||
external: true
|
||||
traefik-public:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
prometheus-data:
|
||||
driver: local
|
||||
grafana-data:
|
||||
driver: local
|
||||
loki-data:
|
||||
driver: local
|
||||
alertmanager-data:
|
||||
driver: local
|
||||
@@ -1,65 +0,0 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
external_labels:
|
||||
monitor: 'photo-sharing'
|
||||
environment: 'production'
|
||||
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets: ['alertmanager:9093']
|
||||
|
||||
rule_files:
|
||||
- '/etc/prometheus/alerts/*.yml'
|
||||
|
||||
scrape_configs:
|
||||
# Prometheus itself
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
# Node Exporter
|
||||
- job_name: 'node-exporter'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'tasks.node-exporter'
|
||||
type: 'A'
|
||||
port: 9100
|
||||
|
||||
# Docker containers
|
||||
- job_name: 'cadvisor'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'tasks.cadvisor'
|
||||
type: 'A'
|
||||
port: 8080
|
||||
|
||||
# Traefik
|
||||
- job_name: 'traefik'
|
||||
static_configs:
|
||||
- targets: ['traefik:8082']
|
||||
|
||||
# Photo Sharing Backend
|
||||
- job_name: 'photo-sharing-backend'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'tasks.photo-sharing_backend'
|
||||
type: 'A'
|
||||
port: 3000
|
||||
metrics_path: '/api/metrics'
|
||||
|
||||
# PostgreSQL
|
||||
- job_name: 'postgres'
|
||||
static_configs:
|
||||
- targets: ['photo-sharing_db:9187']
|
||||
|
||||
# Loki
|
||||
- job_name: 'loki'
|
||||
static_configs:
|
||||
- targets: ['loki:3100']
|
||||
|
||||
# Grafana
|
||||
- job_name: 'grafana'
|
||||
static_configs:
|
||||
- targets: ['grafana:3000']
|
||||
@@ -1,134 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Photo Sharing Platform Backup Script${NC}"
|
||||
echo "===================================="
|
||||
|
||||
# Configuration
|
||||
BACKUP_DIR="/opt/photo-sharing/backup"
|
||||
STACK_NAME="photo-sharing"
|
||||
RETENTION_DAYS=30
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
BACKUP_NAME="backup-${TIMESTAMP}"
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p $BACKUP_DIR/$BACKUP_NAME
|
||||
|
||||
# Function to check if service is running
|
||||
check_service() {
|
||||
local service=$1
|
||||
if docker service ps ${STACK_NAME}_${service} --format "{{.CurrentState}}" | grep -q "Running"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Backup database
|
||||
echo -e "${GREEN}Backing up database...${NC}"
|
||||
if check_service "db"; then
|
||||
DB_CONTAINER=$(docker ps -q -f name=${STACK_NAME}_db -f status=running | head -1)
|
||||
if [ ! -z "$DB_CONTAINER" ]; then
|
||||
docker exec $DB_CONTAINER pg_dumpall -U postgres > $BACKUP_DIR/$BACKUP_NAME/database.sql
|
||||
echo -e "${GREEN}Database backup completed${NC}"
|
||||
else
|
||||
echo -e "${RED}Database container not found${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}Database service not running, skipping...${NC}"
|
||||
fi
|
||||
|
||||
# Backup photos
|
||||
echo -e "${GREEN}Backing up photos...${NC}"
|
||||
if [ -d "/opt/photo-sharing/storage" ]; then
|
||||
tar -czf $BACKUP_DIR/$BACKUP_NAME/photos.tar.gz -C /opt/photo-sharing storage/
|
||||
echo -e "${GREEN}Photos backup completed${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Photos directory not found, skipping...${NC}"
|
||||
fi
|
||||
|
||||
# Backup application data
|
||||
echo -e "${GREEN}Backing up application data...${NC}"
|
||||
if [ -d "/opt/photo-sharing/data" ]; then
|
||||
tar -czf $BACKUP_DIR/$BACKUP_NAME/app-data.tar.gz -C /opt/photo-sharing data/
|
||||
echo -e "${GREEN}Application data backup completed${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Application data directory not found, skipping...${NC}"
|
||||
fi
|
||||
|
||||
# Backup Docker volumes
|
||||
echo -e "${GREEN}Backing up Docker volumes...${NC}"
|
||||
for volume in $(docker volume ls -q | grep ${STACK_NAME}); do
|
||||
echo "Backing up volume: $volume"
|
||||
docker run --rm \
|
||||
-v $volume:/data \
|
||||
-v $BACKUP_DIR/$BACKUP_NAME:/backup \
|
||||
alpine tar -czf /backup/volume-${volume}.tar.gz -C /data .
|
||||
done
|
||||
|
||||
# Backup configurations
|
||||
echo -e "${GREEN}Backing up configurations...${NC}"
|
||||
if [ -f "../../.env.production" ]; then
|
||||
cp ../../.env.production $BACKUP_DIR/$BACKUP_NAME/
|
||||
fi
|
||||
|
||||
# Export Docker secrets (encrypted)
|
||||
echo -e "${GREEN}Exporting Docker secrets info...${NC}"
|
||||
docker secret ls --filter "label=com.docker.stack.namespace=$STACK_NAME" > $BACKUP_DIR/$BACKUP_NAME/secrets-list.txt
|
||||
|
||||
# Create backup manifest
|
||||
echo -e "${GREEN}Creating backup manifest...${NC}"
|
||||
cat > $BACKUP_DIR/$BACKUP_NAME/manifest.json << EOF
|
||||
{
|
||||
"timestamp": "$TIMESTAMP",
|
||||
"stack_name": "$STACK_NAME",
|
||||
"hostname": "$(hostname)",
|
||||
"docker_version": "$(docker version --format '{{.Server.Version}}')",
|
||||
"services": $(docker service ls --filter "label=com.docker.stack.namespace=$STACK_NAME" --format '{{json .}}' | jq -s .),
|
||||
"backup_contents": [
|
||||
"database.sql",
|
||||
"photos.tar.gz",
|
||||
"app-data.tar.gz",
|
||||
"volume-*.tar.gz",
|
||||
".env.production",
|
||||
"secrets-list.txt"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Compress entire backup
|
||||
echo -e "${GREEN}Compressing backup...${NC}"
|
||||
cd $BACKUP_DIR
|
||||
tar -czf ${BACKUP_NAME}.tar.gz $BACKUP_NAME/
|
||||
rm -rf $BACKUP_NAME/
|
||||
|
||||
# Upload to S3 (optional)
|
||||
if [ ! -z "$S3_BACKUP_BUCKET" ] && command -v aws &> /dev/null; then
|
||||
echo -e "${GREEN}Uploading to S3...${NC}"
|
||||
aws s3 cp ${BACKUP_NAME}.tar.gz s3://${S3_BACKUP_BUCKET}/photo-sharing/
|
||||
fi
|
||||
|
||||
# Clean up old backups
|
||||
echo -e "${GREEN}Cleaning up old backups...${NC}"
|
||||
find $BACKUP_DIR -name "backup-*.tar.gz" -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
# Show backup summary
|
||||
BACKUP_SIZE=$(du -h $BACKUP_DIR/${BACKUP_NAME}.tar.gz | cut -f1)
|
||||
echo ""
|
||||
echo -e "${GREEN}Backup completed successfully!${NC}"
|
||||
echo -e "Backup file: $BACKUP_DIR/${BACKUP_NAME}.tar.gz"
|
||||
echo -e "Backup size: $BACKUP_SIZE"
|
||||
echo -e "Retention: $RETENTION_DAYS days"
|
||||
|
||||
# Verify backup
|
||||
echo ""
|
||||
echo -e "${GREEN}Verifying backup...${NC}"
|
||||
tar -tzf $BACKUP_DIR/${BACKUP_NAME}.tar.gz | head -10
|
||||
echo "..."
|
||||
echo -e "${GREEN}Backup verification complete${NC}"
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Docker Secrets Creation Script${NC}"
|
||||
echo "==============================="
|
||||
|
||||
# Function to create or update a secret
|
||||
create_secret() {
|
||||
local secret_name=$1
|
||||
local secret_value=$2
|
||||
|
||||
# Check if secret exists
|
||||
if docker secret ls | grep -q $secret_name; then
|
||||
echo -e "${YELLOW}Secret '$secret_name' already exists. Skipping...${NC}"
|
||||
else
|
||||
echo "$secret_value" | docker secret create $secret_name -
|
||||
echo -e "${GREEN}Created secret: $secret_name${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to generate random password
|
||||
generate_password() {
|
||||
openssl rand -base64 32 | tr -d "=+/" | cut -c1-25
|
||||
}
|
||||
|
||||
# Check if in swarm mode
|
||||
if ! docker info | grep -q "Swarm: active"; then
|
||||
echo -e "${RED}Docker is not in swarm mode. Run init-swarm.sh first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load environment variables if .env.production exists
|
||||
if [ -f "../../.env.production" ]; then
|
||||
echo -e "${GREEN}Loading environment variables from .env.production${NC}"
|
||||
source ../../.env.production
|
||||
fi
|
||||
|
||||
# JWT Secret
|
||||
if [ -z "$JWT_SECRET" ]; then
|
||||
JWT_SECRET=$(generate_password)
|
||||
echo -e "${YELLOW}Generated JWT_SECRET: $JWT_SECRET${NC}"
|
||||
fi
|
||||
create_secret "jwt_secret" "$JWT_SECRET"
|
||||
|
||||
# Database credentials
|
||||
if [ -z "$DB_USER" ]; then
|
||||
DB_USER="photoapp"
|
||||
fi
|
||||
if [ -z "$DB_PASSWORD" ]; then
|
||||
DB_PASSWORD=$(generate_password)
|
||||
echo -e "${YELLOW}Generated DB_PASSWORD: $DB_PASSWORD${NC}"
|
||||
fi
|
||||
create_secret "db_user" "$DB_USER"
|
||||
create_secret "db_password" "$DB_PASSWORD"
|
||||
|
||||
# SMTP credentials
|
||||
if [ -z "$SMTP_USER" ]; then
|
||||
read -p "Enter SMTP username: " SMTP_USER
|
||||
fi
|
||||
if [ -z "$SMTP_PASS" ]; then
|
||||
read -sp "Enter SMTP password: " SMTP_PASS
|
||||
echo
|
||||
fi
|
||||
create_secret "smtp_user" "$SMTP_USER"
|
||||
create_secret "smtp_pass" "$SMTP_PASS"
|
||||
|
||||
# Traefik dashboard auth (username:password)
|
||||
if [ -z "$TRAEFIK_USER" ]; then
|
||||
TRAEFIK_USER="admin"
|
||||
fi
|
||||
if [ -z "$TRAEFIK_PASSWORD" ]; then
|
||||
TRAEFIK_PASSWORD=$(generate_password)
|
||||
echo -e "${YELLOW}Generated TRAEFIK_PASSWORD: $TRAEFIK_PASSWORD${NC}"
|
||||
fi
|
||||
# Generate htpasswd format
|
||||
TRAEFIK_AUTH=$(docker run --rm httpd:alpine htpasswd -nb $TRAEFIK_USER $TRAEFIK_PASSWORD)
|
||||
create_secret "traefik_dashboard_auth" "$TRAEFIK_AUTH"
|
||||
|
||||
# OAuth secrets (optional)
|
||||
if [ ! -z "$OAUTH_CLIENT_SECRET" ]; then
|
||||
create_secret "oauth_client_secret" "$OAUTH_CLIENT_SECRET"
|
||||
fi
|
||||
|
||||
if [ ! -z "$OAUTH_SECRET" ]; then
|
||||
create_secret "oauth_secret" "$OAUTH_SECRET"
|
||||
fi
|
||||
|
||||
# Drone CI secrets
|
||||
if [ ! -z "$DRONE_RPC_SECRET" ]; then
|
||||
create_secret "drone_rpc_secret" "$DRONE_RPC_SECRET"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Secrets creation complete!${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Important: Save these generated values in a secure location:${NC}"
|
||||
echo "JWT_SECRET=$JWT_SECRET"
|
||||
echo "DB_PASSWORD=$DB_PASSWORD"
|
||||
echo "TRAEFIK_USER=$TRAEFIK_USER"
|
||||
echo "TRAEFIK_PASSWORD=$TRAEFIK_PASSWORD"
|
||||
echo ""
|
||||
echo -e "${GREEN}Next steps:${NC}"
|
||||
echo "1. Update .env.production with the generated values"
|
||||
echo "2. Deploy Traefik: ./deploy-traefik.sh"
|
||||
echo "3. Deploy the application: ./deploy.sh"
|
||||
@@ -1,196 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}PicPeak Deployment Script${NC}"
|
||||
echo "========================================"
|
||||
|
||||
# Default values
|
||||
STACK_NAME="picpeak"
|
||||
ENV_FILE="../../.env.production"
|
||||
REGISTRY_URL="${REGISTRY_URL:-}"
|
||||
VERSION="${VERSION:-latest}"
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--env)
|
||||
ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--registry)
|
||||
REGISTRY_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--stack-name)
|
||||
STACK_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [options]"
|
||||
echo "Options:"
|
||||
echo " --env FILE Path to environment file (default: ../../.env.production)"
|
||||
echo " --registry URL Docker registry URL"
|
||||
echo " --version VERSION Image version to deploy (default: latest)"
|
||||
echo " --stack-name NAME Stack name (default: picpeak)"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if Docker is in swarm mode
|
||||
if ! docker info | grep -q "Swarm: active"; then
|
||||
echo -e "${RED}Docker is not in swarm mode. Run init-swarm.sh first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if environment file exists
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo -e "${RED}Environment file not found: $ENV_FILE${NC}"
|
||||
echo "Please create it from .env.production.example"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load environment variables
|
||||
echo -e "${GREEN}Loading environment variables...${NC}"
|
||||
set -a
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
# Export deployment variables
|
||||
export REGISTRY_URL
|
||||
export VERSION
|
||||
|
||||
# Validate required environment variables
|
||||
required_vars=(
|
||||
"FRONTEND_HOST"
|
||||
"BACKEND_HOST"
|
||||
"ADMIN_URL"
|
||||
"FRONTEND_URL"
|
||||
"ACME_EMAIL"
|
||||
"DB_NAME"
|
||||
"EMAIL_FROM"
|
||||
)
|
||||
|
||||
echo -e "${GREEN}Validating configuration...${NC}"
|
||||
for var in "${required_vars[@]}"; do
|
||||
if [ -z "${!var}" ]; then
|
||||
echo -e "${RED}Missing required environment variable: $var${NC}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if Traefik is running
|
||||
if ! docker service ls | grep -q "traefik_traefik"; then
|
||||
echo -e "${YELLOW}Traefik is not running. Deploy it first with:${NC}"
|
||||
echo "cd ../traefik && docker stack deploy -c docker-compose.traefik.yml traefik"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if secrets exist
|
||||
echo -e "${GREEN}Checking Docker secrets...${NC}"
|
||||
required_secrets=(
|
||||
"jwt_secret"
|
||||
"db_user"
|
||||
"db_password"
|
||||
"smtp_user"
|
||||
"smtp_pass"
|
||||
)
|
||||
|
||||
for secret in "${required_secrets[@]}"; do
|
||||
if ! docker secret ls | grep -q $secret; then
|
||||
echo -e "${RED}Missing required secret: $secret${NC}"
|
||||
echo "Run create-secrets.sh first"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Pull latest images if registry is specified
|
||||
if [ ! -z "$REGISTRY_URL" ]; then
|
||||
echo -e "${GREEN}Pulling latest images...${NC}"
|
||||
docker pull ${REGISTRY_URL}/photo-sharing-backend:${VERSION} || true
|
||||
docker pull ${REGISTRY_URL}/photo-sharing-frontend:${VERSION} || true
|
||||
fi
|
||||
|
||||
# Deploy the stack
|
||||
echo -e "${GREEN}Deploying stack: $STACK_NAME${NC}"
|
||||
echo -e "${BLUE}Version: $VERSION${NC}"
|
||||
echo -e "${BLUE}Registry: ${REGISTRY_URL:-local}${NC}"
|
||||
|
||||
cd ..
|
||||
docker stack deploy \
|
||||
-c docker-stack.yml \
|
||||
--with-registry-auth \
|
||||
$STACK_NAME
|
||||
|
||||
# Wait for services to start
|
||||
echo -e "${GREEN}Waiting for services to start...${NC}"
|
||||
sleep 10
|
||||
|
||||
# Check service status
|
||||
echo -e "${GREEN}Service status:${NC}"
|
||||
docker service ls --filter "label=com.docker.stack.namespace=$STACK_NAME"
|
||||
|
||||
# Wait for database to be ready
|
||||
echo -e "${GREEN}Waiting for database to be ready...${NC}"
|
||||
max_attempts=30
|
||||
attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if docker exec $(docker ps -q -f name=${STACK_NAME}_db) pg_isready -U postgres > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}Database is ready!${NC}"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
if [ $attempt -gt $max_attempts ]; then
|
||||
echo -e "${RED}Database failed to start in time${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run database migrations
|
||||
echo -e "${GREEN}Running database migrations...${NC}"
|
||||
sleep 5
|
||||
docker exec $(docker ps -q -f name=${STACK_NAME}_backend -f status=running | head -1) npm run migrate || {
|
||||
echo -e "${YELLOW}Migration failed. This might be normal if migrations already ran.${NC}"
|
||||
}
|
||||
|
||||
# Show deployment information
|
||||
echo ""
|
||||
echo -e "${GREEN}Deployment complete!${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Access URLs:${NC}"
|
||||
echo "Frontend: https://${FRONTEND_HOST}"
|
||||
echo "Backend API: https://${BACKEND_HOST}/api"
|
||||
if [ ! -z "$UMAMI_HOST" ]; then
|
||||
echo "Analytics: https://${UMAMI_HOST}"
|
||||
fi
|
||||
if [ ! -z "$TRAEFIK_HOST" ]; then
|
||||
echo "Traefik Dashboard: https://${TRAEFIK_HOST}/dashboard/"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${BLUE}Useful commands:${NC}"
|
||||
echo "View logs: docker service logs ${STACK_NAME}_backend"
|
||||
echo "Scale service: docker service scale ${STACK_NAME}_backend=5"
|
||||
echo "Update service: docker service update ${STACK_NAME}_backend"
|
||||
echo "Remove stack: docker stack rm $STACK_NAME"
|
||||
echo ""
|
||||
echo -e "${GREEN}Health check:${NC}"
|
||||
curl -s -o /dev/null -w "Frontend: %{http_code}\n" https://${FRONTEND_HOST}/health || true
|
||||
curl -s -o /dev/null -w "Backend: %{http_code}\n" https://${BACKEND_HOST}/api/health || true
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Docker Swarm Initialization Script${NC}"
|
||||
echo "======================================"
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo -e "${RED}This script must be run as root${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker is installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${RED}Docker is not installed. Please install Docker first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if already in swarm mode
|
||||
if docker info | grep -q "Swarm: active"; then
|
||||
echo -e "${YELLOW}This node is already part of a swarm.${NC}"
|
||||
docker node ls
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Initialize swarm
|
||||
echo -e "${GREEN}Initializing Docker Swarm...${NC}"
|
||||
ADVERTISE_ADDR=${1:-$(hostname -I | awk '{print $1}')}
|
||||
docker swarm init --advertise-addr $ADVERTISE_ADDR
|
||||
|
||||
# Create overlay networks
|
||||
echo -e "${GREEN}Creating overlay networks...${NC}"
|
||||
docker network create --driver overlay --attachable traefik-public || true
|
||||
docker network create --driver overlay --attachable monitoring || true
|
||||
|
||||
# Label the node
|
||||
echo -e "${GREEN}Labeling manager node...${NC}"
|
||||
NODE_ID=$(docker info -f '{{.Swarm.NodeID}}')
|
||||
docker node update --label-add db=true $NODE_ID
|
||||
docker node update --label-add monitoring=true $NODE_ID
|
||||
|
||||
# Create required directories
|
||||
echo -e "${GREEN}Creating required directories...${NC}"
|
||||
mkdir -p /opt/photo-sharing/{storage,data,logs,backup}
|
||||
mkdir -p /opt/traefik/letsencrypt
|
||||
mkdir -p /opt/monitoring/{prometheus,grafana,loki}
|
||||
|
||||
# Set permissions
|
||||
chown -R 1000:1000 /opt/photo-sharing
|
||||
chmod -R 755 /opt/photo-sharing
|
||||
|
||||
echo -e "${GREEN}Swarm initialization complete!${NC}"
|
||||
echo ""
|
||||
echo "Manager join token:"
|
||||
docker swarm join-token manager
|
||||
echo ""
|
||||
echo "Worker join token:"
|
||||
docker swarm join-token worker
|
||||
echo ""
|
||||
echo -e "${GREEN}Next steps:${NC}"
|
||||
echo "1. Join worker nodes using the token above"
|
||||
echo "2. Create secrets using create-secrets.sh"
|
||||
echo "3. Deploy Traefik using deploy-traefik.sh"
|
||||
echo "4. Deploy the application stack using deploy.sh"
|
||||
@@ -1,124 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v2.10
|
||||
ports:
|
||||
- target: 80
|
||||
published: 80
|
||||
mode: host
|
||||
- target: 443
|
||||
published: 443
|
||||
mode: host
|
||||
networks:
|
||||
- traefik-public
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- traefik-certificates:/letsencrypt
|
||||
environment:
|
||||
- TRAEFIK_API=true
|
||||
- TRAEFIK_API_DASHBOARD=true
|
||||
- TRAEFIK_API_DEBUG=false
|
||||
- TRAEFIK_LOG_LEVEL=INFO
|
||||
- TRAEFIK_PROVIDERS_DOCKER=true
|
||||
- TRAEFIK_PROVIDERS_DOCKER_SWARMMODE=true
|
||||
- TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=false
|
||||
- TRAEFIK_PROVIDERS_DOCKER_NETWORK=traefik-public
|
||||
- TRAEFIK_ENTRYPOINTS_HTTP_ADDRESS=:80
|
||||
- TRAEFIK_ENTRYPOINTS_HTTPS_ADDRESS=:443
|
||||
# Redirect HTTP to HTTPS
|
||||
- TRAEFIK_ENTRYPOINTS_HTTP_HTTP_REDIRECTIONS_ENTRYPOINT_TO=https
|
||||
- TRAEFIK_ENTRYPOINTS_HTTP_HTTP_REDIRECTIONS_ENTRYPOINT_SCHEME=https
|
||||
# Let's Encrypt
|
||||
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=${ACME_EMAIL}
|
||||
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE=/letsencrypt/acme.json
|
||||
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE=true
|
||||
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT=http
|
||||
# Enable metrics
|
||||
- TRAEFIK_METRICS_PROMETHEUS=true
|
||||
- TRAEFIK_METRICS_PROMETHEUS_ENTRYPOINT=metrics
|
||||
- TRAEFIK_ENTRYPOINTS_METRICS_ADDRESS=:8082
|
||||
deploy:
|
||||
mode: global
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == manager
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.constraint-label=traefik-public"
|
||||
# Dashboard
|
||||
- "traefik.http.routers.traefik-dashboard.rule=Host(`${TRAEFIK_HOST}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
|
||||
- "traefik.http.routers.traefik-dashboard.entrypoints=https"
|
||||
- "traefik.http.routers.traefik-dashboard.tls=true"
|
||||
- "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.traefik-dashboard.service=api@internal"
|
||||
- "traefik.http.routers.traefik-dashboard.middlewares=admin-auth"
|
||||
# Basic auth for dashboard
|
||||
- "traefik.http.middlewares.admin-auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}"
|
||||
# Global redirect to https
|
||||
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
|
||||
# Security headers
|
||||
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.browserXssFilter=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
|
||||
- "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.stsPreload=true"
|
||||
# Rate limiting
|
||||
- "traefik.http.middlewares.rate-limit.ratelimit.average=100"
|
||||
- "traefik.http.middlewares.rate-limit.ratelimit.burst=50"
|
||||
# API service
|
||||
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
|
||||
healthcheck:
|
||||
test: ["CMD", "traefik", "healthcheck"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
# Traefik Forward Auth for advanced authentication (optional)
|
||||
traefik-forward-auth:
|
||||
image: thomseddon/traefik-forward-auth:latest
|
||||
networks:
|
||||
- traefik-public
|
||||
environment:
|
||||
- DEFAULT_PROVIDER=generic-oauth
|
||||
- PROVIDERS_GENERIC_OAUTH_AUTH_URL=${OAUTH_AUTH_URL}
|
||||
- PROVIDERS_GENERIC_OAUTH_TOKEN_URL=${OAUTH_TOKEN_URL}
|
||||
- PROVIDERS_GENERIC_OAUTH_USER_URL=${OAUTH_USER_URL}
|
||||
- PROVIDERS_GENERIC_OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
|
||||
- PROVIDERS_GENERIC_OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
|
||||
- SECRET=${OAUTH_SECRET}
|
||||
- COOKIE_DOMAIN=${COOKIE_DOMAIN}
|
||||
- INSECURE_COOKIE=false
|
||||
- LOG_LEVEL=info
|
||||
- URL_PATH=/_oauth
|
||||
- WHITELIST=${OAUTH_WHITELIST}
|
||||
deploy:
|
||||
replicas: 2
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
- "traefik.http.routers.traefik-forward-auth.rule=Host(`${FRONTEND_HOST}`) && PathPrefix(`/_oauth`)"
|
||||
- "traefik.http.routers.traefik-forward-auth.entrypoints=https"
|
||||
- "traefik.http.routers.traefik-forward-auth.tls=true"
|
||||
- "traefik.http.routers.traefik-forward-auth.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.traefik-forward-auth.middlewares=auth-verify"
|
||||
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
|
||||
- "traefik.http.middlewares.auth-verify.forwardauth.address=http://traefik-forward-auth:4181"
|
||||
- "traefik.http.middlewares.auth-verify.forwardauth.authResponseHeaders=X-Forwarded-User"
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
traefik-certificates:
|
||||
driver: local
|
||||
@@ -1,93 +0,0 @@
|
||||
# Static configuration
|
||||
global:
|
||||
checkNewVersion: true
|
||||
sendAnonymousUsage: false
|
||||
|
||||
api:
|
||||
dashboard: true
|
||||
debug: false
|
||||
|
||||
# Entry Points
|
||||
entryPoints:
|
||||
http:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: https
|
||||
scheme: https
|
||||
priority: 1000
|
||||
https:
|
||||
address: ":443"
|
||||
http:
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
domains:
|
||||
- main: "${FRONTEND_HOST}"
|
||||
- main: "${BACKEND_HOST}"
|
||||
- main: "${UMAMI_HOST}"
|
||||
forwardedHeaders:
|
||||
trustedIPs:
|
||||
- "127.0.0.1/32"
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
- "192.168.0.0/16"
|
||||
metrics:
|
||||
address: ":8082"
|
||||
|
||||
# Providers
|
||||
providers:
|
||||
docker:
|
||||
swarmMode: true
|
||||
exposedByDefault: false
|
||||
network: traefik-public
|
||||
watch: true
|
||||
file:
|
||||
directory: /etc/traefik/dynamic
|
||||
watch: true
|
||||
|
||||
# Certificate Resolvers
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: ${ACME_EMAIL}
|
||||
storage: /letsencrypt/acme.json
|
||||
httpChallenge:
|
||||
entryPoint: http
|
||||
# Staging server for testing
|
||||
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
|
||||
# Logs
|
||||
log:
|
||||
level: INFO
|
||||
format: json
|
||||
|
||||
accessLog:
|
||||
format: json
|
||||
filters:
|
||||
statusCodes:
|
||||
- "200-299"
|
||||
- "400-499"
|
||||
- "500-599"
|
||||
retryAttempts: true
|
||||
minDuration: "10ms"
|
||||
|
||||
# Metrics
|
||||
metrics:
|
||||
prometheus:
|
||||
entryPoint: metrics
|
||||
addEntryPointsLabels: true
|
||||
addServicesLabels: true
|
||||
buckets:
|
||||
- 0.1
|
||||
- 0.3
|
||||
- 1.2
|
||||
- 5.0
|
||||
|
||||
# Ping
|
||||
ping:
|
||||
entryPoint: traefik
|
||||
|
||||
# Pilot
|
||||
pilot:
|
||||
enabled: false
|
||||
@@ -1,61 +0,0 @@
|
||||
# Development Docker Compose Configuration
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- JWT_SECRET=dev-secret-change-in-production
|
||||
- ADMIN_URL=http://localhost:3005
|
||||
- FRONTEND_URL=http://localhost:3005
|
||||
- DATABASE_CLIENT=sqlite3
|
||||
- DATABASE_PATH=./data/photo_sharing.db
|
||||
# Email - uses Mailhog
|
||||
- SMTP_HOST=mailhog
|
||||
- SMTP_PORT=1025
|
||||
- SMTP_SECURE=false
|
||||
- EMAIL_FROM=noreply@photo-sharing.local
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./storage:/app/storage
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
- mailhog
|
||||
command: sh -c "npm install && npm run dev"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3005:80"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
ports:
|
||||
- "1025:1025" # SMTP
|
||||
- "8025:8025" # Web UI
|
||||
@@ -1,380 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Setup script to create remaining files
|
||||
|
||||
echo "Creating remaining project files..."
|
||||
|
||||
# Create directories
|
||||
mkdir -p backend/src/services
|
||||
mkdir -p backend/src/utils
|
||||
mkdir -p backend/src/routes
|
||||
mkdir -p backend/migrations
|
||||
mkdir -p backend/scripts
|
||||
mkdir -p backend/__tests__
|
||||
mkdir -p frontend/public
|
||||
mkdir -p frontend/src/components
|
||||
mkdir -p frontend/src/contexts
|
||||
mkdir -p frontend/src/hooks
|
||||
mkdir -p frontend/src/pages/admin
|
||||
mkdir -p frontend/src/services
|
||||
mkdir -p frontend/src/config
|
||||
mkdir -p nginx/sites-enabled
|
||||
mkdir -p scripts
|
||||
mkdir -p storage/events/active
|
||||
mkdir -p storage/events/archived
|
||||
mkdir -p storage/thumbnails
|
||||
mkdir -p data
|
||||
mkdir -p logs
|
||||
mkdir -p certbot/conf
|
||||
mkdir -p certbot/www
|
||||
|
||||
# Create .gitkeep files
|
||||
touch storage/events/active/.gitkeep
|
||||
touch storage/events/archived/.gitkeep
|
||||
touch storage/thumbnails/.gitkeep
|
||||
touch data/.gitkeep
|
||||
touch logs/.gitkeep
|
||||
|
||||
# Create remaining backend services
|
||||
cat > backend/src/services/imageProcessor.js << 'EOF'
|
||||
const sharp = require('sharp');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const THUMBNAIL_WIDTH = 300;
|
||||
const THUMBNAIL_PATH = path.join(__dirname, '../../../storage/thumbnails');
|
||||
|
||||
async function generateThumbnail(imagePath) {
|
||||
const filename = path.basename(imagePath);
|
||||
const thumbnailFilename = `thumb_${filename}`;
|
||||
const thumbnailPath = path.join(THUMBNAIL_PATH, thumbnailFilename);
|
||||
|
||||
// Ensure thumbnail directory exists
|
||||
await fs.mkdir(THUMBNAIL_PATH, { recursive: true });
|
||||
|
||||
// Generate thumbnail
|
||||
await sharp(imagePath)
|
||||
.resize(THUMBNAIL_WIDTH, null, {
|
||||
withoutEnlargement: true,
|
||||
fit: 'inside'
|
||||
})
|
||||
.jpeg({ quality: 80 })
|
||||
.toFile(thumbnailPath);
|
||||
|
||||
return path.relative(path.join(__dirname, '../../../storage'), thumbnailPath);
|
||||
}
|
||||
|
||||
module.exports = { generateThumbnail };
|
||||
EOF
|
||||
|
||||
# Create logger utility
|
||||
cat > backend/src/utils/logger.js << 'EOF'
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.File({
|
||||
filename: path.join(__dirname, '../../../logs/error.log'),
|
||||
level: 'error'
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: path.join(__dirname, '../../../logs/combined.log')
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||
EOF
|
||||
|
||||
echo "Backend services created."
|
||||
|
||||
# Create migration init script
|
||||
cat > backend/migrations/init.js << 'EOF'
|
||||
const bcrypt = require('bcrypt');
|
||||
const { db, initializeDatabase } = require('../src/database/db');
|
||||
|
||||
async function runMigrations() {
|
||||
console.log('Running database migrations...');
|
||||
|
||||
try {
|
||||
// Initialize tables
|
||||
await initializeDatabase();
|
||||
|
||||
// Create default admin user if none exists
|
||||
const adminExists = await db('admin_users').first();
|
||||
if (!adminExists) {
|
||||
const defaultPassword = 'admin123'; // Change this!
|
||||
const passwordHash = await bcrypt.hash(defaultPassword, 10);
|
||||
|
||||
await db('admin_users').insert({
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
password_hash: passwordHash
|
||||
});
|
||||
|
||||
console.log('Default admin user created:');
|
||||
console.log('Username: admin');
|
||||
console.log('Password: admin123');
|
||||
console.log('⚠️ Please change this password immediately!');
|
||||
}
|
||||
|
||||
console.log('Migrations completed successfully');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runMigrations();
|
||||
EOF
|
||||
|
||||
echo "Migration script created."
|
||||
|
||||
# Create README
|
||||
cat > README.md << 'EOF'
|
||||
# Photo Sharing Platform
|
||||
|
||||
A secure, self-hosted photo sharing platform designed for weddings and events. Features automatic expiration, email notifications, and simple file-based management.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔒 Password Protected Galleries
|
||||
- ⏰ Automatic Expiration
|
||||
- 📧 Email Notifications
|
||||
- 📁 Simple File Management
|
||||
- 📊 Analytics Integration
|
||||
- 🎨 Customizable Themes
|
||||
- 📱 Mobile Responsive
|
||||
- ⚡ Docker Ready
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Clone the repository
|
||||
2. Run `./scripts/install.sh`
|
||||
3. Configure `.env` file
|
||||
4. Setup SSL: `./scripts/setup-ssl.sh`
|
||||
5. Start: `docker-compose -f docker-compose.prod.yml up -d`
|
||||
|
||||
Default credentials: admin / admin123 (change immediately!)
|
||||
|
||||
## Documentation
|
||||
|
||||
See DEPLOYMENT.md for detailed deployment instructions.
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
EOF
|
||||
|
||||
echo "README created."
|
||||
|
||||
# Create main installation script
|
||||
cat > scripts/install.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Photo Sharing Platform - Docker Installation"
|
||||
echo "==========================================="
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
echo "Checking prerequisites..."
|
||||
|
||||
# Install Docker if not present
|
||||
if ! command_exists docker; then
|
||||
echo "Installing Docker..."
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
rm get-docker.sh
|
||||
fi
|
||||
|
||||
# Install Docker Compose if not present
|
||||
if ! command_exists docker-compose; then
|
||||
echo "Installing Docker Compose..."
|
||||
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
fi
|
||||
|
||||
# Create necessary directories
|
||||
echo "Creating directory structure..."
|
||||
mkdir -p storage/events/{active,archived}
|
||||
mkdir -p storage/thumbnails
|
||||
mkdir -p data
|
||||
mkdir -p logs
|
||||
mkdir -p nginx/sites-enabled
|
||||
mkdir -p certbot/{conf,www}
|
||||
|
||||
# Set permissions
|
||||
chmod -R 755 storage
|
||||
chmod -R 755 data
|
||||
chmod -R 755 logs
|
||||
|
||||
# Copy environment file
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
echo "Created .env file. Please edit it with your configuration."
|
||||
fi
|
||||
|
||||
# Generate secure passwords
|
||||
echo "Generating secure passwords..."
|
||||
JWT_SECRET=$(openssl rand -base64 32)
|
||||
DB_PASSWORD=$(openssl rand -base64 32)
|
||||
UMAMI_HASH_SALT=$(openssl rand -base64 32)
|
||||
|
||||
# Update .env file with generated values
|
||||
sed -i "s/JWT_SECRET=.*/JWT_SECRET=$JWT_SECRET/" .env
|
||||
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=$DB_PASSWORD/" .env
|
||||
sed -i "s/UMAMI_HASH_SALT=.*/UMAMI_HASH_SALT=$UMAMI_HASH_SALT/" .env
|
||||
|
||||
echo ""
|
||||
echo "Installation complete!"
|
||||
echo "Next steps:"
|
||||
echo "1. Edit .env file with your domain names and SMTP settings"
|
||||
echo "2. Run: ./scripts/setup-ssl.sh to configure SSL certificates"
|
||||
echo "3. Run: docker-compose -f docker-compose.prod.yml up -d"
|
||||
echo "4. Run: docker-compose -f docker-compose.prod.yml exec backend npm run migrate"
|
||||
EOF
|
||||
|
||||
chmod +x scripts/install.sh
|
||||
|
||||
echo "Installation script created."
|
||||
|
||||
# Create nginx config
|
||||
cat > nginx/nginx.conf << 'EOF'
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
|
||||
# Rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
|
||||
|
||||
include /etc/nginx/sites-enabled/*.conf;
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Nginx config created."
|
||||
|
||||
# Create frontend package.json
|
||||
cat > frontend/package.json << 'EOF'
|
||||
{
|
||||
"name": "photo-sharing-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.0",
|
||||
"axios": "^1.3.2",
|
||||
"react-query": "^3.39.3",
|
||||
"date-fns": "^2.29.3",
|
||||
"react-toastify": "^9.1.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-image-gallery": "^1.2.11",
|
||||
"react-countdown": "^2.3.5",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"postcss": "^8.4.21",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"proxy": "http://localhost:3000"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Frontend package.json created."
|
||||
|
||||
echo ""
|
||||
echo "Setup script complete!"
|
||||
echo "Most important files have been created."
|
||||
echo ""
|
||||
echo "To complete the setup:"
|
||||
echo "1. Run this script: chmod +x setup-remaining-files.sh && ./setup-remaining-files.sh"
|
||||
echo "2. Review and update the created files as needed"
|
||||
echo "3. Install dependencies: cd backend && npm install && cd ../frontend && npm install"
|
||||
echo "4. Follow the deployment instructions in the README"
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}🚀 Photo Sharing Platform - Local Development Setup${NC}"
|
||||
echo "=================================================="
|
||||
|
||||
# Check if Docker is installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${RED}❌ Docker is not installed. Please install Docker Desktop first.${NC}"
|
||||
echo " Visit: https://www.docker.com/products/docker-desktop"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info &> /dev/null; then
|
||||
echo -e "${RED}❌ Docker is not running. Please start Docker Desktop.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create necessary directories
|
||||
echo -e "${YELLOW}📁 Creating directories...${NC}"
|
||||
mkdir -p storage/events/{active,archived}
|
||||
mkdir -p storage/thumbnails
|
||||
mkdir -p data
|
||||
mkdir -p logs
|
||||
mkdir -p backend/node_modules
|
||||
mkdir -p frontend/node_modules
|
||||
|
||||
# Copy local environment file if it doesn't exist
|
||||
if [ ! -f .env ]; then
|
||||
echo -e "${YELLOW}📋 Setting up environment...${NC}"
|
||||
cp .env.local .env
|
||||
fi
|
||||
|
||||
# Stop any existing containers
|
||||
echo -e "${YELLOW}🛑 Stopping existing containers...${NC}"
|
||||
docker-compose -f docker-compose.local.yml down 2>/dev/null || true
|
||||
|
||||
# Build images
|
||||
echo -e "${YELLOW}🔨 Building Docker images...${NC}"
|
||||
docker-compose -f docker-compose.local.yml build
|
||||
|
||||
# Start services
|
||||
echo -e "${YELLOW}🚀 Starting services...${NC}"
|
||||
docker-compose -f docker-compose.local.yml up -d
|
||||
|
||||
# Wait for backend to be ready
|
||||
echo -e "${YELLOW}⏳ Waiting for backend to start...${NC}"
|
||||
max_attempts=30
|
||||
attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if curl -s http://localhost:3001/api/health > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ Backend is ready!${NC}"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
if [ $attempt -gt $max_attempts ]; then
|
||||
echo -e "${RED}❌ Backend failed to start. Check logs with: docker-compose -f docker-compose.local.yml logs backend${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build frontend for production-like testing
|
||||
echo -e "${YELLOW}📦 Building frontend...${NC}"
|
||||
docker-compose -f docker-compose.local.yml exec frontend-dev npm run build
|
||||
|
||||
# Show status
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Local development environment is ready!${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}🌐 Access Points:${NC}"
|
||||
echo " Frontend (Production Build): http://localhost:3000"
|
||||
echo " Frontend (Dev with Hot Reload): http://localhost:3002"
|
||||
echo " Backend API: http://localhost:3001/api"
|
||||
echo " Mailhog (Email Testing): http://localhost:8025"
|
||||
echo ""
|
||||
echo -e "${GREEN}🔑 Default Admin Credentials:${NC}"
|
||||
echo " Username: admin"
|
||||
echo " Password: admin123"
|
||||
echo ""
|
||||
echo -e "${GREEN}📝 Useful Commands:${NC}"
|
||||
echo " View logs: docker-compose -f docker-compose.local.yml logs -f"
|
||||
echo " Stop all: ./stop-local.sh"
|
||||
echo " Backend shell: docker-compose -f docker-compose.local.yml exec backend sh"
|
||||
echo " Reset database: docker-compose -f docker-compose.local.yml exec backend npm run migrate"
|
||||
echo ""
|
||||
echo -e "${GREEN}💡 Tips:${NC}"
|
||||
echo " - Frontend dev server (port 3002) has hot reload enabled"
|
||||
echo " - All emails are caught by Mailhog - check http://localhost:8025"
|
||||
echo " - SQLite database is stored in ./data/photo_sharing.db"
|
||||
echo " - Upload photos to ./storage/events/active/{event-name}/"
|
||||
echo ""
|
||||
|
||||
# Open browser
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open http://localhost:3002
|
||||
elif command -v open &> /dev/null; then
|
||||
open http://localhost:3002
|
||||
fi
|
||||
|
||||
# Show logs
|
||||
echo -e "${YELLOW}📋 Showing logs (Ctrl+C to exit)...${NC}"
|
||||
docker-compose -f docker-compose.local.yml logs -f
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${YELLOW}🛑 Stopping Photo Sharing Platform...${NC}"
|
||||
|
||||
# Stop all containers
|
||||
docker-compose -f docker-compose.local.yml down
|
||||
|
||||
# Optional: Remove volumes (uncomment if you want to reset data)
|
||||
# docker-compose -f docker-compose.local.yml down -v
|
||||
|
||||
echo -e "${GREEN}✅ All services stopped${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}💡 Tips:${NC}"
|
||||
echo " - Your data is preserved in ./data and ./storage"
|
||||
echo " - To completely reset, run: docker-compose -f docker-compose.local.yml down -v"
|
||||
echo " - To restart, run: ./start-local.sh"
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}🔄 Updating Photo Sharing Platform - Local Development${NC}"
|
||||
echo "===================================================="
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info &> /dev/null; then
|
||||
echo -e "${RED}❌ Docker is not running. Please start Docker Desktop.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop all containers
|
||||
echo -e "${YELLOW}🛑 Stopping all containers...${NC}"
|
||||
docker-compose -f docker-compose.local.yml down
|
||||
|
||||
# Remove old images to force rebuild
|
||||
echo -e "${YELLOW}🗑️ Removing old images...${NC}"
|
||||
docker-compose -f docker-compose.local.yml rm -f
|
||||
|
||||
# Pull latest base images
|
||||
echo -e "${YELLOW}📥 Pulling latest base images...${NC}"
|
||||
docker-compose -f docker-compose.local.yml pull
|
||||
|
||||
# Build frontend production files
|
||||
echo -e "${YELLOW}📦 Building frontend production files...${NC}"
|
||||
cd frontend
|
||||
npm install --legacy-peer-deps
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# Rebuild all images with no cache
|
||||
echo -e "${YELLOW}🔨 Rebuilding Docker images (no cache)...${NC}"
|
||||
docker-compose -f docker-compose.local.yml build --no-cache
|
||||
|
||||
# Start all services
|
||||
echo -e "${YELLOW}🚀 Starting services...${NC}"
|
||||
docker-compose -f docker-compose.local.yml up -d
|
||||
|
||||
# Wait for backend to be ready
|
||||
echo -e "${YELLOW}⏳ Waiting for backend to start...${NC}"
|
||||
max_attempts=30
|
||||
attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if curl -s http://localhost:3001/api/health > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ Backend is ready!${NC}"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
if [ $attempt -gt $max_attempts ]; then
|
||||
echo -e "${RED}❌ Backend failed to start. Check logs with: docker-compose -f docker-compose.local.yml logs backend${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wait a bit more for frontend to be ready
|
||||
echo -e "${YELLOW}⏳ Waiting for frontend to be ready...${NC}"
|
||||
sleep 5
|
||||
|
||||
# Show status
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Local development environment has been updated!${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}🌐 Access Points:${NC}"
|
||||
echo " Frontend (Nginx): http://localhost:3005"
|
||||
echo " Frontend (Dev): http://localhost:3002"
|
||||
echo " Backend API: http://localhost:3001"
|
||||
echo " Mailhog: http://localhost:8025"
|
||||
echo ""
|
||||
echo -e "${GREEN}📝 Container Status:${NC}"
|
||||
docker-compose -f docker-compose.local.yml ps
|
||||
echo ""
|
||||
echo -e "${GREEN}💡 Tips:${NC}"
|
||||
echo " - View logs: docker-compose -f docker-compose.local.yml logs -f"
|
||||
echo " - View specific service logs: docker-compose -f docker-compose.local.yml logs -f [service-name]"
|
||||
echo " - Stop all: ./stop-local.sh"
|
||||
echo ""
|
||||
|
||||
# Open browser
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open http://localhost:3005
|
||||
elif command -v open &> /dev/null; then
|
||||
open http://localhost:3005
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✨ Update complete! The browser should open automatically.${NC}"
|
||||
Reference in New Issue
Block a user