# 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 ├── # 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/ ├── / # 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.