Production-grade GitOps infrastructure for deploying and managing Model Context Protocol (MCP) servers across distributed Docker environments.
Modern IT infrastructure demands:
- Automated, repeatable deployments that eliminate configuration drift
- Zero-trust secret management that never commits credentials to source control
- Multi-environment orchestration supporting both persistent and ephemeral workloads
- Infrastructure as Code principles for auditability and version control
This repository demonstrates enterprise-grade DevOps practices applied to container orchestration, showcasing:
- ✅ GitOps methodology - Single source of truth with automated sync
- ✅ Security-first architecture - Secrets delivered via encrypted channels, never stored in Git
- ✅ Production resilience - Health checks, resource limits, logging, and rollback capabilities
- ✅ Multi-platform support - Agent-based (always-on hosts) and Edge-based (roaming devices) deployment models
- ✅ CI/CD automation - Automated testing, security scanning, and deployment workflows
This platform manages MCP server deployments using GitOps principles with Portainer CE across heterogeneous infrastructure
- Overview
- Architecture
- Quick Start
- MCP Servers
- Secrets Management
- Deployment Workflows
- CI/CD Pipeline
- Adding New MCP Servers
- Production Features
- Triggering Redeployments
- Off-LAN Access
- Troubleshooting
- Scripts Reference
This repository manages MCP server deployments using:
- GitOps: Compose files stored in Git, auto-deployed via Portainer
- Multi-environment: Separate configs for desktops (Agent) and laptops (Edge)
- Zero secrets in Git: Edge Configs and agent env files for secrets
- Automated workflows: Scripts for install, validation, and rollback
Key Principle: Never commit secrets to Git
📝 Note: Throughout this documentation,
portainer-server.local:9444is used as an example Portainer server hostname. Replace this with your own Portainer server address (e.g.,portainer.example.com:9443or your server's IP/hostname).
| Type | Endpoints | Connection | GitOps | Secrets Delivery |
|---|---|---|---|---|
| Agent | Desktops, always-on hosts | Direct (port 9001) | Auto-sync on Git commit | Host env file (/run/mcp/mcp.env) |
| Edge | Laptops, roaming hosts | Tunnel (port 8000) | Manual redeploy (CE limitation) | Edge Config (.env file) |
mcp-stacks/
├── stacks/
│ ├── common/docker-compose.yml # Shared MCP services
│ ├── desktop/docker-compose.yml # Agent endpoints (includes common)
│ └── laptop/docker-compose.yml # Edge endpoints (explicit services)
├── edge-configs/
│ ├── README.md # Edge Config documentation
│ └── laptops.zip # Generated (gitignored)
├── scripts/
│ ├── build-edge-config.{ps1,sh} # Create Edge Config bundle
│ ├── install/
│ │ ├── configure-agent-env.{ps1,sh} # Create /run/mcp/mcp.env on agents
│ │ ├── install-agent.{ps1,sh} # Install Portainer Agent (desktops)
│ │ ├── uninstall-agent.{ps1,sh}
│ │ ├── install-edge-agent.{ps1,sh} # Install Edge Agent (laptops)
│ │ └── uninstall-edge-agent.{ps1,sh}
│ ├── api/
│ │ └── redeploy-stack.{ps1,sh} # API-based redeploy helper
│ ├── validation/
│ │ ├── pre-deploy-check.ps1 # Pre-deployment validation
│ │ └── post-deploy-check.ps1 # Post-deployment validation
│ └── rollback-stack.ps1 # Rollback to previous commit
└── README.md # This file
The MCP stack has been tuned for resource-constrained environments, specifically the UGREEN DXP 2800 NAS:
- CPU: Intel N100 (4 cores) or equivalent
- RAM: 8-16GB (shared with other workloads like 4K transcoding)
- Storage: 10GB free space for Docker images and logs
Current resource limits are conservative to accommodate concurrent workloads (e.g., 4K video transcoding):
| Service | CPU Limit | Memory Limit | Notes |
|---|---|---|---|
| context7 | 0.5 core | 256MB | Reduced from 1.0/512MB |
| dockerhub | 0.25 core | 128MB | Reduced from 0.5/256MB |
| playwright | 1.0 core | 1GB | Reduced from 2.0/2GB (browser automation) |
| sequentialthinking | 0.5 core | 256MB | Reduced from 1.0/512MB |
| Total | 2.25 cores | 1.64GB | Leaves ~1.75 cores for transcoding/OS |
If you have more powerful hardware or dedicated systems, you can increase limits:
High-Performance Desktop (8+ cores, 32GB+ RAM):
# In stacks/common/docker-compose.yml or stacks/laptop/docker-compose.yml
deploy:
resources:
limits:
cpus: '2.0' # Increase from 0.5
memory: 1G # Increase from 256MDedicated NAS Without Transcoding:
- Can restore original limits (double the current values)
- Playwright can go back to 2.0 CPUs / 2GB for better browser performance
Lower-End Hardware (2-core systems):
- Consider running only essential services (disable playwright if not needed)
- Reduce limits further or stagger service usage
- Docker Desktop installed on all endpoints
- Portainer CE running at
https://portainer-server.local:9444 - Git and GitHub CLI (
gh) configured - Network access to your Portainer server (on-LAN or via VPN/Tailscale)
git clone https://github.com/unplugged12/mcp-stacks.git
cd mcp-stacksDesktops (Agent):
.\scripts\install\install-agent.ps1Laptops (Edge):
.\scripts\install\install-edge-agent.ps1
# Follow prompts to paste docker run command from Portainer UI- Go to https://portainer-server.local:9444
- Environments → Add environment
- Select Docker Standalone → Agent
- Enter:
- Name:
desktop-<hostname> - Environment URL:
<desktop-ip>:9001
- Name:
- Click Add environment
- Go to https://portainer-server.local:9444
- Environments → Add environment
- Select Docker Standalone → Edge Agent → Standard
- Configure:
- Name:
laptop-<name> - Portainer server URL:
https://portainer-server.local:9444 - Edge Group:
laptops(create if doesn't exist)
- Name:
- Copy the generated
docker runcommand - Run the install script and paste the command when prompted
For Laptops (Edge Config):
.\scripts\build-edge-config.ps1
# Follow prompts to enter secrets interactively
# Upload generated edge-configs/laptops.zip via Portainer UI:
# Edge Configurations → Create → Upload ZIP → Target: laptops groupFor Desktops (Agent env file):
sudo ./scripts/install/configure-agent-env.sh
# or, with PowerShell 7+
sudo pwsh ./scripts/install/configure-agent-env.ps1- Run on each agent host before the first deployment.
- Creates
/run/mcp/mcp.envwith the required secrets and locks down permissions. - Redeploy the stack after updating credentials so containers reload the values.
- Stacks → Add stack
- Git Repository (under "Build method")
- Configure:
- Repository URL:
https://github.com/unplugged12/mcp-stacks - Reference:
refs/heads/main - Compose path:
stacks/desktop/docker-compose.yml
- Repository URL:
- (Optional) Define
MCP_ENV_FILEif you used a non-default path. Leave blank to use/run/mcp/mcp.envcreated by the setup script. - Enable GitOps updates (automatic polling)
- Deploy
- Edge Stacks → Add stack
- Git Repository
- Configure:
- Repository URL:
https://github.com/unplugged12/mcp-stacks - Reference:
refs/heads/main - Compose path:
stacks/laptop/docker-compose.yml - Target: Edge Group
laptops
- Repository URL:
- Deploy
Note: GitOps auto-sync for Edge Stacks is a Business feature. In CE, use Pull and redeploy after commits.
| Server | Image | Description | Auth Required |
|---|---|---|---|
| Context7 | mcp/context7:latest |
Context management | ✅ CONTEXT7_TOKEN |
| Docker Hub | mcp/dockerhub:latest |
Docker Hub integration | ✅ HUB_USERNAME, HUB_PAT_TOKEN |
| Playwright | mcp/mcp-playwright:latest |
Browser automation | ❌ |
| Sequential Thinking | mcp/sequentialthinking:latest |
Sequential reasoning | ❌ |
All images are available on Docker Hub under the mcp/ organization.
- Never commit secrets to Git (protected by
.gitignore) - Edge (laptops): Secrets delivered via Edge Config
.envfile - Agent (desktops): Secrets stored in
/run/mcp/mcp.envon each host - Portainer DB: Consider enabling encryption at rest (Portainer settings)
Path on endpoint: /var/edge/configs/mcp.env
Build and deploy:
# 1. Build config bundle
.\scripts\build-edge-config.ps1
# 2. Upload in Portainer UI
# Edge Configurations → Create configuration
# Name: mcp-env
# Target: Edge Group "laptops"
# Upload: edge-configs/laptops.zipThe bundle contains:
HUB_USERNAME=<value>
HUB_PAT_TOKEN=<value>
CONTEXT7_TOKEN=<value>Set during stack creation:
- Portainer UI → Stacks → Add stack → Environment variables section
Update existing stack:
- Portainer UI → Stacks → Select stack → Editor tab → Environment variables
Portainer stores these in its database (not in Git).
- Make changes to compose files
- Commit and push to Git
git add stacks/ git commit -m "Add new MCP server" git push origin main - Portainer auto-syncs (GitOps polling enabled)
- Stack redeployed automatically
Manual trigger (optional):
.\scripts\api\redeploy-stack.ps1 -ApiKey "ptr_xxxx" -StackName "mcp-desktop"- Make changes to compose files
- Commit and push to Git
- Trigger redeploy via Portainer UI:
- Edge Stacks → Select stack → Pull and redeploy
Or use helper script:
.\scripts\api\redeploy-stack.ps1 -ApiKey "ptr_xxxx" -StackName "mcp-laptop" -Type edge
# Note: Will display instructions for CE usersPortainer honors the Compose profiles declared in the stack files:
- Core services (
mcp-context7,mcp-dockerhub,mcp-sequentialthinking) run under both thedefaultandliteprofiles. - Browser automation (
mcp-playwright) is only part of thedefaultprofile.
To deploy a lightweight stack on laptops, NAS devices, or other constrained hosts:
- Portainer UI → Stacks → Select the stack → Editor.
- In the Environment variables panel add
COMPOSE_PROFILES=lite(or edit the existing variable). - Click Update the stack to redeploy.
With COMPOSE_PROFILES=lite, Portainer omits the Playwright container entirely and applies the reduced resource requests (1 vCPU / 1 GB RAM / 1 GB shm_size). If you later need browser automation, clear the variable or set COMPOSE_PROFILES=default to restore the full profile.
Tip for NAS owners: Skip Playwright unless the NAS has spare CPU and RAM headroom; the lite profile keeps the core MCP services responsive without launching a Chromium worker.
This repository uses GitHub Actions for automated testing, security scanning, and deployment orchestration.
The CI/CD pipeline provides:
- Automated Linting: PSScriptAnalyzer for PowerShell, shellcheck for Bash
- Security Scanning: SAST analysis with Trivy and Semgrep
- Validation: Docker Compose syntax checking and script testing
- SBOM Generation: Software Bill of Materials for dependency tracking
- Deployment Automation: Manual deployment workflows with validation
Runs automatically on every push and pull request:
# View CI status
gh workflow view ci.yml
# Manually trigger CI
gh workflow run ci.ymlPipeline Stages:
- Lint PowerShell and Bash scripts
- Security SAST scanning (Trivy + Semgrep)
- Validate Docker Compose files
- Generate SBOM and scan for vulnerabilities
- Test scripts on Windows and Linux runners
Manual deployment with validation:
# Deploy to desktop environments
gh workflow run deploy.yml -f environment=desktop
# Deploy to laptop environments
gh workflow run deploy.yml -f environment=laptop
# Deploy to all environments
gh workflow run deploy.yml -f environment=allNote: For Portainer CE, the deployment workflow provides detailed manual deployment instructions. Automatic API deployment requires the PORTAINER_API_KEY secret and is primarily informational for CE users.
Configure these secrets in repository settings for CI/CD:
| Secret | Purpose | Required |
|---|---|---|
HUB_USERNAME |
Docker Hub authentication | Yes |
HUB_PAT_TOKEN |
Docker Hub PAT | Yes |
CONTEXT7_TOKEN |
Context7 service auth | Yes |
PORTAINER_API_KEY |
Portainer API access (optional) | No |
See docs/SECRETS.md for detailed secrets configuration.
For comprehensive pipeline documentation, see:
- docs/PIPELINE.md - Complete pipeline architecture and usage
- docs/SECRETS.md - Secrets management and rotation
Edit stacks/common/docker-compose.yml:
services:
mcp-newserver:
image: mcp/newserver:latest
restart: unless-stopped
env_file: ${MCP_ENV_FILE:-/run/mcp/mcp.env}Edit stacks/laptop/docker-compose.yml:
services:
mcp-newserver:
image: mcp/newserver:latest
restart: unless-stopped
env_file: /var/edge/configs/mcp.envFor Edge (laptops):
- Update
scripts/build-edge-config.ps1to prompt for new secrets - Rebuild and redeploy Edge Config
For Agent (desktops):
- Update
scripts/install/configure-agent-env.{ps1,sh}to prompt for the new secret - Re-run the script on each agent host to refresh
/run/mcp/mcp.env
# Validate before deploy
.\scripts\validation\pre-deploy-check.ps1
# Commit and push
git add stacks/
git commit -m "Add mcp-newserver"
git push origin main
# Agent: Auto-deploys
# Edge: Manual redeploy in UI
# Verify after deploy
.\scripts\validation\post-deploy-check.ps1 -StackPrefix "mcp"All MCP services include container-level health checks:
- Health Check Type: TCP connectivity test to port 3000
- Interval: Every 30 seconds
- Retries: 3 consecutive failures before marking unhealthy
- Start Period: 40-60 seconds (varies by service)
View health status:
docker ps # Shows "(healthy)" or "(unhealthy)" status
docker inspect <container-name> --format='{{.State.Health.Status}}'Health check details:
docker inspect <container-name> --format='{{json .State.Health}}' | ConvertFrom-JsonResource limits prevent any single service from consuming excessive resources. Current limits are tuned for the UGREEN DXP 2800 NAS with concurrent 4K transcoding workload:
| Service | CPU Limit | Memory Limit | CPU Reservation | Memory Reservation |
|---|---|---|---|---|
| context7 | 0.5 core | 256MB | 0.25 core | 128MB |
| dockerhub | 0.25 core | 128MB | 0.1 core | 64MB |
| playwright | 1.0 core | 1GB | 0.5 core | 256MB |
| sequentialthinking | 0.5 core | 256MB | 0.25 core | 128MB |
| Total | 2.25 cores | 1.64GB | 1.1 cores | 576MB |
See the Hardware Requirements section for tuning guidance.
Monitor resource usage:
docker statsAll services use structured JSON logging with automatic rotation:
- Max log size per file: 10MB
- Max files retained: 3
- Total max storage: 30MB per container
View logs:
docker logs <container-name> --tail 100 --follow
docker logs <container-name> --since 1h
docker logs <container-name> --timestampsAll services use restart: unless-stopped policy:
- Automatically restart on failure
- Restart after Docker daemon restarts
- Don't restart if manually stopped
Labels enable filtering and observability:
com.mcp.service- Service identifiercom.mcp.version- Image versioncom.mcp.environment- Environment (production/staging/dev)com.mcp.deployment- Deployment type (agent/edge)
Filter by label:
docker ps --filter "label=com.mcp.service=playwright"
docker ps --filter "label=com.mcp.deployment=edge"Run comprehensive health validation after deployment:
.\scripts\smoke-test.ps1 -StackPrefix "mcp"Tests performed:
- Docker daemon availability
- Container discovery
- Running status verification
- Health check status
- Resource limits enforcement
- Environment variable loading
- Logging configuration
- Recent log output analysis
- Port exposure validation
- Restart policy verification
- Container uptime tracking
- Resource usage snapshot
Options:
# Verbose output
.\scripts\smoke-test.ps1 -StackPrefix "mcp" -Verbose
# Custom timeout
.\scripts\smoke-test.ps1 -StackPrefix "mcp" -Timeout 180
# Skip health checks
.\scripts\smoke-test.ps1 -StackPrefix "mcp" -SkipHealthCheckFor comprehensive observability strategy including OpenTelemetry instrumentation, metrics collection, and distributed tracing, see:
Topics covered:
- OpenTelemetry SDK integration for Node.js MCP servers
- Metrics collection with Prometheus
- Log aggregation with Loki
- Distributed tracing with Jaeger
- Alerting with Alertmanager
- Grafana dashboards
- Container metrics with cAdvisor
- The full Prometheus/Grafana/Loki bundle typically consumes 4+ vCPU, 8+ GB RAM, and fast SSD storage for metrics and logs. Avoid running it on the NAS when Plex, backups, or VMs already compete for those resources. Instead, deploy it on a dedicated observability node or cloud VM and point agents at that host.
- For NAS deployments, use the lightweight collectors defined in
stacks/monitoring-lite. They forward telemetry to a remote backend (Grafana Cloud, InfluxDB Cloud, VictoriaMetrics, etc.) while keeping local usage under ~200 MB RAM and <1 vCPU steady state. - Always store remote API tokens in Portainer secrets or stack environment variables—never commit credentials to Git.
git commit -am "Update MCP configuration"
git push origin main
# Portainer auto-syncs within polling interval (default: 5 minutes).\scripts\api\redeploy-stack.ps1 `
-ApiKey "ptr_YOUR_API_KEY" `
-StackName "mcp-desktop"Get API key: Portainer UI → User settings → Access tokens → Add access token
- https://portainer-server.local:9444/#!/edge/stacks
- Select stack
- Click Pull and redeploy
.\scripts\rollback-stack.ps1 `
-ApiKey "ptr_YOUR_API_KEY" `
-StackName "mcp-desktop"
# Follow prompts to select commit hash- Configure WireGuard client on laptops
- DNS: Ensure
portainer-server.localresolves via WireGuard DNS
- MagicDNS: Auto-resolves friendly names across all nodes
- Mesh networking: Direct peer-to-peer when possible
- Easy roaming: Works seamlessly on/off-LAN
- Zero-config: No manual port forwarding
On portainer-server (NAS):
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh --hostname portainer-serverOn Windows Laptops:
winget install -e --id Tailscale.Tailscale
tailscale up --hostname laptop1On macOS/Linux:
# See https://tailscale.com/download
tailscale up --hostname <name>Generate pre-approved keys at: https://login.tailscale.com/admin/settings/keys
tailscale up --auth-key tskey-auth-XXXXX --ssh --hostname <name>Once connected, access via Tailscale hostname:
https://portainer-server.tail<YOUR_TAILNET>.ts.net:9444
Or update your hosts to use Tailscale IP.
Symptoms: Edge environment shows "offline" in Portainer
Solutions:
- Check Edge tunnel reachability:
Test-NetConnection portainer-server.local -Port 8000
- Verify agent container is running:
docker ps --filter "name=portainer_edge_agent" - Check agent logs:
docker logs portainer_edge_agent
- Ensure Portainer server URL is
https://portainer-server.local:9444(not 9443!)
Check compose syntax:
.\scripts\validation\pre-deploy-check.ps1View Portainer logs:
- Portainer UI → Stacks → Select stack → Logs tab
Inspect container logs:
docker logs <container-name>Verify Docker Hub credentials (if private images):
docker login
docker pull mcp/context7:latestFor stack deployments: Ensure credentials are in secrets (Edge Config or agent env file)
Verify GitOps is enabled:
- Portainer UI → Stacks → Select stack → ensure "Auto update" is ON
Check polling interval:
- Default is 5 minutes; manually trigger if needed via API script
Verify webhook (if configured):
- Portainer UI → Stacks → Select stack → Webhook tab
Edge (laptops):
- Verify Edge Config deployed to correct group:
- Portainer UI → Edge Configurations → Check target
- Check config file path in compose:
/var/edge/configs/mcp.env - Inspect container:
docker exec <container> cat /var/edge/configs/mcp.env
Agent (desktops):
- Verify the env file on the host:
sudo cat /run/mcp/mcp.env
- If you used a custom path, confirm the stack's
MCP_ENV_FILEvariable matches the location. - Inspect container environment:
docker inspect <container> --format='{{.Config.Env}}'
| Script | Purpose | Platform |
|---|---|---|
scripts/install/install-agent.ps1 |
Install Portainer Agent on desktops | PowerShell |
scripts/install/install-agent.sh |
Install Portainer Agent on desktops | Bash |
scripts/install/install-edge-agent.ps1 |
Install Edge Agent on laptops | PowerShell |
scripts/install/install-edge-agent.sh |
Install Edge Agent on laptops | Bash |
scripts/install/uninstall-agent.ps1 |
Remove Agent | PowerShell |
scripts/install/uninstall-edge-agent.ps1 |
Remove Edge Agent | PowerShell |
| Script | Purpose | Platform |
|---|---|---|
scripts/build-edge-config.ps1 |
Build Edge Config bundle with secrets | PowerShell |
scripts/build-edge-config.sh |
Build Edge Config bundle with secrets | Bash |
scripts/install/configure-agent-env.ps1 |
Create /run/mcp/mcp.env on agent hosts |
PowerShell |
scripts/install/configure-agent-env.sh |
Create /run/mcp/mcp.env on agent hosts |
Bash |
| Script | Purpose | Platform |
|---|---|---|
scripts/api/redeploy-stack.ps1 |
Trigger stack redeploy via API | PowerShell |
scripts/api/redeploy-stack.sh |
Trigger stack redeploy via API | Bash |
scripts/rollback-stack.ps1 |
Rollback to previous Git commit | PowerShell |
| Script | Purpose | Platform |
|---|---|---|
scripts/validation/pre-deploy-check.ps1 |
Pre-deployment validation | PowerShell |
scripts/validation/post-deploy-check.ps1 |
Post-deployment verification | PowerShell |
scripts/smoke-test.ps1 |
Comprehensive health validation suite | PowerShell |
When adding new MCP servers or making infrastructure changes:
- Never commit secrets - Use Edge Configs or agent env files
- Validate before commit:
.\scripts\validation\pre-deploy-check.ps1
- Test locally:
docker compose -f stacks/desktop/docker-compose.yml up -d
- Document changes in this README
- Create meaningful commits - helpful for rollbacks
The mcp-stacks platform is evolving toward production-grade maturity. Planned enhancements include:
- Monitoring & Alerting - Prometheus/Grafana stack with health metrics, log aggregation via Loki, and PagerDuty/Opsgenie integration for on-call alerting
- Multi-Environment Support - Isolated dev/staging/prod environments with automated promotion workflows and environment-specific configurations
- CI/CD Integration - GitHub Actions or Azure Pipelines for automated testing, secrets scanning, and deployment orchestration
- Disaster Recovery - Automated backup/restore procedures for Portainer configs and Edge settings
- Performance Optimization - Container profiling, image layer optimization, and network tuning
- Tailscale Integration - Mesh networking with MagicDNS for seamless off-LAN connectivity
- Expanded MCP Catalog - Evaluation and deployment of additional MCP servers (filesystem, database, git, etc.)
- Enhanced Documentation - Architecture diagrams, operational runbooks, troubleshooting decision trees
For detailed roadmap, risks, and work breakdown, see docs/ROADMAP.md.
For the complete backlog ready for Azure DevOps/Jira import, see docs/backlog.csv.
This project is licensed under the MIT License - see the LICENSE file for details.
You are free to use, modify, and distribute this code in accordance with the MIT License terms.
Portainer Documentation: https://docs.portainer.io MCP Protocol: https://modelcontextprotocol.io
For issues with this setup, check:
- Portainer logs
- Container logs (
docker logs <name>) - This repository's documentation