From 10ed0e46d8cc511831970d3bda6381379944edaf Mon Sep 17 00:00:00 2001 From: Oussama Douhou Date: Tue, 13 Jan 2026 11:51:48 +0100 Subject: [PATCH] feat: add multi-environment deployment with Gitea Actions & Dokploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update Gitea workflow to build dev/staging/main branches - Create environment-specific docker-compose files - docker-compose.dev.yml (pulls dev image) - docker-compose.staging.yml (pulls staging image) - docker-compose.prod.yml (pulls latest image) - docker-compose.local.yml (builds locally for development) - Remove generic docker-compose.yml (replaced by env-specific files) - Update .dockerignore to exclude docs/ and .gitea/ from production images - Add comprehensive deployment guide (docs/DOKPLOY_DEPLOYMENT.md) Image Tags: - dev branch → :dev - staging branch → :staging - main branch → :latest - All branches → :{branch}-{sha} Benefits: - Separate deployments for dev/staging/prod - Automated CI/CD via Gitea Actions + Dokploy webhooks - Leaner production images (excludes dev tools/docs) - Local development support (docker-compose.local.yml) - Rollback support via SHA-tagged images --- .dockerignore | 2 + .gitea/workflows/docker-publish.yaml | 10 +- docker-compose.dev.yml | 38 ++ docker-compose.local.yml | 43 ++ docker-compose.yml => docker-compose.prod.yml | 5 +- docker-compose.staging.yml | 38 ++ docs/DOKPLOY_DEPLOYMENT.md | 401 ++++++++++++++++++ 7 files changed, 531 insertions(+), 6 deletions(-) create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.local.yml rename docker-compose.yml => docker-compose.prod.yml (91%) create mode 100644 docker-compose.staging.yml create mode 100644 docs/DOKPLOY_DEPLOYMENT.md diff --git a/.dockerignore b/.dockerignore index dcd558f..3ee9194 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,6 +13,7 @@ node_modules # Documentation *.md !README.md +docs # IDE .vscode @@ -49,6 +50,7 @@ docker-compose*.yml # CI/CD .github .gitlab-ci.yml +.gitea # Scripts scripts diff --git a/.gitea/workflows/docker-publish.yaml b/.gitea/workflows/docker-publish.yaml index 3cc7dde..f575ea4 100644 --- a/.gitea/workflows/docker-publish.yaml +++ b/.gitea/workflows/docker-publish.yaml @@ -3,10 +3,14 @@ name: Build and Push Docker Image on: push: branches: + - dev + - staging - main paths: - 'src/**' + - 'client/**' - 'Dockerfile' + - 'package.json' - '.gitea/workflows/**' workflow_dispatch: @@ -41,8 +45,10 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=sha,prefix= + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} + type=raw,value=staging,enable=${{ github.ref == 'refs/heads/staging' }} + type=sha,prefix={{branch}}- - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..ddbf390 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,38 @@ +version: "3.8" + +services: + ai-stack-deployer: + image: git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev + container_name: ai-stack-deployer-dev + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - PORT=3000 + - HOST=0.0.0.0 + - DOKPLOY_URL=${DOKPLOY_URL} + - DOKPLOY_API_TOKEN=${DOKPLOY_API_TOKEN} + - STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl} + - STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest} + - RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal} + env_file: + - .env + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "bun", + "--eval", + "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))", + ] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + networks: + - ai-stack-network + +networks: + ai-stack-network: + driver: bridge diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..4d7ff74 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,43 @@ +version: "3.8" + +services: + ai-stack-deployer: + build: + context: . + dockerfile: Dockerfile + container_name: ai-stack-deployer-local + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - PORT=3000 + - HOST=0.0.0.0 + - DOKPLOY_URL=${DOKPLOY_URL} + - DOKPLOY_API_TOKEN=${DOKPLOY_API_TOKEN} + - STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl} + - STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest} + - RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal} + env_file: + - .env + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "bun", + "--eval", + "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))", + ] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + networks: + - ai-stack-network + volumes: + - ./src:/app/src:ro + - ./client:/app/client:ro + +networks: + ai-stack-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.prod.yml similarity index 91% rename from docker-compose.yml rename to docker-compose.prod.yml index 09cf4f6..325c41a 100644 --- a/docker-compose.yml +++ b/docker-compose.prod.yml @@ -1,11 +1,8 @@ version: "3.8" -# ***NEVER FORGET THE PRINCIPLES*** services: ai-stack-deployer: - build: - context: . - dockerfile: Dockerfile + image: git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest container_name: ai-stack-deployer ports: - "3000:3000" diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..71552fe --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,38 @@ +version: "3.8" + +services: + ai-stack-deployer: + image: git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:staging + container_name: ai-stack-deployer-staging + ports: + - "3000:3000" + environment: + - NODE_ENV=staging + - PORT=3000 + - HOST=0.0.0.0 + - DOKPLOY_URL=${DOKPLOY_URL} + - DOKPLOY_API_TOKEN=${DOKPLOY_API_TOKEN} + - STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl} + - STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest} + - RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal} + env_file: + - .env + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "bun", + "--eval", + "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))", + ] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + networks: + - ai-stack-network + +networks: + ai-stack-network: + driver: bridge diff --git a/docs/DOKPLOY_DEPLOYMENT.md b/docs/DOKPLOY_DEPLOYMENT.md new file mode 100644 index 0000000..45f9531 --- /dev/null +++ b/docs/DOKPLOY_DEPLOYMENT.md @@ -0,0 +1,401 @@ +# Dokploy Deployment Guide + +## Overview + +This project uses **Gitea Actions** to build Docker images and **Dokploy** to deploy them. Each branch (dev, staging, main) has its own: +- Docker image tag +- Docker Compose file +- Dokploy application +- Domain + +--- + +## Architecture + +``` +┌─────────────┐ +│ Gitea │ +│ (Source) │ +└──────┬──────┘ + │ push event + ↓ +┌─────────────┐ +│ Gitea │ +│ Actions │ Builds Docker images +│ (CI/CD) │ Tags: dev, staging, latest +└──────┬──────┘ + │ + ↓ +┌─────────────┐ +│ Gitea │ +│ Registry │ git.app.flexinit.nl/oussamadouhou/ai-stack-deployer +└──────┬──────┘ + │ webhook (push event) + ↓ +┌─────────────┐ +│ Dokploy │ Pulls & deploys image +│ (Deploy) │ Uses docker-compose.{env}.yml +└─────────────┘ +``` + +--- + +## Branch Strategy + +| Branch | Image Tag | Compose File | Domain (suggested) | +|-----------|-----------|----------------------------|------------------------------| +| `dev` | `dev` | `docker-compose.dev.yml` | portal-dev.ai.flexinit.nl | +| `staging` | `staging` | `docker-compose.staging.yml` | portal-staging.ai.flexinit.nl | +| `main` | `latest` | `docker-compose.prod.yml` | portal.ai.flexinit.nl | + +--- + +## Gitea Actions Workflow + +**File**: `.gitea/workflows/docker-publish.yaml` + +**Triggers**: Push to `dev`, `staging`, or `main` branches + +**Builds**: +```yaml +dev branch → git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev +staging branch → git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:staging +main branch → git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest +``` + +**Also creates SHA tags**: `{branch}-{short-sha}` + +--- + +## Docker Compose Files + +### `docker-compose.dev.yml` +- Pulls: `git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev` +- Environment: `NODE_ENV=development` +- Container name: `ai-stack-deployer-dev` + +### `docker-compose.staging.yml` +- Pulls: `git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:staging` +- Environment: `NODE_ENV=staging` +- Container name: `ai-stack-deployer-staging` + +### `docker-compose.prod.yml` +- Pulls: `git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest` +- Environment: `NODE_ENV=production` +- Container name: `ai-stack-deployer` + +### `docker-compose.local.yml` +- **Builds locally** (doesn't pull from registry) +- For local development only +- Includes volume mounts for hot reload + +--- + +## Setting Up Dokploy + +### Step 1: Create Dev Application + +1. **In Dokploy UI**, create new application: + - **Name**: `ai-stack-deployer-dev` + - **Type**: Docker Compose + - **Repository**: `ssh://git@git.app.flexinit.nl:22222/oussamadouhou/ai-stack-deployer.git` + - **Branch**: `dev` + - **Compose File**: `docker-compose.dev.yml` + +2. **Configure Domain**: + - Add domain: `portal-dev.ai.flexinit.nl` + - Enable SSL (via Traefik wildcard cert) + +3. **Set Environment Variables**: + ```env + DOKPLOY_URL=http://10.100.0.20:3000 + DOKPLOY_API_TOKEN= + STACK_DOMAIN_SUFFIX=ai.flexinit.nl + STACK_IMAGE=git.app.flexinit.nl/flexinit/agent-stack:latest + ``` + +4. **Configure Webhook**: + - Event: **Push** + - Branch: `dev` + - This will auto-deploy when you push to dev branch + +5. **Deploy** + +### Step 2: Create Staging Application + +Repeat Step 1 with these changes: +- **Name**: `ai-stack-deployer-staging` +- **Branch**: `staging` +- **Compose File**: `docker-compose.staging.yml` +- **Domain**: `portal-staging.ai.flexinit.nl` +- **Webhook Branch**: `staging` + +### Step 3: Create Production Application + +Repeat Step 1 with these changes: +- **Name**: `ai-stack-deployer-prod` +- **Branch**: `main` +- **Compose File**: `docker-compose.prod.yml` +- **Domain**: `portal.ai.flexinit.nl` +- **Webhook Branch**: `main` + +--- + +## Deployment Workflow + +### Development Cycle + +```bash +# 1. Make changes on dev branch +git checkout dev +# ... make changes ... +git commit -m "feat: add new feature" +git push origin dev + +# 2. Gitea Actions automatically builds dev image +# 3. Dokploy webhook triggers and deploys to portal-dev.ai.flexinit.nl + +# 4. Test on dev environment +curl https://portal-dev.ai.flexinit.nl/health + +# 5. When ready, merge to staging +git checkout staging +git merge dev +git push origin staging + +# 6. Gitea Actions builds staging image +# 7. Dokploy deploys to portal-staging.ai.flexinit.nl + +# 8. Final testing on staging, then merge to main +git checkout main +git merge staging +git push origin main + +# 9. Gitea Actions builds production image (latest) +# 10. Dokploy deploys to portal.ai.flexinit.nl +``` + +--- + +## Image Tags Explained + +Each push creates multiple tags: + +### Example: Push to `dev` branch (commit `abc1234`) + +Gitea Actions creates: +``` +git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev ← Latest dev +git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev-abc1234 ← Specific commit +``` + +### Example: Push to `main` branch (commit `xyz5678`) + +Gitea Actions creates: +``` +git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest ← Latest production +git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:main-xyz5678 ← Specific commit +``` + +**Why?** +- Branch tags (`dev`, `staging`, `latest`) always point to latest build +- SHA tags allow you to rollback to specific commits if needed + +--- + +## Rollback Strategy + +### Quick Rollback in Dokploy + +If a deployment breaks, you can quickly rollback: + +1. **In Dokploy UI**, go to the application +2. **Edit** the docker-compose file +3. Change the image tag to a previous SHA: + ```yaml + image: git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:main-abc1234 + ``` +4. **Redeploy** + +### Manual Rollback via Git + +```bash +# Find the last working commit +git log --oneline + +# Revert to that commit +git revert HEAD # or git reset --hard + +# Push to trigger rebuild +git push origin main +``` + +--- + +## Local Development + +### Using docker-compose.local.yml + +```bash +# Build and run locally +docker-compose -f docker-compose.local.yml up -d + +# View logs +docker-compose -f docker-compose.local.yml logs -f + +# Stop +docker-compose -f docker-compose.local.yml down +``` + +### Using Bun directly (without Docker) + +```bash +# Install dependencies +bun install + +# Run dev server (API + Vite) +bun run dev + +# Run API only +bun run dev:api + +# Run client only +bun run dev:client +``` + +--- + +## Environment Variables + +### Required in Dokploy + +```env +DOKPLOY_URL=http://10.100.0.20:3000 +DOKPLOY_API_TOKEN= +``` + +### Optional (with defaults) + +```env +PORT=3000 +HOST=0.0.0.0 +STACK_DOMAIN_SUFFIX=ai.flexinit.nl +STACK_IMAGE=git.app.flexinit.nl/flexinit/agent-stack:latest +RESERVED_NAMES=admin,api,www,root,system,test,demo,portal +``` + +### Per-Environment Overrides + +If dev/staging/prod need different configs, set them in Dokploy: + +**Dev**: +```env +STACK_DOMAIN_SUFFIX=dev-ai.flexinit.nl +``` + +**Staging**: +```env +STACK_DOMAIN_SUFFIX=staging-ai.flexinit.nl +``` + +**Prod**: +```env +STACK_DOMAIN_SUFFIX=ai.flexinit.nl +``` + +--- + +## Troubleshooting + +### Build Fails in Gitea Actions + +Check the workflow logs in Gitea: +``` +https://git.app.flexinit.nl/oussamadouhou/ai-stack-deployer/actions +``` + +Common issues: +- **AVX error**: Fixed in Dockerfile (uses Node.js for build) +- **Registry auth**: Check `REGISTRY_TOKEN` secret in Gitea + +### Deployment Fails in Dokploy + +1. **Check Dokploy logs**: Application → Logs +2. **Verify image exists**: + ```bash + docker pull git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev + ``` +3. **Check environment variables**: Make sure all required vars are set + +### Health Check Failing + +```bash +# SSH into Dokploy host +ssh user@10.100.0.20 + +# Check container logs +docker logs ai-stack-deployer-dev + +# Test health endpoint +curl http://localhost:3000/health +``` + +### Webhook Not Triggering + +1. **In Dokploy**, check webhook configuration +2. **In Gitea**, go to repo Settings → Webhooks +3. Verify webhook URL and secret match +4. Check recent deliveries for errors + +--- + +## Production Considerations + +### 1. Image Size Optimization + +The Docker image excludes dev files via `.dockerignore`: +- ✅ `docs/` - excluded +- ✅ `scripts/` - excluded +- ✅ `.gitea/` - excluded +- ✅ `*.md` (except README.md) - excluded + +Current image size: ~150MB + +### 2. Security + +- Container runs as non-root user (`nodejs:1001`) +- No secrets in source code (uses `.env`) +- Dokploy API accessible only on internal network + +### 3. Monitoring + +Set up alerts for: +- Container health check failures +- Memory/CPU usage spikes +- Deployment failures + +### 4. Backup Strategy + +- **Database**: This app has no database (stateless) +- **Configuration**: Environment variables stored in Dokploy (backed up) +- **Code**: Stored in Gitea (backed up) + +--- + +## Summary + +| Environment | Domain | Image Tag | Auto-Deploy? | +|-------------|------------------------------|-----------|--------------| +| Dev | portal-dev.ai.flexinit.nl | `dev` | ✅ On push | +| Staging | portal-staging.ai.flexinit.nl | `staging` | ✅ On push | +| Production | portal.ai.flexinit.nl | `latest` | ✅ On push | + +**Next Steps**: +1. ✅ Push changes to `dev` branch +2. ⏳ Create 3 Dokploy applications (dev, staging, prod) +3. ⏳ Configure webhooks for each branch +4. ⏳ Deploy and test each environment + +--- + +**Questions?** Check the main README.md or CLAUDE.md for more details.