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:
2025-07-06 20:23:13 +02:00
parent 032bbae50d
commit 6c82958c79
73 changed files with 10611 additions and 2 deletions
+265
View File
@@ -0,0 +1,265 @@
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
@@ -0,0 +1,189 @@
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
@@ -0,0 +1,65 @@
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
@@ -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}"
+111
View File
@@ -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"
+196
View File
@@ -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
+70
View File
@@ -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"
+124
View File
@@ -0,0 +1,124 @@
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
@@ -0,0 +1,93 @@
# 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