Skip to content

thinkbig1979/capstan

Repository files navigation

Capstan

Capstan

Build and publish image GHCR image Architectures License: MIT Built with AI assistance

A web-based Docker Compose stack manager with Git integration, backups, and a built-in terminal. Capstan ships as a single multi-arch container that serves both the API and the web UI on one port.

Quick Start

Run the published image (any machine)

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:latest

The fastest way to run it is with docker-compose.prod.yaml, which already points at the published image. Create a .env first (see Production Deployment), then:

docker compose -f docker-compose.prod.yaml up -d

Pin a version tag (e.g. ghcr.io/thinkbig1979/capstan:0.11.0) for reproducible deployments; :latest tracks the most recent release.

Run from source (local development)

start-local.sh builds the same all-in-one image and runs it via docker-compose.yaml, serving the whole app on port 5001:

./start-local.sh

Then open http://localhost:5001. Authentication is disabled in this local mode (backend/.env is created from backend/.env.example). Set STACKS_DIR in docker-compose.yaml to the directory that holds your compose projects.

For frontend hot-reload, run the backend and the Vite dev server separately:

cd backend  && ./run-local.sh    # API on :5001
cd frontend && ./run-dev.sh      # Vite dev server on :5173 (proxies /api to :5001)

Features

  • Docker Compose Management: create, start, stop, restart, and delete stacks
  • Bulk Actions: select multiple stacks and start, stop, restart, or pull them together
  • Pinned Stacks: keep frequently-used stacks at the top of the sidebar
  • Command Palette: press ⌘K / Ctrl-K to jump to any stack, action, or settings page
  • Log Viewer: ANSI-colored output, per-container colors, container and level filters, search, and preferences that persist across sessions
  • 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
  • Image Updates: detect and apply image updates per service, with an at-a-glance count in the sidebar
  • Dashboard Metrics: sortable CPU and memory view with per-container sparklines
  • Backups: built-in restic snapshots with optional rclone cloud sync, with last-run and next-run shown in the sidebar
  • Web Terminal: an in-browser shell into running containers
  • Real-time Updates: file watching for automatic stack detection
  • Action Logging: an audit trail of all operations

Backups

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

What gets backed up

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.

Configuration

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

Bind-mount requirement

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 persistence

Never replace this with a Docker named volume. The docker-compose.prod.yaml and docker/compose.yaml both use a bind mount by default.

Running a backup

# Via the UI: Settings → Backup → Run Backup Now
# Via the API:
curl -X POST http://localhost:5001/api/v1/backups/run

Cloud sync (optional)

Configure 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

Restore

# List snapshots for a stack:
curl http://localhost:5001/api/v1/backups/snapshots?stackId=<id>

# Restore a snapshot via UI: Settings → Backup → Snapshots → Restore
# Or via API:
curl -X POST http://localhost:5001/api/v1/backups/restore \
  -H 'Content-Type: application/json' \
  -d '{"stackId":"<id>","snapshotId":"<short-id>"}'

Disaster recovery

If the host is lost, recover from an rclone remote:

  1. Deploy a fresh Capstan instance with the same ./data bind mount path.
  2. Trigger a DR restore (Settings, Backup, DR Restore). This syncs the full restic repository from the configured remote back to /app/data/restic-repo.
  3. 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).

Volume Path Identity

Important: the STACKS_DIR path inside the container must match the path on the host for Docker Compose operations to work correctly. Compose records the project's directory, and the host's Docker daemon must be able to find that same path when Capstan runs commands against it.

Set both variables to the same value:

environment:
  - STACKS_DIR=/opt/stacks
  - HOST_STACKS_DIR=/opt/stacks

On startup, Capstan validates path identity and logs a warning if the paths don't match:

docker compose logs | grep "Volume path identity"

Docker Socket & Security

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:

It runs as non-root, with zero host changes

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 projects

On startup the container briefly runs as root only to:

  1. discover the actual group GID of the mounted socket (it differs per host — Debian/Ubuntu often use 999, others 998/130/…) and join it, and
  2. 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.

Matching file ownership (PUID / PGID)

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=1001

Capstan 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.

