Files
scopone/.github/skills/deploy-app/README.md
2026-04-10 22:35:01 +02:00

18 KiB

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

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

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

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

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:

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.comyour-server-ip
  • Individual: my-api.sal.giize.comyour-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)
  1. 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:

JWT_SECRET=a1b2c3d4e5f6...  # 32-byte hex string

🔌 Inferred Service URLs
Database and service connections are constructed from container names based on detected dependencies:

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:

NODE_ENV=production
LOG_LEVEL=info
TZ=UTC

User Input
External API keys and credentials are identified and you're prompted for values:

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

# Login to Gitea
# Navigate to Settings → Applications → Generate New Token
# Select scopes: write:package, read:package
# Copy the generated token

2. Configure SSH

# 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

# 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):

export DEPLOY_CONFIG_PRODUCTION='{ ... your JSON config ... }'

Windows 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

# 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:

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

# 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:

ssh prod-server
cd /srv/docker/stacks/my-app
nano compose.yaml

Add service definitions:

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

{
  "volumes": [
    "./uploads:/app/uploads",
    "./cache:/app/cache:ro"
  ]
}

Multiple Targets

Create multiple configs:

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.