Changes:
- Create Gitea workflow for ai-stack-deployer
- Trigger on main branch (default branch)
- Use oussamadouhou + REGISTRY_TOKEN for authentication
- Build from ./Dockerfile
This enables :latest tag creation via {{is_default_branch}}.
Tags created:
- git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest
- git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:<sha>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
5.8 KiB
5.8 KiB
AI Agent Instructions - AI Stack Deployer
Project-specific guidelines for AI coding agents
Project Context
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.
Key Technologies:
- Bun + Hono (backend)
- Vanilla HTML/CSS/JS (frontend)
- Docker + Dokploy (deployment)
- Hetzner DNS API + Traefik (networking)
Critical Information
API Endpoints
Hetzner Cloud API (DNS)
# 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 API
# Base URL
http://10.100.0.20:3000/api
# 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
BWS Secrets
| Purpose | BWS ID |
|---|---|
| Dokploy Token | 6b3618fc-ba02-49bc-bdc8-b3c9004087bc |
| Hetzner Token | Search BWS or ask user |
Infrastructure IPs
- Traefik: 144.76.116.169 (public, SSL termination)
- Dokploy: 10.100.0.20:3000 (internal)
- DNS Zone: flexinit.nl, ID 343733
Implementation Guidelines
Backend (Bun + Hono)
// Use Hono for routing
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { serveStatic } from 'hono/bun';
const app = new Hono();
// Serve frontend
app.use('/*', serveStatic({ root: './src/frontend' }));
// API routes
app.post('/api/deploy', deployHandler);
app.get('/api/status/:id', statusHandler);
app.get('/api/check/:name', checkHandler);
SSE Implementation
// Server-Sent Events for progress updates
app.get('/api/status/:id', (c) => {
return streamSSE(c, async (stream) => {
// Send progress updates
await stream.writeSSE({
event: 'progress',
data: JSON.stringify({ step: 'dns', status: 'completed' })
});
// ... more updates
await stream.writeSSE({
event: 'complete',
data: JSON.stringify({ url: 'https://...' })
});
});
});
Frontend (Vanilla JS State Machine)
// State: 'idle' | 'deploying' | 'success' | 'error'
let state = 'idle';
function setState(newState, data = {}) {
state = newState;
render(state, data);
}
// SSE connection
const eventSource = new EventSource(`/api/status/${deploymentId}`);
eventSource.addEventListener('progress', (e) => {
const data = JSON.parse(e.data);
updateProgress(data);
});
eventSource.addEventListener('complete', (e) => {
setState('success', JSON.parse(e.data));
});
Docker Stack Template
The user stack needs:
- OpenCode server (port 8080)
- ttyd web terminal (port 7681)
FROM git.app.flexinit.nl/oussamadouhou/oh-my-opencode-free:latest
# Install ttyd
RUN apt-get update && apt-get install -y ttyd
# Expose ports
EXPOSE 8080 7681
# Start both services
CMD ["sh", "-c", "opencode serve --host 0.0.0.0 --port 8080 & ttyd -W -p 7681 opencode attach http://localhost:8080"]
Code Style
TypeScript
- Use strict mode
- Prefer
constoverlet - Use async/await over callbacks
- Handle all errors explicitly
- Type all function parameters and returns
CSS
- Use CSS variables for theming
- Mobile-first responsive design
- BEM-like naming:
.component__element--modifier - Dark theme as default
JavaScript (Frontend)
- No frameworks, vanilla only
- Module pattern for organization
- Event delegation where possible
- Graceful degradation
Testing Checklist
Before considering implementation complete:
- Name validation works (alphanumeric, 3-20 chars)
- Reserved names blocked (admin, api, www, root, etc.)
- DNS record created successfully
- Dokploy project created
- Application deployed and healthy
- SSL certificate provisioned
- ttyd accessible in browser
- Error states handled gracefully
- Mobile responsive
- Loading states smooth (no flicker)
Common Gotchas
- Hetzner API - Use
api.hetzner.cloud, NOTdns.hetzner.com(deprecated) - Dokploy domain - Must create domain AFTER application exists
- SSL delay - Let's Encrypt cert may take 30-60 seconds
- ttyd WebSocket - Needs proper Traefik WebSocket support
- Container startup - OpenCode server takes ~10 seconds to be ready
File Reading Order
When starting implementation, read in this order:
README.md- Full project specificationAGENTS.md- This file- Check existing oh-my-opencode-free for reference patterns
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~/projecten/infrastructure- Infrastructure patterns and docs