The honest security note

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:

  • :ro on 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: ghcr.io/thinkbig1979/capstan:latest
        environment:
          - DOCKER_HOST=tcp://docker-proxy:2375
        # no socket mount on Capstan itself
        networks: [capstan-network]

Application Security

Capstan holds credentials, runs commands against your host's Docker daemon, and serves an in-browser terminal, so the application is hardened by default rather than left to the operator to secure. The codebase has been through a security audit; the measures below are in place and covered by tests.

Authentication and sessions

  • Authentication is on by default. Passwords are hashed with bcrypt and must meet a strength policy (length, character classes, common-password blocklist).
  • Login is rate-limited and runs a constant-time comparison whether or not the username exists, so it does not leak which accounts are valid.
  • Sessions are tracked server-side and revoked on logout and on password change. The session cookie is HttpOnly, SameSite=Lax, and marked Secure when the request arrives over HTTPS. JWTs are bound to an issuer claim.

Secrets at rest

  • Stored secrets (git HTTPS tokens, the restic repository password) are encrypted with AES-256-GCM. The key is derived with HKDF from a dedicated STORAGE_KEY, independent of the JWT signing secret, so leaking one does not expose the other.
  • The restic password is passed to restic via a private file, never on the command line or in logs. Secrets are never returned in API responses.

Command execution and file access

  • Every call to docker, docker compose, and git is made with an explicit argument vector, not a shell string, so stack names, container names, and file paths cannot inject commands.
  • Reads and writes to compose files, .env files, and backup targets are confined to the configured stacks and data directories. Containment is symlink-aware, so a symlink inside a stack directory cannot redirect a write onto a host file outside it.

Web layer

  • State-changing requests require a CSRF token (double-submit cookie + header).
  • CORS uses an exact-match allowlist; credentialed requests are never paired with a wildcard origin.
  • WebSocket endpoints (including the terminal) validate the request Origin, so the shell cannot be driven from another site (cross-site WebSocket hijacking).
  • Responses carry a Content-Security-Policy with frame-ancestors 'none', plus X-Frame-Options, X-Content-Type-Options, and Referrer-Policy. Generic error responses avoid leaking stack traces or internals.

Dependencies and build

  • Go and npm dependencies are scanned (govulncheck, pnpm audit) and kept current; the binary is built with a patched Go toolchain and base images are pinned.

The honest boundary. None of this changes the fact that access to the Docker socket is root-equivalent control of the host (see Docker Socket & Security above). Treat a Capstan login as administrative access, run it on a trusted network behind TLS, and use a socket proxy if you need least-privilege. The application hardening above reduces the ways that trust can be abused; it is not a substitute for protecting access to Capstan itself.

Project Structure

capstan/
├── backend/                  # Go backend (Gin API, SQLite, Docker/Git services)
│   ├── cmd/server/           # main entrypoint
│   └── internal/             # handlers, services, middleware, models
├── frontend/                 # React + Vite + Tailwind SPA
│   └── src/                  # components, hooks, stores, lib
├── docker/                   # production Dockerfile + compose
├── docker-compose.yaml       # local dev (builds the all-in-one image)
└── docker-compose.prod.yaml  # runs the published image

Quick Commands

# Full stack (Docker)
./start-local.sh           # build + start
docker compose logs -f     # view logs
docker compose down        # stop

# Backend only
cd backend
./run-local.sh             # quick start
make run                   # run the server
make test                  # run tests

# Frontend only
cd frontend
./run-dev.sh               # Vite dev server (:5173)
npm run build              # production build

API Endpoints

All routes are under /api/v1 and require authentication (unless AUTH_DISABLED=true), except /health. The web UI is the primary interface; these are the main REST routes.

Health

  • GET /health — health check (restricted to localhost)

Authentication

  • POST /api/v1/auth/setup — create the first admin (only when no user exists)
  • POST /api/v1/auth/login — log in
  • POST /api/v1/auth/logout — log out
  • GET /api/v1/auth/me — current user

Stacks

  • GET /api/v1/stacks — list stacks
  • POST /api/v1/stacks — create a stack
  • GET /api/v1/stacks/:id — stack details
  • POST /api/v1/stacks/:id/start · /stop · /restart · /pull — lifecycle actions
  • DELETE /api/v1/stacks/:id — delete a stack

