Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Docker Compose

For local development and small single-host deployments. The simplest pattern: a small entry-point script that reads from ScaiVault, exports env vars, then exec's the real process.

This page focuses on dev-loop ergonomics. For Kubernetes-style production deployments, see Kubernetes.

Authentication for dev#

Use a personal access token from ScaiKey, scoped narrowly (read-only on the paths you care about). Put it in your shell — not in docker-compose.yml:

bash
1
2
3
# ~/.bashrc or ~/.zshrc
export SCAIVAULT_URL="https://scaivault.scailabs.ai"
export SCAIVAULT_TOKEN="skt_..."

docker-compose.yml references the env:

yaml
1
2
3
4
5
6
7
services:
  billing:
    image: acme/billing:dev
    environment:
      SCAIVAULT_URL: ${SCAIVAULT_URL}
      SCAIVAULT_TOKEN: ${SCAIVAULT_TOKEN}
    # ... rest

The container inherits whatever's in your shell. Never commit a .env with a real SCAIVAULT_TOKEN.

Pattern: entrypoint wrapper#

Add a small script to your image that reads secrets and exec's the real binary:

dockerfile
1
2
3
4
5
6
7
8
# Dockerfile
FROM python:3.11-slim
RUN pip install --no-cache-dir scaivault
COPY entrypoint.sh /usr/local/bin/scaivault-entrypoint
COPY . /app
WORKDIR /app
ENTRYPOINT ["scaivault-entrypoint"]
CMD ["python", "-m", "billing"]
bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/bin/sh
# entrypoint.sh
set -euo pipefail

if [ -n "${SECRETS:-}" ]; then
  while IFS= read -r line; do
    [ -z "$line" ] && continue
    name=$(echo "$line" | cut -d= -f1)
    path=$(echo "$line" | cut -d= -f2-)
    val=$(scaivault secrets read "$path" --field "${name,,}" 2>/dev/null \
       || scaivault secrets read "$path" --json | jq -r ".data.${name,,}")
    export "$name=$val"
  done <<EOF
$SECRETS
EOF
fi

exec "$@"

Compose service:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  billing:
    image: acme/billing:dev
    environment:
      SCAIVAULT_URL: ${SCAIVAULT_URL}
      SCAIVAULT_TOKEN: ${SCAIVAULT_TOKEN}
      SECRETS: |
        STRIPE_KEY=environments/dev/billing/stripe
        DB_URL=environments/dev/billing/database
    ports: ["3000:3000"]

App code reads STRIPE_KEY, DB_URL from env like it always did. The container fetches at start, the app doesn't know ScaiVault exists.

Pattern: file-mount instead of env#

When the app expects to read a file (TLS cert, JSON config), write to disk:

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/sh
# entrypoint-with-files.sh
mkdir -p /run/secrets
scaivault secrets read environments/dev/billing/database --json \
  | jq -r '.data | "\(.username)\n\(.password)"' > /run/secrets/db-creds

scaivault secrets read environments/dev/billing/tls --field cert_pem > /run/secrets/tls.crt
scaivault secrets read environments/dev/billing/tls --field key_pem  > /run/secrets/tls.key
chmod 600 /run/secrets/tls.key

exec "$@"

Mount as tmpfs so the values don't hit disk:

yaml
1
2
3
4
5
6
services:
  billing:
    image: acme/billing:dev
    tmpfs:
      - /run/secrets:size=1M
    # ... entrypoint sets up /run/secrets at start

Pattern: shared fetcher service#

For a Compose stack with several services needing the same secrets, run one "secrets-fetcher" service that writes to a shared volume, and have the others wait for it:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
services:
  secrets:
    image: scailabs/scaivault-fetcher:1.0
    environment:
      SCAIVAULT_URL: ${SCAIVAULT_URL}
      SCAIVAULT_TOKEN: ${SCAIVAULT_TOKEN}
      WATCH_PATHS: |
        environments/dev/billing/stripe -> /vault/billing-stripe.json
        environments/dev/billing/database -> /vault/billing-database.json
      REFRESH_INTERVAL: 5m
    volumes:
      - secrets:/vault
    restart: unless-stopped

  billing:
    image: acme/billing:dev
    depends_on:
      secrets:
        condition: service_healthy
    volumes:
      - secrets:/run/secrets:ro
    # ... reads from /run/secrets/billing-*.json

  reporting:
    image: acme/reporting:dev
    depends_on:
      secrets:
        condition: service_healthy
    volumes:
      - secrets:/run/secrets:ro

volumes:
  secrets:
    driver: local
    driver_opts:
      type: tmpfs
      device: tmpfs

The fetcher refreshes every 5 minutes. Apps either restart on file change (process supervisor watches), or accept up-to-5-min staleness.

Local development with throwaway tokens#

For laptops, the cleanest setup is a scaivault dev-token command that mints a 24h personal token bound to your laptop session. Add to your ~/.zshrc:

bash
1
2
3
4
sv-login() {
  scaivault auth login --browser \
    && export SCAIVAULT_TOKEN=$(scaivault config get token)
}

Run sv-login once per day. Token expires automatically. No persistent .env file with a long-lived credential.

Production single-host deployments#

Compose is fine for production on small single-host setups (often the case for self-hosted SaaS instances). Two changes from the dev pattern:

  1. Service account, not personal token. Configure the host with a SCAIVAULT_TOKEN from a ScaiKey service account scoped to that machine.
  2. No fallback to env. The entrypoint should hard-fail if ScaiVault is unreachable. Better to crash and have orchestration retry than to start with missing values.
bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh
# production entrypoint — no fallback
set -euo pipefail

if [ -z "${SCAIVAULT_TOKEN:-}" ]; then
  echo "FATAL: SCAIVAULT_TOKEN not set" >&2
  exit 1
fi

for line in $SECRETS; do
  name=$(echo "$line" | cut -d= -f1)
  path=$(echo "$line" | cut -d= -f2-)
  val=$(scaivault secrets read "$path" --field "${name,,}") || {
    echo "FATAL: failed to read $path" >&2
    exit 1
  }
  export "$name=$val"
done

exec "$@"

Common questions#

"Can I use Docker secrets instead?" Sort of. Docker secrets (via Swarm) are an alternative for cluster setups but they have their own quirks (file-mount only, no rotation, no audit). ScaiVault gives you a unified model across dev and production; mixing Docker secrets with ScaiVault adds confusion.

"What about Compose secrets:?" The secrets: directive on Compose works with files, not arbitrary APIs. If you really want to use it, point a secrets: mount at the file your fetcher service writes. But at that point you've just added indirection without value.

"How do I rotate without restarting?" Compose-managed processes don't have a clean reload story without app cooperation. Easiest: signal-on-file-change via a small sidecar watching inotify. Or accept that rotation requires docker compose restart <service>.

"My laptop is offline; how do I work?" Cache the values in a local file the first time you fetch:

bash
1
scaivault secrets list --prefix environments/dev/ --json > .dev-secrets-cache.json

Then on offline runs, an entrypoint variant reads from the cache. Don't commit the cache. Consider this a debugging tool, not a deployment pattern.

What's next#

Updated 2026-05-17 13:26:50 View source (.md) rev 1