---
title: Docker Compose
path: integrations/docker-compose
status: published
---

# 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](./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
# ~/.bashrc or ~/.zshrc
export SCAIVAULT_URL="https://scaivault.scailabs.ai"
export SCAIVAULT_TOKEN="skt_..."
```

`docker-compose.yml` references the env:

```yaml
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
# 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
#!/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
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
#!/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
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
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
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
#!/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
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

- [Kubernetes](./kubernetes) — the production-grade version of these patterns.
- [CLI](../sdks/cli) — what the entrypoints are calling.
- [Migrate from .env files](../tutorials/migrate-from-env-files) — to bring existing setups into ScaiVault.
