A web-based Docker Compose stack manager with Git integration.
Pre-built multi-arch images (linux/amd64, linux/arm64) are published to the
GitHub Container Registry on each release:
docker pull ghcr.io/thinkbig1979/capstan:latestThe fastest way to run it is with docker-compose.prod.yaml, which already
points at the published image:
docker compose -f docker-compose.prod.yaml up -dPin a version tag (e.g. ghcr.io/thinkbig1979/capstan:0.7.0) for reproducible
deployments; :latest tracks the most recent release.
# Start everything (backend + frontend)
./start-local.shThen open http://localhost:3001
- Docker Compose Management: Create, start, stop, restart, and delete stacks
- Compose Editor: Edit docker-compose.yaml files with live linting
- Environment Files: Manage .env files with comment preservation
- Git Integration: Status, pull, log, and diff for git-managed stacks
- Real-time Updates: File watching for automatic stack detection
- Action Logging: Audit trail of all operations
Capstan includes a built-in backup engine powered by restic and rclone, both shipped inside the container image at pinned versions:
| Tool | Version | Purpose |
|---|---|---|
| restic | 0.18.0 | Local encrypted snapshot backups |
| rclone | 1.68.2 | Cloud sync / offsite DR copies |
Each stack's compose directory is backed up as a restic snapshot tagged with the stack ID. Backups can be triggered manually (Settings, Backup tab) or run on a schedule.
The quickest path is the Settings UI (Settings, Backup). All fields save to the encrypted database and take effect without a restart.
Alternatively, set env vars in your .env file (see .env.example for the full list).
The UI always wins over env vars.
Key variables:
| Variable | Default | Notes |
|---|---|---|
RESTIC_REPOSITORY |
/app/data/restic-repo |
Path inside the container |
RESTIC_PASSWORD |
(none) | Required; stored encrypted when set in UI |
RCLONE_REMOTE |
(none) | rclone remote name (optional) |
RCLONE_PATH |
capstan-backups |
Destination path on the remote |
The restic repository lives inside /app/data. Your compose file MUST mount this
as a host bind mount so that snapshots survive container recreation:
volumes:
- ./data:/app/data # host bind mount — required for backup persistenceNever replace this with a Docker named volume. The docker-compose.prod.yaml and
docker/compose.yaml both use a bind mount by default.
# Via the UI: Settings → Backup → Run Backup Now
# Via the API:
curl -X POST http://localhost:5001/api/v1/backup/runConfigure an rclone remote and set RCLONE_REMOTE (or use the UI). Enable
"Sync after backup" to push every snapshot to the remote automatically.
Rclone config file: mount it read-only into the container if you manage it externally:
volumes:
- ~/.config/rclone/rclone.conf:/home/appuser/.config/rclone/rclone.conf:ro# List snapshots for a stack:
curl http://localhost:5001/api/v1/backup/snapshots?stackId=<id>
# Restore a snapshot via UI: Settings → Backup → Snapshots → Restore
# Or via API:
curl -X POST http://localhost:5001/api/v1/backup/restore \
-H 'Content-Type: application/json' \
-d '{"stackId":"<id>","snapshotId":"<short-id>"}'If the host is lost, recover from an rclone remote:
- Deploy a fresh Capstan instance with the same
./databind mount path. - Trigger a DR restore (Settings, Backup, DR Restore) pointing at your rclone remote.
This syncs the full restic repository back to
/app/data/restic-repo. - Restore individual stacks via the Snapshots panel.
The restic repository password is required for DR recovery. Store it separately from the server (e.g. a password manager).
Important: Capstan requires that the STACKS_DIR path inside the container must match the path on the host system for Docker Compose operations to work correctly.
Add both environment variables to your docker-compose.yaml:
environment:
- STACKS_DIR=/opt/stacks
- HOST_STACKS_DIR=/opt/stacksOn startup, Capstan validates path identity and logs warnings if paths don't match:
docker-compose logs backend | grep "Volume path identity"See Volume Path Identity for:
- Why this requirement exists
- Correct vs incorrect examples
- Troubleshooting steps
- Migration guide from Dockge
Capstan manages your stacks by talking to the host's Docker daemon through the
mounted socket (/var/run/docker.sock). A few things worth understanding:
You do not need to create users, edit groups, or change any permissions on your host. Just mount the socket and a data directory:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data
- /opt/stacks:/opt/stacks # your compose projectsOn startup the container briefly runs as root only to:
- discover the actual group GID of the mounted socket (it differs per
host — Debian/Ubuntu often use
999, others998/130/…) and join it, and - align its runtime user to your file owner,
then it drops privileges and runs the app as the non-root appuser. Because the
socket's group is detected at runtime, the same image works on any host without
rebuilding.
appuser defaults to UID/GID 1000 — the typical first Linux user, so on most
single-user hosts it "just works" and your stack files stay editable from the
host. If the user that owns your stacks/data is a different ID, set:
environment:
- PUID=1001
- PGID=1001Capstan owns and chowns its own ./data dir; it does not rewrite ownership
of your existing compose projects — it matches their owner via PUID/PGID instead.
Anyone who can reach /var/run/docker.sock has root-equivalent control of the
host (the Docker API can start a privileged container that mounts /). This is
true regardless of the in-container user, so the non-root appuser is
defense-in-depth for Capstan's own files — not a containment boundary for Docker
itself. Two consequences:
-
:roon the socket mount is cosmetic. It makes the socket file read-only but the Docker API still accepts write commands (create/start/delete) through it, so it is not a safeguard. Capstan does not use it. -
For real least-privilege, put a socket proxy in front of Capstan and expose only the API endpoints it needs (containers, exec, and the system/version endpoints), denying the rest:
services: docker-proxy: image: tecnativa/docker-socket-proxy environment: CONTAINERS: 1 SERVICES: 1 TASKS: 1 POST: 1 # required for start/stop/create EXEC: 1 # required for the in-app terminal volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: [capstan-network] capstan: image: capstan:latest environment: - DOCKER_HOST=tcp://docker-proxy:2375 # no socket mount on Capstan itself networks: [capstan-network]
capstan/
├── backend/ # Go backend API
│ ├── cmd/ # Main application
│ ├── internal/ # Internal packages
│ └── services/ # Service layer
├── frontend/ # React frontend
│ └── src/ # Source code
└── .agent-os/ # Agent OS configuration
- TESTING.md - Local testing and development guide
- CLAUDE.md - Agent OS framework instructions
- Deployment Guide - Production deployment, SSL/TLS configuration, environment variables, reverse proxy setup
- Migration from Dockge - Step-by-step migration guide from Dockge
- Troubleshooting Guide - Common issues and solutions
- Volume Path Identity - Critical configuration requirement for Docker Compose operations
./start-local.sh # Start all services
docker-compose logs -f # View logs
docker-compose down # Stop all servicescd backend
./run-local.sh # Quick start
make run # Make target
make test # Run testscd frontend
./run-dev.sh # Quick start (dev server)
npm run build # Build for productionGET /health- Health check
POST /api/v1/auth/login- LoginPOST /api/v1/auth/logout- Logout
GET /api/v1/stacks- List all stacksGET /api/v1/stacks/:id- Get stack detailsPOST /api/v1/stacks- Create new stackPOST /api/v1/stacks/:id/start- Start stackPOST /api/v1/stacks/:id/stop- Stop stackPOST /api/v1/stacks/:id/restart- Restart stackDELETE /api/v1/stacks/:id- Delete stack
GET /api/v1/stacks/:id/compose- Get compose filePUT /api/v1/stacks/:id/compose- Save compose filePOST /api/v1/stacks/:id/compose/lint- Lint compose file
GET /api/v1/stacks/:id/env- Get env filePUT /api/v1/stacks/:id/env- Save env file
GET /api/v1/directories/:path/git- Get git statusPOST /api/v1/directories/:path/git/pull- Pull changesGET /api/v1/directories/:path/git/log- Get commit logGET /api/v1/directories/:path/git/diff/:hash- Get commit diff
Migrating from Dockge? See the comprehensive Migration from Dockge guide for:
- Prerequisites and backup procedures
- Side-by-side setup (both apps running)
- Port differences (Dockge 5001 → Capstan 5001)
- Account migration (manual: create new admin)
- Complete feature comparison table
- Troubleshooting common migration issues
For a quick overview:
-
Backup existing stacks:
cp -r /opt/stacks /opt/stacks.backup
-
Update environment variables (Dockge uses
DOCKGE_STACKS_DIR, Capstan usesSTACKS_DIR):environment: - STACKS_DIR=/opt/stacks - HOST_STACKS_DIR=/opt/stacks
-
Restart service:
docker-compose down docker-compose up -d
-
Verify path validation:
docker-compose logs backend | grep "Volume path identity"
-
Test with a simple stack before migrating production data
For detailed migration steps, see the Migration from Dockge guide.
For production deployment, see the comprehensive Deployment Guide which covers:
- Quick Start: Basic deployment steps
- Production Checklist: Security, SSL, monitoring, backups
- Environment Variables: Complete list with descriptions
- Reverse Proxy: nginx, Traefik, Caddy examples
- SSL/TLS: Certbot examples
- Docker Socket Security: Permissions and best practices
# Generate secure JWT secret
JWT_SECRET=$(openssl rand -hex 32)
# Create production .env file
cat > .env << EOF
PORT=5001
LOG_LEVEL=info
JWT_SECRET=$JWT_SECRET
AUTH_DISABLED=false
STACKS_DIR=/opt/stacks
HOST_STACKS_DIR=/opt/stacks
DATA_DIR=/app/data
TRUSTED_NETWORKS=172.16.0.0/12,10.0.0.0/8,192.168.0.0/16,127.0.0.1
EOF- Always set a strong JWT secret (min 32 characters)
- Enable authentication in production (
AUTH_DISABLED=false) - Use SSL/TLS for all connections
- Mount Docker socket as read-only (
/var/run/docker.sock:/var/run/docker.sock:ro) - Configure trusted networks for access control
- Set up regular backups of stack configurations
- Monitor resource usage and set appropriate limits
- Use a reverse proxy (nginx, Traefik, Caddy) with SSL termination
For detailed production deployment instructions, see the Deployment Guide.
- Language: Go 1.24
- Database: SQLite
- Framework: Gin
- Docker SDK: go-docker
- Git Library: go-git
- Language: TypeScript
- Framework: React + Vite
- UI: Tailwind CSS
- State: TanStack Query
- Editor: CodeMirror 6
Capstan follows Semantic Versioning (MAJOR.MINOR.PATCH),
published as git tags prefixed with v (e.g. v0.1.0).
While in the 0.x range, Capstan is pre-stable: it offers no
backward-compatibility guarantees yet. During this phase, treat a MINOR bump
(0.1.x → 0.2.0) as potentially breaking and a PATCH bump (0.1.0 →
0.1.1) as fixes only. The first stable release will be v1.0.0, after which
standard SemVer rules apply (MAJOR for breaking changes).
Each release tag publishes the matching container image tags:
| Git tag | Image tags |
|---|---|
v0.7.0 |
:0.7.0, :0.7, :latest |
v0.8.0-rc.1 |
:0.8.0-rc.1 (pre-release; not :latest) |
Pin a specific version (e.g. ghcr.io/thinkbig1979/capstan:0.7.0) for
reproducible deployments; :latest always tracks the most recent stable
release.
MIT