From devops
Convert Docker Compose files to Docker Swarm stack format for eRegistrations deployments. Handles env var replacement, secrets management, init-secrets.sh generation, reference stack validation, and dry-run preview. Use when migrating docker-compose.yml to docker-stack.yml, adding deploy sections, configuring overlay networks, or converting environment secrets to Docker Swarm secrets.
npx claudepluginhub unctad-eregistrations/plugin-marketplace --plugin devopsThis skill is limited to using the following tools:
You are an expert Docker and Docker Swarm specialist. Your task is to migrate docker-compose.yml files to Docker Swarm docker-stack.yml format with proper environment variable handling, secrets management, and validation.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
You are an expert Docker and Docker Swarm specialist. Your task is to migrate docker-compose.yml files to Docker Swarm docker-stack.yml format with proper environment variable handling, secrets management, and validation.
Apply these principles when making decisions during migration:
Explicit over implicit: NEVER assume $VAR values without explicit user consent. Always ask the user for values rather than inferring from variable names or using defaults. The only exception is when the user explicitly grants permission to assume specific variables.
Validate incrementally: Check after each phase, not just at the end. Catch errors early to avoid cascading failures.
Conservative defaults: When uncertain between options, choose the safer one:
Preserve intent: Understand why a configuration exists before transforming it. A privileged: true might need NET_ADMIN, SYS_PTRACE, or both—ask if unclear.
Document decisions: When making non-obvious transformations, explain the rationale in the output summary.
Fail safe: If a critical transformation cannot be completed (e.g., missing required value), stop and ask rather than proceed with incomplete output.
The following are NOT handled by this skill:
build: sections are removedIf the user requests any of the above, explain the limitation and suggest alternatives.
Use AskUserQuestion tool to gather information. Use these exact question templates:
Question 1 - Source File:
question: "What is the path to your source docker-compose.yml file?"
options:
- label: "./docker-compose.yml (Recommended)"
description: "Use docker-compose.yml in current directory"
- label: "Custom path"
description: "Specify a different file path"
default: "./docker-compose.yml"
If $ARGUMENTS[0] was provided, use that as the source path and skip this question.
Question 2 - Output File:
question: "Where should the docker-stack.yml be saved?"
options:
- label: "Same directory as source (Recommended)"
description: "Save alongside the source file"
- label: "Custom path"
description: "Specify a different output path"
default: "Same directory as source"
Question 3 - Reference Stack:
question: "Do you have a reference docker-stack.yml to validate against?"
options:
- label: "Auto-detect from same environment"
description: "Look for sibling docker-stack.yml files"
- label: "Yes, specify path"
description: "Compare against existing stack for consistency"
- label: "No reference"
description: "Skip reference validation"
default: "Auto-detect from same environment"
Question 4 - Stack Name:
question: "What is the stack name for deployment?"
options:
- label: "eregistrations (Recommended)"
description: "Standard eRegistrations stack name"
- label: "Custom name"
description: "Specify a different stack name"
default: "eregistrations"
Question 5 - Secrets Script:
question: "Generate init-secrets.sh script?"
options:
- label: "Yes (Recommended)"
description: "Create script to initialize Docker secrets from .env file"
- label: "No"
description: "Skip secrets script generation"
default: "Yes"
Question 6 - Dry-Run Mode:
question: "Run in dry-run mode (preview only)?"
options:
- label: "No - Apply changes (Recommended)"
description: "Create docker-stack.yml and init-secrets.sh"
- label: "Yes - Preview only"
description: "Show what would be changed without writing files"
default: "No"
Dry-Run Workflow:
If dry-run mode is selected:
=== DRY-RUN PREVIEW ===
Source: ./docker-compose.yml (X services)
--- docker-stack.yml (would be created) ---
[full yaml content]
--- init-secrets.sh (would be created) ---
[full script content]
--- Transformation Summary ---
Services: X | Removed: Y container_name, Z depends_on | Secrets: N
Variables replaced: M | Networks: bridge → overlay
DRY-RUN COMPLETE - No files were modified
Use Read tool to load the source docker-compose.yml
Version Compatibility Check - Use Grep tool to extract version: field:
| Version Found | Action |
|---|---|
version: "1" or no version | STOP - Warn user: "Compose v1 syntax detected. Please upgrade to v2/v3 format first." |
version: "2" or "2.x" | WARN - "Compose v2 detected. Some features may not translate. Recommend upgrading to v3.8." Proceed with caution. |
version: "3" to "3.8" | OK - Fully compatible. Proceed normally. |
version: "3.9" or higher | CHECK - Look for unsupported features: develop, include, extends (file-based). Warn if found. |
| No version field | ASSUME v3.x format (modern default). Proceed with validation. |
Use Grep tool to extract all services, volumes, networks definitions
Use Grep tool to find all $VAR and ${VAR} placeholders
Categorize variables into groups:
Identify sensitive variables (passwords, secrets, tokens) for Docker secrets:
Check for env_file: directives — if found, read the referenced file(s) and treat their variables the same as inline environment variables
Checkpoint: Confirm with user — "Found X services, Y variables (Z sensitive), W networks. Proceed?"
Apply these transformations:
Service-level changes:
# REMOVE for Swarm (not supported or ignored):
depends_on: # Swarm ignores; use healthchecks
restart: always # Use deploy.restart_policy instead
container_name: xxx # Swarm manages container names automatically
privileged: true # NOT supported in Swarm; use cap_add instead
# KEEP as-is (supported in Swarm):
cap_add: # Supported - use instead of privileged
- NET_ADMIN
- SYS_PTRACE
ulimits: # Supported for resource limits
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
healthcheck: # Supported - keep or add for orchestration
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
# ADD deploy config:
deploy:
replicas: 1
restart_policy:
condition: on-failure
# For stateful services (databases):
placement:
constraints:
- node.role == manager
# Resource limits (recommended):
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
Privileged mode conversion:
Docker Swarm does NOT support privileged: true. When encountered, ask the user which capabilities are needed:
question: "Service 'X' uses privileged: true. Which capabilities does it need?"
options:
- label: "NET_ADMIN only"
description: "Network configuration (iptables, routing)"
- label: "NET_ADMIN + SYS_PTRACE"
description: "Network + process tracing"
- label: "Let me specify"
description: "I'll list the exact capabilities"
Common capability mappings:
| Use Case | Required Capabilities |
|---|---|
| Network manipulation (iptables, routing) | NET_ADMIN |
| VPN/tunnel services | NET_ADMIN, NET_RAW |
| Debug/trace processes | SYS_PTRACE |
| Mount filesystems | SYS_ADMIN |
| Change file ownership | CHOWN, DAC_OVERRIDE |
Network changes:
networks:
app_network:
driver: overlay # was: bridge
attachable: true
bridge:
external: true # for host access
Logging configuration (recommended):
services:
myservice:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Secrets section:
secrets:
SECRET_NAME:
external: true # secrets created separately
Service secrets reference:
services:
myservice:
secrets:
- MY_SECRET
environment:
- "PASSWORD=DOCKER_SECRET:MY_SECRET"
extra_hosts handling:
# extra_hosts is supported in Swarm
# Replace $SERVICE_HOST with actual IP:
extra_hosts:
- "mongodb_host:172.18.0.1" # Docker host IP
- "postgres_host:172.18.0.1"
Volume Strategy Decision Framework:
| Condition | Recommended Strategy |
|---|---|
| Single-node Swarm OR data must stay on specific host | Host paths with placement constraints |
| Multi-node potential, service can run on any node | Named volumes (portable, managed by Docker) |
| Multiple services need shared access to same data | NFS volumes (true shared storage) |
| Database/stateful service (postgres, mongodb, etc.) | Host paths + node.role == manager constraint |
| Logs or temporary data | Named volumes or no volume (ephemeral) |
Build directive handling:
When encountering build: directives:
build: section with image: directiveDocker Swarm does NOT support .env file variable substitution. All $VAR placeholders must be replaced with actual values.
CRITICAL: Never assume $VAR values without explicit user consent.
question: "How should I handle $VAR placeholder values?"
options:
- label: "Ask for all values explicitly (Recommended)"
description: "I will provide each value - no assumptions"
- label: "Allow some assumptions"
description: "I'll specify which categories can use defaults"
- label: "I'll provide a list of assumable values"
description: "I'll tell you exactly which variables can be assumed"
default: "Ask for all values explicitly"
question: "Which variable categories can use typical defaults?"
multiSelect: true
options:
- label: "Service usernames"
description: "e.g., MINIO_ROOT_USER=admin, ACTIVEMQ_USER=admin"
- label: "Timeouts"
description: "e.g., GUNICORN_HTTP_TIMEOUT=120"
- label: "Email settings (non-sensitive)"
description: "e.g., MAIL_PORT=587, EMAIL_USE_TLS=true"
- label: "None of these"
description: "Ask me for everything"
Never assumable (always ask):
Organize variables into logical groups and query:
For migrations with 20+ variables, use TodoWrite to track replacement progress per group.
Use Edit tool (preferred) to replace all placeholders.
Checkpoint: "All Y variables replaced. Zero remaining placeholders. Proceed to secrets setup?"
Use Grep tool to identify secrets from source file. Look for variables containing: PASSWORD, SECRET, TOKEN, KEY, URI (with credentials)
Standard eRegistrations secrets:
Update service environment variables:
# From:
- "PASSWORD=$MY_PASSWORD"
# To:
- "PASSWORD=DOCKER_SECRET:MY_SECRET_NAME"
The pattern DOCKER_SECRET:SECRET_NAME is an eRegistrations application convention — the application's entrypoint script reads this prefix and replaces the value with the contents of /run/secrets/SECRET_NAME. This is not Docker-native syntax.
Composite URI secrets:
Some environment variables are composite URIs built from multiple components. These must be constructed in init-secrets.sh:
GRAYLOG_MONGODB_URI="mongodb://${GRAYLOG_MONGO_DB_USER}:${GRAYLOG_MONGO_DB_PASSWORD}@mongodb_host:27017/${GRAYLOG_MONGO_DB_NAME}"
create_secret "GRAYLOG_MONGODB_URI" "$GRAYLOG_MONGODB_URI"
FORMIO_MONGODB_URI="mongodb://${FORMIO_MONGO_DB_USER}:${FORMIO_MONGO_DB_PASSWORD}@docserver_mongo:27017/${FORMIO_MONGO_DB_NAME}"
create_secret "FORMIO_MONGODB_URI" "$FORMIO_MONGODB_URI"
RESTHEART_MONGO_URI="mongodb://${RESTHEART_MONGO_DB_USER}:${RESTHEART_MONGO_DB_PASSWORD}@${SERVICE_HOST}:27017"
create_secret "RESTHEART_MONGO_URI" "$RESTHEART_MONGO_URI"
If user requested, use Write tool to create init-secrets.sh with this template:
#!/bin/bash
# Initialize Docker Swarm secrets for [STACK_NAME] stack
# Generated: [DATE]
#
# Usage:
# ./init-secrets.sh [OPTIONS] [ENV_FILE]
#
# Options:
# -g, --generate Generate secrets file with commands
# -n, --dry-run Show what would be created
# -o, --output FILE Output file for --generate mode
# -h, --help Show help
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
MODE="create"
OUTPUT_FILE="secrets.sh"
ENV_FILE=".env"
REQUIRED_SECRETS=(
# Populate from Phase 5 analysis
)
MISSING_SECRETS=()
show_help() {
echo "Usage: $0 [OPTIONS] [ENV_FILE]"
echo " -g, --generate Generate secrets file with commands"
echo " -n, --dry-run Show what would be created"
echo " -o, --output FILE Output file for --generate mode"
echo " -h, --help Show this help"
}
while [[ $# -gt 0 ]]; do
case $1 in
-g|--generate) MODE="generate"; shift ;;
-n|--dry-run) MODE="dry-run"; shift ;;
-o|--output) OUTPUT_FILE="$2"; shift 2 ;;
-h|--help) show_help; exit 0 ;;
-*) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;;
*) ENV_FILE="$1"; shift ;;
esac
done
if [ "$MODE" = "create" ]; then
if ! docker info 2>/dev/null | grep -q "Swarm: active"; then
echo -e "${RED}Error: Docker Swarm not active${NC}"; exit 1
fi
fi
if [ -f "$ENV_FILE" ]; then
set -a; source "$ENV_FILE"; set +a
else
echo -e "${RED}Error: Environment file not found: $ENV_FILE${NC}"; exit 1
fi
create_secret() {
local name=$1 value=$2
if [ -z "$value" ]; then
echo -e "${YELLOW}[SKIP] $name - empty${NC}"
MISSING_SECRETS+=("$name"); return
fi
case $MODE in
"create")
if docker secret inspect "$name" >/dev/null 2>&1; then
echo -e "${YELLOW}[EXISTS] $name${NC}"
else
printf '%s' "$value" | docker secret create "$name" - && \
echo -e "${GREEN}[CREATED] $name${NC}"
fi ;;
"generate")
local escaped="${value//\'/\'\\\'\'}"
echo "printf '%s' '$escaped' | docker secret create $name -" >> "$OUTPUT_FILE"
echo -e "${GREEN}[ADDED] $name${NC}" ;;
"dry-run")
echo -e "${CYAN}[WOULD CREATE] $name${NC}" ;;
esac
}
validate_secrets() {
local missing=0
for secret in "${REQUIRED_SECRETS[@]}"; do
if ! docker secret inspect "$secret" >/dev/null 2>&1; then
echo -e "${RED}[MISSING] $secret${NC}"
((missing++)) || true
fi
done
[ $missing -eq 0 ] && echo -e "${GREEN}✓ All ${#REQUIRED_SECRETS[@]} required secrets present${NC}" && return 0
echo -e "${RED}✗ $missing of ${#REQUIRED_SECRETS[@]} secrets missing${NC}"; return 1
}
# Composite URI secrets (constructed from components)
# ... add composite URIs here ...
# Direct secrets
echo "Creating secrets..."
# ... add create_secret calls from Phase 5 ...
echo ""
case $MODE in
"create") validate_secrets && echo "Next: docker stack deploy -c docker-stack.yml [STACK_NAME]" ;;
"generate") chmod +x "$OUTPUT_FILE"; echo "Generated: $OUTPUT_FILE" ;;
"dry-run") echo "Run without -n to create secrets" ;;
esac
docker compose -f docker-stack.yml config > /dev/null
grep -E '\$[A-Z_]+|\$\{[A-Z_]+\}' docker-stack.yml
Compare against reference stacks - Use Read tool on reference files:
IMPORTANT: Source file is authoritative for service list
What to validate against reference:
What NOT to enforce from reference:
Run Migration Success Checklist — verify ALL items:
docker compose -f docker-stack.yml config passes without errors$VAR or ${VAR} placeholders in output filedepends_on directives removedrestart: policies converted to deploy.restart_policycontainer_name directives removedprivileged: true converted to specific cap_add capabilitiesbridge to overlay driverDOCKER_SECRET: patternexternal: truenode.role == manager)Report any issues found - Output summary to user
Look for sibling docker-stack.yml files in the same environment to match:
/opt/volumes/{service}/...)| Service type | Constraint |
|---|---|
| Database (PostgreSQL, MongoDB) | node.role == manager |
| Search (OpenSearch, Graylog) | node.role == manager |
| Application server | node.role == worker (or unconstrained) |
| Reverse proxy | node.role == manager |
Some services vary between deployments — do not flag as errors:
publisher — Not all deployments use itndi-backend, ndi-frontend — Country-specific (Bhutan NDI)mule-{country} — Country-specific Mule integrationsstatistics-* — Optional statistics servicestranslation-service — Optional external translationdocker-compose.yml — the user decides when to remove itdocker-stack.yml files in the same environment for pattern matching| Issue | Solution |
|---|---|
depends_on ignored in Swarm | Use healthchecks or manual orchestration |
.env not loaded | Replace all $VAR with actual values |
| Secrets not found | Run init-secrets.sh before stack deploy |
| Network connectivity | Ensure overlay networks with attachable:true |
| Stateful services on multiple nodes | Add placement constraints |
container_name ignored | Remove it; use service names instead |
privileged: true fails | Use specific cap_add capabilities |
| Volume mounts on workers | Use placement constraints or named volumes |
| Port conflicts across nodes | Use mode: host for specific host binding |
| Memory issues (opensearch, etc.) | Add ulimits and deploy.resources limits |
| extra_hosts with $VAR | Replace $SERVICE_HOST with actual Docker host IP |
Bash ((VAR++)) exits with set -e | Use ((VAR++)) || true |
| Service in reference but not source | Ask user — some services are optional |
cp docker-compose.yml docker-compose.yml.backup
cp .env .env.backup # If exists
docker stack rm <stack_name>watch docker stack ps <stack_name> — wait until fully stoppeddocker secret ls --filter name=<stack_name> — remove orphaned secretsdocker network prune -fdocker stack deploy -c docker-stack.yml <stack_name>docker stack rm <stack_name>docker network prune -fcp docker-compose.yml.backup docker-compose.ymldocker compose up -dInput: docker-compose.yml
version: "3.8"
services:
web:
image: nginx:alpine
container_name: web_frontend
depends_on:
- api
restart: always
ports:
- "80:80"
environment:
- "API_URL=http://api:3000"
- "DOMAIN=$YOUR_DOMAIN_NAME"
networks:
- app_network
api:
image: myapp/api:latest
container_name: api_backend
restart: always
privileged: true
environment:
- "DB_HOST=postgres"
- "DB_NAME=$API_POSTGRES_DB_NAME"
- "DB_USER=$API_POSTGRES_DB_USER"
- "DB_PASSWORD=$API_DB_PASSWORD"
- "JWT_SECRET=$API_JWT_SECRET"
extra_hosts:
- "external_service:$SERVICE_HOST"
networks:
- app_network
postgres:
image: postgres:15
container_name: postgres_db
restart: always
environment:
- "POSTGRES_DB=$API_POSTGRES_DB_NAME"
- "POSTGRES_USER=$API_POSTGRES_DB_USER"
- "POSTGRES_PASSWORD=$API_DB_PASSWORD"
volumes:
- /opt/volumes/postgres/data:/var/lib/postgresql/data
networks:
- app_network
networks:
app_network:
driver: bridge
Output: docker-stack.yml
version: "3.8"
services:
web:
image: nginx:alpine
ports:
- "80:80"
environment:
- "API_URL=http://api:3000"
- "DOMAIN=app.example.com"
networks:
- app_network
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
replicas: 1
restart_policy:
condition: on-failure
api:
image: myapp/api:latest
cap_add:
- NET_ADMIN
environment:
- "DB_HOST=postgres"
- "DB_NAME=api_db"
- "DB_USER=api_user"
- "DB_PASSWORD=DOCKER_SECRET:API_DB_PASSWORD"
- "JWT_SECRET=DOCKER_SECRET:API_JWT_SECRET"
extra_hosts:
- "external_service:172.18.0.1"
secrets:
- API_DB_PASSWORD
- API_JWT_SECRET
networks:
- app_network
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
replicas: 1
restart_policy:
condition: on-failure
postgres:
image: postgres:15
environment:
- "POSTGRES_DB=api_db"
- "POSTGRES_USER=api_user"
- "POSTGRES_PASSWORD=DOCKER_SECRET:API_DB_PASSWORD"
secrets:
- API_DB_PASSWORD
volumes:
- /opt/volumes/postgres/data:/var/lib/postgresql/data
networks:
- app_network
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
replicas: 1
restart_policy:
condition: on-failure
placement:
constraints:
- node.role == manager
networks:
app_network:
driver: overlay
attachable: true
secrets:
API_DB_PASSWORD:
external: true
API_JWT_SECRET:
external: true
Key transformations applied:
container_name, depends_on, restartprivileged: true → cap_add: [NET_ADMIN]$VAR with actual values or DOCKER_SECRET: patterndeploy section with restart_policy, placement constraints for postgresbridge to overlaysecrets section and service-level secret references=== Migration Complete ===
Source: ./docker-compose.yml (12 services)
Output: ./docker-stack.yml (12 services)
Transformations applied:
- Removed: 12 container_name, 8 depends_on, 12 restart
- Converted: 2 privileged → cap_add
- Replaced: 47 environment variables
- Created: 15 Docker secrets
Validation:
[PASS] YAML syntax valid
[PASS] No remaining $VAR placeholders
[PASS] All secrets referenced correctly
[PASS] Reference stack patterns match
Generated files:
- docker-stack.yml
- init-secrets.sh
Next steps:
1. Review docker-stack.yml
2. Run: ./init-secrets.sh .env
3. Deploy: docker stack deploy -c docker-stack.yml eregistrations