diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5c2a202 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,81 @@ +# Dependencies +node_modules/ +.npm +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Development files +.env.local +.env.development.local +.env.test.local +*.local + +# Build outputs (will be created during Docker build) +dist/ +build/ +.astro/ + +# Version control +.git/ +.gitignore + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs (will be mounted as volume) +logs/ +*.log + +# Test files +coverage/ +*.lcov +.nyc_output/ + +# Cache +.eslintcache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Documentation +docs/ +README.md +*.md +!CLAUDE.md + +# Docker files (prevent recursion) +Dockerfile* +docker-compose*.yml +.dockerignore + +# Development scripts +scripts/ +test-*.js +test-*.mjs + +# Backup files +*.backup +*_backup.* + +# Style guide and design files +styleGuide/ +*.pdf +*.jpg +*.png +cookies.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..797d161 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# Multi-stage build for Black Canyon Tickets +# Stage 1: Build stage +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install all dependencies (including dev dependencies for build) +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Stage 2: Production stage +FROM node:20-alpine AS production + +# Install security updates +RUN apk update && apk upgrade && apk add --no-cache dumb-init + +# Create app user for security +RUN addgroup -g 1001 -S nodejs +RUN adduser -S astro -u 1001 + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install only production dependencies +RUN npm ci --only=production && npm cache clean --force + +# Copy built application from builder stage +COPY --from=builder --chown=astro:nodejs /app/dist ./dist + +# Copy additional necessary files +COPY --chown=astro:nodejs setup-schema.js ./ +COPY --chown=astro:nodejs setup-super-admins.js ./ + +# Create logs directory +RUN mkdir -p logs && chown astro:nodejs logs + +# Switch to non-root user +USER astro + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "const http=require('http');const options={hostname:'localhost',port:3000,path:'/api/health',timeout:2000};const req=http.request(options,(res)=>{process.exit(res.statusCode===200?0:1)});req.on('error',()=>{process.exit(1)});req.end();" || exit 1 + +# Start the application with dumb-init for proper signal handling +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "./dist/server/entry.mjs"] \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..a7558eb --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,58 @@ +version: '3.8' + +services: + bct-app: + image: bct-whitelabel:latest + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - HOST=0.0.0.0 + - PORT=3000 + # Supabase + - PUBLIC_SUPABASE_URL=${PUBLIC_SUPABASE_URL} + - PUBLIC_SUPABASE_ANON_KEY=${PUBLIC_SUPABASE_ANON_KEY} + - SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY} + # Stripe + - STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY} + - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} + # Email + - RESEND_API_KEY=${RESEND_API_KEY} + # Monitoring + - SENTRY_DSN=${SENTRY_DSN} + - SENTRY_RELEASE=${SENTRY_RELEASE:-latest} + env_file: + - .env + volumes: + - ./logs:/app/logs + - /etc/localtime:/etc/localtime:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "const http=require('http');const options={hostname:'localhost',port:3000,path:'/api/health',timeout:2000};const req=http.request(options,(res)=>{process.exit(res.statusCode===200?0:1)});req.on('error',()=>{process.exit(1)});req.end();"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + networks: + - bct-network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 1G + cpus: '1.0' + reservations: + memory: 512M + cpus: '0.5' + +networks: + bct-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ef6f333 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +version: '3.8' + +services: + bct-app: + build: + context: . + dockerfile: Dockerfile + target: production + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - HOST=0.0.0.0 + - PORT=3000 + # Supabase + - PUBLIC_SUPABASE_URL=${PUBLIC_SUPABASE_URL} + - PUBLIC_SUPABASE_ANON_KEY=${PUBLIC_SUPABASE_ANON_KEY} + - SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY} + # Stripe + - STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY} + - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} + # Email + - RESEND_API_KEY=${RESEND_API_KEY} + # Monitoring + - SENTRY_DSN=${SENTRY_DSN} + - SENTRY_RELEASE=${SENTRY_RELEASE:-unknown} + volumes: + - ./logs:/app/logs + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "const http=require('http');const options={hostname:'localhost',port:3000,path:'/api/health',timeout:2000};const req=http.request(options,(res)=>{process.exit(res.statusCode===200?0:1)});req.on('error',()=>{process.exit(1)});req.end();"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - bct-network + +networks: + bct-network: + driver: bridge \ No newline at end of file diff --git a/package.json b/package.json index 3d012d5..3da448e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,15 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "mcp:stripe": "npx @stripe/mcp --tools=all --api-key=$STRIPE_SECRET_KEY", - "mcp:stripe:debug": "npx @modelcontextprotocol/inspector npx @stripe/mcp --tools=all --api-key=$STRIPE_SECRET_KEY" + "mcp:stripe:debug": "npx @modelcontextprotocol/inspector npx @stripe/mcp --tools=all --api-key=$STRIPE_SECRET_KEY", + "docker:build": "./scripts/docker-build.sh", + "docker:build:version": "./scripts/docker-build.sh", + "docker:deploy": "./scripts/docker-deploy.sh", + "docker:up": "docker-compose up -d", + "docker:down": "docker-compose down", + "docker:logs": "docker-compose logs -f", + "docker:prod:up": "docker-compose -f docker-compose.prod.yml up -d", + "docker:prod:down": "docker-compose -f docker-compose.prod.yml down" }, "dependencies": { "@astrojs/check": "^0.9.4", diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh new file mode 100755 index 0000000..3cf9c16 --- /dev/null +++ b/scripts/docker-build.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Docker build script for Black Canyon Tickets +# Usage: ./scripts/docker-build.sh [version] + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +IMAGE_NAME="bct-whitelabel" +REGISTRY="${DOCKER_REGISTRY:-}" # Optional registry prefix +VERSION="${1:-latest}" +PLATFORM="${PLATFORM:-linux/amd64}" + +echo -e "${GREEN}๐Ÿณ Building Docker image for Black Canyon Tickets${NC}" +echo -e "${YELLOW}Image: ${IMAGE_NAME}:${VERSION}${NC}" +echo -e "${YELLOW}Platform: ${PLATFORM}${NC}" + +# Build the image +echo -e "${GREEN}๐Ÿ“ฆ Building Docker image...${NC}" +if [ -n "$REGISTRY" ]; then + FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}" +else + FULL_IMAGE_NAME="${IMAGE_NAME}" +fi + +docker build \ + --platform "$PLATFORM" \ + --tag "${FULL_IMAGE_NAME}:${VERSION}" \ + --tag "${FULL_IMAGE_NAME}:latest" \ + --build-arg BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + --build-arg VCS_REF="$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')" \ + --build-arg VERSION="$VERSION" \ + . + +echo -e "${GREEN}โœ… Docker image built successfully!${NC}" +echo -e "${YELLOW}Image tags:${NC}" +echo -e " - ${FULL_IMAGE_NAME}:${VERSION}" +echo -e " - ${FULL_IMAGE_NAME}:latest" + +# Optional: Push to registry +if [ "$2" = "--push" ] && [ -n "$REGISTRY" ]; then + echo -e "${GREEN}๐Ÿš€ Pushing to registry...${NC}" + docker push "${FULL_IMAGE_NAME}:${VERSION}" + docker push "${FULL_IMAGE_NAME}:latest" + echo -e "${GREEN}โœ… Images pushed to registry!${NC}" +fi + +# Show image size +echo -e "${GREEN}๐Ÿ“Š Image information:${NC}" +docker images "${FULL_IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + +echo -e "${GREEN}๐ŸŽ‰ Build complete!${NC}" +echo -e "${YELLOW}To run locally: docker run -p 3000:3000 ${FULL_IMAGE_NAME}:${VERSION}${NC}" +echo -e "${YELLOW}Or use: docker-compose up${NC}" \ No newline at end of file diff --git a/scripts/docker-deploy.sh b/scripts/docker-deploy.sh new file mode 100755 index 0000000..a8d5234 --- /dev/null +++ b/scripts/docker-deploy.sh @@ -0,0 +1,178 @@ +#!/bin/bash + +# Docker deployment script for Black Canyon Tickets +# Usage: ./scripts/docker-deploy.sh [environment] + +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 + +# Configuration +ENVIRONMENT="${1:-production}" +IMAGE_NAME="bct-whitelabel" +VERSION="${2:-latest}" +COMPOSE_FILE="docker-compose.prod.yml" + +echo -e "${GREEN}๐Ÿš€ Deploying Black Canyon Tickets${NC}" +echo -e "${YELLOW}Environment: ${ENVIRONMENT}${NC}" +echo -e "${YELLOW}Version: ${VERSION}${NC}" + +# Function to check if required environment variables are set +check_env_vars() { + local required_vars=( + "PUBLIC_SUPABASE_URL" + "PUBLIC_SUPABASE_ANON_KEY" + "SUPABASE_SERVICE_ROLE_KEY" + "STRIPE_PUBLISHABLE_KEY" + "STRIPE_SECRET_KEY" + "STRIPE_WEBHOOK_SECRET" + "RESEND_API_KEY" + ) + + echo -e "${BLUE}๐Ÿ” Checking environment variables...${NC}" + local missing_vars=() + + for var in "${required_vars[@]}"; do + if [ -z "${!var}" ]; then + missing_vars+=("$var") + fi + done + + if [ ${#missing_vars[@]} -ne 0 ]; then + echo -e "${RED}โŒ Missing required environment variables:${NC}" + for var in "${missing_vars[@]}"; do + echo -e " - $var" + done + echo -e "${YELLOW}๐Ÿ’ก Please set these variables in your .env file or environment${NC}" + exit 1 + fi + + echo -e "${GREEN}โœ… All required environment variables are set${NC}" +} + +# Function to check if .env file exists +check_env_file() { + if [ ! -f ".env" ]; then + echo -e "${YELLOW}โš ๏ธ No .env file found. Creating from environment variables...${NC}" + # You might want to create a template .env file here + echo -e "${YELLOW}๐Ÿ’ก Please ensure all required environment variables are available${NC}" + else + echo -e "${GREEN}โœ… .env file found${NC}" + fi +} + +# Function to pull latest image +pull_image() { + echo -e "${BLUE}๐Ÿ“ฅ Pulling latest image...${NC}" + if ! docker pull "${IMAGE_NAME}:${VERSION}" 2>/dev/null; then + echo -e "${YELLOW}โš ๏ธ Could not pull image from registry. Using local image.${NC}" + fi +} + +# Function to stop existing containers +stop_containers() { + echo -e "${BLUE}๐Ÿ›‘ Stopping existing containers...${NC}" + docker-compose -f "$COMPOSE_FILE" down --remove-orphans || true +} + +# Function to start containers +start_containers() { + echo -e "${BLUE}๐Ÿ”„ Starting containers...${NC}" + + # Set the image version + export DOCKER_IMAGE_TAG="$VERSION" + + # Start services + docker-compose -f "$COMPOSE_FILE" up -d + + echo -e "${GREEN}โœ… Containers started!${NC}" +} + +# Function to wait for health check +wait_for_health() { + echo -e "${BLUE}๐Ÿฅ Waiting for application to be healthy...${NC}" + local max_attempts=30 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + if docker-compose -f "$COMPOSE_FILE" ps --services --filter "status=running" | grep -q "bct-app"; then + if docker-compose -f "$COMPOSE_FILE" exec -T bct-app node -e "const http=require('http');const options={hostname:'localhost',port:4321,path:'/api/health',timeout:2000};const req=http.request(options,(res)=>{process.exit(res.statusCode===200?0:1)});req.on('error',()=>{process.exit(1)});req.end();" 2>/dev/null; then + echo -e "${GREEN}โœ… Application is healthy!${NC}" + return 0 + fi + fi + + echo -e "${YELLOW}โณ Waiting... (attempt $attempt/$max_attempts)${NC}" + sleep 10 + ((attempt++)) + done + + echo -e "${RED}โŒ Application failed to become healthy${NC}" + return 1 +} + +# Function to show deployment status +show_status() { + echo -e "${GREEN}๐Ÿ“Š Deployment Status:${NC}" + docker-compose -f "$COMPOSE_FILE" ps + + echo -e "\n${GREEN}๐Ÿ“‹ Container Logs (last 20 lines):${NC}" + docker-compose -f "$COMPOSE_FILE" logs --tail=20 bct-app + + echo -e "\n${GREEN}๐ŸŒ Application should be available at:${NC}" + echo -e "${BLUE} http://localhost:3000${NC}" +} + +# Function to cleanup old images +cleanup() { + echo -e "${BLUE}๐Ÿงน Cleaning up old images...${NC}" + docker image prune -f || true + echo -e "${GREEN}โœ… Cleanup complete${NC}" +} + +# Main deployment process +main() { + echo -e "${GREEN}Starting deployment process...${NC}" + + # Pre-deployment checks + check_env_file + check_env_vars + + # Deployment steps + pull_image + stop_containers + start_containers + + # Post-deployment verification + if wait_for_health; then + show_status + cleanup + echo -e "${GREEN}๐ŸŽ‰ Deployment completed successfully!${NC}" + else + echo -e "${RED}๐Ÿ’ฅ Deployment failed!${NC}" + echo -e "${YELLOW}๐Ÿ“‹ Check logs with: docker-compose -f $COMPOSE_FILE logs${NC}" + exit 1 + fi +} + +# Handle script arguments +case "${1:-}" in + --help|-h) + echo "Usage: $0 [environment] [version]" + echo " environment: production (default)" + echo " version: Docker image tag (default: latest)" + echo "" + echo "Examples:" + echo " $0 # Deploy latest to production" + echo " $0 production v1.2.3 # Deploy specific version" + exit 0 + ;; + *) + main + ;; +esac \ No newline at end of file diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts new file mode 100644 index 0000000..2c0c4d7 --- /dev/null +++ b/src/pages/api/health.ts @@ -0,0 +1,34 @@ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = async () => { + try { + // Basic health check - could be extended to check database connectivity + const healthStatus = { + status: 'healthy', + timestamp: new Date().toISOString(), + version: process.env.npm_package_version || '1.0.0', + environment: process.env.NODE_ENV || 'development', + uptime: process.uptime() + }; + + return new Response(JSON.stringify(healthStatus), { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' + } + }); + } catch (error) { + return new Response(JSON.stringify({ + status: 'unhealthy', + timestamp: new Date().toISOString(), + error: error instanceof Error ? error.message : 'Unknown error' + }), { + status: 503, + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' + } + }); + } +}; \ No newline at end of file