# How to deploy with Docker Compose

This guide shows you how to run DeltaGlider Proxy with Docker Compose using a secret-free, commit-safe config. The files are also in the repo under [`examples/docker-compose/`](https://github.com/beshu-tech/deltaglider_proxy/tree/main/examples/docker-compose).

The container reads its configuration when it starts, so you write the configuration as files before the first `docker compose up`, and the admin UI plays no part in this guide. The compose file mounts `deltaglider_proxy.yaml` read-only (`:ro`). A change that you make later in the admin UI therefore works in the running container, but the proxy cannot write it into the file, and the change is lost when the container restarts. Settings that need a restart (the listener, TLS, the blocking thread pool and the sync bucket) never take effect from the UI at all, and a backup restore that includes the config answers `409 config_file_read_only`. The admin UI shows a warning banner about the read-only file. To keep a change, edit `deltaglider_proxy.yaml` (or export the YAML from the UI and commit it) and restart the container. [Two ways to configure DeltaGlider](https://deltaglider.com/docs/explanation/two-ways-to-configure.md) explains how the file and the UI relate.

The proxy expands `${env:NAME}` and `${env:NAME:-default}` references inside its config file, in-process, at load time. This replaces an external `envsubst` step. You ship a secret-free `deltaglider_proxy.yaml` with placeholders, you supply the real values as environment variables, and the proxy fills them in at startup. When a placeholder is unset and has no default, the proxy stops with an error, so it never starts with a blank secret.

> The `env:` prefix is required. It keeps load-time config placeholders distinct from the request-time IAM permission templates (`${iam:username}`, `${iam:access_key_id}`). A bare `${...}` is left untouched.

> When a placeholder is the whole value of a field, you do not need to quote it. The proxy gives the value the type of its field, so an AES key that has only digits stays a string, and `force_path_style: ${env:S3_PATH_STYLE:-false}` still becomes a boolean. When a placeholder is only part of a value (for example `https://${env:HOST}:9000`), the proxy inserts the value as text.

> The proxy does not expand a placeholder on a comment line, that is, a line whose first character other than a space is `#`. For this reason, a commented-out template line with an unset variable does not stop the proxy.

You need three files:

- `deltaglider_proxy.yaml`: the config. The secret values in it are `${env:...}` placeholders. Commit it.
- `secrets.env`: your real secret values (`KEY=value`). **Never commit it.**
- `docker-compose.yml`: runs the proxy with the config mounted and the secrets supplied through `env_file`.

## 1. Write the config

The block uses `${env:...}` references. The proxy expands them from the environment of the container when it loads the file, so the file holds no secret. A reference can also be the whole value of a field that is not text, such as the boolean `force_path_style`. The proxy gives the value the type of the field, and the JSON Schema of the admin UI editor accepts the reference in that field. To lint the file, load the secrets first, as the Verify section shows.

```yaml
# validate
# deltaglider_proxy.yaml — secret-free, safe to commit
storage:
  backends:
    - name: hetzner-fsn1
      type: s3
      endpoint: ${env:S3_ENDPOINT}
      region: ${env:S3_REGION:-us-east-1}
      force_path_style: ${env:S3_PATH_STYLE:-false}   # true for MinIO / non-AWS
      access_key_id: ${env:S3_ACCESS_KEY_ID}
      secret_access_key: ${env:S3_SECRET_ACCESS_KEY}
  default_backend: hetzner-fsn1

access:
  # A single bootstrap SigV4 pair to get started (add per-user IAM later
  # from the admin GUI, or manage users as code — see below).
  access_key_id: ${env:PROXY_ACCESS_KEY_ID}
  secret_access_key: ${env:PROXY_SECRET_ACCESS_KEY}

advanced:
  listen_addr: 0.0.0.0:9000
  max_object_size: 536870912   # 512 MiB
```

## 2. Write the secrets file

```bash
# secrets.env — your real values; gitignored, never committed.
# This is a docker-compose env_file: KEY=value, taken literally (no shell quoting).

DGP_BOOTSTRAP_PASSWORD_HASH=   # bcrypt hash; see "Set the admin password" below

S3_ENDPOINT=https://s3.eu-central-1.amazonaws.com
S3_REGION=eu-central-1
S3_PATH_STYLE=false
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=

PROXY_ACCESS_KEY_ID=
PROXY_SECRET_ACCESS_KEY=
```

## 3. Write the compose file

```yaml
# not-proxy-config: docker-compose
services:
  # One-time: make the config volume writable by the proxy's non-root user (999)
  # so it can create its encrypted config DB. (A fresh named volume is root-owned.)
  init-perms:
    image: beshultd/deltaglider_proxy:${DGP_IMAGE_TAG:-latest}
    user: "0:0"
    entrypoint: ["/bin/sh", "-c", "chown 999:999 /etc/deltaglider_proxy && chmod 750 /etc/deltaglider_proxy"]
    volumes:
      - dgp-config:/etc/deltaglider_proxy
    restart: "no"

  deltaglider:
    image: beshultd/deltaglider_proxy:${DGP_IMAGE_TAG:-latest}
    depends_on:
      init-perms:
        condition: service_completed_successfully
    env_file:
      - secrets.env
    environment:
      DGP_CONFIG: /etc/deltaglider_proxy/deltaglider_proxy.yaml
    ports:
      - "9000:9000"
    volumes:
      - ./deltaglider_proxy.yaml:/etc/deltaglider_proxy/deltaglider_proxy.yaml:ro
      - dgp-config:/etc/deltaglider_proxy    # encrypted config DB — back this up
      - dgp-data:/data                       # objects, if you use the filesystem backend
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/_/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    restart: unless-stopped

volumes:
  dgp-config:
  dgp-data:
```

## 4. Set the admin password

`DGP_BOOTSTRAP_PASSWORD_HASH` is a bcrypt hash, not plaintext. Generate it:

```bash
# inside the image:
docker run --rm beshultd/deltaglider_proxy:latest --set-bootstrap-password
# or with htpasswd:
htpasswd -nbBC 12 "" 'your-password' | tr -d ':\n' | sed 's/^\$2y/\$2b/'
```

## 5. Run it

```bash
cp secrets.env.example secrets.env   # then fill secrets.env
docker compose up -d
docker compose logs -f deltaglider
```

If you have the binary locally, validate the config before you ship it. The lint catches a missing `${env:...}` early:

```bash
deltaglider_proxy config lint deltaglider_proxy.yaml
```

## Filesystem backend instead of S3

For a self-contained deployment with no external object store, replace the `storage.backends` block with a filesystem backend pointed at the data volume:

```yaml
# fragment
storage:
  filesystem: /data
```

## Managing users as code

To manage IAM users and groups from the YAML instead of the admin GUI, set `access.iam_mode: declarative`. The YAML then becomes the source of truth, and the proxy reconciles it into the encrypted DB on every apply. See [How to manage IAM as code](https://deltaglider.com/docs/how-to/manage-iam-as-code.md).

## Persistence and backups

The `dgp-config` volume holds the encrypted config DB (`deltaglider_config.db`) with your IAM users, groups, and OAuth providers. Next to it, the volume holds the key file `deltaglider_config.db.key` that the proxy generates on the first start. The database is useless without that key. Back up both (see [How to back up and restore](https://deltaglider.com/docs/how-to/back-up-and-restore.md)). With an S3 backend, object data lives in the backend, not in these volumes, and the `dgp-data` volume holds objects only when you use the filesystem backend. The proxy writes its temporary files (delta encoding and reconstruction of objects larger than 16 MiB, multipart parts) to the spool directory, which is `/tmp/dgp-spool` in the container unless you set `DGP_SPOOL_DIR`. Point `DGP_SPOOL_DIR` at a volume, for example `/data/spool`, if the container file system is small.

## Verify

```bash
# Health
curl -fsS http://localhost:9000/_/health

# S3 API answers signed requests
AWS_ACCESS_KEY_ID=$PROXY_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY=$PROXY_SECRET_ACCESS_KEY \
  aws --endpoint-url http://localhost:9000 s3 ls
```

Then open the admin GUI at `http://localhost:9000/_/` and log in with the bootstrap password. The S3 API and the GUI share port 9000. On a fresh proxy, the setup wizard at `/_/admin/setup` walks you through the first backend, bucket and user; remember that its changes, like every UI change, last only until the next restart while the config file is mounted read-only.

Lint the config with the secrets loaded, so that every `${env:...}` placeholder resolves:

```bash
set -a; . ./secrets.env; set +a
deltaglider_proxy config lint deltaglider_proxy.yaml
```

## Related

- [How to take a proxy to production](https://deltaglider.com/docs/how-to/go-to-production.md): the rest of the production checklist
- [How to serve TLS](https://deltaglider.com/docs/how-to/serve-tls.md): put HTTPS in front of this stack
- [Configuration reference](https://deltaglider.com/docs/reference/configuration.md): every YAML field and `DGP_*` env var