Compose & environment files

  • GET / PUT /api/v1/stacks/:id/compose — read/write compose file
  • POST /api/v1/stacks/:id/compose/lint — lint compose file
  • GET / PUT /api/v1/stacks/:id/env — read/write .env file

Git

  • GET /api/v1/git?stackId=<id> — status
  • POST /api/v1/git/pull — pull changes
  • GET /api/v1/git/log — commit log
  • GET /api/v1/git/diff/:hash — commit diff

Backups

  • POST /api/v1/backups/run · /sync · /restore · /dr-restore · /prune
  • GET /api/v1/backups/status · /history · /snapshots

Migration from Dockge

Capstan reads the same on-disk stack layout as Dockge, so migration is mostly a matter of pointing it at your existing stacks directory.

  1. Back up existing stacks:

    cp -r /opt/stacks /opt/stacks.backup
  2. Map the stacks directory (Dockge uses DOCKGE_STACKS_DIR; Capstan uses STACKS_DIR, and requires HOST_STACKS_DIR to match — see Volume Path Identity):

    environment:
      - STACKS_DIR=/opt/stacks
      - HOST_STACKS_DIR=/opt/stacks
  3. Start Capstan and create an admin account on first run (/auth/setup). Accounts are not migrated from Dockge.

  4. Verify path validation:

    docker compose logs | grep "Volume path identity"
  5. Test with a single stack before relying on it for production.

You can run Dockge and Capstan side by side during migration as long as only one manages a given stack at a time.

Production Deployment

# Generate secrets (use two distinct values)
JWT_SECRET=$(openssl rand -hex 32)
STORAGE_KEY=$(openssl rand -hex 32)

# Create production .env file
cat > .env << EOF
PORT=5001
LOG_LEVEL=info
JWT_SECRET=$JWT_SECRET
STORAGE_KEY=$STORAGE_KEY
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

docker compose -f docker-compose.prod.yaml up -d

STORAGE_KEY encrypts stored secrets (git tokens, restic password) at rest with a key independent of JWT_SECRET; if unset it falls back to JWT_SECRET. Using a separate value means rotating JWT_SECRET doesn't require re-encryption and a leaked JWT_SECRET alone can't decrypt stored secrets.

Security checklist

  • Set a strong JWT_SECRET (min 32 characters) and a separate STORAGE_KEY.
  • Keep AUTH_DISABLED=false for anything reachable off localhost.
  • Terminate TLS at a reverse proxy (nginx, Traefik, Caddy) and forward X-Forwarded-Proto: https — Capstan uses it to set Secure cookies and HSTS.
  • Configure TRUSTED_NETWORKS for access control.
  • Set up and test backups (Settings → Backup).
  • For least-privilege Docker access, front the socket with a proxy (see Docker Socket & Security).

Upgrading: this release binds JWTs to an issuer claim, so existing sessions are invalidated on upgrade — log in again once. Previously stored secrets stay readable and are re-encrypted under the new key scheme on next save.

Development

See TESTING.md for local testing and development workflow. Example environment files: .env.example (production) and backend/.env.example (local dev).

Backend

  • Language: Go 1.25
  • Database: SQLite
  • Framework: Gin
  • Docker SDK: docker/docker (Moby) client
  • Git library: go-git

Frontend

  • Language: TypeScript
  • Framework: React + Vite
  • UI: Tailwind CSS
  • State: TanStack Query
  • Editor: CodeMirror 6

Versioning

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.x0.2.0) as potentially breaking and a PATCH bump (0.1.00.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.11.0 :0.11.0, :0.11, :latest
v0.12.0-rc.1 :0.12.0-rc.1 (pre-release; not :latest)

Pin a specific version (e.g. ghcr.io/thinkbig1979/capstan:0.11.0) for reproducible deployments; :latest always tracks the most recent stable release.

License

MIT — see LICENSE.

About

Web-based Docker Compose stack manager with Git integration, multi-arch container images, and a non-root runtime. A self-hostable Dockge alternative.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors