From fa2956e8b64df7d1f3c1ce98c2e79d1a763229a9 Mon Sep 17 00:00:00 2001 From: Frank John Begornia Date: Mon, 22 Dec 2025 23:23:10 +0800 Subject: [PATCH] Refactor deployment workflows and add SSL setup documentation for production --- .gitea/workflows/deploy-dev.yml | 6 +- .gitea/workflows/deploy.yml | 196 +++++++++++++++++++++++--------- DIGICERT_SSL_SETUP.md | 125 ++++++++++++++++++++ docker-compose.dev.yml | 58 ++++++++++ docker-compose.prod.yml | 57 ++++++++++ docker-compose.yml | 23 +++- traefik-certs.yml | 17 +++ 7 files changed, 421 insertions(+), 61 deletions(-) create mode 100644 DIGICERT_SSL_SETUP.md create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.prod.yml create mode 100644 traefik-certs.yml diff --git a/.gitea/workflows/deploy-dev.yml b/.gitea/workflows/deploy-dev.yml index 499c4ba..b2615bb 100644 --- a/.gitea/workflows/deploy-dev.yml +++ b/.gitea/workflows/deploy-dev.yml @@ -51,7 +51,7 @@ jobs: run: | scp -i ~/.ssh/id_ed25519 \ /workspace/repo/merchbay_dev.tar.gz \ - /workspace/repo/docker-compose.yml \ + /workspace/repo/docker-compose.dev.yml \ ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/ # 5๏ธโƒฃ Deploy on server @@ -75,7 +75,7 @@ jobs: docker images | grep merchbay | grep -v "$(docker images merchbay:dev -q)" | awk '{print $3}' | xargs -r docker rmi -f || true echo "๐Ÿ“„ Updating compose file" - cp /tmp/docker-compose.yml "$DEPLOY_DIR/" + cp /tmp/docker-compose.dev.yml "$DEPLOY_DIR/docker-compose.yml" cd "$DEPLOY_DIR" @@ -115,7 +115,7 @@ jobs: fi echo "๐Ÿงน Cleanup" - rm -f /tmp/merchbay_dev.tar.gz /tmp/docker-compose.yml + rm -f /tmp/merchbay_dev.tar.gz /tmp/docker-compose.dev.yml echo "๐Ÿงน Aggressive Docker cleanup to reclaim space" docker image prune -af --filter "until=24h" || true diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index aa31187..24f6e7f 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Deploy Production +name: Deploy Production (merchbay.com) on: push: @@ -12,84 +12,170 @@ jobs: runs-on: ubuntu-latest container: image: catthehacker/ubuntu:act-latest - + steps: + # 1๏ธโƒฃ Checkout code - name: Checkout code shell: sh run: | - git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git /workspace/repo || true + git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git /workspace/repo cd /workspace/repo - git fetch origin $GITHUB_REF_NAME git checkout $GITHUB_REF_NAME - git pull origin $GITHUB_REF_NAME - - name: Build Docker Image + # 2๏ธโƒฃ Build image + - name: Build Docker image shell: sh run: | cd /workspace/repo docker build -t merchbay:latest . docker save merchbay:latest | gzip > merchbay.tar.gz - - name: Setup SSH and Deploy + # 3๏ธโƒฃ Setup SSH + - name: Setup SSH shell: sh + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} run: | mkdir -p ~/.ssh chmod 700 ~/.ssh - echo "$PROD_DEPLOY_SSH_KEY" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - ssh-keygen -y -f ~/.ssh/deploy_key > /dev/null 2>&1 || { echo "Error: Invalid SSH key format"; exit 1; } - - cd /workspace/repo - scp -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key merchbay.tar.gz docker-compose.yml "$PROD_DEPLOY_USER@$PROD_DEPLOY_HOST:/tmp/" - - ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key "$PROD_DEPLOY_USER@$PROD_DEPLOY_HOST" " - DEPLOY_DIR='/var/www/merchbay' - mkdir -p \$DEPLOY_DIR - cd /tmp - docker load < merchbay.tar.gz - - echo 'Removing old merchbay images' - docker images | grep merchbay | grep -v "\$(docker images merchbay:latest -q)" | awk '{print \$3}' | xargs -r docker rmi -f || true - - cp docker-compose.yml \$DEPLOY_DIR/ - cd \$DEPLOY_DIR - - # .env file should already exist on server with all required variables - # Required: DB_*, PROD_PRIVATE, IMAGES_URL, UPLOAD_URL - # Required: MAIL_*, CAPTCHA_*, ANALYTICS_* - # If it doesn't exist, deployment will fail (this is intentional for security) - - docker compose down || true - docker image prune -f - docker network inspect traefik-public >/dev/null 2>&1 || docker network create traefik-public - export DOMAIN=merchbay.app - export APP_URL=https://merchbay.app + echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts + + # 4๏ธโƒฃ Upload artifacts + - name: Upload image and compose + shell: sh + env: + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + run: | + scp -i ~/.ssh/id_ed25519 \ + /workspace/repo/merchbay.tar.gz \ + /workspace/repo/docker-compose.prod.yml \ + ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/ + + # 5๏ธโƒฃ Deploy on server + - name: Deploy on server + shell: sh + env: + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + + run: | + ssh -i ~/.ssh/id_ed25519 $DEPLOY_USER@$DEPLOY_HOST << 'EOF' + set -e + + DEPLOY_DIR="/var/www/merchbay" + sudo mkdir -p "$DEPLOY_DIR" + sudo chown $USER:$USER "$DEPLOY_DIR" + + echo "๏ฟฝ Stopping dev environment" + DEV_DIR="/var/www/apps/merchbay_dev" + if [ -d "$DEV_DIR" ]; then + cd "$DEV_DIR" + docker compose down || true + cd - + echo "โœ… Dev environment stopped" + echo "๐Ÿ“‹ Copying .env from dev to production" + if [ -f "$DEV_DIR/.env" ]; then + cp "$DEV_DIR/.env" "$DEPLOY_DIR/.env" + echo "โœ… .env file copied from dev to production" + else + echo "โš ๏ธ No .env file found in dev directory" + fi else + echo "โ„น๏ธ No dev environment found" + fi + + echo "๐Ÿ“ฆ Loading image" + docker load < /tmp/merchbay.tar.gz + + echo "๐Ÿงน Removing dev images and old merchbay images" + docker images | grep "merchbay:dev" | awk '{print $3}' | xargs -r docker rmi -f || true + docker images | grep merchbay | grep -v "$(docker images merchbay:latest -q)" | awk '{print $3}' | xargs -r docker rmi -f || true + + echo "๐Ÿ“„ Updating compose file" + cp /tmp/docker-compose.prod.yml "$DEPLOY_DIR/docker-compose.yml" + + cd "$DEPLOY_DIR" + + echo "๐Ÿ” Checking .env file" + if [ ! -f .env ]; then + echo "โŒ .env file not found at $DEPLOY_DIR/.env" + echo "Please create it first with required variables:" + echo " - DB_*, PROD_PRIVATE, IMAGES_URL, UPLOAD_URL" + echo " - MAIL_*, CAPTCHA_*, ANALYTICS_*" + exit 1 + fi + + echo "๐Ÿ”ง Fixing .env permissions" + sudo chown $USER:$USER .env + sudo chmod 600 .env + + echo "๐ŸŒ Ensure networks" + docker network inspect traefik-public >/dev/null 2>&1 || \ + docker network create traefik-public + docker network inspect crew-app-net >/dev/null 2>&1 || \ + docker network create crew-app-net + + echo "๐Ÿš€ Starting containers (env vars from .env file)" docker compose up -d - sleep 10 - docker compose exec -T app php artisan migrate --force - docker compose exec -T app php artisan config:cache - docker compose exec -T app php artisan route:cache - docker compose exec -T app php artisan view:cache - rm -f /tmp/merchbay.tar.gz /tmp/docker-compose.yml + + echo "โณ Waiting for app container" + sleep 15 + + if docker ps --format '{{.Names}}' | grep -q merchbay_app; then + echo "๐Ÿงน Clearing and rebuilding config cache" + docker compose exec -T app php artisan config:clear + docker compose exec -T app php artisan config:cache + else + echo "โŒ App container not running" + docker compose logs + exit 1 + fi + + echo "๐Ÿงน Cleanup" + rm -f /tmp/merchbay.tar.gz /tmp/docker-compose.prod.yml - echo 'Aggressive Docker cleanup to reclaim space' + echo "๐Ÿงน Aggressive Docker cleanup to reclaim space" docker image prune -af --filter "until=24h" || true docker container prune -f || true docker volume prune -f || true docker builder prune -af --filter "until=48h" || true - echo 'Docker space usage:' + echo "๐Ÿ“Š Docker space usage:" docker system df - - echo 'Production deployment completed successfully!' - echo 'Application available at: https://merchbay.app' - " - env: - PROD_DEPLOY_SSH_KEY: ${{ secrets.PROD_DEPLOY_SSH_KEY }} - PROD_DEPLOY_USER: ${{ secrets.PROD_DEPLOY_USER }} - PROD_DEPLOY_HOST: ${{ secrets.PROD_DEPLOY_HOST }} - - name: Health Check + echo "โœ… Production deployment completed!" + echo "๐ŸŒ Application available at: https://merchbay.com" + EOF + + # 6๏ธโƒฃ Health check + - name: Health check shell: sh run: | - sleep 10 - curl -f https://merchbay.app || exit 1 + echo "โณ Waiting for app to be ready..." + sleep 20 + + echo "๐Ÿ” Testing health check..." + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 https://merchbay.com || echo "000") + + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "301" ]; then + echo "โœ… Health check passed! (HTTP $HTTP_CODE)" + echo "๐ŸŽ‰ Production deployment successful!" + else + echo "โŒ Health check failed! (HTTP $HTTP_CODE)" + echo "" + echo "๐Ÿ’ก Troubleshooting:" + echo " 1. Check if container is running:" + echo " docker ps | grep merchbay_app" + echo "" + echo " 2. Check app logs:" + echo " docker logs merchbay_app" + echo "" + echo " 3. Check Traefik logs:" + echo " docker logs traefik" + echo "" + echo " 4. Test manually:" + echo " curl -Ik https://merchbay.com" + exit 1 + fi diff --git a/DIGICERT_SSL_SETUP.md b/DIGICERT_SSL_SETUP.md new file mode 100644 index 0000000..9c76b6d --- /dev/null +++ b/DIGICERT_SSL_SETUP.md @@ -0,0 +1,125 @@ +# DigiCert SSL Certificate Setup for Production + +## Certificate Files Required + +From DigiCert, you'll receive these files: +- `merchbay_com.crt` - Your domain certificate +- `merchbay_com.key` - Private key (generated during CSR creation) +- `DigiCertCA.crt` - Intermediate certificate +- `TrustedRoot.crt` - Root certificate (optional) + +## Step 1: Combine Certificate Chain (on your local machine) + +```bash +# Create full chain certificate +cat merchbay_com.crt DigiCertCA.crt > merchbay.com.crt + +# Copy private key +cp merchbay_com.key merchbay.com.key +``` + +## Step 2: Upload to Production Server + +```bash +# SSH to production server +ssh PROD_DEPLOY_USER@PROD_DEPLOY_HOST + +# Create certificates directory +sudo mkdir -p /srv/certs +sudo chmod 700 /srv/certs + +# Exit SSH, then upload from local machine +scp merchbay.com.crt PROD_DEPLOY_USER@PROD_DEPLOY_HOST:/tmp/ +scp merchbay.com.key PROD_DEPLOY_USER@PROD_DEPLOY_HOST:/tmp/ + +# SSH back to server and move files +ssh PROD_DEPLOY_USER@PROD_DEPLOY_HOST +sudo mv /tmp/merchbay.com.crt /srv/certs/ +sudo mv /tmp/merchbay.com.key /srv/certs/ +sudo chmod 600 /srv/certs/* +sudo chown root:root /srv/certs/* +``` + +## Step 3: Upload Traefik Configuration + +```bash +# From local machine +scp traefik-certs.yml PROD_DEPLOY_USER@PROD_DEPLOY_HOST:/tmp/ + +# SSH to server +ssh PROD_DEPLOY_USER@PROD_DEPLOY_HOST +sudo mkdir -p /srv/traefik +sudo mv /tmp/traefik-certs.yml /srv/traefik/dynamic-certs.yml +sudo chmod 644 /srv/traefik/dynamic-certs.yml +``` + +## Step 4: Update Traefik Container + +Ensure your Traefik docker-compose.yml includes: + +```yaml +services: + traefik: + image: traefik:v2.10 + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --providers.file.filename=/dynamic-certs.yml + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /srv/certs:/srv/certs:ro + - /srv/traefik/dynamic-certs.yml:/dynamic-certs.yml:ro + networks: + - traefik-public + restart: unless-stopped +``` + +## Step 5: Restart Traefik + +```bash +cd /opt/traefik # or wherever your traefik docker-compose.yml is +docker compose restart traefik + +# Verify certificate is loaded +docker compose logs traefik | grep -i certificate +``` + +## Step 6: Deploy merchbay Application + +Once Traefik is configured, deploy merchbay: + +```bash +cd /var/www/merchbay +docker compose up -d +``` + +## Verification + +```bash +# Check certificate +openssl s_client -connect merchbay.com:443 -servername merchbay.com < /dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates + +# Should show: +# subject=CN = merchbay.com +# issuer=O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1 +# notBefore=... +# notAfter=... +``` + +## Certificate Renewal + +DigiCert certificates typically last 1-2 years. Set a reminder to renew 30 days before expiration and repeat Steps 1-3 and 5. + +## Security Notes + +- Never commit `.key` files to git +- Keep private keys secure (600 permissions) +- Use strong encryption for private key storage +- Consider using a certificate management system for automatic renewal diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..ca397e7 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,58 @@ +services: + app: + image: merchbay:dev + container_name: merchbay_app_dev + restart: unless-stopped + environment: + - APP_ENV=${APP_ENV:-development} + - APP_DEBUG=${APP_DEBUG:-true} + - APP_URL=${APP_URL:-https://dev.merchbay.app} + - DB_CONNECTION=mysql + - DB_HOST=${DB_HOST} + - DB_PORT=${DB_PORT:-3306} + - DB_DATABASE=${DB_DATABASE} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + - PROD_PRIVATE=${PROD_PRIVATE} + - IMAGES_URL=${IMAGES_URL} + - UPLOAD_URL=${UPLOAD_URL} + - FORCE_HTTPS=true + - MAIL_DRIVER=${MAIL_DRIVER} + - MAIL_HOST=${MAIL_HOST} + - MAIL_PORT=${MAIL_PORT} + - MAIL_USERNAME=${MAIL_USERNAME} + - MAIL_PASSWORD=${MAIL_PASSWORD} + - MAIL_ENCRYPTION=${MAIL_ENCRYPTION} + - CAPTCHA_SITE_KEY=${CAPTCHA_SITE_KEY} + - CAPTCHA_SECRET_KEY=${CAPTCHA_SECRET_KEY} + - ANALYTICS_SITE_ID=${ANALYTICS_SITE_ID} + - ANALYTICS_CLIENT_ID=${ANALYTICS_CLIENT_ID} + - ANALYTICS_SERVICE_EMAIL=${ANALYTICS_SERVICE_EMAIL} + volumes: + - ./storage:/var/www/html/storage + - ./public/uploads:/var/www/html/public/uploads + labels: + - "traefik.enable=true" + # Development environment (dev.merchbay.app) + - "traefik.http.routers.merchbay-dev.rule=Host(`dev.merchbay.app`)" + - "traefik.http.routers.merchbay-dev.entrypoints=websecure" + - "traefik.http.routers.merchbay-dev.tls=true" + - "traefik.http.routers.merchbay-dev.tls.certresolver=le" + - "traefik.http.services.merchbay-dev.loadbalancer.server.port=80" + # HTTP to HTTPS redirect + - "traefik.http.routers.merchbay-dev-http.rule=Host(`dev.merchbay.app`)" + - "traefik.http.routers.merchbay-dev-http.entrypoints=web" + - "traefik.http.routers.merchbay-dev-http.middlewares=https-redirect" + - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https" + networks: + - traefik-public + - crew-app-net + - default + +networks: + traefik-public: + external: true + crew-app-net: + external: true + default: + driver: bridge diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..3a0d47b --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,57 @@ +services: + app: + image: merchbay:latest + container_name: merchbay_app_prod + restart: unless-stopped + environment: + - APP_ENV=${APP_ENV:-production} + - APP_DEBUG=${APP_DEBUG:-false} + - APP_URL=${APP_URL:-https://merchbay.com} + - DB_CONNECTION=mysql + - DB_HOST=${DB_HOST} + - DB_PORT=${DB_PORT:-3306} + - DB_DATABASE=${DB_DATABASE} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + - PROD_PRIVATE=${PROD_PRIVATE} + - IMAGES_URL=${IMAGES_URL} + - UPLOAD_URL=${UPLOAD_URL} + - FORCE_HTTPS=true + - MAIL_DRIVER=${MAIL_DRIVER} + - MAIL_HOST=${MAIL_HOST} + - MAIL_PORT=${MAIL_PORT} + - MAIL_USERNAME=${MAIL_USERNAME} + - MAIL_PASSWORD=${MAIL_PASSWORD} + - MAIL_ENCRYPTION=${MAIL_ENCRYPTION} + - CAPTCHA_SITE_KEY=${CAPTCHA_SITE_KEY} + - CAPTCHA_SECRET_KEY=${CAPTCHA_SECRET_KEY} + - ANALYTICS_SITE_ID=${ANALYTICS_SITE_ID} + - ANALYTICS_CLIENT_ID=${ANALYTICS_CLIENT_ID} + - ANALYTICS_SERVICE_EMAIL=${ANALYTICS_SERVICE_EMAIL} + volumes: + - ./storage:/var/www/html/storage + - ./public/uploads:/var/www/html/public/uploads + labels: + - "traefik.enable=true" + # Production environment (merchbay.com) - Uses DigiCert SSL + - "traefik.http.routers.merchbay-prod.rule=Host(`merchbay.com`) || Host(`www.merchbay.com`)" + - "traefik.http.routers.merchbay-prod.entrypoints=websecure" + - "traefik.http.routers.merchbay-prod.tls=true" + - "traefik.http.services.merchbay-prod.loadbalancer.server.port=80" + # HTTP to HTTPS redirect + - "traefik.http.routers.merchbay-prod-http.rule=Host(`merchbay.com`) || Host(`www.merchbay.com`)" + - "traefik.http.routers.merchbay-prod-http.entrypoints=web" + - "traefik.http.routers.merchbay-prod-http.middlewares=https-redirect" + - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https" + networks: + - traefik-public + - crew-app-net + - default + +networks: + traefik-public: + external: true + crew-app-net: + external: true + default: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 951c536..e3302b3 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,13 @@ +# โš ๏ธ DEPRECATED: Use docker-compose.dev.yml or docker-compose.prod.yml instead +# This file is kept for reference only +# +# For development: docker-compose.dev.yml (dev.merchbay.app) +# For production: docker-compose.prod.yml (merchbay.com) +# For local dev: docker-compose.local.yml (localhost:8080) + services: app: - image: merchbay:dev + image: merchbay:latest container_name: merchbay_app restart: unless-stopped environment: @@ -33,15 +40,25 @@ services: - ./public/uploads:/var/www/html/public/uploads labels: - "traefik.enable=true" + # Development environment (dev.merchbay.app) - "traefik.http.routers.merchbay-dev.rule=Host(`dev.merchbay.app`)" - "traefik.http.routers.merchbay-dev.entrypoints=websecure" - "traefik.http.routers.merchbay-dev.tls=true" - - "traefik.http.routers.merchbay-dev.tls.certresolver=le" + - "traefik.http.routers.merchbay-dev.tls.certresolver=letsencrypt" - "traefik.http.services.merchbay-dev.loadbalancer.server.port=80" - # HTTP to HTTPS redirect + # Production environment (merchbay.com) - Uses DigiCert SSL + - "traefik.http.routers.merchbay-prod.rule=Host(`merchbay.com`) || Host(`www.merchbay.com`)" + - "traefik.http.routers.merchbay-prod.entrypoints=websecure" + - "traefik.http.routers.merchbay-prod.tls=true" + - "traefik.http.services.merchbay-prod.loadbalancer.server.port=80" + # HTTP to HTTPS redirect - Development - "traefik.http.routers.merchbay-dev-http.rule=Host(`dev.merchbay.app`)" - "traefik.http.routers.merchbay-dev-http.entrypoints=web" - "traefik.http.routers.merchbay-dev-http.middlewares=https-redirect" + # HTTP to HTTPS redirect - Production + - "traefik.http.routers.merchbay-prod-http.rule=Host(`merchbay.com`) || Host(`www.merchbay.com`)" + - "traefik.http.routers.merchbay-prod-http.entrypoints=web" + - "traefik.http.routers.merchbay-prod-http.middlewares=https-redirect" - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https" networks: - traefik-public diff --git a/traefik-certs.yml b/traefik-certs.yml new file mode 100644 index 0000000..9eb0934 --- /dev/null +++ b/traefik-certs.yml @@ -0,0 +1,17 @@ +# Traefik Dynamic Configuration for Custom SSL Certificates +# This file should be uploaded to /srv/traefik/dynamic/traefik-certs.yml on production server + +tls: + certificates: + # Production SSL Certificate (DigiCert) + # Note: merchbay.com.crt should be a bundle containing www_merchbay_com.crt + DigiCertCA.crt + - certFile: /srv/certs/merchbay.com.crt + keyFile: /srv/certs/merchbay.com.key + stores: + - default + + stores: + default: + defaultCertificate: + certFile: /srv/certs/merchbay.com.crt + keyFile: /srv/certs/merchbay.com.key