602 lines
18 KiB
Markdown
602 lines
18 KiB
Markdown
# 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.
|