Docker Compose interprets $ as variable substitution, so we need to escape
Dokploy's project-level variable syntax by doubling the dollar sign.
Changes:
- docker-compose.*.yml: ${{project.VAR}} → $${{project.VAR}}
- Updated DOKPLOY_DEPLOYMENT.md with correct syntax and explanation
- Updated SHARED_PROJECT_DEPLOYMENT.md with correct syntax and explanation
This fixes the 'You may need to escape any $ with another $' error when
deploying via Dokploy.
Evidence: Tested in Dokploy deployment - error resolved with $$ escaping.
12 KiB
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:
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
Shared Project Configuration (IMPORTANT)
What is Shared Project Deployment?
The portal deploys all user AI stacks as applications within a single shared Dokploy project, instead of creating a new project for each user. This provides:
- ✅ Better organization (all stacks in one place)
- ✅ Shared environment variables
- ✅ Centralized monitoring
- ✅ Easier management
How It Works
Dokploy Project: ai-stack-portal
├── Environment: deployments
│ ├── Application: john-dev
│ ├── Application: jane-prod
│ └── Application: alice-test
Setting Up the Shared Project
Step 1: Create the Shared Project in Dokploy
-
In Dokploy UI, create a new project:
- Name:
ai-stack-portal(or any name you prefer) - Description: "Shared project for all user AI stacks"
- Name:
-
Note the Project ID (visible in URL or API response)
- Example:
2y2Glhz5Wy0dBNf6BOR_-
- Example:
-
Get the Environment ID:
curl -s "http://10.100.0.20:3000/api/project.one?projectId=2y2Glhz5Wy0dBNf6BOR_-" \ -H "Authorization: Bearer $DOKPLOY_API_TOKEN" | jq -r '.environments[0].id'- Example:
RqE9OFMdLwkzN7pif1xN8
- Example:
Step 2: Configure Project-Level Variables
In the shared project (ai-stack-portal), add these project-level environment variables:
| Variable Name | Value | Purpose |
|---|---|---|
SHARED_PROJECT_ID |
2y2Glhz5Wy0dBNf6BOR_- |
The project where user stacks deploy |
SHARED_ENVIRONMENT_ID |
RqE9OFMdLwkzN7pif1xN8 |
The environment within that project |
Step 3: Reference Variables in Portal Applications
The portal's docker-compose files use Dokploy's variable syntax to reference these:
environment:
- SHARED_PROJECT_ID=$${{project.SHARED_PROJECT_ID}}
- SHARED_ENVIRONMENT_ID=$${{project.SHARED_ENVIRONMENT_ID}}
This syntax $${{project.VARIABLE}} tells Dokploy: "Get this value from the project-level environment variables"
Note: The double $$ is required to escape the dollar sign in Docker Compose files.
Important Notes
- ⚠️ Both variables MUST be set in the shared project for deployment to work
- ⚠️ If not set, portal will fall back to creating separate projects per user (legacy behavior)
- ✅ You can have different shared projects for dev/staging/prod environments
- ✅ All 3 portal deployments (dev/staging/prod) should point to their respective shared projects
Setting Up Dokploy
Step 1: Create Dev Application
-
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
- Name:
-
Configure Domain:
- Add domain:
portal-dev.ai.flexinit.nl - Enable SSL (via Traefik wildcard cert)
- Add domain:
-
Set Environment Variables:
Important: The portal application should be deployed inside the shared project (e.g.,
ai-stack-portal-dev).Then set these project-level variables in that shared project:
SHARED_PROJECT_ID=<your-shared-project-id> SHARED_ENVIRONMENT_ID=<your-shared-environment-id>And these application-level variables in the portal app:
DOKPLOY_URL=http://10.100.0.20:3000 DOKPLOY_API_TOKEN=<your-token> STACK_DOMAIN_SUFFIX=ai.flexinit.nl STACK_IMAGE=git.app.flexinit.nl/flexinit/agent-stack:latestThe docker-compose file will automatically reference the project-level variables using:
SHARED_PROJECT_ID=${{project.SHARED_PROJECT_ID}} SHARED_ENVIRONMENT_ID=${{project.SHARED_ENVIRONMENT_ID}} -
Configure Webhook:
- Event: Push
- Branch:
dev - This will auto-deploy when you push to dev branch
-
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
# 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:
- In Dokploy UI, go to the application
- Edit the docker-compose file
- Change the image tag to a previous SHA:
image: git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:main-abc1234 - Redeploy
Manual Rollback via Git
# Find the last working commit
git log --oneline
# Revert to that commit
git revert HEAD # or git reset --hard <commit-sha>
# Push to trigger rebuild
git push origin main
Local Development
Using docker-compose.local.yml
# 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)
# 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
DOKPLOY_URL=http://10.100.0.20:3000
DOKPLOY_API_TOKEN=<your-token>
Optional (with defaults)
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:
STACK_DOMAIN_SUFFIX=dev-ai.flexinit.nl
Staging:
STACK_DOMAIN_SUFFIX=staging-ai.flexinit.nl
Prod:
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_TOKENsecret in Gitea
Deployment Fails in Dokploy
- Check Dokploy logs: Application → Logs
- Verify image exists:
docker pull git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev - Check environment variables: Make sure all required vars are set
Health Check Failing
# 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
- In Dokploy, check webhook configuration
- In Gitea, go to repo Settings → Webhooks
- Verify webhook URL and secret match
- 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:
- ✅ Push changes to
devbranch - ⏳ Create 3 Dokploy applications (dev, staging, prod)
- ⏳ Configure webhooks for each branch
- ⏳ Deploy and test each environment
Questions? Check the main README.md or CLAUDE.md for more details.