configuration for self hosting a spindle in docker

fix: restructure config layout and harden docker compose setup

- rename Dockerfile.spindle to Dockerfile
- move openbao .hcl files into config/openbao/
- fix init-openbao.sh: dynamic volume name lookup, correct policy path
- inline all spindle env vars in docker-compose.yml, remove env_file
- add explicit name/driver to all named volumes
- add .gitignore and .dockerignore

+192 -61
+20
.dockerignore
··· 1 + # Version control 2 + .git/ 3 + .jj/ 4 + 5 + # Compose / local config — not needed in the build context 6 + .env 7 + .env.sample 8 + docker-compose.yml 9 + config/ 10 + init-openbao.sh 11 + 12 + # Docs 13 + *.md 14 + CLAUDE.md 15 + 16 + # Editor / OS 17 + .vscode/ 18 + .idea/ 19 + .DS_Store 20 + Thumbs.db
+9
.env.sample
··· 1 + # ── Required ────────────────────────────────────────────────────────────────── 2 + 3 + # Public hostname of this spindle (e.g. spindle.example.com or an IP) 4 + SPINDLE_SERVER_HOSTNAME= 5 + 6 + # ATProto DID of the spindle owner (e.g. did:plc:xxxxxxxxxxxxxxxxxxxx) 7 + # Find yours at: https://bsky.app → Settings → Privacy and Security → Advanced 8 + SPINDLE_SERVER_OWNER= 9 + is i
+12
.gitignore
··· 1 + # Environment — never commit real credentials 2 + .env 3 + 4 + # OS 5 + .DS_Store 6 + Thumbs.db 7 + 8 + # Editor 9 + .vscode/ 10 + .idea/ 11 + *.swp 12 + *.swo
+73
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## What this repo is 6 + 7 + A Docker Compose deployment stack for [Spindle](https://tangled.org) (a CI/CD pipeline tool) backed by [OpenBao](https://openbao.org) (an open-source HashiCorp Vault fork) for secrets management. Spindle is not developed here — it is cloned from `tangled.org/core` and built inside `Dockerfile.spindle`. 8 + 9 + ## First-time setup 10 + 11 + ```bash 12 + cp .env.sample .env 13 + # Set SPINDLE_SERVER_HOSTNAME and SPINDLE_SERVER_OWNER in .env 14 + 15 + docker compose up -d openbao 16 + # Wait ~5s for health check, then: 17 + 18 + chmod +x scripts/init-openbao.sh 19 + ./scripts/init-openbao.sh # ONE-TIME ONLY — save the unseal key and root token it prints 20 + 21 + docker compose up -d 22 + ``` 23 + 24 + > `init-openbao.sh` writes AppRole credentials into the `openbao-approle` Docker volume using a temporary Alpine container. The volume name it targets is hardcoded as `tangled-spindle_openbao-approle` — if the Compose project name changes, this line must be updated. 25 + 26 + ## After every restart 27 + 28 + OpenBao seals itself on restart. Unseal before the proxy or Spindle can start: 29 + 30 + ```bash 31 + docker compose exec openbao bao operator unseal http://localhost:8200 <unseal_key> 32 + ``` 33 + 34 + ## Verify the stack 35 + 36 + ```bash 37 + curl http://localhost:8201/v1/sys/health # OpenBao proxy 38 + curl http://localhost:6555/ # Spindle 39 + ``` 40 + 41 + ## Architecture 42 + 43 + ``` 44 + ┌──────────┐ AppRole ┌───────────────┐ KV v2 ┌─────────┐ 45 + │ spindle │ ─────────────► │ openbao-proxy │ ─────────────► │ openbao │ 46 + │ :6555 │ │ :8201 │ │ :8200 │ 47 + └──────────┘ └───────────────┘ └─────────┘ 48 + 49 + │ /var/run/docker.sock 50 + 51 + Host Docker daemon (pipeline containers run here) 52 + ``` 53 + 54 + - **openbao** — vault backend with file storage; sealed on every start 55 + - **openbao-proxy** — AppRole sidecar that auto-authenticates and caches a token at `/tmp/openbao-token`; Spindle reads secrets through this proxy 56 + - **spindle** — starts only after the proxy is healthy; mounts the Docker socket to spawn CI pipeline containers on the host daemon 57 + 58 + ## Key config files 59 + 60 + | File | Purpose | 61 + |------|---------| 62 + | `docker-compose.yml` | Service definitions, volumes, healthchecks, dependency order | 63 + | `Dockerfile.spindle` | Clones `tangled.org/core`, builds with Go 1.23, produces minimal Alpine image | 64 + | `config/openbao/server.hcl` | OpenBao server (file storage, TCP listener on 8200, TLS off) | 65 + | `config/openbao/proxy.hcl` | Proxy AppRole auto-auth, token sink at `/tmp/openbao-token`, listener on 8201 | 66 + | `config/openbao/spindle-policy.hcl` | Grants Spindle KV v2 CRUD on `spindle/data/*` and `spindle/metadata/*` | 67 + | `scripts/init-openbao.sh` | One-time bootstrap: init vault, enable KV v2, create AppRole, write credentials to shared volume | 68 + 69 + ## Notes 70 + 71 + - TLS is disabled on both OpenBao listeners. Put nginx or Caddy in front for production. 72 + - The `openbao-approle` volume is mounted **read-only** by the proxy and **read-write** by the init script (via a temporary container). 73 + - Spindle's Docker socket mount means pipeline containers run on the **host** daemon — ensure the container user has socket access.
Dockerfile.spindle Dockerfile
+44 -40
README.md
··· 1 - # tangled spindle + openbao 1 + # spindle-docker 2 2 3 - ## Layout 3 + Docker Compose stack for self-hosting a [Tangled](https://tangled.org) spindle (CI runner) with [OpenBao](https://openbao.org) for secrets management. 4 4 5 5 ``` 6 6 . 7 7 ├── docker-compose.yml 8 - ├── Dockerfile.spindle # clones & builds from tangled.org/core 9 - ├── .env.example # copy to .env and fill in 10 - ├── config/ 11 - │ └── openbao/ 12 - │ ├── server.hcl # production file-backend config 13 - │ ├── proxy.hcl # AppRole auto-auth proxy 14 - │ └── spindle-policy.hcl # KV policy for spindle 15 - └── scripts/ 16 - └── init-openbao.sh # one-time vault bootstrap 8 + ├── Dockerfile 9 + ├── init-openbao.sh # one-time vault bootstrap 10 + └── config/openbao/ 11 + ├── server.hcl # OpenBao server config 12 + ├── proxy.hcl # AppRole auto-auth proxy config 13 + └── spindle-policy.hcl # KV access policy for spindle 17 14 ``` 18 15 16 + ## Prerequisites 17 + 18 + - Docker + Docker Compose 19 + - A domain or IP reachable by the Tangled network 20 + - Your ATProto DID (find it in Bluesky → Settings → Advanced) 21 + 19 22 ## First-time setup 20 23 21 - ### 1. Configure spindle 24 + **1. Configure environment** 25 + 26 + Edit `docker-compose.yml` and set these two values under the `spindle` service: 22 27 23 - ```bash 24 - cp .env.example .env 25 - # edit .env — set SPINDLE_SERVER_HOSTNAME and SPINDLE_SERVER_OWNER 28 + ```yaml 29 + SPINDLE_SERVER_HOSTNAME: "spindle.example.com" # your public hostname 30 + SPINDLE_SERVER_OWNER: "did:plc:xxxx" # your ATProto DID 26 31 ``` 27 32 28 - ### 2. Start OpenBao only 33 + **2. Start OpenBao** 29 34 30 35 ```bash 31 36 docker compose up -d openbao 32 37 ``` 33 38 34 - Wait for it to be healthy (~5 s), then: 39 + Wait ~5 seconds for it to be healthy. 35 40 36 - ### 3. Initialise the vault 41 + **3. Initialize the vault** (once only) 37 42 38 43 ```bash 39 - chmod +x scripts/init-openbao.sh 40 - ./scripts/init-openbao.sh 44 + chmod +x init-openbao.sh 45 + ./init-openbao.sh 41 46 ``` 42 47 43 - Save the unseal key and root token printed to stdout. 48 + Save the **unseal key** and **root token** printed to stdout — they are not stored anywhere. 44 49 45 - ### 4. Start the rest of the stack 50 + **4. Start the full stack** 46 51 47 52 ```bash 48 53 docker compose up -d 49 54 ``` 50 55 51 - Spindle comes up only after the proxy is healthy, which is after the vault 52 - is unsealed and AppRole credentials are in the shared volume. 53 - 54 56 ## After a restart 55 57 56 - OpenBao is **sealed on every restart** — that's intentional for production. 57 - Unseal it before the proxy (and therefore spindle) can authenticate: 58 + OpenBao seals itself on every restart. Unseal it before the proxy and spindle can start: 58 59 59 60 ```bash 60 - docker compose exec openbao bao operator unseal http://localhost:8200 <unseal_key> 61 + docker compose exec openbao bao operator unseal <unseal_key> 61 62 ``` 62 63 63 - Or automate this with a secrets manager / HSM if you don't want manual steps. 64 - 65 64 ## Verify 66 65 67 66 ```bash 68 - # proxy health 69 - curl http://localhost:8201/v1/sys/health 67 + curl http://localhost:8201/v1/sys/health # OpenBao proxy 68 + curl http://localhost:6555/ # Spindle 69 + ``` 70 70 71 - # spindle port 72 - curl http://localhost:6555/ 71 + ## Architecture 72 + 73 + ``` 74 + spindle (:6555) → openbao-proxy (:8201) → openbao (:8200) 75 + spindle → /var/run/docker.sock (pipeline containers run on the host daemon) 73 76 ``` 74 77 78 + - **openbao** — secrets vault; sealed on every start 79 + - **openbao-proxy** — AppRole sidecar; auto-authenticates and exposes a token-authenticated proxy to spindle 80 + - **spindle** — the CI runner; starts only after the proxy is healthy 81 + 75 82 ## Notes 76 83 77 - - Spindle mounts `/var/run/docker.sock` so it can spawn pipeline containers 78 - on the **host** Docker daemon — make sure the spindle user has permission. 79 - - TLS is disabled on both listeners; put a reverse proxy (nginx/caddy) in 80 - front for production traffic. 81 - - The `openbao-approle` volume holds the role-id/secret-id written by the 82 - init script and mounted read-only by the proxy container. 84 + - Port 8200 is exposed for local CLI access. Remove that port mapping in production. 85 + - TLS is disabled on both listeners. Put nginx or Caddy in front for production traffic. 86 + - Spindle mounts the Docker socket, so pipeline containers run on the **host** daemon.
+25 -17
docker-compose.yml
··· 1 1 services: 2 2 3 - # ── OpenBao server (production) ──────────────────────────────────────────── 3 + # ── OpenBao (secrets vault) ──────────────────────────────────────────────── 4 4 openbao: 5 5 image: quay.io/openbao/openbao:latest 6 6 container_name: openbao 7 7 restart: unless-stopped 8 + command: server 8 9 cap_add: 9 - - IPC_LOCK # required to prevent secrets being swapped to disk 10 - command: server 10 + - IPC_LOCK # prevents secrets from being swapped to disk 11 + environment: 12 + BAO_ADDR: "http://0.0.0.0:8200" 11 13 volumes: 12 14 - ./config/openbao/server.hcl:/openbao/config/server.hcl:ro 13 15 - openbao-data:/openbao/data 14 16 ports: 15 - - "8200:8200" # expose only if you need CLI access from the host 16 - environment: 17 - BAO_ADDR: "http://0.0.0.0:8200" 17 + - "8200:8200" # remove if you don't need local CLI access 18 18 networks: 19 19 - spindle-net 20 20 healthcheck: ··· 35 35 condition: service_healthy 36 36 volumes: 37 37 - ./config/openbao/proxy.hcl:/openbao/config/proxy.hcl:ro 38 - - openbao-approle:/openbao/approle:ro # role-id + secret-id written by init script 38 + - openbao-approle:/openbao/approle:ro # role-id + secret-id written by init-openbao.sh 39 39 networks: 40 40 - spindle-net 41 41 healthcheck: ··· 45 45 retries: 5 46 46 start_period: 10s 47 47 48 - # ── Spindle (built from tangled.org/core) ────────────────────────────────── 48 + # ── Spindle (CI runner) ──────────────────────────────────────────────────── 49 49 spindle: 50 50 build: 51 51 context: . 52 - dockerfile: Dockerfile.spindle 52 + dockerfile: Dockerfile 53 53 container_name: spindle 54 54 restart: unless-stopped 55 55 depends_on: 56 56 openbao-proxy: 57 57 condition: service_healthy 58 - volumes: 59 - - /var/run/docker.sock:/var/run/docker.sock # spindle spawns pipeline containers 60 - - spindle-db:/data 61 - - spindle-logs:/var/log/spindle 62 - ports: 63 - - "6555:6555" 64 - env_file: 65 - - .env # SPINDLE_SERVER_HOSTNAME, SPINDLE_SERVER_OWNER 66 58 environment: 59 + SPINDLE_SERVER_HOSTNAME: "" # set to your public hostname 60 + SPINDLE_SERVER_OWNER: "" # set to your ATProto DID 67 61 SPINDLE_SERVER_LISTEN_ADDR: "0.0.0.0:6555" 68 62 SPINDLE_SERVER_DB_PATH: "/data/spindle.db" 69 63 SPINDLE_SERVER_SECRETS_PROVIDER: "openbao" 70 64 SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR: "http://openbao-proxy:8201" 71 65 SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT: "spindle" 72 66 SPINDLE_PIPELINES_LOG_DIR: "/var/log/spindle" 67 + volumes: 68 + - /var/run/docker.sock:/var/run/docker.sock # spindle spawns pipeline containers on the host daemon 69 + - spindle-db:/data 70 + - spindle-logs:/var/log/spindle 71 + ports: 72 + - "6555:6555" 73 73 networks: 74 74 - spindle-net 75 75 76 76 volumes: 77 77 openbao-data: 78 + name: openbao-data 79 + driver: local 78 80 openbao-approle: 81 + name: openbao-approle 82 + driver: local 79 83 spindle-db: 84 + name: spindle-db 85 + driver: local 80 86 spindle-logs: 87 + name: spindle-logs 88 + driver: local 81 89 82 90 networks: 83 91 spindle-net:
+9 -4
init-openbao.sh
··· 54 54 SECRET_ID=$($BAO write -address="$BAO_ADDR" -f -field=secret_id auth/approle/role/spindle/secret-id) 55 55 56 56 echo "==> Writing credentials into the openbao-approle volume..." 57 - # Write into a temp container that mounts the shared volume 57 + # Resolve the volume name regardless of compose project name 58 + APPROLE_VOL=$(docker volume ls --format '{{.Name}}' | grep '_openbao-approle$' | head -1) 59 + if [ -z "$APPROLE_VOL" ]; then 60 + echo "ERROR: openbao-approle volume not found. Did you run 'docker compose up -d openbao' first?" 61 + exit 1 62 + fi 58 63 docker run --rm \ 59 - -v tangled-spindle_openbao-approle:/openbao/approle \ 64 + -v "${APPROLE_VOL}:/openbao/approle" \ 60 65 alpine sh -c " 61 - echo '$ROLE_ID' > /openbao/approle/role-id && chmod 600 /openbao/approle/role-id 62 - echo '$SECRET_ID' > /openbao/approle/secret-id && chmod 600 /openbao/approle/secret-id 66 + printf '%s' '$ROLE_ID' > /openbao/approle/role-id && chmod 600 /openbao/approle/role-id 67 + printf '%s' '$SECRET_ID' > /openbao/approle/secret-id && chmod 600 /openbao/approle/secret-id 63 68 " 64 69 65 70 echo ""
proxy.hcl config/openbao/proxy.hcl
server.hcl config/openbao/server.hcl
spindle-policy.hcl config/openbao/spindle-policy.hcl