feat: production-ready deployment with multi-language UI

- Add multi-language support (NL, AR, EN) with RTL
- Improve health checks (SSL-tolerant, multi-endpoint)
- Add DELETE /api/stack/:name for cleanup
- Add persistent storage (portal-ai-workspace-{name})
- Improve rollback (delete domain, app, project)
- Increase SSE timeout to 255s
- Add deployment strategy documentation
This commit is contained in:
Oussama Douhou
2026-01-10 09:56:33 +01:00
parent eb6d5142ca
commit 2f306f7d68
10 changed files with 1196 additions and 462 deletions

View File

@@ -1,94 +1,170 @@
# AI Agent Instructions - AI Stack Deployer
# AI Stack Deployer - Agent Instructions
**Project-specific guidelines for AI coding agents**
**MANDATORY: Read before implementing**
---
## Project Context
## PROJECT IDENTITY
This is a self-service portal for deploying OpenCode AI stacks. Users enter their name and get a fully deployed AI assistant at `{name}.ai.flexinit.nl`.
Self-service portal: Users enter name → Get deployed AI assistant at `{name}.ai.flexinit.nl`
**Key Technologies:**
- Bun + Hono (backend)
- Vanilla HTML/CSS/JS (frontend)
- Docker + Dokploy (deployment)
- Hetzner DNS API + Traefik (networking)
**Stack:**
- Backend: Bun + Hono
- Frontend: Vanilla HTML/CSS/JS (NO frameworks)
- Deploy: Docker + Dokploy
- Network: Hetzner DNS + Traefik
---
## Critical Information
## MANDATORY: DEPLOYMENT WORKFLOW
### API Endpoints
**User submits name → Deploy AI stack (5 steps):**
1. **Validate** - Check name format, reserved names, availability
2. **DNS** - Create `{name}.ai.flexinit.nl` → 144.76.116.169 (or rely on wildcard)
3. **Dokploy Project** - Create project via API
4. **Dokploy App** - Create application, add domain, deploy
5. **Verify** - Wait for SSL, test OpenCode + ttyd endpoints
**CRITICAL:** Each step MUST succeed before proceeding. No parallel execution.
---
## MANDATORY: SECRETS (Get FIRST)
#### Hetzner Cloud API (DNS)
```bash
# Base URL
https://api.hetzner.cloud/v1
# IMPORTANT: Use /zones/{zone_id}/rrsets NOT /dns/zones
# The old dns.hetzner.com API is DEPRECATED
# List records (RRSets)
GET /zones/343733/rrsets
Authorization: Bearer {HETZNER_API_TOKEN}
# Create DNS record (individual A record for user)
# NOTE: Wildcard *.ai.flexinit.nl already exists pointing to Traefik
# For per-user records (optional, wildcard handles it):
POST /zones/343733/rrsets
Authorization: Bearer {HETZNER_API_TOKEN}
Content-Type: application/json
{
"name": "{name}.ai",
"type": "A",
"ttl": 300,
"records": [
{
"value": "144.76.116.169",
"comment": "AI Stack for {name}"
}
]
}
# Zone ID for flexinit.nl: 343733
# Traefik IP: 144.76.116.169
# Wildcard *.ai.flexinit.nl -> 144.76.116.169 (already configured)
DOKPLOY_TOKEN=$(bws-wrapper get 6b3618fc-ba02-49bc-bdc8-b3c9004087bc)
HETZNER_TOKEN=$(bws-wrapper get <HETZNER_BWS_ID>) # Ask user or search BWS
```
#### Dokploy API
---
## MANDATORY: API COMMANDS (COPY EXACTLY)
### Hetzner DNS API (MODIFY ONLY: name, comment)
**CRITICAL:** Use `api.hetzner.cloud/v1`, NOT `dns.hetzner.com` (deprecated)
```bash
# Base URL
http://10.100.0.20:3000/api
# List existing DNS records
curl -s "https://api.hetzner.cloud/v1/zones/343733/rrsets" \
-H "Authorization: Bearer $HETZNER_TOKEN"
# All requests need:
Authorization: Bearer {DOKPLOY_API_TOKEN}
Content-Type: application/json
# Key endpoints:
POST /project.create # Create project
POST /application.create # Create application
POST /domain.create # Add domain to application
POST /application.deploy # Trigger deployment
GET /application.one # Get application status
# Create A record (OPTIONAL - wildcard *.ai.flexinit.nl already exists)
curl -X POST "https://api.hetzner.cloud/v1/zones/343733/rrsets" \
-H "Authorization: Bearer $HETZNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "{name}.ai",
"type": "A",
"ttl": 300,
"records": [{"value": "144.76.116.169", "comment": "AI Stack for {name}"}]
}'
```
### BWS Secrets
**Constants:**
- Zone ID: `343733` (flexinit.nl)
- Traefik IP: `144.76.116.169`
- Wildcard: `*.ai.flexinit.nl` → 144.76.116.169 (pre-configured)
| Purpose | BWS ID |
|---------|--------|
| Dokploy Token | `6b3618fc-ba02-49bc-bdc8-b3c9004087bc` |
| Hetzner Token | Search BWS or ask user |
### Dokploy API (MODIFY ONLY: projectId, applicationId, name, domain)
### Infrastructure IPs
**Base:** `http://10.100.0.20:3000/api`
- **Traefik**: 144.76.116.169 (public, SSL termination)
- **Dokploy**: 10.100.0.20:3000 (internal)
- **DNS Zone**: flexinit.nl, ID 343733
```bash
# Create project
curl -X POST "http://10.100.0.20:3000/api/project.create" \
-H "Authorization: Bearer $DOKPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "{username}-ai-stack"}'
# Create application
curl -X POST "http://10.100.0.20:3000/api/application.create" \
-H "Authorization: Bearer $DOKPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"projectId": "...", "name": "{username}-opencode"}'
# Add domain
curl -X POST "http://10.100.0.20:3000/api/domain.create" \
-H "Authorization: Bearer $DOKPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"applicationId": "...", "host": "{name}.ai.flexinit.nl"}'
# Deploy application
curl -X POST "http://10.100.0.20:3000/api/application.deploy" \
-H "Authorization: Bearer $DOKPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"applicationId": "..."}'
# Check status
curl -s "http://10.100.0.20:3000/api/application.one?applicationId=..." \
-H "Authorization: Bearer $DOKPLOY_TOKEN"
```
---
## Implementation Guidelines
## CRITICAL: MANDATORY RULES
**NEVER:**
- Use frontend frameworks (React, Vue, Svelte, etc.)
- Use old Hetzner API (`dns.hetzner.com`)
- Create Dokploy domain BEFORE application exists
- Skip error handling on ANY API call
- Store secrets in code
- Proceed to next step if current step fails
**ALWAYS:**
- Validate name (alphanumeric, 3-20 chars, not reserved)
- Get tokens from BWS first
- Wait for SSL certificate (30-60 seconds)
- Verify each deployment step
- Handle errors gracefully with user feedback
- Mobile-responsive design
---
## CRITICAL: GOTCHAS
1. **Hetzner API** - Use `api.hetzner.cloud`, NOT `dns.hetzner.com` (deprecated)
2. **Dokploy domain** - MUST create domain AFTER application exists (or 404)
3. **SSL delay** - Let's Encrypt cert takes 30-60 seconds to provision
4. **ttyd WebSocket** - Requires Traefik WebSocket support configured
5. **Container startup** - OpenCode server takes ~10 seconds to be ready
6. **Reserved names** - Block: admin, api, www, root, test, staging, prod, etc.
---
## MANDATORY: VALIDATION
**Before deployment, check:**
```javascript
// Name validation regex
const isValid = /^[a-z0-9]{3,20}$/.test(name);
// Reserved names
const reserved = ['admin', 'api', 'www', 'root', 'test', 'staging', 'prod'];
const isReserved = reserved.includes(name);
```
---
## MANDATORY: VERIFICATION
**After deployment, verify:**
```bash
# 1. SSL certificate provisioned
curl -I "https://{name}.ai.flexinit.nl" # Should return 200
# 2. OpenCode server responding
curl "https://{name}.ai.flexinit.nl" # Should show OpenCode UI
# 3. ttyd terminal accessible
curl -I "https://{name}.ai.flexinit.nl:7681" # Should return 200
```
---
## Implementation Patterns
### Backend (Bun + Hono)
@@ -197,9 +273,9 @@ CMD ["sh", "-c", "opencode serve --host 0.0.0.0 --port 8080 & ttyd -W -p 7681 op
---
## Testing Checklist
## MANDATORY: TESTING CHECKLIST
Before considering implementation complete:
**Before marking complete, ALL must pass:**
- [ ] Name validation works (alphanumeric, 3-20 chars)
- [ ] Reserved names blocked (admin, api, www, root, etc.)
@@ -212,17 +288,7 @@ Before considering implementation complete:
- [ ] Mobile responsive
- [ ] Loading states smooth (no flicker)
---
## Common Gotchas
1. **Hetzner API** - Use `api.hetzner.cloud`, NOT `dns.hetzner.com` (deprecated)
2. **Dokploy domain** - Must create domain AFTER application exists
3. **SSL delay** - Let's Encrypt cert may take 30-60 seconds
4. **ttyd WebSocket** - Needs proper Traefik WebSocket support
5. **Container startup** - OpenCode server takes ~10 seconds to be ready
---
## File Reading Order
@@ -233,17 +299,6 @@ When starting implementation, read in this order:
---
## Do NOT
- Do NOT use any frontend framework (React, Vue, etc.)
- Do NOT add unnecessary dependencies
- Do NOT store secrets in code
- Do NOT skip error handling
- Do NOT make the UI overly complex
- Do NOT forget mobile responsiveness
---
## Reference Projects
- `~/locale-projects/oh-my-opencode-free` - The stack being deployed

290
docs/DEPLOYMENT_STRATEGY.md Normal file
View File

@@ -0,0 +1,290 @@
# AI Stack Deployer - Deployment Strategy
## Overview
Deploy the AI Stack Deployer portal to production, making it accessible at `deploy.ai.flexinit.nl`.
## User Stack Container (oh-my-opencode-free)
### Image Details
| Property | Value |
|----------|-------|
| Registry | `git.app.flexinit.nl` |
| Image | `oussamadouhou/oh-my-opencode-free:latest` |
| Base | `oven/bun:debian` |
| Port | 8080 (OpenCode server) |
| CI/CD | Gitea Actions (auto-build on push) |
### Pre-configured Free Models
| Agent | Model | Purpose |
|-------|-------|---------|
| Sisyphus (main) | `glm-4.7-free` | Primary coding |
| Oracle | `gpt-5-nano` | Architecture |
| Librarian | `minimax-m2.1-free` | Documentation |
| Explore | `grok-code` | Codebase search |
| Frontend | `glm-4.7-free` | UI/UX |
| Document Writer | `gpt-5-nano` | Docs |
### Baked Configuration
```
/shared/config/
├── opencode.json # CLI config
├── opencode.jsonc # Permissions
└── oh-my-opencode.json # Agent model assignments
```
### Persistent Storage (per user)
| Volume | Mount Path | Purpose |
|--------|------------|---------|
| `workspace-{name}` | `/workspace` | User projects |
## Architecture
```
Internet
┌─────────────────────────────────────────────┐
│ Traefik (144.76.116.169) │
│ - SSL termination (wildcard *.ai.flexinit.nl)│
│ - Routes deploy.ai.flexinit.nl → portal │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ AI Stack Deployer Container │
│ - Port 3000 │
│ - Needs access to 10.100.0.20 (Dokploy) │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Dokploy (10.100.0.20:3000) │
│ - Creates user stacks │
│ - Manages containers │
└─────────────────────────────────────────────┘
```
## Deployment Options
### Option A: Deploy via Dokploy (Recommended)
**Pros**: Self-service, consistent with stack deployments, automatic restarts
**Cons**: Circular dependency (if Dokploy is down, can't redeploy)
### Option B: Direct Docker on Traefik Host
**Pros**: Independent of Dokploy, simpler network (same host as Traefik)
**Cons**: Manual management, no Dokploy UI
### Option C: Separate VM with Docker Compose
**Pros**: Isolation, can use docker-compose.yml directly
**Cons**: Extra VM, network routing complexity
**Decision: Option A (Dokploy)** - Eat our own dog food, leverage existing infrastructure.
## Step-by-Step Deployment
### Phase 1: Prepare Image
```bash
# 1. Build and tag image
docker build -t git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest .
# 2. Push to registry
docker push git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest
```
### Phase 2: Create Dokploy Project
1. Open Dokploy UI: https://app.flexinit.nl
2. Create project: `ai-stack-deployer-portal`
3. Create application:
- Name: `deployer`
- Type: Docker Image
- Image: `git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest`
### Phase 3: Configure Environment
Set these environment variables in Dokploy:
| Variable | Value | Source |
|----------|-------|--------|
| `NODE_ENV` | `production` | Static |
| `PORT` | `3000` | Static |
| `HOST` | `0.0.0.0` | Static |
| `DOKPLOY_URL` | `http://10.100.0.20:3000` | Static |
| `DOKPLOY_API_TOKEN` | `<token>` | BWS: `6b3618fc-ba02-49bc-bdc8-b3c9004087bc` |
| `STACK_DOMAIN_SUFFIX` | `ai.flexinit.nl` | Static |
| `STACK_IMAGE` | `git.app.flexinit.nl/oussamadouhou/oh-my-opencode-free:latest` | Static |
| `RESERVED_NAMES` | `admin,api,www,root,system,test,demo,portal,deploy` | Static |
### Phase 4: Configure Domain
1. In Dokploy application settings, add domain:
- Host: `deploy.ai.flexinit.nl`
- HTTPS: Enabled
- Port: 3000
2. DNS is already configured (wildcard `*.ai.flexinit.nl` → 144.76.116.169)
### Phase 5: Deploy
1. Click "Deploy" in Dokploy UI
2. Wait for container to start (~30s)
3. Verify health: `curl https://deploy.ai.flexinit.nl/health`
## Network Requirements
The deployer container MUST be able to reach:
- `10.100.0.20:3000` - Dokploy API (internal network)
This works automatically when deployed via Dokploy since containers share the internal network.
## Secrets Management
### Using BWS
```bash
# Get Dokploy token
bws secret get 6b3618fc-ba02-49bc-bdc8-b3c9004087bc
# Set in Dokploy environment variables (manually or via API)
```
### Rotation Strategy
1. Generate new token in Dokploy UI
2. Store in BWS
3. Update environment variable in Dokploy
4. Redeploy application
## Monitoring
### Health Checks
- **Endpoint**: `GET /health`
- **Interval**: 30 seconds
- **Expected**: `{"status":"healthy",...}`
### Alerting
Set up monitoring for:
- Health endpoint failures
- High error rates in deployment logs
- Circuit breaker state changes
## Rollback Procedure
### Quick Rollback
```bash
# Via Dokploy UI
1. Go to application → Deployments
2. Click "Rollback" on previous successful deployment
```
### Manual Rollback
```bash
# Re-deploy previous image tag
docker pull git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:v0.1.0
# Update image in Dokploy and redeploy
```
## CI/CD Pipeline (Future)
```yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push
run: |
docker build -t git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:${{ github.sha }} .
docker push git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:${{ github.sha }}
- name: Deploy via Dokploy API
run: |
curl -X POST "$DOKPLOY_URL/api/application.update" \
-H "x-api-key: $DOKPLOY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"applicationId": "$APP_ID", "dockerImage": "...:${{ github.sha }}"}'
curl -X POST "$DOKPLOY_URL/api/application.deploy" \
-H "x-api-key: $DOKPLOY_API_TOKEN" \
-d '{"applicationId": "$APP_ID"}'
```
## Pre-Deployment Checklist
- [ ] Docker image builds successfully: `docker build -t test .`
- [ ] TypeScript compiles: `bun run typecheck`
- [ ] Health endpoint works locally: `curl localhost:3000/health`
- [ ] Dokploy API token is valid
- [ ] Registry credentials configured
- [ ] Domain DNS resolves correctly
## Post-Deployment Verification
```bash
# 1. Health check
curl https://deploy.ai.flexinit.nl/health
# 2. Name validation
curl https://deploy.ai.flexinit.nl/api/check/test-name
# 3. Frontend loads
curl -s https://deploy.ai.flexinit.nl | grep -q "AI Stack Deployer"
# 4. Full deployment test (optional)
curl -X POST https://deploy.ai.flexinit.nl/api/deploy \
-H "Content-Type: application/json" \
-d '{"name": "smoke-test"}'
# 5. Cleanup test
curl -X DELETE https://deploy.ai.flexinit.nl/api/stack/smoke-test
```
## Security Considerations
1. **No public auth yet** - Consider adding authentication for production
2. **Rate limiting** - Not implemented, consider adding
3. **API token exposure** - Stored in Dokploy env vars (encrypted at rest)
4. **Delete endpoint** - Should require authentication in production
## Estimated Timeline
| Phase | Duration | Notes |
|-------|----------|-------|
| Image build & push | 5 min | Automated |
| Dokploy project setup | 10 min | One-time |
| Environment config | 5 min | One-time |
| Deployment | 2 min | Per deploy |
| Verification | 5 min | Per deploy |
| **Total first deploy** | ~30 min | |
| **Subsequent deploys** | ~10 min | |
## Next Steps
1. Build and push Docker image
2. Create Dokploy project and application
3. Configure environment variables
4. Add domain
5. Deploy and verify
6. Set up monitoring
7. (Optional) Implement CI/CD

View File

@@ -1,178 +1,110 @@
# AI Stack Deployer - Testing Documentation
# Testing Guide
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
## Quick Verification
Your only and main job now is to accurate follow the orders from the user. Your job is to validate and audit all code for issues, failures or misconfiguration. Your test plan should look like:
Phase 1: Preparation & Static Analysis
Code Review (Audit): Have the code reviewed by a colleague or use a static analysis tool (linter) to identify syntax and style errors prior to execution.
Logic Validation: Verify that the code logic aligns with the initial requirements (checking what it is supposed to do, not necessarily if it runs yet).
Phase 2: Unit Testing (Automated)
3. Run Unit Tests: Execute tests on small, isolated components (e.g., verifying that the authentication function returns the correct token).
4. Check Code Coverage: Ensure that critical paths and functions are actually being tested.
Phase 3: Integration & Functional Testing
5. Authentication Test: Verify that the application or script can successfully connect to required external systems (databases, APIs, login services). Note: This is a prerequisite for the next steps.
6. Execute Scripts (Happy Path): Run the script or application in the standard, intended way.
7. Monitor Logs: Monitor the output for error logs, warnings, or unexpected behavior during execution.
Phase 4: Evaluation & Reporting
8. Analyze Results: Compare the actual output against the expected results.
9. Report Status: If all tests pass, approve the code for release and inform the user.
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
**Last Updated**: 2026-01-09
## Infrastructure Context (from memory/docs)
| Service | IP | Port | Notes |
|---------|-----|------|-------|
| Dokploy | 10.100.0.20 | 3000 | Container orchestration, Grafana Loki also here |
| Loki | 10.100.0.20 | 3100 | Logging aggregation |
| Grafana | 10.100.0.20 | 3000 (UI) | Dashboards at https://logs.intra.flexinit.nl |
| Traefik | 10.100.0.12 | - | VM 202 - Reverse proxy, SSL |
| AI Server | 10.100.0.19 | - | VM 209 - OpenCode agents |
**Dokploy config location**: `/etc/dokploy/compose/` on 10.100.0.20
---
## Phase 1 Test Results
### 1. Hono Server
| Test | Status | Notes |
|------|--------|-------|
| Server starts | ✅ PASS | Runs on port 3000 |
| Health endpoint | ✅ PASS | Returns JSON with status, timestamp, version |
| Root endpoint | ✅ PASS | Returns API endpoint list |
**Commands:**
```bash
# Start dev server
# 1. Start server
bun run dev
# Test health endpoint
# 2. Health check
curl http://localhost:3000/health
# Response: {"status":"healthy","timestamp":"2026-01-09T14:13:50.237Z","version":"0.1.0","service":"ai-stack-deployer"}
# Test root endpoint
curl http://localhost:3000/
# Response: {"message":"AI Stack Deployer API","endpoints":{...}}
# 3. Open browser
open http://localhost:3000
```
### 2. Hetzner DNS Client
## API Endpoints
| Test | Status | Notes |
|------|--------|-------|
| Connection test | ✅ PASS | Successfully connects to Hetzner Cloud API |
| Zone access | ✅ PASS | Zone "flexinit.nl" (ID: 343733) accessible |
| RRSets listing | ✅ PASS | Returns 75 RRSets |
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/health` | GET | Server status |
| `/api/check/:name` | GET | Name availability |
| `/api/deploy` | POST | Start deployment |
| `/api/status/:id` | GET | SSE progress stream |
| `/api/stack/:name` | DELETE | Delete stack and cleanup |
**IMPORTANT FINDING:**
- Hetzner DNS has been **migrated from dns.hetzner.com to api.hetzner.cloud**
- The old DNS Console API at `dns.hetzner.com/api/v1` is deprecated
- Must use new Hetzner Cloud API at `api.hetzner.cloud/v1`
- Authentication: `Authorization: Bearer {token}` (NOT `Auth-API-Token`)
- Endpoints: `/zones`, `/zones/{id}/rrsets`
## Test Checklist
**Commands:**
```bash
# Test Hetzner client
bun run src/test-clients.ts
### Backend
# Manual API test
curl -s "https://api.hetzner.cloud/v1/zones" \
-H "Authorization: Bearer $HETZNER_API_TOKEN"
```
| Test | Command | Expected |
|------|---------|----------|
| Server starts | `bun run dev` | "starting on http://0.0.0.0:3000" |
| Health endpoint | `curl localhost:3000/health` | `{"status":"healthy"...}` |
| TypeScript | `bun run typecheck` | No errors |
| Dokploy connection | Check `/api/check/test-name` | Returns availability |
### 3. Dokploy Client
### Frontend
| Test | Status | Notes |
|------|--------|-------|
| Connection test | ❌ FAIL | Returns "Unauthorized" |
| Server accessible | ✅ PASS | Dokploy UI loads at http://10.100.0.20:3000 |
| Test | Action | Expected |
|------|--------|----------|
| Page loads | Open localhost:3000 | Dark theme, centered content |
| Typewriter | Wait 2s | "Choose Your Stack Name" animates |
| Language switch | Click 🇲🇦 | Arabic text, RTL layout |
| Name validation | Type "ab" | Error: too short |
| Reserved name | Type "admin" | Error: reserved |
| Valid name | Type "my-stack" | "✓ Name is available!" |
**BLOCKER:**
- Token `app_deployment...` returns 401 Unauthorized
- Token was created 2026-01-05 but may be expired or have insufficient permissions
- **ACTION REQUIRED**: Generate new token from Dokploy dashboard
### Deployment Flow
**Steps to generate new Dokploy API token:**
1. Navigate to https://deploy.intra.flexinit.nl or http://10.100.0.20:3000
2. Login with admin credentials
3. Go to: Settings (gear icon) → Profile
4. Scroll to "API Tokens" section
5. Click "Generate" button
6. Copy the new token (format: `app_deployment<random>`)
7. Update BWS secret: `bws secret edit 6b3618fc-ba02-49bc-bdc8-b3c9004087bc`
8. Update local `.env` file
| Step | Indicator |
|------|-----------|
| Submit form | Progress bar appears |
| SSE updates | Log entries animate in |
| Success | Typewriter: "Deployment Complete" |
| Error | Typewriter: "Deployment Failed" |
**Commands:**
```bash
# Test Dokploy API (currently failing)
curl -s "http://10.100.0.20:3000/api/project.all" \
-H "Authorization: Bearer $DOKPLOY_API_TOKEN"
# Response: {"message":"Unauthorized"}
```
## Infrastructure
---
| Service | URL | Purpose |
|---------|-----|---------|
| Dokploy | https://app.flexinit.nl | Container orchestration |
| Traefik | 144.76.116.169 | SSL termination |
| Stacks | *.ai.flexinit.nl | Deployed AI assistants |
## Environment Configuration
### .env File (from .env.example)
## Full Deployment Test
```bash
PORT=3000
HOST=0.0.0.0
# 1. Generate unique name
NAME="test-$(date +%s | tail -c 5)"
# Hetzner Cloud DNS API (WORKING)
HETZNER_API_TOKEN=<from BWS - HETZNER_DNS_TOKEN>
HETZNER_ZONE_ID=343733
# 2. Check availability
curl -s http://localhost:3000/api/check/$NAME
# Dokploy API (NEEDS NEW TOKEN)
DOKPLOY_URL=http://10.100.0.20:3000
DOKPLOY_API_TOKEN=<generate from Dokploy dashboard>
# 3. Deploy
curl -s -X POST http://localhost:3000/api/deploy \
-H "Content-Type: application/json" \
-d "{\"name\": \"$NAME\"}"
STACK_DOMAIN_SUFFIX=ai.flexinit.nl
STACK_IMAGE=git.app.flexinit.nl/oussamadouhou/oh-my-opencode-free:latest
TRAEFIK_IP=144.76.116.169
# 4. Monitor SSE (wait ~2-3 min)
curl -N http://localhost:3000/api/status/<deployment-id>
RESERVED_NAMES=admin,api,www,root,system,test,demo,portal
# 5. Verify stack accessible
curl -s https://$NAME.ai.flexinit.nl
# 6. Cleanup
curl -s -X DELETE http://localhost:3000/api/stack/$NAME
```
---
## Cleanup Commands
## BWS Secrets Reference
```bash
# Delete specific stack
curl -s -X DELETE http://localhost:3000/api/stack/my-stack
| Secret | BWS Key | Status |
|--------|---------|--------|
| Hetzner API Token | `HETZNER_DNS_TOKEN` | ✅ Working |
| Dokploy API Token | `DOKPLOY_API_TOKEN` (ID: 6b3618fc-ba02-49bc-bdc8-b3c9004087bc) | ❌ Expired/Invalid |
# List all projects (direct Dokploy)
source .env && curl -s -H "x-api-key: $DOKPLOY_API_TOKEN" \
"$DOKPLOY_URL/api/project.all" | jq '.[].name'
```
---
## Common Issues
## Gotchas & Learnings
### 1. Hetzner DNS API Migration
- **Old API**: `dns.hetzner.com/api/v1` with `Auth-API-Token` header
- **New API**: `api.hetzner.cloud/v1` with `Authorization: Bearer` header
- Zone ID 343733 works in new API
- RRSets replace Records concept
### 2. Dokploy Token Format
- Format: `app_deployment<random>`
- Created from: Dashboard > Settings > Profile > API Tokens
- Must have permissions for project/application management
---
## Next Steps
1. [ ] Generate new Dokploy API token from dashboard
2. [ ] Update BWS with new token
3. [ ] Verify Dokploy client works
4. [ ] Proceed to Phase 2 implementation
| Issue | Solution |
|-------|----------|
| CSS not loading | Check `/style.css` returns CSS, not HTML |
| 401 on Dokploy | Regenerate API token in Dokploy dashboard |
| Typewriter not running | Check browser console for JS errors |
| RTL not working | Verify `dir="rtl"` on `<html>` element |
| Health check timeout | Container startup can take 1-2 min, timeout is 3 min |
| SSL cert errors | Health check treats SSL errors as "alive" during provisioning |
| SSE disconnects | idleTimeout set to 255s (max), long deployments should complete |