# Stack Automation Guide ## Overview The `update-stack.sh` script enables programmatic stack updates via Portainer's REST API. This allows LLM agents (like Claude) and automation scripts to safely update Portainer stacks without requiring UI access. ## Quick Start ```bash # Navigate to stacks directory cd /home/jpmschweitzer/Projects/portainer-core/stacks # Update a stack (interactive mode - first time) ./update-stack.sh open-webui.yml # Subsequent updates (uses stored token) ./update-stack.sh open-webui.yml ``` ## How It Works ### Authentication Flow 1. **First Run:** - Prompts for Portainer username/password - Authenticates with Portainer API - Generates JWT access token - Saves token to `.portainer-token` (gitignored) 2. **Subsequent Runs:** - Reads token from `.portainer-token` - Uses token for API calls - No credential prompts needed ### Update Process 1. Reads YAML file from `stacks/` directory 2. Authenticates with Portainer (or uses cached token) 3. Looks up stack by name (filename without .yml) 4. Sends updated stack configuration via API 5. Portainer validates and applies changes ## Usage Modes ### Interactive Mode (Human Operators) ```bash ./update-stack.sh open-webui.yml ``` **First run prompts for:** - Portainer username - Portainer password **Token persists for subsequent runs.** ### Non-Interactive Mode (Automation/LLM Agents) ```bash export PORTAINER_USERNAME="admin" export PORTAINER_PASSWORD="your-secure-password" ./update-stack.sh open-webui.yml ``` **Use this mode for:** - CI/CD pipelines - LLM agent workflows - Automated deployment scripts - Cron jobs ### Environment Variables | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `PORTAINER_URL` | No | `http://localhost:8080` | Portainer instance URL | | `PORTAINER_USERNAME` | Non-interactive only | - | Admin username | | `PORTAINER_PASSWORD` | Non-interactive only | - | Admin password | ## Examples ### Update Single Stack ```bash ./update-stack.sh open-webui.yml ``` ### Update Multiple Stacks ```bash for stack in open-webui.yml ollama.yml core-api.yml; do ./update-stack.sh "$stack" echo "---" done ``` ### LLM Agent Integration ```bash # Claude Code workflow example export PORTAINER_USERNAME="admin" export PORTAINER_PASSWORD="${PORTAINER_ADMIN_PASSWORD}" # from secure env # Update stack after modifying YAML ./update-stack.sh open-webui.yml # Check result echo $? # 0 = success, 1 = failure ``` ### Remote Portainer Instance ```bash export PORTAINER_URL="https://portainer.example.com" ./update-stack.sh my-stack.yml ``` ## Security Considerations ### Token Storage - Token stored in `.portainer-token` (gitignored) - File permissions: `600` (owner read/write only) - Token expires based on Portainer settings (default: 8 hours) - Re-authentication automatic if token expires ### Credentials **DO NOT:** - ❌ Commit `.portainer-token` to git - ❌ Hardcode passwords in scripts - ❌ Share tokens between users - ❌ Use root/admin account for automation (create dedicated API user) **DO:** - ✅ Use environment variables for non-interactive mode - ✅ Store credentials in secure password manager - ✅ Create dedicated Portainer user for automation - ✅ Rotate passwords regularly - ✅ Use `.gitignore` to exclude token file ### Best Practices 1. **Create Automation User:** ``` Portainer → Users → Add User Username: portainer-automation Role: Environment Administrator (or custom) ``` 2. **Use Environment Variables:** ```bash # In ~/.bashrc or secure environment export PORTAINER_USERNAME="portainer-automation" export PORTAINER_PASSWORD="$(pass show portainer/automation)" # from password manager ``` 3. **Restrict Permissions:** - Grant minimum required permissions - Limit to specific environments/stacks if possible ## Troubleshooting ### Authentication Failed ``` [ERROR] Failed to authenticate. Check credentials and try again. ``` **Solutions:** - Verify username/password are correct - Check Portainer is accessible: `curl http://localhost:8080/api/status` - Ensure user has admin/environment admin role - Try removing `.portainer-token` and re-authenticating ### Stack Not Found ``` [ERROR] Stack 'my-stack' not found in Portainer Available stacks: - open-webui - ollama - core-api ``` **Solutions:** - Verify stack name matches filename (without .yml) - Check stack exists in Portainer UI - Stack name is case-sensitive - Create stack in Portainer first if it doesn't exist ### Connection Refused ``` [ERROR] Failed to connect to Portainer at http://localhost:8080 ``` **Solutions:** - Check Portainer is running: `docker ps | grep portainer` - Verify port: Portainer default is 8080 - Set `PORTAINER_URL` if using different port/host - Check firewall rules if accessing remote instance ### Token Expired ``` [ERROR] Invalid authentication token ``` **Solutions:** - Delete token file: `rm .portainer-token` - Re-run script to re-authenticate - Check Portainer token expiration settings ### YAML Validation Error ``` [ERROR] Stack update failed: invalid compose file ``` **Solutions:** - Validate YAML syntax: `yamllint open-webui.yml` - Check Docker Compose version compatibility - Review Portainer logs: `docker logs portainer` - Test with `docker compose config -f open-webui.yml` ## Integration with LLM Agents ### Claude Code Workflow This script is designed to integrate seamlessly with Claude Code workflows: 1. **Agent modifies YAML file:** ```python # Claude uses Edit tool to update open-webui.yml ``` 2. **Agent calls update script:** ```bash cd /home/jpmschweitzer/Projects/portainer-core/stacks ./update-stack.sh open-webui.yml ``` 3. **Agent verifies deployment:** ```bash docker logs open-webui --tail 20 curl http://localhost:82 # Verify service ``` ### Setting Up for Claude Add to user profile or environment: ```bash # In ~/.bashrc or secure location export PORTAINER_USERNAME="admin" export PORTAINER_PASSWORD="your-secure-password" # Or use password manager export PORTAINER_PASSWORD="$(pass show portainer/admin)" ``` Then Claude can directly call: ```bash ./update-stack.sh ``` ## Advanced Usage ### Custom Portainer URL ```bash # Connect to remote Portainer export PORTAINER_URL="https://portainer.mydomain.com" ./update-stack.sh open-webui.yml ``` ### Token Management ```bash # View current token (for debugging) cat .portainer-token | base64 -d | jq # Force re-authentication rm .portainer-token ./update-stack.sh open-webui.yml # Use specific token echo "your-jwt-token-here" > .portainer-token chmod 600 .portainer-token ``` ### Dry Run (Check Only) ```bash # Validate YAML before updating docker compose -f open-webui.yml config # Check current stack status curl -s http://localhost:8080/api/stacks \ -H "Authorization: Bearer $(cat .portainer-token)" | jq ``` ## API Reference The script uses these Portainer API endpoints: - `POST /api/auth` - Authenticate and get token - `GET /api/endpoints` - List Docker endpoints - `GET /api/stacks` - List all stacks - `PUT /api/stacks/{id}` - Update specific stack For full API documentation: https://docs.portainer.io/api/docs ## Maintenance ### Regular Tasks - **Monthly:** Rotate automation user password - **Quarterly:** Review and audit API access logs - **After incidents:** Revoke and regenerate tokens ### Token Rotation ```bash # Revoke old token (Portainer UI) Portainer → Users → [user] → Access Tokens → Revoke All # Re-authenticate rm .portainer-token ./update-stack.sh open-webui.yml ``` ## Support For issues or questions: 1. Check Portainer logs: `docker logs portainer` 2. Review this guide's Troubleshooting section 3. Check Portainer API docs: https://docs.portainer.io/api/docs 4. Open issue in project repository --- *Last updated: 2025-11-14*