feat(SCOPONE-0010): vendor agent assets and clean docs
This commit is contained in:
601
.github/skills/deploy-app/README.md
vendored
Normal file
601
.github/skills/deploy-app/README.md
vendored
Normal file
@@ -0,0 +1,601 @@
|
||||
# Deploy Application Skill
|
||||
|
||||
Automated deployment of Dockerized applications to remote servers.
|
||||
|
||||
## Features
|
||||
|
||||
✅ **Automated Docker builds and registry pushes** to Gitea
|
||||
✅ **SSH-based remote deployment** with compose orchestration
|
||||
✅ **Automatic reverse proxy** configuration with Nginx Proxy Manager
|
||||
✅ **Subdomain routing** - Apps served as `{app-name}.{root-domain}`
|
||||
✅ **SSL certificate** generation via Let's Encrypt
|
||||
✅ **Smart .env handling** - Auto-generates secrets, infers service URLs
|
||||
✅ **Network topology** management (proxy, ollama)
|
||||
✅ **No host port publishing** - Webapps are reached through Docker network + Nginx Proxy Manager
|
||||
✅ **Organized data storage** - All volumes under `./data/`
|
||||
✅ **Health monitoring** and log inspection
|
||||
✅ **Rollback guidance** on failures
|
||||
✅ **Update-aware deployments** - Detects existing stack/proxy and performs safe in-place updates
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Run Setup Wizard
|
||||
|
||||
```bash
|
||||
cd .github/skills/deploy-app
|
||||
chmod +x setup-wizard.sh
|
||||
./setup-wizard.sh
|
||||
```
|
||||
|
||||
Follow the prompts to generate your deployment configuration.
|
||||
|
||||
### 2. Deploy Your App
|
||||
|
||||
```
|
||||
deploy production
|
||||
```
|
||||
|
||||
That's it! The agent will handle both first deploys and updates.
|
||||
|
||||
If the remote stack directory or proxy host already exists, deployment is treated as an **update** (with idempotent proxy handling and additive env/volume changes).
|
||||
|
||||
## Files
|
||||
|
||||
- **SKILL.md** - Complete skill documentation (for the agent)
|
||||
- **setup-wizard.sh** - Interactive configuration generator
|
||||
- **README.md** - This file (user documentation)
|
||||
|
||||
## What Gets Deployed
|
||||
|
||||
```
|
||||
Remote Server: /srv/docker/stacks/my-app/
|
||||
├── compose.yaml # Generated Docker Compose file
|
||||
├── .env # Environment variables (if detected)
|
||||
└── data/ # Persistent application data
|
||||
├── <app-data> # Main application data
|
||||
├── uploads/ # If app needs uploads (auto-detected)
|
||||
├── cache/ # If app needs cache (auto-detected)
|
||||
└── logs/ # If app needs logs (auto-detected)
|
||||
```
|
||||
|
||||
All volumes are organized under the `data/` directory for clean separation.
|
||||
|
||||
## Supported Application Types
|
||||
|
||||
- **webapp** - Web applications with reverse proxy and SSL
|
||||
- **service** - Backend services without public access
|
||||
- **worker** - Background workers and queue processors
|
||||
|
||||
## Requirements
|
||||
|
||||
**Local:**
|
||||
- Docker installed
|
||||
- Access to workspace with Dockerfile
|
||||
- Environment variable `DEPLOY_CONFIG_{TARGET}` configured
|
||||
|
||||
**Remote Server:**
|
||||
- Docker and Docker Compose installed
|
||||
- SSH access configured
|
||||
- Docker networks created (`proxy`, optionally `ollama`)
|
||||
|
||||
**Services:**
|
||||
- Gitea instance with container registry enabled
|
||||
- Nginx Proxy Manager (for webapps)
|
||||
|
||||
## Configuration
|
||||
|
||||
See configuration examples below for different deployment scenarios.
|
||||
|
||||
### Production Server
|
||||
|
||||
```bash
|
||||
export DEPLOY_CONFIG_PRODUCTION='{
|
||||
"gitea_registry_url": "gitea.mycompany.com",
|
||||
"gitea_username": "deploy-bot",
|
||||
"gitea_token": "a1b2c3d4e5f6g7h8i9j0",
|
||||
"gitea_namespace": "production-apps",
|
||||
"ssh_host": "prod-server",
|
||||
"stacks_dir": "/srv/docker/stacks",
|
||||
"domain": "sal.giize.com",
|
||||
"npm_url": "https://proxy.mycompany.com",
|
||||
"npm_username": "admin@mycompany.com",
|
||||
"npm_password": "npm-admin-password"
|
||||
}'
|
||||
|
||||
# All app-specific settings (name, port, env vars, volumes, type)
|
||||
# are auto-detected from your codebase!
|
||||
# App will be served at: {app-name}.sal.giize.com
|
||||
```
|
||||
|
||||
### Staging Server
|
||||
|
||||
```bash
|
||||
export DEPLOY_CONFIG_STAGING='{
|
||||
"gitea_registry_url": "gitea.mycompany.com",
|
||||
"gitea_username": "deploy-bot",
|
||||
"gitea_token": "a1b2c3d4e5f6g7h8i9j0",
|
||||
"gitea_namespace": "staging-apps",
|
||||
"ssh_host": "staging-server",
|
||||
"stacks_dir": "/home/deploy/stacks",
|
||||
"domain": "staging.sal.giize.com",
|
||||
"npm_url": "https://proxy-staging.mycompany.com",
|
||||
"npm_username": "admin@mycompany.com",
|
||||
"npm_password": "npm-admin-password"
|
||||
}'
|
||||
```
|
||||
|
||||
### AI Service with LLM
|
||||
|
||||
```bash
|
||||
export DEPLOY_CONFIG_AI_SERVICE='{
|
||||
"gitea_registry_url": "gitea.mycompany.com",
|
||||
"gitea_username": "deploy-bot",
|
||||
"gitea_token": "a1b2c3d4e5f6g7h8i9j0",
|
||||
"gitea_namespace": "ai-services",
|
||||
"ssh_host": "ai-server",
|
||||
"stacks_dir": "/srv/docker/stacks",
|
||||
"domain": "ai.sal.giize.com",
|
||||
"npm_url": "https://proxy.mycompany.com",
|
||||
"npm_username": "admin@mycompany.com",
|
||||
"npm_password": "npm-admin-password",
|
||||
"proxy_network": "proxy",
|
||||
"ollama_network": "ollama",
|
||||
"ollama_container_name": "ollama"
|
||||
}'
|
||||
|
||||
# Network names are customizable
|
||||
# App will auto-connect to Ollama at: http://ollama:11434
|
||||
# (or your custom ollama_container_name:11434)
|
||||
```
|
||||
|
||||
### Background Worker (No Web Interface)
|
||||
|
||||
For services without a web interface, omit the NPM fields:
|
||||
|
||||
```bash
|
||||
export DEPLOY_CONFIG_WORKER='{
|
||||
"gitea_registry_url": "gitea.mycompany.com",
|
||||
"gitea_username": "deploy-bot",
|
||||
"gitea_token": "a1b2c3d4e5f6g7h8i9j0",
|
||||
"gitea_namespace": "workers",
|
||||
"ssh_host": "worker-server",
|
||||
"stacks_dir": "/srv/docker/stacks"
|
||||
}'
|
||||
```
|
||||
|
||||
### Configuration Fields
|
||||
|
||||
**Required for All Deployments:**
|
||||
- `gitea_registry_url` - Your Gitea instance domain (without https://)
|
||||
- `gitea_username` - Gitea username
|
||||
- `gitea_token` - Access token with package permissions
|
||||
- `gitea_namespace` - Organization or username
|
||||
- `ssh_host` - SSH alias from ~/.ssh/config
|
||||
- `stacks_dir` - Directory on remote server for stacks
|
||||
|
||||
**Required for Webapps:**
|
||||
- `domain` - Root domain (e.g., `sal.giize.com`). App will be served at `{app-name}.{domain}`
|
||||
- `npm_url` - Nginx Proxy Manager URL
|
||||
- `npm_username` - NPM admin email
|
||||
- `npm_password` - NPM admin password
|
||||
|
||||
**Optional Network Configuration:**
|
||||
- `proxy_network` - Docker network name for reverse proxy (default: `proxy`)
|
||||
- `ollama_network` - Docker network name for Ollama LLM (default: `ollama`)
|
||||
- `ollama_container_name` - Ollama container name for URL generation (default: `ollama`)
|
||||
- Apps requiring LLM will auto-set `OLLAMA_BASE_URL=http://{ollama_container_name}:11434`
|
||||
|
||||
**Auto-Detected from Codebase:**
|
||||
- **App name** - From package.json, Cargo.toml, pyproject.toml, go.mod, or directory name
|
||||
- **Port** - From Dockerfile EXPOSE, .env PORT, code patterns, or defaults to 3000
|
||||
- Used as internal container port for proxy upstream (not published on host by default)
|
||||
- **App type** - webapp/service/worker based on dependencies and keywords
|
||||
- **Environment variables** - Smart .env processing:
|
||||
- 🔐 Auto-generates secrets (JWT_SECRET, APP_KEY, etc.) using secure random values
|
||||
- 🔌 Infers service URLs (DATABASE_URL, REDIS_URL) from dependencies
|
||||
- 📋 Copies simple values (NODE_ENV, LOG_LEVEL) as-is
|
||||
- ❓ Asks for external API keys and credentials
|
||||
- **Volume mounts** - From README, Dockerfile VOLUME, common directories (bound under ./data/)
|
||||
- **LLM requirements** - From dependencies (openai, ollama, llama)
|
||||
|
||||
All detected settings are confirmed with you before deployment.
|
||||
|
||||
**Volume Binding:** All volumes are automatically bound under `./data/` on the host.
|
||||
Example: If app needs uploads, it creates `./data/uploads:/app/uploads`
|
||||
|
||||
**Domain Handling:** The `domain` field is the root domain. Your app is served as a subdomain.
|
||||
Example: `domain: "sal.giize.com"` + app name `my-api` → URL: `my-api.sal.giize.com`
|
||||
|
||||
**DNS Requirements:** Ensure you have a wildcard DNS record or individual A/CNAME records:
|
||||
- Wildcard: `*.sal.giize.com` → `your-server-ip`
|
||||
- Individual: `my-api.sal.giize.com` → `your-server-ip`
|
||||
|
||||
## Deployment Flow
|
||||
|
||||
1. **Parse target** - Extract deployment target name
|
||||
2. **Auto-detect app** - Scan codebase for app name, port, type, env vars, volumes, LLM usage
|
||||
3. **Process .env** - Generate secrets, infer service URLs, identify user inputs
|
||||
4. **Load config** - Get server/credential configuration
|
||||
5. **Detect mode** - Classify deployment as `new` or `update` using existing remote stack/proxy state
|
||||
6. **Confirm detection** - Show detected config and mode, ask for approval
|
||||
7. **Confirm environment** - Review .env processing and provide any needed values
|
||||
8. **Validate** - Check Dockerfile exists
|
||||
9. **Build** - Test build locally
|
||||
10. **Push** - Tag and push to Gitea registry
|
||||
11. **Transfer** - SSH to remote server
|
||||
12. **Setup** - Create/update directories, deploy merged `.env` and compose files
|
||||
13. **Deploy** - `docker compose down`, `docker compose pull`, `docker compose up -d`
|
||||
14. **Configure** - Create or update reverse proxy (webapps)
|
||||
- Upstream target uses Docker service + internal port (for example `my-app:3000`)
|
||||
15. **Verify** - Inspect logs and test application availability
|
||||
|
||||
## Key Features Explained
|
||||
|
||||
### Subdomain Routing
|
||||
|
||||
Apps are automatically served as subdomains of your root domain:
|
||||
- Config: `"domain": "sal.giize.com"`
|
||||
- App name: `my-api` (auto-detected)
|
||||
- **Result:** App accessible at `my-api.sal.giize.com`
|
||||
|
||||
**DNS Setup:** Configure wildcard DNS: `*.sal.giize.com` → your server IP
|
||||
|
||||
### Volume Organization
|
||||
|
||||
All persistent data is organized under `./data/`:
|
||||
```
|
||||
data/
|
||||
├── <app-data>/ # Main app data (always created)
|
||||
├── uploads/ # Auto-detected from codebase
|
||||
├── cache/ # Auto-detected from codebase
|
||||
└── logs/ # Auto-detected from codebase
|
||||
```
|
||||
|
||||
Docker compose mounts: `./data/uploads:/app/uploads`
|
||||
|
||||
### Auto-Detection
|
||||
|
||||
The skill scans your codebase to detect:
|
||||
- App name from package.json, Cargo.toml, pyproject.toml, go.mod, or directory
|
||||
- Container port from Dockerfile EXPOSE, .env, or code patterns
|
||||
- App type (webapp/service/worker) from dependencies
|
||||
- Environment variables from .env files
|
||||
- Volume requirements from README, Dockerfile, common patterns
|
||||
- LLM dependencies (openai, ollama)
|
||||
|
||||
All detected settings shown to you for confirmation before deployment.
|
||||
|
||||
### Smart .env Handling
|
||||
|
||||
When a `.env`, `.env.example`, or `.env.template` file is detected, the skill intelligently processes it:
|
||||
|
||||
**🔐 Auto-Generated Secrets**
|
||||
Variables like `JWT_SECRET`, `APP_KEY`, `SESSION_SECRET` are automatically generated using secure random values:
|
||||
```bash
|
||||
JWT_SECRET=a1b2c3d4e5f6... # 32-byte hex string
|
||||
```
|
||||
|
||||
**🔌 Inferred Service URLs**
|
||||
Database and service connections are constructed from container names based on detected dependencies:
|
||||
```bash
|
||||
DATABASE_URL=postgresql://my-app-db:5432/my-app
|
||||
REDIS_URL=redis://my-app-redis:6379
|
||||
MONGODB_URI=mongodb://my-app-mongo:27017/my-app
|
||||
```
|
||||
|
||||
**📋 Copied Values**
|
||||
Simple configuration values are preserved:
|
||||
```bash
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
TZ=UTC
|
||||
```
|
||||
|
||||
**❓ User Input**
|
||||
External API keys and credentials are identified and you're prompted for values:
|
||||
```bash
|
||||
STRIPE_SECRET_KEY=sk_live_... # You'll be asked to provide this
|
||||
SENDGRID_API_KEY=SG... # You'll be asked to provide this
|
||||
```
|
||||
|
||||
**Confirmation**
|
||||
Before deployment, you'll see:
|
||||
- What was generated (with secure random values)
|
||||
- What was inferred (service URLs)
|
||||
- What was copied (simple configs)
|
||||
- What needs your input (API keys)
|
||||
|
||||
You can proceed as-is or provide specific values. The complete `.env` file is deployed to the server with secure permissions (chmod 600).
|
||||
|
||||
### Update-Safe Drift Handling
|
||||
|
||||
When deployment mode is `update`, the skill applies additive and non-destructive changes:
|
||||
|
||||
- **Environment merge:** existing remote `.env` keys are preserved; newly required keys are appended.
|
||||
- **Required secrets:** unresolved required placeholders still block deployment until provided.
|
||||
- **Volume drift:** new host directories are created as needed; existing volume paths are kept intact.
|
||||
- **Compose stability:** service naming remains stable for idempotent updates.
|
||||
|
||||
This minimizes downtime and avoids accidental configuration loss during redeployments.
|
||||
|
||||
## Quick Setup Guide
|
||||
|
||||
### 1. Create Gitea Token
|
||||
|
||||
```bash
|
||||
# Login to Gitea
|
||||
# Navigate to Settings → Applications → Generate New Token
|
||||
# Select scopes: write:package, read:package
|
||||
# Copy the generated token
|
||||
```
|
||||
|
||||
### 2. Configure SSH
|
||||
|
||||
```bash
|
||||
# Edit ~/.ssh/config
|
||||
cat >> ~/.ssh/config << 'EOF'
|
||||
|
||||
Host prod-server
|
||||
HostName 192.168.1.100
|
||||
User deploy
|
||||
IdentityFile ~/.ssh/deploy_key
|
||||
StrictHostKeyChecking accept-new
|
||||
EOF
|
||||
|
||||
# Test connection
|
||||
ssh prod-server "echo 'Connection successful'"
|
||||
```
|
||||
|
||||
### 3. Prepare Remote Server
|
||||
|
||||
```bash
|
||||
# SSH to server
|
||||
ssh prod-server
|
||||
|
||||
# Install Docker
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Create networks (run once per server)
|
||||
# Use your configured network names (defaults shown below)
|
||||
docker network create proxy # or your proxy_network value
|
||||
docker network create ollama # or your ollama_network value (for LLM apps)
|
||||
|
||||
# Create stacks directory
|
||||
sudo mkdir -p /srv/docker/stacks
|
||||
sudo chown $USER:$USER /srv/docker/stacks
|
||||
```
|
||||
|
||||
### 4. Set Environment Variable
|
||||
|
||||
**Linux/macOS** (add to ~/.bashrc or ~/.zshrc):
|
||||
```bash
|
||||
export DEPLOY_CONFIG_PRODUCTION='{ ... your JSON config ... }'
|
||||
```
|
||||
|
||||
**Windows PowerShell**:
|
||||
```powershell
|
||||
$config = @'
|
||||
{ ... your JSON config ... }
|
||||
'@
|
||||
[System.Environment]::SetEnvironmentVariable('DEPLOY_CONFIG_PRODUCTION', $config, 'User')
|
||||
```
|
||||
|
||||
### 5. Deploy
|
||||
|
||||
Just tell the agent:
|
||||
```
|
||||
deploy production
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Credentials stored in environment variables only
|
||||
- SSH key-based authentication
|
||||
- Secrets never logged to output
|
||||
- HTTPS with Let's Encrypt certificates
|
||||
- Docker registry authentication
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Quick Diagnostics
|
||||
|
||||
```bash
|
||||
# Check environment variable
|
||||
echo $DEPLOY_CONFIG_PRODUCTION
|
||||
|
||||
# Test SSH
|
||||
ssh prod-server "docker ps"
|
||||
|
||||
# Test Gitea registry
|
||||
docker login gitea.example.com -u username
|
||||
|
||||
# Check remote deployment
|
||||
ssh prod-server "cd /srv/docker/stacks/my-app && docker compose logs"
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
**"DEPLOY_CONFIG_{TARGET} not found"**
|
||||
- Make sure you've exported the environment variable
|
||||
- Restart your terminal/VS Code after setting it
|
||||
- Check spelling and capitalization
|
||||
|
||||
**"Cannot connect to {ssh_host}"**
|
||||
- Verify SSH config: `cat ~/.ssh/config`
|
||||
- Test connection: `ssh {ssh_host} "echo test"`
|
||||
- Check SSH key permissions: `chmod 600 ~/.ssh/deploy_key`
|
||||
|
||||
**"Authentication failed" (Gitea)**
|
||||
- Regenerate token with correct permissions
|
||||
- Ensure token hasn't expired
|
||||
- Verify username is correct
|
||||
|
||||
**"Docker login failed"**
|
||||
- Check gitea_registry_url doesn't have protocol (no https://)
|
||||
- Verify token has package write permission
|
||||
- Try manual login: `echo "TOKEN" | docker login gitea.example.com -u user --password-stdin`
|
||||
|
||||
**Container fails to start**
|
||||
- Check logs: `ssh server "cd /srv/docker/stacks/app && docker compose logs"`
|
||||
- Verify environment variables are correct
|
||||
- Check port conflicts on server
|
||||
- Ensure image pulled successfully
|
||||
|
||||
**Environment variables not working**
|
||||
- Check .env file exists: `ssh server "cat /srv/docker/stacks/app/.env"`
|
||||
- Verify .env file permissions: `ssh server "ls -la /srv/docker/stacks/app/.env"` (should be 600)
|
||||
- Ensure compose.yaml has `env_file: [.env]` directive
|
||||
- Test variable loading: `ssh server "cd /srv/docker/stacks/app && docker compose config"` (shows resolved env vars)
|
||||
- Restart containers after .env changes: `ssh server "cd /srv/docker/stacks/app && docker compose restart"`
|
||||
|
||||
**Missing or incorrect service URLs**
|
||||
- Verify service containers are running in same network
|
||||
- Check container names match .env URLs (e.g., `my-app-db` for DATABASE_URL)
|
||||
- Test connectivity: `ssh server "docker exec my-app ping my-app-db"`
|
||||
- Review auto-generated URLs in deployment confirmation step
|
||||
|
||||
**NPM SSL certificate fails**
|
||||
- Verify domain DNS points to your server
|
||||
- Check ports 80 and 443 are open
|
||||
- Ensure domain is publicly accessible
|
||||
- Check NPM can reach Let's Encrypt servers
|
||||
|
||||
## API Documentation
|
||||
|
||||
### Gitea Container Registry
|
||||
|
||||
Gitea uses an OCI-compliant container registry. No special API calls needed - just standard Docker commands:
|
||||
|
||||
```bash
|
||||
docker login {gitea_url}
|
||||
docker tag image:tag {gitea_url}/{namespace}/{name}:tag
|
||||
docker push {gitea_url}/{namespace}/{name}:tag
|
||||
```
|
||||
|
||||
Images are automatically visible in Gitea under Packages.
|
||||
|
||||
### Nginx Proxy Manager API
|
||||
|
||||
Main endpoints used:
|
||||
|
||||
- `POST /api/tokens` - Authentication
|
||||
- `POST /api/nginx/proxy-hosts` - Create reverse proxy
|
||||
- `POST /api/nginx/certificates` - Request SSL cert
|
||||
- `PUT /api/nginx/proxy-hosts/{id}` - Update with SSL
|
||||
|
||||
## Examples
|
||||
|
||||
### Deploy to Production
|
||||
|
||||
```
|
||||
deploy production
|
||||
```
|
||||
|
||||
If `production` already exists remotely, this runs as an update rollout.
|
||||
|
||||
### Deploy to Staging
|
||||
|
||||
```
|
||||
deploy staging
|
||||
```
|
||||
|
||||
If the stack already exists, the skill updates it in place.
|
||||
|
||||
### Deploy AI Service with LLM
|
||||
|
||||
```
|
||||
deploy ai-service
|
||||
```
|
||||
|
||||
For existing deployments, proxy and stack are updated idempotently.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Modifying Environment Variables After Deployment
|
||||
|
||||
If you need to update environment variables after deployment:
|
||||
|
||||
**Option 1: Edit .env on server**
|
||||
```bash
|
||||
# SSH to server and edit
|
||||
ssh prod-server
|
||||
cd /srv/docker/stacks/my-app
|
||||
nano .env
|
||||
|
||||
# Restart containers to apply changes
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
**Option 2: Redeploy with updated .env**
|
||||
Update your local `.env` file and redeploy - the skill will process it again in update mode and merge new required keys without deleting existing ones.
|
||||
|
||||
### Adding Service Containers
|
||||
|
||||
If your app needs a database or other services, create them in the same stack:
|
||||
|
||||
```bash
|
||||
ssh prod-server
|
||||
cd /srv/docker/stacks/my-app
|
||||
nano compose.yaml
|
||||
```
|
||||
|
||||
Add service definitions:
|
||||
```yaml
|
||||
services:
|
||||
my-app:
|
||||
# ... existing config
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
container_name: my-app-db
|
||||
environment:
|
||||
POSTGRES_DB: my-app
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
container_name: my-app-redis
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
```
|
||||
|
||||
The DATABASE_URL and REDIS_URL will already be correctly set by the auto-detection!
|
||||
|
||||
### Additional Volumes
|
||||
|
||||
```json
|
||||
{
|
||||
"volumes": [
|
||||
"./uploads:/app/uploads",
|
||||
"./cache:/app/cache:ro"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Targets
|
||||
|
||||
Create multiple configs:
|
||||
|
||||
```bash
|
||||
export DEPLOY_CONFIG_PRODUCTION='{ ... }'
|
||||
export DEPLOY_CONFIG_STAGING='{ ... }'
|
||||
export DEPLOY_CONFIG_DEVELOPMENT='{ ... }'
|
||||
```
|
||||
|
||||
Deploy to any target:
|
||||
|
||||
```
|
||||
deploy development
|
||||
deploy staging
|
||||
deploy production
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Part of the Copilot Agents skill library.
|
||||
177
.github/skills/deploy-app/SKILL.md
vendored
Normal file
177
.github/skills/deploy-app/SKILL.md
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: deploy-app
|
||||
description: Automated deployment of Dockerized applications to remote servers with Gitea registry, Docker Compose orchestration, and Nginx Proxy Manager integration.
|
||||
---
|
||||
|
||||
# Deploy Application
|
||||
|
||||
Lean-mode deployment contract for token-efficient execution. Use this file for runtime behavior. For deep examples and full walkthroughs, reference [README.md](./README.md).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `DEPLOY_CONFIG_{TARGET}` env var exists and is valid JSON
|
||||
- SSH host alias works from `~/.ssh/config`
|
||||
- `Dockerfile` exists in workspace root
|
||||
- Docker available locally and on target server
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
### 1. Parse target
|
||||
|
||||
Extract `{TARGET}` from user request (for example: `deploy production` → `PRODUCTION`).
|
||||
|
||||
### 2. Detect app metadata (minimal set)
|
||||
|
||||
Detect and store:
|
||||
- `{app_name}`: from `package.json` / `Cargo.toml` / `pyproject.toml` / `go.mod` / directory fallback
|
||||
- `{port}`: from `EXPOSE`, `.env PORT`, or framework patterns; fallback `3000` (container internal port)
|
||||
- `{app_type}`: `webapp`, `service`, or `worker`
|
||||
- `{requires_llm}`: true if `openai|ollama|llama` detected
|
||||
- `{detected_volumes}`: bind all host paths under `./data/` (including secrets paths)
|
||||
|
||||
If `.env*` exists (excluding `.env.local`), process variables into:
|
||||
- generated secrets (`*SECRET*`, `*KEY`, `*TOKEN`)
|
||||
- inferred service URLs (`DATABASE_URL`, `REDIS_URL`, etc.)
|
||||
- copied simple values (`NODE_ENV`, `PORT`, `LOG_LEVEL`, ...)
|
||||
- user-required values (placeholders/external API keys)
|
||||
|
||||
### 3. Load and validate deploy config
|
||||
|
||||
Read `DEPLOY_CONFIG_{TARGET}` and validate required fields:
|
||||
- `gitea_registry_url`, `gitea_username`, `gitea_token`, `gitea_namespace`
|
||||
- `ssh_host`, `stacks_dir`
|
||||
|
||||
If missing/invalid: STOP with explicit error.
|
||||
|
||||
### 4. Confirm detected configuration
|
||||
|
||||
Use `ask_questions` with detected summary (app, port, type, env, volumes, URL if webapp, deploy mode).
|
||||
If user declines, STOP and request adjustments.
|
||||
|
||||
If user-input env values are required, collect them before continuing.
|
||||
|
||||
### 5. Validate and build locally
|
||||
|
||||
Verify Dockerfile exists, then run:
|
||||
- `docker build -t {app_name}:test-build .`
|
||||
|
||||
If build fails: STOP and report error output.
|
||||
|
||||
### 6. Push image to registry
|
||||
|
||||
1. Login with `--password-stdin`
|
||||
2. Tag as `latest` and `{timestamp}` (`YYYYMMDD-HHMMSS`)
|
||||
3. Push both tags
|
||||
|
||||
If auth/network/permission fails: STOP with targeted troubleshooting.
|
||||
|
||||
### 7. Detect deploy mode (new vs update)
|
||||
|
||||
On remote host and in NPM API (when configured), detect:
|
||||
- stack path exists: `{stacks_dir}/{app_name}`
|
||||
- compose file exists: `{stacks_dir}/{app_name}/compose.yaml`
|
||||
- proxy host exists for `{app_name}.{domain}` (webapp only)
|
||||
|
||||
Classification:
|
||||
- `new`: none of the above exist
|
||||
- `update`: stack path or proxy host already exists
|
||||
|
||||
If only one side exists (for example proxy exists but stack does not, or inverse), continue in cautious update mode and include this mismatch in confirmation.
|
||||
|
||||
### 8. Prepare remote stack
|
||||
|
||||
1. Verify SSH connectivity
|
||||
2. Create `{stacks_dir}/{app_name}/data` and required subdirectories
|
||||
3. Process `.env` with update-safe merge strategy:
|
||||
- if update and remote `.env` exists, preserve existing keys/values
|
||||
- append newly required keys not present remotely
|
||||
- never delete existing keys automatically
|
||||
- block on unresolved required user-input secrets/placeholders
|
||||
4. Apply secure permissions to remote `.env` (`chmod 600`)
|
||||
5. Handle volume drift safely:
|
||||
- create newly required host directories
|
||||
- enforce all volume and secrets host paths as children of `./data/`
|
||||
- keep existing volume paths intact
|
||||
- never remove existing volume mounts automatically
|
||||
|
||||
### 9. Generate and upload compose.yaml
|
||||
|
||||
Compose requirements:
|
||||
- image: `{gitea_registry_url}/{gitea_namespace}/{app_name}:latest`
|
||||
- restart: `unless-stopped`
|
||||
- all volumes and secrets host paths under `./data`
|
||||
- do NOT publish ports on host (no `ports:` mapping by default)
|
||||
- use external proxy network for `webapp`
|
||||
- add ollama network and `OLLAMA_BASE_URL` when `{requires_llm}=true`
|
||||
|
||||
Update behavior for compose generation:
|
||||
- keep service naming stable for idempotent updates
|
||||
- preserve existing mounts/networks unless user explicitly confirms removal
|
||||
- if update and env + volume definitions are unchanged, do NOT rewrite `compose.yaml`
|
||||
- if update requires compose rewrite, fetch remote `compose.yaml` and use it as template baseline before applying required deltas
|
||||
|
||||
Upload compose file to `{stacks_dir}/{app_name}/compose.yaml`.
|
||||
|
||||
### 10. Deploy on server
|
||||
|
||||
On remote host:
|
||||
1. `docker login` to registry
|
||||
2. `docker compose down`
|
||||
3. `docker compose pull`
|
||||
4. `docker compose up -d`
|
||||
4. Inspect logs (`--tail=50`) after short wait
|
||||
|
||||
If container exits or logs show startup failure: STOP and report logs.
|
||||
|
||||
### 11. Configure reverse proxy (webapp only)
|
||||
|
||||
If `{app_type}=webapp` and NPM config exists:
|
||||
1. Authenticate to NPM API
|
||||
2. Check if proxy host for `{app_name}.{domain}` already exists
|
||||
3. Set upstream target to Docker service name + internal container port (`{app_name}:{port}`) on shared proxy network
|
||||
4. If existing: update upstream target and SSL settings (idempotent update)
|
||||
5. If not existing: create proxy host, request certificate, then enable SSL
|
||||
|
||||
If SSL creation fails: WARN and continue HTTP-only.
|
||||
|
||||
### 12. Verify deployment
|
||||
|
||||
- Webapp: `curl -I https://{app_name}.{domain}` and expect `2xx/3xx`
|
||||
- Non-webapp: verify running container and health/log readiness
|
||||
|
||||
Return structured success summary (image, host, location, URL/container).
|
||||
|
||||
## Error Recovery
|
||||
|
||||
On failure:
|
||||
1. Show concrete error
|
||||
2. Provide immediate remediation steps
|
||||
3. STOP (no implicit continuation)
|
||||
4. Offer rollback option using ONLY non-destructive actions:
|
||||
- run `docker compose down` in the app stack directory
|
||||
- rollback changes in Nginx Proxy Manager (remove/disable created proxy host entries for this deploy)
|
||||
|
||||
## Forbidden Operations
|
||||
|
||||
Safety policy for this skill is strict:
|
||||
- NEVER execute deletion commands on local or remote systems.
|
||||
- NEVER run any `rm`-family or delete operations, including recursive deletes.
|
||||
- NEVER use destructive cleanup commands that remove files, directories, volumes, or containers.
|
||||
|
||||
Allowed remediation is limited to:
|
||||
- `docker compose down`
|
||||
- rollback changes in Nginx Proxy Manager
|
||||
|
||||
If a remediation path would require deletion, STOP and report manual follow-up steps without executing destructive commands.
|
||||
|
||||
## Security
|
||||
|
||||
- Never print secrets in logs
|
||||
- Use `--password-stdin` for all docker logins
|
||||
- Quote shell variables
|
||||
- Validate all required inputs before executing remote operations
|
||||
|
||||
## Notes
|
||||
|
||||
- Keep runtime behavior minimal and deterministic.
|
||||
- For advanced examples, edge-case recipes, and setup wizard usage, see [README.md](./README.md).
|
||||
139
.github/skills/deploy-app/setup-wizard.sh
vendored
Normal file
139
.github/skills/deploy-app/setup-wizard.sh
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/bin/bash
|
||||
# Quick setup script for deployment configuration
|
||||
|
||||
set -e
|
||||
|
||||
echo "==================================="
|
||||
echo "Deployment Skill Setup Wizard"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
|
||||
# Function to read input with default
|
||||
read_input() {
|
||||
local prompt="$1"
|
||||
local default="$2"
|
||||
local var_name="$3"
|
||||
|
||||
if [ -n "$default" ]; then
|
||||
read -p "$prompt [$default]: " input
|
||||
eval "$var_name=\"${input:-$default}\""
|
||||
else
|
||||
read -p "$prompt: " input
|
||||
eval "$var_name=\"$input\""
|
||||
fi
|
||||
}
|
||||
|
||||
# Target name
|
||||
read_input "Enter deployment target name (e.g., PRODUCTION, STAGING)" "" TARGET
|
||||
TARGET=$(echo "$TARGET" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
echo ""
|
||||
echo "--- Gitea Configuration ---"
|
||||
read_input "Gitea registry URL (without https://)" "gitea.example.com" GITEA_URL
|
||||
read_input "Gitea username" "" GITEA_USER
|
||||
echo "Gitea token (will be hidden): "
|
||||
read -s GITEA_TOKEN
|
||||
echo ""
|
||||
read_input "Gitea namespace (org or username)" "$GITEA_USER" GITEA_NAMESPACE
|
||||
|
||||
echo ""
|
||||
echo "--- Remote Server Configuration ---"
|
||||
read_input "SSH host alias (from ~/.ssh/config)" "" SSH_HOST
|
||||
read_input "Stacks directory on remote server" "/srv/docker/stacks" STACKS_DIR
|
||||
|
||||
echo ""
|
||||
echo "--- Webapp Configuration (optional, press Enter to skip) ---"
|
||||
read_input "Root domain (e.g., sal.giize.com - app served as subdomain)" "" DOMAIN
|
||||
|
||||
# Conditional NPM fields
|
||||
if [ -n "$DOMAIN" ]; then
|
||||
read_input "Nginx Proxy Manager URL" "https://npm.example.com" NPM_URL
|
||||
read_input "NPM admin email" "admin@example.com" NPM_USER
|
||||
echo "NPM password (will be hidden): "
|
||||
read -s NPM_PASS
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Network Configuration (optional, press Enter for defaults) ---"
|
||||
read_input "Proxy network name" "proxy" PROXY_NETWORK
|
||||
read_input "Ollama network name (for AI/LLM apps)" "ollama" OLLAMA_NETWORK
|
||||
read_input "Ollama container name (for deriving connection URL)" "ollama" OLLAMA_CONTAINER
|
||||
|
||||
# Generate JSON config
|
||||
if [ -n "$DOMAIN" ]; then
|
||||
JSON_CONFIG=$(cat <<EOF
|
||||
{
|
||||
"gitea_registry_url": "$GITEA_URL",
|
||||
"gitea_username": "$GITEA_USER",
|
||||
"gitea_token": "$GITEA_TOKEN",
|
||||
"gitea_namespace": "$GITEA_NAMESPACE",
|
||||
"ssh_host": "$SSH_HOST",
|
||||
"stacks_dir": "$STACKS_DIR",
|
||||
"domain": "$DOMAIN",
|
||||
"npm_url": "$NPM_URL",
|
||||
"npm_username": "$NPM_USER",
|
||||
"npm_password": "$NPM_PASS",
|
||||
"proxy_network": "$PROXY_NETWORK",
|
||||
"ollama_network": "$OLLAMA_NETWORK",
|
||||
"ollama_container_name": "$OLLAMA_CONTAINER"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
else
|
||||
JSON_CONFIG=$(cat <<EOF
|
||||
{
|
||||
"gitea_registry_url": "$GITEA_URL",
|
||||
"gitea_username": "$GITEA_USER",
|
||||
"gitea_token": "$GITEA_TOKEN",
|
||||
"gitea_namespace": "$GITEA_NAMESPACE",
|
||||
"ssh_host": "$SSH_HOST",
|
||||
"stacks_dir": "$STACKS_DIR",
|
||||
"proxy_network": "$PROXY_NETWORK",
|
||||
"ollama_network": "$OLLAMA_NETWORK",
|
||||
"ollama_container_name": "$OLLAMA_CONTAINER"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==================================="
|
||||
echo "Configuration generated!"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
echo "Add this to your shell configuration file (~/.bashrc, ~/.zshrc, etc.):"
|
||||
echo ""
|
||||
echo "export DEPLOY_CONFIG_$TARGET='$JSON_CONFIG'"
|
||||
echo ""
|
||||
echo "Then restart your terminal or run:"
|
||||
echo "source ~/.bashrc # or ~/.zshrc"
|
||||
echo ""
|
||||
|
||||
# Optionally write to file
|
||||
read -p "Save to file? (y/n): " SAVE_FILE
|
||||
if [ "$SAVE_FILE" = "y" ] || [ "$SAVE_FILE" = "Y" ]; then
|
||||
CONFIG_FILE="deploy_config_${TARGET}.env"
|
||||
echo "export DEPLOY_CONFIG_$TARGET='$JSON_CONFIG'" > "$CONFIG_FILE"
|
||||
echo ""
|
||||
echo "✓ Saved to $CONFIG_FILE"
|
||||
echo "Load it with: source $CONFIG_FILE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==================================="
|
||||
echo "Next Steps:"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
echo "1. Set the environment variable (see above)"
|
||||
echo "2. Verify SSH access: ssh $SSH_HOST \"echo 'Test OK'\""
|
||||
echo "3. Create Docker networks on remote server (run once per server):"
|
||||
echo " ssh $SSH_HOST \"docker network create $PROXY_NETWORK\""
|
||||
if [ -n "$OLLAMA_NETWORK" ]; then
|
||||
echo " ssh $SSH_HOST \"docker network create $OLLAMA_NETWORK\" # For AI/LLM apps"
|
||||
fi
|
||||
echo "4. Navigate to your application directory"
|
||||
echo "5. Tell the agent: 'deploy $TARGET'"
|
||||
echo ""
|
||||
echo "The agent will auto-detect your app's configuration and ask for confirmation!"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user