From 574d5a8a9a9cba97295265ad252d674bd22bce85 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 26 Jan 2026 02:19:48 +0100 Subject: [PATCH] deploy --- .env.example | 25 +++ .gitea/workflows/deploy.yml | 11 +- README.md | 66 ++++-- components/ContactForm.tsx | 6 +- docs/DEPLOYMENT.md | 197 ++++++++++++++++++ .../analytics/umami-analytics-service.ts | 15 +- 6 files changed, 293 insertions(+), 27 deletions(-) create mode 100644 .env.example create mode 100644 docs/DEPLOYMENT.md diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..5e49e3f1 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# WooCommerce API (Legacy - not currently used) +WOOCOMMERCE_URL=https://klz-cables.com +WOOCOMMERCE_CONSUMER_KEY= +WOOCOMMERCE_CONSUMER_SECRET= +WORDPRESS_APP_PASSWORD= + +# Umami Analytics +NEXT_PUBLIC_UMAMI_WEBSITE_ID= +NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js + +# GlitchTip (Sentry protocol) +SENTRY_DSN= +NEXT_PUBLIC_SENTRY_DSN= + +# SMTP Configuration +MAIL_HOST=smtp.eu.mailgun.org +MAIL_PORT=587 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_FROM=KLZ Cables +MAIL_RECIPIENTS=info@klz-cables.com + +# Redis Configuration (optional) +REDIS_URL=redis://redis:6379/2 +REDIS_KEY_PREFIX=klz: diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 25e70153..a79a7653 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -39,6 +39,9 @@ jobs: # docker push registry.infra.mintel.me/mintel/klz-cables.com:latest - name: Deploy to production server + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }} run: | mkdir -p ~/.ssh echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519 @@ -46,13 +49,11 @@ jobs: ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null - ssh -o StrictHostKeyChecking=accept-new deploy@alpha.mintel.me << EOF + ssh -o StrictHostKeyChecking=accept-new deploy@alpha.mintel.me bash -c "' set -e - echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me \ - -u "${{ secrets.REGISTRY_USER }}" --password-stdin - + echo \"$REGISTRY_PASS\" | docker login registry.infra.mintel.me -u \"$REGISTRY_USER\" --password-stdin cd /home/deploy/sites/klz-cables.com docker compose pull docker compose up -d --force-recreate --remove-orphans docker image prune -f - EOF \ No newline at end of file + '" \ No newline at end of file diff --git a/README.md b/README.md index 38577796..c467646e 100644 --- a/README.md +++ b/README.md @@ -225,31 +225,61 @@ GET /robots.txt ## 🚀 Deployment -### Vercel (Recommended) -```bash -# Install Vercel CLI -npm i -g vercel +### Automatic Deployment (Current Setup) -# Deploy -vercel --prod +The project uses **Gitea Actions** for CI/CD. Every push to `main` triggers: + +1. **Build**: Docker image built for `linux/arm64` +2. **Push**: Image pushed to `registry.infra.mintel.me` +3. **Deploy**: SSH to production server, pull and restart containers + +**Workflow**: `.gitea/workflows/deploy.yml` + +**Required Secrets** (configure in Gitea repository settings): +- `REGISTRY_USER` - Docker registry username +- `REGISTRY_PASS` - Docker registry password +- `ALPHA_SSH_KEY` - SSH private key for deployment +- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID +- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL +- `SENTRY_DSN` - Error tracking DSN + +### Manual Deployment + +```bash +# SSH into production server +ssh deploy@alpha.mintel.me + +# Navigate to project +cd /home/deploy/sites/klz-cables.com + +# Pull latest image and restart +docker compose pull +docker compose up -d --force-recreate --remove-orphans +docker image prune -f ``` -### Static Export +Or use the convenience script: ```bash -# Build and export -npm run build -npm run export - -# Deploy to any static host -# Upload /out directory +bash scripts/deploy-webhook.sh ``` -### Netlify -```bash -# Connect repository -# Set build command: npm run build -# Set publish directory: out +### Architecture + ``` +Client → Traefik (TLS) → Varnish (Cache) → Next.js App +``` + +**Domains**: +- `klz-cables.com` - Production +- `www.klz-cables.com` - Production (www) +- `staging.klz-cables.com` - Staging + +**Services**: +- `app`: Next.js application (port 3000) +- `varnish`: HTTP cache layer +- `traefik`: Reverse proxy (external) + +For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md). ## 📈 Performance diff --git a/components/ContactForm.tsx b/components/ContactForm.tsx index f9612d5a..b0eac7eb 100644 --- a/components/ContactForm.tsx +++ b/components/ContactForm.tsx @@ -16,21 +16,23 @@ export default function ContactForm() { setStatus('submitting'); const formData = new FormData(e.currentTarget); + const email = formData.get('email') as string; try { const result = await sendContactFormAction(formData); if (result.success) { trackEvent('contact_form_submission', { form_type: 'general', - email: formData.get('email') as string, + email, }); setStatus('success'); (e.target as HTMLFormElement).reset(); } else { + console.error('Contact form submission failed:', { email, error: result.error }); setStatus('error'); } } catch (error) { - console.error('Form submission error:', error); + console.error('Contact form submission error:', { email, error }); setStatus('error'); } } diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 00000000..be6059c4 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,197 @@ +# Deployment Guide + +This document describes the deployment setup for KLZ Cables website. + +## Automatic Deployment (Gitea Actions) + +The project uses Gitea Actions for CI/CD. On every push to the `main` branch: + +1. **Build**: Docker image is built with platform `linux/arm64` +2. **Push**: Image is pushed to `registry.infra.mintel.me/mintel/klz-cables.com:latest` +3. **Deploy**: SSH connection to production server pulls and restarts containers + +### Workflow File + +Location: `.gitea/workflows/deploy.yml` + +### Required Secrets + +Configure these in your Gitea repository settings: + +- `REGISTRY_USER` - Docker registry username +- `REGISTRY_PASS` - Docker registry password +- `ALPHA_SSH_KEY` - SSH private key for deployment user +- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Umami analytics website ID +- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Umami analytics script URL +- `SENTRY_DSN` - Sentry/GlitchTip DSN for error tracking + +## Manual Deployment + +If you need to deploy manually: + +### On the Production Server + +```bash +# SSH into the server +ssh deploy@alpha.mintel.me + +# Navigate to the project directory +cd /home/deploy/sites/klz-cables.com + +# Pull the latest image +docker compose pull + +# Restart containers +docker compose up -d --force-recreate --remove-orphans + +# Clean up old images +docker image prune -f +``` + +## Troubleshooting + +### Workflow Not Triggering + +1. Check Gitea Actions is enabled in repository settings +2. Verify the workflow file syntax +3. Check runner availability with label `docker` + +### Build Failures + +1. Check build logs in Gitea Actions tab +2. Verify all secrets are configured correctly +3. Ensure Dockerfile is valid + +### Deployment Failures + +1. Verify SSH key has correct permissions (600) +2. Check deploy user has Docker permissions +3. Verify registry credentials are correct +4. Check server disk space: `df -h` + +### Container Issues + +```bash +# Check container status +docker compose ps + +# View logs +docker compose logs -f app +docker compose logs -f varnish + +# Check health +docker compose exec app wget -O- http://localhost:3000/health +``` + +## Architecture + +``` +Client + ↓ +Traefik (TLS termination, routing) + ↓ +Varnish (HTTP caching) + ↓ +Next.js App (port 3000) +``` + +### Services + +- **app**: Next.js application +- **varnish**: HTTP cache layer +- **traefik**: Reverse proxy (external network) + +### Domains + +- `klz-cables.com` - Production +- `www.klz-cables.com` - Production (www) +- `staging.klz-cables.com` - Staging + +## Environment Variables + +### Build-time (in Dockerfile/Workflow) + +- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` +- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` +- `NEXT_PUBLIC_SENTRY_DSN` + +### Runtime (in docker-compose.yml) + +- `SENTRY_DSN` +- `REDIS_URL` +- `REDIS_KEY_PREFIX` + +## Monitoring + +### Health Checks + +- App: `https://klz-cables.com/health` +- Varnish: Configured in docker-compose.yml + +### Logs + +```bash +# Application logs +docker compose logs -f app + +# Varnish logs +docker compose logs -f varnish + +# All logs +docker compose logs -f +``` + +### Analytics + +- Umami: Configured via environment variables +- Sentry/GlitchTip: Error tracking + +## Rollback + +To rollback to a previous version: + +```bash +# On the server +cd /home/deploy/sites/klz-cables.com + +# Pull a specific version (if tagged) +docker pull registry.infra.mintel.me/mintel/klz-cables.com:TAG + +# Or rebuild from a specific commit +# (requires access to the repository on the server) + +# Restart with the older image +docker compose up -d --force-recreate +``` + +## Performance + +### Cache Invalidation + +Varnish caches static assets. To clear cache: + +```bash +docker compose exec varnish varnishadm "ban req.url ~ ." +``` + +### Cache Configuration + +Edit `varnish/default.vcl` and restart: + +```bash +docker compose restart varnish +``` + +## Security + +- All secrets are stored in Gitea repository settings +- SSH key is injected at deployment time +- Registry credentials are not stored in the repository +- Deploy webhook requires secret token + +## Support + +For issues or questions: +1. Check logs first +2. Review this documentation +3. Contact the development team diff --git a/lib/services/analytics/umami-analytics-service.ts b/lib/services/analytics/umami-analytics-service.ts index bfb14afe..6724cc95 100644 --- a/lib/services/analytics/umami-analytics-service.ts +++ b/lib/services/analytics/umami-analytics-service.ts @@ -1,4 +1,5 @@ import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service'; +import { getServerAppServices } from '../create-services.server'; /** * Type definition for the Umami global object. @@ -79,11 +80,16 @@ export class UmamiAnalyticsService implements AnalyticsService { if (!websiteId) return; + const logger = getServerAppServices().logger.child({ component: 'analytics' }); + logger.info('Sending analytics event', { eventName, props }); + fetch(`${umamiUrl}/api/send`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' }, body: JSON.stringify({ type: 'event', payload: { website: websiteId, name: eventName, data: props } }), - }).catch(() => {}); + }).catch((error) => { + logger.error('Failed to send analytics event', { eventName, props, error }); + }); return; } @@ -121,11 +127,16 @@ export class UmamiAnalyticsService implements AnalyticsService { if (!websiteId || !url) return; + const logger = getServerAppServices().logger.child({ component: 'analytics' }); + logger.info('Sending analytics pageview', { url }); + fetch(`${umamiUrl}/api/send`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' }, body: JSON.stringify({ type: 'event', payload: { website: websiteId, url } }), - }).catch(() => {}); + }).catch((error) => { + logger.error('Failed to send analytics pageview', { url, error }); + }); return; }