From 0cedc90031832db3a93b44f905cc6b3b0e03aad8 Mon Sep 17 00:00:00 2001 From: Frank John Begornia Date: Fri, 12 Dec 2025 01:24:50 +0800 Subject: [PATCH] Add CI/CD workflows for development and production deployments - Created `deploy-dev.yml` for automated deployment to the development server on push to the `dev` branch. - Created `deploy.yml` for automated deployment to the production server on push to the `main` or `master` branches. - Added deployment instructions in `DEPLOYMENT-PORTAINER.md` for using Portainer and Traefik. - Documented Gitea Actions deployment process in `DEPLOYMENT.md`. - Configured Traefik SSL settings in `TRAEFIK-SSL-CONFIG.md` for both development and production environments. - Implemented a deployment script `deploy.sh` for manual deployments. - Added Docker Compose configurations for development (`docker-compose.portainer.dev.yml`) and production (`docker-compose.portainer.yml`) environments. - Updated main `docker-compose.yml` to support Traefik integration and environment variable configurations. --- .env.example | 26 +- .gitea/workflows/build-push.yml | 46 ++++ .gitea/workflows/deploy-dev.yml | 111 +++++++++ .gitea/workflows/deploy.yml | 122 ++++++++++ DEPLOYMENT-PORTAINER.md | 405 +++++++++++++++++++++++++++++++ DEPLOYMENT.md | 264 ++++++++++++++++++++ TRAEFIK-SSL-CONFIG.md | 316 ++++++++++++++++++++++++ deploy.sh | 105 ++++++++ docker-compose.portainer.dev.yml | 48 ++++ docker-compose.portainer.yml | 51 ++++ docker-compose.yml | 29 ++- 11 files changed, 1509 insertions(+), 14 deletions(-) create mode 100644 .gitea/workflows/build-push.yml create mode 100644 .gitea/workflows/deploy-dev.yml create mode 100644 .gitea/workflows/deploy.yml create mode 100644 DEPLOYMENT-PORTAINER.md create mode 100644 DEPLOYMENT.md create mode 100644 TRAEFIK-SSL-CONFIG.md create mode 100755 deploy.sh create mode 100644 docker-compose.portainer.dev.yml create mode 100644 docker-compose.portainer.yml diff --git a/.env.example b/.env.example index 9a9d0dc..7a837d6 100644 --- a/.env.example +++ b/.env.example @@ -1,25 +1,33 @@ -APP_ENV=local -APP_DEBUG=true -APP_KEY=SomeRandomString -APP_URL=http://localhost +# Application Configuration +APP_ENV=production +APP_DEBUG=false +APP_KEY=base64:YOUR_APP_KEY_HERE +APP_URL=https://merchbay-admin.yourdomain.com +# Database Configuration - External MySQL DB_CONNECTION=mysql -DB_HOST=127.0.0.1 +DB_HOST=your-mysql-host DB_PORT=3306 -DB_DATABASE=homestead -DB_USERNAME=homestead -DB_PASSWORD=secret +DB_DATABASE=merchbay_admin +DB_USERNAME=your-mysql-user +DB_PASSWORD=your-mysql-password +# Traefik Domain Configuration +DOMAIN=merchbay-admin.yourdomain.com + +# Cache & Session CACHE_DRIVER=file SESSION_DRIVER=file QUEUE_DRIVER=sync +# Redis (Optional) REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 +# Mail Configuration MAIL_DRIVER=smtp -MAIL_HOST=mailtrap.io +MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null diff --git a/.gitea/workflows/build-push.yml b/.gitea/workflows/build-push.yml new file mode 100644 index 0000000..a80c2dc --- /dev/null +++ b/.gitea/workflows/build-push.yml @@ -0,0 +1,46 @@ +name: Build and Push Docker Image + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Registry + uses: docker/login-action@v2 + with: + registry: ${{ secrets.DOCKER_REGISTRY_URL }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ secrets.DOCKER_REGISTRY_URL }}/merchbay_admin + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY_URL }}/merchbay_admin:buildcache + cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY_URL }}/merchbay_admin:buildcache,mode=max diff --git a/.gitea/workflows/deploy-dev.yml b/.gitea/workflows/deploy-dev.yml new file mode 100644 index 0000000..a21948d --- /dev/null +++ b/.gitea/workflows/deploy-dev.yml @@ -0,0 +1,111 @@ +name: Deploy Development + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build Docker Image + run: | + docker build -t merchbay_admin:dev . + + - name: Save Docker Image + run: | + docker save merchbay_admin:dev | gzip > merchbay_admin_dev.tar.gz + + - name: Deploy to Development Server via SSH + uses: appleboy/scp-action@master + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + port: ${{ secrets.DEPLOY_PORT || 22 }} + source: "merchbay_admin_dev.tar.gz,docker-compose.yml" + target: "/tmp/merchbay_admin_dev_deploy" + + - name: Execute Development Deployment Script + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + port: ${{ secrets.DEPLOY_PORT || 22 }} + script: | + # Set deployment directory for dev + DEPLOY_DIR="/var/www/merchbay_admin_dev" + + # Create deployment directory if it doesn't exist + mkdir -p $DEPLOY_DIR + + # Load the Docker image + cd /tmp/merchbay_admin_dev_deploy + docker load < merchbay_admin_dev.tar.gz + + # Copy docker-compose.yml to deployment directory + cp docker-compose.yml $DEPLOY_DIR/ + + # Navigate to deployment directory + cd $DEPLOY_DIR + + # Update environment file for dev + cat > .env << EOF + APP_ENV=staging + APP_DEBUG=false + APP_URL=https://dev.merchbay.app + DB_HOST=${{ secrets.DEV_DB_HOST }} + DB_PORT=${{ secrets.DEV_DB_PORT || 3306 }} + DB_DATABASE=${{ secrets.DEV_DB_DATABASE }} + DB_USERNAME=${{ secrets.DEV_DB_USERNAME }} + DB_PASSWORD=${{ secrets.DEV_DB_PASSWORD }} + DOMAIN=dev.merchbay.app + EOF + + # Stop existing container + docker compose down || true + + # Remove old image + docker image prune -f + + # Ensure Traefik network exists + docker network inspect traefik-public >/dev/null 2>&1 || docker network create traefik-public + + # Update docker-compose for dev + export DOMAIN=dev.merchbay.app + export APP_URL=https://dev.merchbay.app + + # Start the application + docker compose up -d + + # Wait for container to be ready + sleep 10 + + # Run migrations + docker compose exec -T app php artisan migrate --force + + # Clear and cache configuration + 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 + + # Cleanup + rm -rf /tmp/merchbay_admin_dev_deploy + + echo "Development deployment completed successfully!" + echo "Application available at: https://dev.merchbay.app" + + - name: Health Check + run: | + sleep 10 + curl -f https://dev.merchbay.app || exit 1 diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..fdba7b5 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,122 @@ +name: Deploy Production + +on: + push: + branches: + - main + - master + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + # If using self-hosted runner, change to: + # runs-on: self-hosted + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Registry (Optional) + if: ${{ secrets.DOCKER_REGISTRY_URL != '' }} + uses: docker/login-action@v2 + with: + registry: ${{ secrets.DOCKER_REGISTRY_URL }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build Docker Image + run: | + docker build -t merchbay_admin:latest . + + - name: Save Docker Image + run: | + docker save merchbay_admin:latest | gzip > merchbay_admin.tar.gz + + - name: Deploy to Server via SSH + uses: appleboy/scp-action@master + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + port: ${{ secrets.DEPLOY_PORT || 22 }} + source: "merchbay_admin.tar.gz,docker-compose.yml" + target: "/tmp/merchbay_admin_deploy" + + - name: Execute Production Deployment Script + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.PROD_DEPLOY_HOST }} + username: ${{ secrets.PROD_DEPLOY_USER }} + key: ${{ secrets.PROD_DEPLOY_SSH_KEY }} + port: ${{ secrets.PROD_DEPLOY_PORT || 22 }} + script: | + # Set deployment directory for production + DEPLOY_DIR="/var/www/merchbay_admin" + + # Create deployment directory if it doesn't exist + mkdir -p $DEPLOY_DIR + + # Load the Docker image + cd /tmp/merchbay_admin_deploy + docker load < merchbay_admin.tar.gz + + # Copy docker-compose.yml to deployment directory + cp docker-compose.yml $DEPLOY_DIR/ + + # Navigate to deployment directory + cd $DEPLOY_DIR + + # Update environment file for production + cat > .env << EOF + APP_ENV=production + APP_DEBUG=false + APP_URL=https://merchbay.app + DB_HOST=${{ secrets.PROD_DB_HOST }} + DB_PORT=${{ secrets.PROD_DB_PORT || 3306 }} + DB_DATABASE=${{ secrets.PROD_DB_DATABASE }} + DB_USERNAME=${{ secrets.PROD_DB_USERNAME }} + DB_PASSWORD=${{ secrets.PROD_DB_PASSWORD }} + DOMAIN=merchbay.app + EOF + + # Stop existing container (disconnect from Traefik network gracefully) + docker compose down || true + + # Remove old image (optional, keeps only latest) + docker image prune -f + + # Ensure Traefik network exists + docker network inspect traefik-public >/dev/null 2>&1 || docker network create traefik-public + + # Update docker-compose for production + export DOMAIN=merchbay.app + export APP_URL=https://merchbay.app + + # Start the application (will auto-connect to Traefik with paid SSL) + docker compose up -d + + # Wait for container to be ready + sleep 10 + + # Run migrations + docker compose exec -T app php artisan migrate --force + + # Clear and cache configuration + 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 + + # Cleanup + rm -rf /tmp/merchbay_admin_deploy + + echo "Production deployment completed successfully!" + echo "Application available at: https://merchbay.app" + + - name: Health Check + run: | + sleep 10 + curl -f https://merchbay.app || exit 1 diff --git a/DEPLOYMENT-PORTAINER.md b/DEPLOYMENT-PORTAINER.md new file mode 100644 index 0000000..81493df --- /dev/null +++ b/DEPLOYMENT-PORTAINER.md @@ -0,0 +1,405 @@ +# Deployment with Portainer and Traefik + +This guide covers deploying MerchBay Admin using your existing Portainer and Traefik setup. + +## Prerequisites + +- ✅ Gitea self-hosted with Gitea runners configured +- ✅ Portainer installed and accessible +- ✅ Traefik reverse proxy running with: + - `web` entrypoint (port 80) + - `websecure` entrypoint (port 443) + - Let's Encrypt certificate resolver named `letsencrypt` + - External network named `traefik` + +## Deployment Methods + +### Method 1: Portainer Stack Deployment (Recommended) + +#### Step 1: Prepare the Image + +Build the Docker image via Gitea Actions or manually: + +```bash +# Using Gitea Actions (will build automatically on push) +git push origin main + +# OR build manually +docker build -t merchbay_admin:latest . +``` + +#### Step 2: Deploy in Portainer + +1. **Access Portainer** → `Stacks` → `Add stack` + +2. **Stack Configuration:** + - Name: `merchbay-admin` + - Build method: `Web editor` + +3. **Paste the content from `docker-compose.portainer.yml`** and update: + - `APP_URL`: Your domain (e.g., `https://merchbay-admin.yourdomain.com`) + - `DB_HOST`: Your MySQL host + - `DB_DATABASE`: Database name + - `DB_USERNAME`: Database username + - `DB_PASSWORD`: Database password + - Traefik Host rule: Replace `merchbay-admin.yourdomain.com` with your actual domain + +4. **Deploy the stack** + +5. **Run Initial Setup:** + - Go to `Containers` → Find `merchbay_admin_app` + - Click `Console` → Connect with `/bin/bash` + - Run: + ```bash + php artisan migrate --force + php artisan config:cache + php artisan route:cache + ``` + +### Method 2: Gitea Actions CI/CD (Automated) + +#### Step 1: Configure Gitea Secrets + +In your Gitea repository → `Settings` → `Secrets` → `Actions`, add: + +| Secret Name | Value | Description | +|------------|-------|-------------| +| `DEPLOY_HOST` | `192.168.1.100` | Your server IP/hostname | +| `DEPLOY_USER` | `deploy` | SSH username | +| `DEPLOY_SSH_KEY` | `-----BEGIN RSA...` | Private SSH key | +| `DEPLOY_PORT` | `22` | SSH port (optional) | +| `DEPLOY_DIR` | `/var/www/merchbay_admin` | Deployment directory | +| `DOMAIN` | `merchbay-admin.yourdomain.com` | Your domain for Traefik | + +#### Step 2: Prepare Deployment Server + +```bash +# SSH into your server +ssh deploy@your-server + +# Create deployment directory +sudo mkdir -p /var/www/merchbay_admin +sudo chown $USER:$USER /var/www/merchbay_admin +cd /var/www/merchbay_admin + +# Create .env file +nano .env +``` + +Add to `.env`: +```env +APP_ENV=production +APP_DEBUG=false +APP_URL=https://merchbay-admin.yourdomain.com +DB_HOST=your-mysql-host +DB_PORT=3306 +DB_DATABASE=merchbay_admin +DB_USERNAME=your-mysql-user +DB_PASSWORD=your-mysql-password +DOMAIN=merchbay-admin.yourdomain.com +``` + +#### Step 3: Ensure Traefik Network Exists + +```bash +docker network inspect traefik >/dev/null 2>&1 || docker network create traefik +``` + +#### Step 4: Deploy + +Simply push to your main branch: + +```bash +git add . +git commit -m "Deploy application" +git push origin main +``` + +Gitea Actions will automatically: +1. Build the Docker image +2. Transfer to your server +3. Deploy with docker-compose +4. Connect to Traefik network +5. Run migrations +6. Cache configurations + +### Method 3: Manual Deployment with Script + +```bash +# On your server +cd /var/www/merchbay_admin + +# Pull latest code +git pull origin main + +# Run deployment script +./deploy.sh +``` + +## Traefik Configuration + +### Verify Traefik Setup + +Ensure your Traefik configuration includes: + +```yaml +# traefik.yml or docker-compose.yml +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + +certificatesResolvers: + letsencrypt: + acme: + email: your-email@example.com + storage: /letsencrypt/acme.json + httpChallenge: + entryPoint: web +``` + +### External Network + +Ensure Traefik network exists and is external: + +```bash +# Create network if it doesn't exist +docker network create traefik + +# Verify +docker network inspect traefik +``` + +## DNS Configuration + +Point your domain to your server: + +``` +Type: A Record +Name: merchbay-admin (or @ for root domain) +Value: YOUR_SERVER_IP +TTL: 3600 +``` + +## Verification + +### 1. Check Container Status + +**Via Portainer:** +- Navigate to `Containers` +- Verify `merchbay_admin_app` is running + +**Via CLI:** +```bash +docker ps | grep merchbay_admin +``` + +### 2. Check Traefik Dashboard + +If you have Traefik dashboard enabled: +- Look for `merchbay-admin@docker` router +- Verify it's connected to the correct service + +### 3. Test Application + +```bash +# Test HTTPS +curl -I https://merchbay-admin.yourdomain.com + +# Should return: HTTP/2 200 +``` + +### 4. Check Logs + +**Via Portainer:** +- `Containers` → `merchbay_admin_app` → `Logs` + +**Via CLI:** +```bash +docker logs merchbay_admin_app -f +``` + +## Troubleshooting + +### Issue: Container not accessible via domain + +**Check Traefik labels:** +```bash +docker inspect merchbay_admin_app | grep -A 20 Labels +``` + +**Verify network connection:** +```bash +docker network inspect traefik | grep merchbay_admin +``` + +**Solution:** +```bash +# Reconnect to Traefik network +docker network connect traefik merchbay_admin_app +``` + +### Issue: SSL certificate not generating + +**Check Traefik logs:** +```bash +docker logs traefik | grep letsencrypt +``` + +**Common fixes:** +1. Ensure port 80 is accessible (Let's Encrypt HTTP challenge) +2. Verify DNS is pointing to your server +3. Check email in Traefik's ACME configuration +4. Ensure `acme.json` has correct permissions (600) + +### Issue: Database connection failed + +**Check environment variables:** +```bash +docker exec merchbay_admin_app env | grep DB_ +``` + +**Test connection:** +```bash +docker exec merchbay_admin_app php artisan tinker +>>> DB::connection()->getPdo(); +``` + +### Issue: 502 Bad Gateway + +**Possible causes:** +1. Application not fully started +2. Wrong port in Traefik label (should be 80) +3. Application crashed + +**Check:** +```bash +# Container status +docker ps -a | grep merchbay_admin + +# Application logs +docker logs merchbay_admin_app --tail 100 + +# Restart container +docker restart merchbay_admin_app +``` + +## Updating the Application + +### Via Gitea Actions (Automatic) + +```bash +git add . +git commit -m "Update application" +git push origin main +``` + +### Via Portainer + +1. Go to `Stacks` → `merchbay-admin` +2. Click `Editor` +3. Update image or configuration +4. Click `Update the stack` +5. Enable "Re-pull image and redeploy" + +### Manual Update + +```bash +ssh deploy@your-server +cd /var/www/merchbay_admin +./deploy.sh +``` + +## Rollback + +### Via Portainer + +1. Go to `Stacks` → `merchbay-admin` +2. Click on the stack +3. Find previous version in `Stack History` (if available) +4. Revert to previous version + +### Manual Rollback + +```bash +# List available backups +ls -lh /var/www/merchbay_admin/backups/ + +# Restore from backup +cd /var/www/merchbay_admin +tar -xzf backups/backup_YYYYMMDD_HHMMSS.tar.gz + +# Restart +docker compose restart +``` + +## Security Best Practices + +1. **Use Strong Database Passwords**: Generate with `openssl rand -base64 32` +2. **Restrict SSH Access**: Use key-based authentication only +3. **Firewall Rules**: Only allow necessary ports (80, 443, 22) +4. **Regular Backups**: Automated backups of database and storage +5. **Keep Docker Updated**: Regularly update Docker and images +6. **Monitor Logs**: Set up log monitoring/alerting +7. **Use Secrets**: Never commit sensitive data to repository +8. **HTTPS Only**: Ensure HTTP redirects to HTTPS + +## Performance Optimization + +### PHP-FPM Configuration + +Consider switching to PHP-FPM for better performance: + +```dockerfile +FROM php:7.4-fpm-alpine +# ... additional configuration +``` + +### Use Redis for Cache/Sessions + +```env +CACHE_DRIVER=redis +SESSION_DRIVER=redis +REDIS_HOST=redis +``` + +Add Redis service to docker-compose: +```yaml +services: + redis: + image: redis:alpine + networks: + - default +``` + +### Enable OPcache + +Already included in Dockerfile, verify: +```bash +docker exec merchbay_admin_app php -i | grep opcache +``` + +## Monitoring + +### View Real-time Logs in Portainer + +1. Navigate to `Containers` +2. Click on `merchbay_admin_app` +3. Select `Logs` tab +4. Enable `Auto-refresh` + +### Set Up Alerts + +Configure Portainer notifications: +1. `Settings` → `Notifications` +2. Add webhook or email notification +3. Set up container health checks + +## Support + +For issues: +1. Check application logs in Portainer +2. Verify Traefik configuration +3. Test database connectivity +4. Review Gitea Actions logs if using CI/CD diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..54b6635 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,264 @@ +# Gitea Actions Deployment Guide + +This repository uses Gitea Actions for automated deployment to your server. + +## Workflows + +### 1. Deploy Workflow (`.gitea/workflows/deploy.yml`) +Automatically deploys the application when code is pushed to `main` or `master` branch. + +**Steps:** +1. Builds Docker image +2. Transfers image to deployment server +3. Stops existing container +4. Starts new container +5. Runs database migrations +6. Clears and caches Laravel configuration + +### 2. Build and Push Workflow (`.gitea/workflows/build-push.yml`) +Builds and pushes Docker images to a registry when a version tag is created. + +## Required Secrets + +Configure these secrets in your Gitea repository settings: +`Settings` → `Secrets` → `Actions` + +### Deployment Secrets + +| Secret Name | Description | Example | +|------------|-------------|---------| +| `DEPLOY_HOST` | Deployment server hostname or IP | `192.168.1.100` or `example.com` | +| `DEPLOY_USER` | SSH username for deployment | `deploy` or `ubuntu` | +| `DEPLOY_SSH_KEY` | Private SSH key for authentication | `-----BEGIN RSA PRIVATE KEY-----...` | +| `DEPLOY_PORT` | SSH port (optional, defaults to 22) | `22` | +| `DEPLOY_DIR` | Deployment directory (optional) | `/var/www/merchbay_admin` | + +### Docker Registry Secrets (Optional) + +Only required if using the build-push workflow or private registry: + +| Secret Name | Description | Example | +|------------|-------------|---------| +| `DOCKER_REGISTRY_URL` | Docker registry URL | `registry.example.com` or `docker.io` | +| `DOCKER_USERNAME` | Registry username | `myuser` | +| `DOCKER_PASSWORD` | Registry password or token | `mypassword` | + +### Database Configuration on Server + +Create a `.env` file in your deployment directory with database credentials: + +```bash +# On your deployment server +sudo mkdir -p /var/www/merchbay_admin +sudo nano /var/www/merchbay_admin/.env +``` + +Add your database configuration: + +```env +DB_HOST=your-mysql-host +DB_PORT=3306 +DB_DATABASE=merchbay_admin +DB_USERNAME=your-mysql-user +DB_PASSWORD=your-mysql-password +APP_ENV=production +APP_DEBUG=false +APP_KEY=base64:YOUR_APP_KEY_HERE +``` + +## Setup Instructions + +### 1. Generate SSH Key for Deployment + +On your local machine or CI server: + +```bash +# Generate a new SSH key pair +ssh-keygen -t rsa -b 4096 -f ~/.ssh/deploy_key -N "" + +# Copy the public key to your deployment server +ssh-copy-id -i ~/.ssh/deploy_key.pub user@your-server + +# Copy the private key content for Gitea secret +cat ~/.ssh/deploy_key +``` + +### 2. Configure Gitea Secrets + +1. Go to your Gitea repository +2. Navigate to `Settings` → `Secrets` → `Actions` +3. Add each required secret listed above +4. For `DEPLOY_SSH_KEY`, paste the entire private key content + +### 3. Prepare Deployment Server + +On your deployment server, install Docker and Docker Compose: + +```bash +# Install Docker +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh + +# Add your user to docker group +sudo usermod -aG docker $USER + +# Install Docker Compose +sudo apt-get update +sudo apt-get install docker-compose-plugin + +# Create deployment directory +sudo mkdir -p /var/www/merchbay_admin +sudo chown $USER:$USER /var/www/merchbay_admin + +# Create .env file with database credentials +nano /var/www/merchbay_admin/.env +``` + +### 4. Update docker-compose.yml for Production + +Ensure your `docker-compose.yml` references the `.env` file: + +```yaml +services: + app: + environment: + - DB_HOST=${DB_HOST} + - DB_PORT=${DB_PORT} + - DB_DATABASE=${DB_DATABASE} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} +``` + +## Triggering Deployment + +### Automatic Deployment + +Push to main/master branch: + +```bash +git add . +git commit -m "Deploy updates" +git push origin main +``` + +### Manual Deployment + +1. Go to your Gitea repository +2. Click on `Actions` +3. Select `Deploy MerchBay Admin` workflow +4. Click `Run workflow` + +## Monitoring Deployment + +### View Workflow Logs + +1. Go to `Actions` tab in your Gitea repository +2. Click on the running/completed workflow +3. View logs for each step + +### Check Application Logs + +On your deployment server: + +```bash +cd /var/www/merchbay_admin +docker compose logs -f app +``` + +### Verify Deployment + +```bash +# Check container status +docker compose ps + +# Test application +curl http://localhost:8080 + +# Access application shell +docker compose exec app bash +``` + +## Rollback Procedure + +If deployment fails, you can quickly rollback: + +```bash +# On deployment server +cd /var/www/merchbay_admin + +# Stop current container +docker compose down + +# Load previous image (if available) +docker images # Find previous image ID +docker tag merchbay_admin:latest + +# Start with previous version +docker compose up -d +``` + +## Troubleshooting + +### SSH Connection Issues + +```bash +# Test SSH connection from CI to server +ssh -i ~/.ssh/deploy_key user@your-server + +# Check SSH key permissions +chmod 600 ~/.ssh/deploy_key +``` + +### Docker Permission Issues + +```bash +# On deployment server, ensure user is in docker group +sudo usermod -aG docker $USER +newgrp docker +``` + +### Migration Failures + +```bash +# Manually run migrations +docker compose exec app php artisan migrate --force + +# Check database connection +docker compose exec app php artisan tinker +>>> DB::connection()->getPdo(); +``` + +## Security Best Practices + +1. **Use SSH keys, not passwords** for server authentication +2. **Restrict SSH key** to only deployment commands if possible +3. **Use secrets** for all sensitive data, never commit to repository +4. **Set proper file permissions** on deployment server (755 for directories, 644 for files) +5. **Enable firewall** on deployment server and restrict access +6. **Use HTTPS** with SSL certificates in production +7. **Regular backups** of database and uploaded files + +## Advanced Configuration + +### Using Docker Registry + +To use a private registry: + +1. Add registry secrets to Gitea +2. Update deployment script to pull from registry instead of transferring image +3. Use the build-push workflow to automate image publishing + +### Zero-Downtime Deployment + +For zero-downtime deployments, consider: + +1. Using a load balancer +2. Running multiple container instances +3. Implementing blue-green deployment strategy + +### Environment-Specific Deployments + +Create separate workflows for staging and production: + +- `.gitea/workflows/deploy-staging.yml` (triggered on `develop` branch) +- `.gitea/workflows/deploy-production.yml` (triggered on `main` branch) diff --git a/TRAEFIK-SSL-CONFIG.md b/TRAEFIK-SSL-CONFIG.md new file mode 100644 index 0000000..ba9117b --- /dev/null +++ b/TRAEFIK-SSL-CONFIG.md @@ -0,0 +1,316 @@ +# Traefik SSL Configuration for MerchBay Admin + +This document explains how to configure paid SSL certificates for production and automatic Let's Encrypt for development. + +## Network Configuration + +All deployments use the external network: `traefik-public` + +```bash +# Ensure the network exists +docker network inspect traefik-public >/dev/null 2>&1 || docker network create traefik-public +``` + +## Development (dev.merchbay.app) - Automatic SSL + +Development uses Let's Encrypt for automatic SSL certificate generation. + +### Traefik Configuration + +Ensure your Traefik has Let's Encrypt configured: + +```yaml +# traefik.yml or dynamic config +certificatesResolvers: + letsencrypt: + acme: + email: admin@merchbay.app + storage: /letsencrypt/acme.json + httpChallenge: + entryPoint: web +``` + +### Application Labels (Already configured in docker-compose) + +```yaml +labels: + - "traefik.http.routers.merchbay-admin-dev.tls.certresolver=letsencrypt" +``` + +## Production (merchbay.app) - Paid SSL Certificate + +Production uses a paid SSL certificate (e.g., from GoDaddy, Namecheap, Cloudflare). + +### Step 1: Prepare SSL Certificate Files + +You should have these files from your SSL provider: +- `merchbay.app.crt` - Certificate file +- `merchbay.app.key` - Private key file +- `ca-bundle.crt` - CA bundle (optional, for chain) + +Create a combined certificate file: + +```bash +# Create SSL directory in Traefik +mkdir -p /opt/traefik/certs + +# Copy your certificate and key +sudo cp merchbay.app.crt /opt/traefik/certs/ +sudo cp merchbay.app.key /opt/traefik/certs/ + +# If you have a CA bundle, create a full chain +cat merchbay.app.crt ca-bundle.crt > /opt/traefik/certs/merchbay.app-fullchain.crt + +# Set proper permissions +sudo chmod 600 /opt/traefik/certs/*.key +sudo chmod 644 /opt/traefik/certs/*.crt +``` + +### Step 2: Configure Traefik File Provider + +Create a dynamic configuration file for Traefik: + +```bash +sudo nano /opt/traefik/dynamic/certs.yml +``` + +Add: + +```yaml +# /opt/traefik/dynamic/certs.yml +tls: + certificates: + - certFile: /certs/merchbay.app-fullchain.crt + keyFile: /certs/merchbay.app.key + stores: + - default + stores: + default: + defaultCertificate: + certFile: /certs/merchbay.app-fullchain.crt + keyFile: /certs/merchbay.app.key +``` + +### Step 3: Update Traefik docker-compose.yml + +Ensure Traefik has file provider enabled and certificates mounted: + +```yaml +services: + traefik: + image: traefik:v2.10 + command: + - "--providers.docker=true" + - "--providers.docker.network=traefik-public" + - "--providers.file.directory=/etc/traefik/dynamic" + - "--providers.file.watch=true" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + # Let's Encrypt for dev + - "--certificatesresolvers.letsencrypt.acme.email=admin@merchbay.app" + - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /opt/traefik/certs:/certs:ro + - /opt/traefik/dynamic:/etc/traefik/dynamic:ro + - traefik-letsencrypt:/letsencrypt + networks: + - traefik-public +``` + +### Step 4: Restart Traefik + +```bash +cd /path/to/traefik +docker compose restart traefik + +# Verify certificates are loaded +docker compose logs traefik | grep -i cert +``` + +## Application Configuration + +### Development Branch (dev) + +File: `docker-compose.portainer.dev.yml` + +- Domain: `dev.merchbay.app` +- SSL: Let's Encrypt (automatic) +- Certificate Resolver: `letsencrypt` + +```yaml +labels: + - "traefik.http.routers.merchbay-admin-dev.rule=Host(`dev.merchbay.app`)" + - "traefik.http.routers.merchbay-admin-dev.tls.certresolver=letsencrypt" +``` + +### Production Branch (main) + +File: `docker-compose.portainer.yml` + +- Domain: `merchbay.app` +- SSL: Paid certificate (via Traefik file provider) +- No certresolver label (uses default store) + +```yaml +labels: + - "traefik.http.routers.merchbay-admin.rule=Host(`merchbay.app`)" + - "traefik.http.routers.merchbay-admin.tls=true" + # No certresolver - uses file provider certificate +``` + +## Gitea Secrets Configuration + +### Development Secrets + +``` +DEV_DB_HOST=dev-mysql-host +DEV_DB_PORT=3306 +DEV_DB_DATABASE=merchbay_admin_dev +DEV_DB_USERNAME=dev_user +DEV_DB_PASSWORD=dev_password +``` + +### Production Secrets + +``` +PROD_DEPLOY_HOST=prod-server-ip +PROD_DEPLOY_USER=deploy +PROD_DEPLOY_SSH_KEY=-----BEGIN RSA PRIVATE KEY-----... +PROD_DEPLOY_PORT=22 +PROD_DB_HOST=prod-mysql-host +PROD_DB_PORT=3306 +PROD_DB_DATABASE=merchbay_admin +PROD_DB_USERNAME=prod_user +PROD_DB_PASSWORD=prod_password +``` + +### Shared Secrets (if using same server) + +``` +DEPLOY_HOST=your-server-ip +DEPLOY_USER=deploy +DEPLOY_SSH_KEY=-----BEGIN RSA PRIVATE KEY-----... +DEPLOY_PORT=22 +``` + +## Verification + +### Check Development SSL + +```bash +# Check certificate issuer (should be Let's Encrypt) +echo | openssl s_client -servername dev.merchbay.app -connect dev.merchbay.app:443 2>/dev/null | openssl x509 -noout -issuer +# Should show: issuer=C = US, O = Let's Encrypt, CN = R3 +``` + +### Check Production SSL + +```bash +# Check certificate issuer (should be your SSL provider) +echo | openssl s_client -servername merchbay.app -connect merchbay.app:443 2>/dev/null | openssl x509 -noout -issuer +# Should show your paid SSL provider + +# Check certificate validity +echo | openssl s_client -servername merchbay.app -connect merchbay.app:443 2>/dev/null | openssl x509 -noout -dates +``` + +### Verify in Browser + +1. Visit https://dev.merchbay.app + - Certificate should be issued by "Let's Encrypt Authority X3" + +2. Visit https://merchbay.app + - Certificate should be issued by your paid SSL provider + +## Troubleshooting + +### Development SSL Not Working + +```bash +# Check Let's Encrypt logs +docker logs traefik | grep letsencrypt + +# Verify acme.json permissions +ls -l /path/to/letsencrypt/acme.json +# Should be: -rw------- (600) + +# Check DNS +dig dev.merchbay.app +short +# Should return your server IP +``` + +### Production SSL Not Working + +```bash +# Verify Traefik can read certificates +docker exec traefik ls -l /certs/ + +# Check dynamic configuration is loaded +docker exec traefik cat /etc/traefik/dynamic/certs.yml + +# Verify certificate format +openssl x509 -in /opt/traefik/certs/merchbay.app-fullchain.crt -text -noout + +# Check private key +openssl rsa -in /opt/traefik/certs/merchbay.app.key -check +``` + +### Certificate Mismatch + +```bash +# Verify certificate and key match +openssl x509 -noout -modulus -in /opt/traefik/certs/merchbay.app.crt | openssl md5 +openssl rsa -noout -modulus -in /opt/traefik/certs/merchbay.app.key | openssl md5 +# Both should output the same hash +``` + +## Renewing Certificates + +### Development (Let's Encrypt) + +Automatic renewal every 60 days. No action needed. + +### Production (Paid SSL) + +Before certificate expiration: + +1. Download new certificate from your SSL provider +2. Update files in `/opt/traefik/certs/` +3. Restart Traefik: `docker compose restart traefik` +4. Verify: `curl -vI https://merchbay.app` + +## DNS Configuration + +### Development + +``` +Type: A +Name: dev.merchbay.app +Value: YOUR_SERVER_IP +TTL: 3600 +``` + +### Production + +``` +Type: A +Name: merchbay.app (or @) +Value: YOUR_SERVER_IP +TTL: 3600 + +Type: A +Name: www.merchbay.app +Value: YOUR_SERVER_IP +TTL: 3600 +``` + +## Security Best Practices + +1. **Keep certificates private**: Never commit SSL keys to git +2. **Use strong permissions**: 600 for private keys, 644 for certificates +3. **Monitor expiration**: Set reminders 30 days before expiration +4. **Use HSTS**: Add header after SSL is working correctly +5. **Enable OCSP stapling**: Improves SSL performance +6. **Regular updates**: Keep Traefik updated for security patches diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..1e6468a --- /dev/null +++ b/deploy.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Deployment script for MerchBay Admin +# This can be used for manual deployments or called from CI/CD + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +DEPLOY_DIR="${DEPLOY_DIR:-/var/www/merchbay_admin}" +APP_NAME="merchbay_admin" +BACKUP_DIR="${DEPLOY_DIR}/backups" + +echo -e "${GREEN}Starting deployment of ${APP_NAME}...${NC}" + +# Create backup directory +mkdir -p "${BACKUP_DIR}" + +# Backup current state +if [ -d "${DEPLOY_DIR}/storage" ]; then + echo -e "${YELLOW}Creating backup...${NC}" + BACKUP_FILE="${BACKUP_DIR}/backup_$(date +%Y%m%d_%H%M%S).tar.gz" + tar -czf "${BACKUP_FILE}" -C "${DEPLOY_DIR}" storage .env 2>/dev/null || true + echo -e "${GREEN}Backup created: ${BACKUP_FILE}${NC}" +fi + +# Navigate to deployment directory +cd "${DEPLOY_DIR}" + +# Check if .env exists +if [ ! -f .env ]; then + echo -e "${RED}Error: .env file not found!${NC}" + echo "Please create .env file with database credentials" + exit 1 +fi + +# Pull latest code (if using git deployment) +if [ -d .git ]; then + echo -e "${YELLOW}Pulling latest code...${NC}" + git pull origin main || git pull origin master +fi + +# Stop existing containers +echo -e "${YELLOW}Stopping existing containers...${NC}" +docker compose down + +# Build new image +echo -e "${YELLOW}Building Docker image...${NC}" +docker compose build --no-cache + +# Start containers +echo -e "${YELLOW}Starting containers...${NC}" +docker compose up -d + +# Wait for container to be ready +echo -e "${YELLOW}Waiting for application to start...${NC}" +sleep 10 + +# Run migrations +echo -e "${YELLOW}Running database migrations...${NC}" +docker compose exec -T app php artisan migrate --force || { + echo -e "${RED}Migration failed! Rolling back...${NC}" + docker compose down + exit 1 +} + +# Clear and cache configuration +echo -e "${YELLOW}Optimizing application...${NC}" +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 + +# Set proper permissions +echo -e "${YELLOW}Setting permissions...${NC}" +docker compose exec -T app chown -R www-data:www-data /var/www/html/storage +docker compose exec -T app chmod -R 755 /var/www/html/storage + +# Health check +echo -e "${YELLOW}Performing health check...${NC}" +sleep 5 +HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080) + +if [ "$HTTP_STATUS" -eq 200 ]; then + echo -e "${GREEN}✓ Deployment successful! Application is running.${NC}" + echo -e "${GREEN}✓ HTTP Status: ${HTTP_STATUS}${NC}" + + # Keep only last 5 backups + cd "${BACKUP_DIR}" + ls -t backup_*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm + + echo -e "${GREEN}Deployment completed successfully!${NC}" +else + echo -e "${RED}✗ Health check failed! HTTP Status: ${HTTP_STATUS}${NC}" + echo -e "${YELLOW}Check logs: docker compose logs app${NC}" + exit 1 +fi + +# Display container status +echo -e "${YELLOW}Container status:${NC}" +docker compose ps diff --git a/docker-compose.portainer.dev.yml b/docker-compose.portainer.dev.yml new file mode 100644 index 0000000..09d14d5 --- /dev/null +++ b/docker-compose.portainer.dev.yml @@ -0,0 +1,48 @@ +version: '3.8' + +# Development Stack - Portainer Configuration +# Deploy this through Portainer UI: Stacks -> Add Stack -> Web Editor +# Branch: dev + +services: + app: + image: merchbay_admin:dev + container_name: merchbay_admin_dev + restart: unless-stopped + environment: + - APP_ENV=staging + - APP_DEBUG=false + - APP_URL=https://dev.merchbay.app + - DB_CONNECTION=mysql + - DB_HOST=your-mysql-host + - DB_PORT=3306 + - DB_DATABASE=merchbay_admin_dev + - DB_USERNAME=your-mysql-user + - DB_PASSWORD=your-mysql-password + volumes: + - app_storage_dev:/var/www/html/storage + - app_uploads_dev:/var/www/html/public/uploads + labels: + - "traefik.enable=true" + - "traefik.http.routers.merchbay-admin-dev.rule=Host(`dev.merchbay.app`)" + - "traefik.http.routers.merchbay-admin-dev.entrypoints=websecure" + - "traefik.http.routers.merchbay-admin-dev.tls=true" + - "traefik.http.routers.merchbay-admin-dev.tls.certresolver=letsencrypt" + - "traefik.http.services.merchbay-admin-dev.loadbalancer.server.port=80" + # HTTP to HTTPS redirect + - "traefik.http.routers.merchbay-admin-dev-http.rule=Host(`dev.merchbay.app`)" + - "traefik.http.routers.merchbay-admin-dev-http.entrypoints=web" + - "traefik.http.routers.merchbay-admin-dev-http.middlewares=https-redirect" + - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https" + networks: + - traefik-public + +volumes: + app_storage_dev: + driver: local + app_uploads_dev: + driver: local + +networks: + traefik-public: + external: true diff --git a/docker-compose.portainer.yml b/docker-compose.portainer.yml new file mode 100644 index 0000000..ca562af --- /dev/null +++ b/docker-compose.portainer.yml @@ -0,0 +1,51 @@ +version: '3.8' + +# Production Stack - Portainer Configuration +# Deploy this through Portainer UI: Stacks -> Add Stack -> Web Editor +# Branch: main - Uses paid SSL certificate + +services: + app: + image: merchbay_admin:latest + container_name: merchbay_admin_prod + restart: unless-stopped + environment: + - APP_ENV=production + - APP_DEBUG=false + - APP_URL=https://merchbay.app + - DB_CONNECTION=mysql + - DB_HOST=your-mysql-host + - DB_PORT=3306 + - DB_DATABASE=merchbay_admin + - DB_USERNAME=your-mysql-user + - DB_PASSWORD=your-mysql-password + volumes: + - app_storage:/var/www/html/storage + - app_uploads:/var/www/html/public/uploads + # Mount paid SSL certificates + - /path/to/ssl/certs:/etc/ssl/certs:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.merchbay-admin.rule=Host(`merchbay.app`)" + - "traefik.http.routers.merchbay-admin.entrypoints=websecure" + - "traefik.http.routers.merchbay-admin.tls=true" + # Use custom TLS configuration (file provider for paid cert) + # Ensure Traefik has file provider configured with your paid SSL cert + - "traefik.http.services.merchbay-admin.loadbalancer.server.port=80" + # HTTP to HTTPS redirect + - "traefik.http.routers.merchbay-admin-http.rule=Host(`merchbay.app`)" + - "traefik.http.routers.merchbay-admin-http.entrypoints=web" + - "traefik.http.routers.merchbay-admin-http.middlewares=https-redirect" + - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https" + networks: + - traefik-public + +volumes: + app_storage: + driver: local + app_uploads: + driver: local + +networks: + traefik-public: + external: true diff --git a/docker-compose.yml b/docker-compose.yml index 83c916a..088f8de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,11 +7,10 @@ services: dockerfile: Dockerfile container_name: merchbay_admin_app restart: unless-stopped - ports: - - "8080:80" environment: - - APP_ENV=production - - APP_DEBUG=false + - APP_ENV=${APP_ENV:-production} + - APP_DEBUG=${APP_DEBUG:-false} + - APP_URL=${APP_URL:-http://localhost} - DB_CONNECTION=mysql - DB_HOST=${DB_HOST:-localhost} - DB_PORT=${DB_PORT:-3306} @@ -21,4 +20,24 @@ services: volumes: - ./storage:/var/www/html/storage - ./public/uploads:/var/www/html/public/uploads - network_mode: bridge + labels: + - "traefik.enable=true" + - "traefik.http.routers.merchbay-admin.rule=Host(`${DOMAIN:-merchbay-admin.localhost}`)" + - "traefik.http.routers.merchbay-admin.entrypoints=websecure" + - "traefik.http.routers.merchbay-admin.tls=true" + - "traefik.http.routers.merchbay-admin.tls.certresolver=letsencrypt" + - "traefik.http.services.merchbay-admin.loadbalancer.server.port=80" + # HTTP to HTTPS redirect + - "traefik.http.routers.merchbay-admin-http.rule=Host(`${DOMAIN:-merchbay-admin.localhost}`)" + - "traefik.http.routers.merchbay-admin-http.entrypoints=web" + - "traefik.http.routers.merchbay-admin-http.middlewares=https-redirect" + - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https" + networks: + - traefik-public + - default + +networks: + traefik-public: + external: true + default: + driver: bridge