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

- 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:
2025-07-14 20:22:52 +02:00
parent 0a21856a8d
commit 85e7fbe73f
17 changed files with 1 additions and 2907 deletions
+1
View File
@@ -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
View File
@@ -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! 🎨
-862
View File
@@ -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!)"
-265
View File
@@ -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
-65
View File
@@ -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']
-134
View File
@@ -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}"
-111
View File
@@ -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"
-196
View File
@@ -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
-70
View File
@@ -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"
-124
View File
@@ -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
-93
View File
@@ -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
-61
View File
@@ -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
-380
View File
@@ -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
View File
@@ -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
-22
View File
@@ -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"
-94
View File
@@ -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}"