Add complete frontend implementation and Docker deployment setup
- Implement React frontend with TypeScript and Tailwind CSS - Add scrappbook.de-inspired UI design with photo galleries - Implement authentication, photo viewing, and download features - Add Docker Swarm configuration with Traefik reverse proxy - Set up Drone CI/CD pipeline for automated deployments - Add monitoring stack with Prometheus and Grafana - Create comprehensive deployment documentation - Add simple local development setup with docker-compose.local.yml Features: - Password-protected galleries with expiration warnings - Responsive photo grid with lightbox viewer - Bulk download functionality - Hot reload development environment - Email testing with Mailhog - Production-ready deployment scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/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}"
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/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"
|
||||
Executable
+196
@@ -0,0 +1,196 @@
|
||||
#!/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}Photo Sharing Platform Deployment Script${NC}"
|
||||
echo "========================================"
|
||||
|
||||
# Default values
|
||||
STACK_NAME="photo-sharing"
|
||||
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: photo-sharing)"
|
||||
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
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user