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, optionallyollama)
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 usernamegitea_token- Access token with package permissionsgitea_namespace- Organization or usernamessh_host- SSH alias from ~/.ssh/configstacks_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 URLnpm_username- NPM admin emailnpm_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
- Apps requiring LLM will auto-set
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
- Parse target - Extract deployment target name
- Auto-detect app - Scan codebase for app name, port, type, env vars, volumes, LLM usage
- Process .env - Generate secrets, infer service URLs, identify user inputs
- Load config - Get server/credential configuration
- Detect mode - Classify deployment as
neworupdateusing existing remote stack/proxy state - Confirm detection - Show detected config and mode, ask for approval
- Confirm environment - Review .env processing and provide any needed values
- Validate - Check Dockerfile exists
- Build - Test build locally
- Push - Tag and push to Gitea registry
- Transfer - SSH to remote server
- Setup - Create/update directories, deploy merged
.envand compose files - Deploy -
docker compose down,docker compose pull,docker compose up -d - Configure - Create or update reverse proxy (webapps)
- Upstream target uses Docker service + internal port (for example
my-app:3000)
- 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
.envkeys 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-dbfor 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- AuthenticationPOST /api/nginx/proxy-hosts- Create reverse proxyPOST /api/nginx/certificates- Request SSL certPUT /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.