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.
10 KiB
Shared Project Deployment Architecture
Overview
The AI Stack Deployer portal deploys all user AI stacks to a single shared Dokploy project instead of creating a new project for each user.
Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ Dokploy: ai-stack-portal (Shared Project) │
│ ID: 2y2Glhz5Wy0dBNf6BOR_- │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 📦 Portal Application: ai-stack-deployer-prod │
│ ├─ Domain: portal.ai.flexinit.nl │
│ ├─ Image: git.app.flexinit.nl/.../ai-stack-deployer:latest│
│ └─ Env: SHARED_PROJECT_ID=$${{project.SHARED_PROJECT_ID}} │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ 📦 User Stack: john-dev │
│ ├─ Domain: john-dev.ai.flexinit.nl │
│ ├─ Image: git.app.flexinit.nl/.../agent-stack:latest │
│ └─ Deployed by: Portal │
│ │
│ 📦 User Stack: jane-prod │
│ ├─ Domain: jane-prod.ai.flexinit.nl │
│ ├─ Image: git.app.flexinit.nl/.../agent-stack:latest │
│ └─ Deployed by: Portal │
│ │
│ 📦 User Stack: alice-test │
│ ├─ Domain: alice-test.ai.flexinit.nl │
│ ├─ Image: git.app.flexinit.nl/.../agent-stack:latest │
│ └─ Deployed by: Portal │
│ │
└─────────────────────────────────────────────────────────────────┘
How It Works
Step 1: Portal Reads Configuration
When a user submits a stack name (e.g., "john-dev"), the portal:
-
Reads environment variables:
const sharedProjectId = process.env.SHARED_PROJECT_ID; const sharedEnvironmentId = process.env.SHARED_ENVIRONMENT_ID; -
These are set via Dokploy's project-level variables:
environment: - SHARED_PROJECT_ID=$${{project.SHARED_PROJECT_ID}} - SHARED_ENVIRONMENT_ID=$${{project.SHARED_ENVIRONMENT_ID}}Note: The double
$$is required to escape the dollar sign in Docker Compose.
Step 2: Portal Deploys to Shared Project
Instead of creating a new project, the portal:
// OLD BEHAVIOR (legacy):
// createProject(`ai-stack-${username}`) ❌ Creates new project per user
// NEW BEHAVIOR (current):
// Uses existing shared project ID ✅
const projectId = sharedProjectId; // From environment variable
const environmentId = sharedEnvironmentId;
// Creates application IN the shared project
createApplication({
projectId: projectId,
environmentId: environmentId,
name: `${username}-stack`,
image: 'git.app.flexinit.nl/.../agent-stack:latest',
domain: `${username}.ai.flexinit.nl`
});
Step 3: User Accesses Their Stack
User visits https://john-dev.ai.flexinit.nl → Traefik routes to their application inside the shared project.
Configuration Steps
1. Create Shared Project in Dokploy
-
In Dokploy UI, create project:
- Name:
ai-stack-portal - Description: "Shared project for all user AI stacks"
- Name:
-
Get the Project ID:
# Via API curl -s "http://10.100.0.20:3000/api/project.all" \ -H "Authorization: Bearer $DOKPLOY_API_TOKEN" | \ jq -r '.[] | select(.name=="ai-stack-portal") | .id' # Output: 2y2Glhz5Wy0dBNf6BOR_- -
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' # Output: RqE9OFMdLwkzN7pif1xN8
2. Set Project-Level Variables
In the shared project (ai-stack-portal), add these project-level environment variables:
| Variable | Value | Example |
|---|---|---|
SHARED_PROJECT_ID |
Your project ID | 2y2Glhz5Wy0dBNf6BOR_- |
SHARED_ENVIRONMENT_ID |
Your environment ID | RqE9OFMdLwkzN7pif1xN8 |
How to set in Dokploy UI:
- Go to Project → Settings → Environment Variables
- Add variables at project level (not application level)
3. Deploy Portal Application
Deploy the portal inside the same shared project:
-
Application Details:
- Name:
ai-stack-deployer-prod - Type: Docker Compose
- Compose File:
docker-compose.prod.yml - Branch:
main
- Name:
-
The docker-compose file automatically references project variables:
environment: - SHARED_PROJECT_ID=${{project.SHARED_PROJECT_ID}} # ← Magic happens here - SHARED_ENVIRONMENT_ID=${{project.SHARED_ENVIRONMENT_ID}} -
Dokploy resolves
${{project.VAR}}to the actual value from project-level variables.
Benefits
✅ Centralized Management
All user stacks in one place:
- Easy to list all active stacks
- Shared monitoring dashboard
- Centralized logging
✅ Resource Efficiency
- No overhead of separate projects per user
- Shared network and resources
- Easier to manage quotas
✅ Simplified Configuration
- Project-level environment variables shared by all stacks
- Single source of truth for common configs
- Easy to update STACK_IMAGE for all users
✅ Better Organization
Projects in Dokploy:
├── ai-stack-portal (500 user applications) ✅ Clean
└── NOT:
├── ai-stack-john
├── ai-stack-jane
├── ai-stack-alice
└── ... (500 separate projects) ❌ Messy
Fallback Behavior
If SHARED_PROJECT_ID and SHARED_ENVIRONMENT_ID are not set, the portal falls back to legacy behavior:
// Code in src/orchestrator/production-deployer.ts (lines 187-196)
const sharedProjectId = config.sharedProjectId || process.env.SHARED_PROJECT_ID;
const sharedEnvironmentId = config.sharedEnvironmentId || process.env.SHARED_ENVIRONMENT_ID;
if (sharedProjectId && sharedEnvironmentId) {
// Use shared project ✅
state.resources.projectId = sharedProjectId;
state.resources.environmentId = sharedEnvironmentId;
return;
}
// Fallback: Create separate project per user ⚠️
const projectName = `ai-stack-${config.stackName}`;
const existingProject = await this.client.findProjectByName(projectName);
// ...
This ensures backwards compatibility but is not recommended.
Troubleshooting
Portal Creates Separate Projects Instead of Using Shared Project
Cause: SHARED_PROJECT_ID or SHARED_ENVIRONMENT_ID not set.
Solution:
-
Check project-level variables in Dokploy:
curl -s "http://10.100.0.20:3000/api/project.one?projectId=YOUR_PROJECT_ID" \ -H "Authorization: Bearer $DOKPLOY_API_TOKEN" | \ jq '.environmentVariables' -
Ensure the portal application's docker-compose references them:
environment: - SHARED_PROJECT_ID=${{project.SHARED_PROJECT_ID}} - SHARED_ENVIRONMENT_ID=${{project.SHARED_ENVIRONMENT_ID}} -
Redeploy the portal application.
Variable Reference Not Working
Symptom: Portal logs show undefined for SHARED_PROJECT_ID.
Cause: Using wrong syntax.
Correct syntax:
- SHARED_PROJECT_ID=${{project.SHARED_PROJECT_ID}} ✅
Wrong syntax:
- SHARED_PROJECT_ID=${SHARED_PROJECT_ID} ❌ (shell substitution, not Dokploy)
- SHARED_PROJECT_ID={{project.SHARED_PROJECT_ID}} ❌ (missing $)
How to Verify Configuration
Check portal container environment:
# SSH into Dokploy host
ssh user@10.100.0.20
# Inspect portal container
docker exec ai-stack-deployer env | grep SHARED
# Should show:
SHARED_PROJECT_ID=2y2Glhz5Wy0dBNf6BOR_-
SHARED_ENVIRONMENT_ID=RqE9OFMdLwkzN7pif1xN8
Environment-Specific Shared Projects
You can have separate shared projects for dev/staging/prod:
| Portal Environment | Shared Project | Purpose |
|---|---|---|
| Dev | ai-stack-portal-dev |
Development user stacks |
| Staging | ai-stack-portal-staging |
Staging user stacks |
| Prod | ai-stack-portal |
Production user stacks |
Each portal deployment references its own shared project:
portal-dev.ai.flexinit.nl→ai-stack-portal-devportal-staging.ai.flexinit.nl→ai-stack-portal-stagingportal.ai.flexinit.nl→ai-stack-portal
Migration from Legacy
If you're currently using the legacy behavior (separate projects per user):
Option 1: Gradual Migration
- New deployments use shared project
- Old deployments remain in separate projects
- Migrate old stacks manually over time
Option 2: Full Migration
- Create shared project
- Set project-level variables
- Redeploy all user stacks to shared project
- Delete old separate projects
Note: Migration requires downtime for each stack being moved.
Reference
- Environment Variable Syntax: See Dokploy docs on project-level variables
- Code Location:
src/orchestrator/production-deployer.ts(lines 178-200) - Example IDs:
.env.example(lines 25-27)
Questions? Check the main deployment guide: DOKPLOY_DEPLOYMENT.md