7.8 KiB
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
# 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
-
First Run:
- Prompts for Portainer username/password
- Authenticates with Portainer API
- Generates JWT access token
- Saves token to
.portainer-token(gitignored)
-
Subsequent Runs:
- Reads token from
.portainer-token - Uses token for API calls
- No credential prompts needed
- Reads token from
Update Process
- Reads YAML file from
stacks/directory - Authenticates with Portainer (or uses cached token)
- Looks up stack by name (filename without .yml)
- Sends updated stack configuration via API
- Portainer validates and applies changes
Usage Modes
Interactive Mode (Human Operators)
./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)
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
./update-stack.sh open-webui.yml
Update Multiple Stacks
for stack in open-webui.yml ollama.yml core-api.yml; do
./update-stack.sh "$stack"
echo "---"
done
LLM Agent Integration
# 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
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-tokento 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
.gitignoreto exclude token file
Best Practices
-
Create Automation User:
Portainer → Users → Add User Username: portainer-automation Role: Environment Administrator (or custom) -
Use Environment Variables:
# In ~/.bashrc or secure environment export PORTAINER_USERNAME="portainer-automation" export PORTAINER_PASSWORD="$(pass show portainer/automation)" # from password manager -
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-tokenand 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_URLif 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:
-
Agent modifies YAML file:
# Claude uses Edit tool to update open-webui.yml -
Agent calls update script:
cd /home/jpmschweitzer/Projects/portainer-core/stacks ./update-stack.sh open-webui.yml -
Agent verifies deployment:
docker logs open-webui --tail 20 curl http://localhost:82 # Verify service
Setting Up for Claude
Add to user profile or environment:
# 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:
./update-stack.sh <stack-file.yml>
Advanced Usage
Custom Portainer URL
# Connect to remote Portainer
export PORTAINER_URL="https://portainer.mydomain.com"
./update-stack.sh open-webui.yml
Token Management
# 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)
# 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 tokenGET /api/endpoints- List Docker endpointsGET /api/stacks- List all stacksPUT /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
# 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:
- Check Portainer logs:
docker logs portainer - Review this guide's Troubleshooting section
- Check Portainer API docs: https://docs.portainer.io/api/docs
- Open issue in project repository
Last updated: 2025-11-14