spindle-docker#
Early development / personal project — This stack was built for personal use and tested on Ubuntu Linux. It has not been tested across a wide range of environments and may have rough edges or undocumented assumptions. Use it at your own risk.
Docker Compose stack for self-hosting a Tangled spindle (CI runner) with OpenBao for secrets management.
.
├── docker-compose.yml
├── Dockerfile
├── init-openbao.sh # one-time vault bootstrap
└── config/openbao/
├── server/
│ └── server.hcl # OpenBao server config
├── proxy/
│ └── proxy.hcl # AppRole auto-auth proxy config
└── spindle-policy.hcl # KV access policy for spindle
Prerequisites#
- Docker + Docker Compose
- A domain or IP reachable by the Tangled network
- Your ATProto DID
Configuration#
Docker Compose loads .env automatically. Copy the sample and fill in the two required values:
cp .env.sample .env
| Variable | Required | Default | Description |
|---|---|---|---|
SPINDLE_SERVER_HOSTNAME |
yes | — | Public hostname or IP (e.g. spindle.example.com) |
SPINDLE_SERVER_OWNER |
yes | — | Your ATProto DID (e.g. did:plc:xxxx) |
SPINDLE_PORT |
no | 6555 |
Host port Spindle is exposed on |
OPENBAO_PORT |
no | 8200 |
Host port OpenBao is exposed on (local CLI access) |
SPINDLE_SERVER_LISTEN_ADDR |
no | 0.0.0.0:6555 |
Bind address inside the container |
SPINDLE_SERVER_DB_PATH |
no | /data/spindle.db |
SQLite database path inside the container |
SPINDLE_PIPELINES_LOG_DIR |
no | /var/log/spindle |
Pipeline log directory inside the container |
SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT |
no | spindle |
KV v2 mount name |
First-time setup#
1. Configure environment
git clone https://tangled.org/danieldaum.net/spindle-docker
cd spindle-docker
cp .env.sample .env
# Edit .env — set SPINDLE_SERVER_HOSTNAME and SPINDLE_SERVER_OWNER
2. Start OpenBao
docker compose up -d openbao
Wait until you see the following line in the logs before continuing (docker compose logs -f openbao):
core: seal configuration missing, not initialized
3. Initialize the vault (once only)
chmod +x init-openbao.sh
./init-openbao.sh
The script fixes permissions, initialises the vault, and configures AppRole automatically. It will print an unseal key and root token — save both somewhere safe, they are not stored anywhere and cannot be recovered. You will also be prompted to choose a Secret ID TTL (press enter for no expiry).
4. Start the full stack
Once the init script completes successfully:
docker compose up -d
After a restart#
OpenBao seals itself on every restart. Run the unseal command once OpenBao is running (you can confirm it's ready when docker compose logs openbao shows core: seal configuration missing, not initialized or the container is healthy):
docker compose exec openbao bao operator unseal <unseal_key>
The proxy and Spindle will start automatically once OpenBao is unsealed and healthy.
Verify#
curl http://localhost:6555/ # Spindle (should return the spindle welcome page)
Architecture#
spindle (:6555) → openbao-proxy (:8201) → openbao (:8200)
spindle → /var/run/docker.sock (pipeline containers run on the host daemon)
- openbao — secrets vault; sealed on every start
- openbao-proxy — AppRole sidecar; auto-authenticates and exposes a token-authenticated proxy to spindle
- spindle — the CI runner; starts only after the proxy is healthy
Pinned versions#
All images and source are pinned to specific versions and verified by digest or commit SHA to prevent unexpected changes on rebuild.
| Component | Version | Where |
|---|---|---|
| OpenBao | 2.5.2 |
docker-compose.yml |
| Go (builder) | 1.25.8-alpine3.23 |
Dockerfile |
| Alpine (runtime) | 3.23.3 |
Dockerfile |
| Spindle source | v1.13.0-alpha (c3f60dc1) |
Dockerfile |
To upgrade any component, update the tag/version and its corresponding @sha256:... digest (or commit SHA for Spindle). All versions are currently alpha — there are no stable Spindle releases yet.
Notes#
- Port 8200 is exposed for local CLI access only (
127.0.0.1). Remove that port mapping entirely if you don't need it. - TLS is disabled on both listeners. Put nginx or Caddy in front for production traffic.
- Spindle mounts the Docker socket, so pipeline containers run on the host daemon.
AppRole credential handling#
By default, the openbao-approle volume is mounted read-only (:ro) in the proxy container. This means the proxy can read the role-id and secret-id written by init-openbao.sh on every startup, but cannot delete them. The credentials persist on the volume indefinitely, so the proxy can re-authenticate automatically after any restart with no user intervention beyond unsealing OpenBao.
The tradeoff: the secret-id is never rotated or deleted. For a self-hosted server where Docker volumes are only accessible to the server owner, this is a reasonable default.
If you want the secret-id deleted after first use (higher security, more operational overhead):
-
In
docker-compose.yml, remove:rofrom the approle volume mount:- openbao-approle:/openbao/approle -
After any restart or proxy container recreation, generate and write a new secret-id before starting the proxy:
# Unseal first, then: SECRET_ID=$(docker compose exec -T openbao bao write \ -address=http://localhost:8200 -f -field=secret_id \ auth/approle/role/spindle/secret-id) docker run --rm \ -v "openbao-approle:/openbao/approle" \ --entrypoint="" \ alpine:3.23.3 \ sh -c "printf '%s' '$SECRET_ID' > /openbao/approle/secret-id \ && chown 100:1000 /openbao/approle/secret-id \ && chmod 640 /openbao/approle/secret-id" docker compose restart openbao-proxy
This repository is hosted on Tangled and mirrored to GitHub.