Skip to content

Configuration

Accelero is configured through environment variables. Only API_KEY is required.

Server Configuration

Variable Default Description
API_KEY (required) Bootstrap admin API key, sent via the X-API-KEY header. Always has full access. Create additional role-scoped keys at runtime (see Roles & API keys).
SERVER_PORT 8000 HTTP server port
DOCKER_SOCK unix:///var/run/docker.sock Docker daemon socket path
DATABASE_PATH ./data/accelero.db Path to the SQLite database file

Logging

Variable Default Description
LOG_LEVEL info Log verbosity: debug, info, warn, error
LOG_FORMAT text Log output format: text (human-readable) or json (structured)

Performance Tuning

Variable Default Description
WORKER_COUNT 2 * CPU cores Number of worker goroutines (min: 2, max: 50)
QUEUE_SIZE 15 * workers Task queue buffer size (min: 50, max: 1000)

Cleanup

Variable Default Description
STATUS_CLEANUP_INTERVAL 1h How often to clean up old deployment records from the database
STATUS_MAX_AGE 24h Maximum age of completed deployment records before cleanup

Docker resource cleanup (stopped containers, dangling images, unused volumes/networks) runs every 24 hours, scoped to resources labeled managed-by=accelero.

Rate limiting

Variable Default Description
RATE_LIMIT_RPS 0 (disabled) Sustained refill rate (tokens/second) for a per-API-key token bucket. Applied after authentication, so probes (/health, /readyz, /metrics) are unaffected.
RATE_LIMIT_BURST max(2*RPS, 10) when RPS set Bucket capacity — how many requests a quiescent caller may send at once. Clamped to >=1.

When a caller exhausts their bucket, Accelero responds with 429 Too Many Requests, a Retry-After header (seconds, >=1), and a JSON body:

{"error": "rate limit exceeded", "retry_after": "1s"}

The rejection is counted in the accelero_rate_limited_requests_total Prometheus counter, labelled by mux route template (so high-cardinality stack IDs don't explode the label set).

Roles & API keys (RBAC)

Every request authenticates with an API key in the X-API-KEY header. Keys carry one of four hierarchical roles:

Role Can do
none Nothing by itself — a deny-all base, only useful with per-stack grants (below).
viewer Read-only — all GET endpoints (stacks, deployments, logs, stats, metrics).
operator Everything a viewer can, plus mutations: deploy, approve/reject, stack/secret/registry CRUD, container restart & exec.
admin Everything — including /admin/* (backup/restore/encrypt-existing) and API-key management.

Authorization is derived from the request's method and path: GETviewer, other methods → operator, /admin/* and /apikeysadmin. Container exec requires operator even though it's a WebSocket GET (it's a mutation).

Per-stack grants. A key's role is its base role — it governs non-stack endpoints (the /stacks collection, /approvals, /audit) and any stack without a specific grant. A key may also carry per-stack grants that override the base role on individual stacks. This expresses things like "deploy prod but only view staging, and nothing else":

curl -sX POST $ACCELERO/api/v1/apikeys -H "X-API-KEY: $ADMIN_KEY" \
  -H 'Content-Type: application/json' -d '{
    "name": "team-x",
    "role": "none",
    "stack_grants": { "prod": "operator", "staging": "viewer" }
  }'

Grants are specified by stack name or id and stored keyed by the canonical stack id (so they survive a rename). Referencing an unknown stack, or an invalid role, is rejected at creation. The effective role for a request is the grant for the target stack if one exists, otherwise the base role. A base role of none means the key can only touch the stacks it's been granted (and cannot use collection/non-stack endpoints).

Bootstrap key. The API_KEY env var is the bootstrap admin key — it always has full access and is how you create the first managed key. Keep it safe; treat it like a root credential.

Managed keys. Additional keys are created at runtime, stored as a SHA-256 hash (the plaintext is shown once, at creation, and cannot be recovered):

# Create an operator key (admin only)
curl -sX POST $ACCELERO/api/v1/apikeys -H "X-API-KEY: $ADMIN_KEY" \
  -H 'Content-Type: application/json' -d '{"name":"ci-bot","role":"operator"}'
# → {"id":"...","name":"ci-bot","role":"operator","key":"acc_...","created_at":"..."}

curl -s $ACCELERO/api/v1/apikeys -H "X-API-KEY: $ADMIN_KEY"        # list (no key/hash)
curl -sX DELETE $ACCELERO/api/v1/apikeys/<id> -H "X-API-KEY: $ADMIN_KEY"  # revoke

A missing key returns 401; an unknown key or a valid key with an insufficient role returns 403. Key create/delete are audited (apikey.create / apikey.delete), and the audit actor for every action is now the key's name (the bootstrap key is env-admin).

Scheduled backups

Accelero can write periodic consistent snapshots of its SQLite database to a local directory (the same VACUUM INTO snapshot as the on-demand POST /api/v1/admin/backup). Disabled by default.

Variable Default Description
BACKUP_INTERVAL 0 (disabled) How often to write a snapshot (Go duration, e.g. 6h, 24h). 0 disables the scheduler. When set, one snapshot is also written immediately on startup.
BACKUP_DIR ./data/backups Directory the snapshots are written to (created if missing).
BACKUP_KEEP 7 Retain only the newest N snapshots; older ones are pruned after each backup. 0 keeps all. Only files named accelero-backup-*.db/.db.age are ever pruned.
BACKUP_ENCRYPTION_PASSPHRASE (unset — plaintext) When set, every snapshot (scheduled and POST /admin/backup) is age-encrypted with this passphrase.

Snapshots are named accelero-backup-<UTC timestamp>.db (or .db.age when encrypted). Restore is a file swap: stop Accelero, copy a snapshot over the file at DATABASE_PATH, and start again.

Encryption. A snapshot contains everything Accelero persists — including repo tokens and per-stack secrets — so on-disk backups are a real exposure. Set BACKUP_ENCRYPTION_PASSPHRASE and every snapshot is written as a standard age stream (.db.age). Decrypt anywhere with the age CLI:

age -d -o accelero.db accelero-backup-20260101T000000Z.db.age   # prompts for the passphrase

This is independent of ACCELERO_ENCRYPTION_KEY (which encrypts individual DB fields); backup encryption wraps the whole snapshot file. Without the passphrase set, snapshots are plaintext — point BACKUP_DIR somewhere protected.

Remote destination (S3-compatible). Set BACKUP_S3_BUCKET and each scheduled snapshot is uploaded off-host after it's written locally (so encryption applies to the uploaded copy too). Works with AWS S3, Cloudflare R2, MinIO, Backblaze B2, and GCS (interop mode).

Variable Default Description
BACKUP_S3_BUCKET (unset — no upload) Target bucket. Setting it enables S3 upload for scheduled backups.
BACKUP_S3_ENDPOINT (AWS: s3.<region>.amazonaws.com) host[:port], no scheme. Set for R2/MinIO/B2/GCS-interop.
BACKUP_S3_REGION us-east-1 Signing region.
BACKUP_S3_ACCESS_KEY_ID / BACKUP_S3_SECRET_ACCESS_KEY Credentials.
BACKUP_S3_PREFIX (none) Optional object-key prefix, e.g. accelero/.
BACKUP_S3_USE_SSL true HTTPS. Set false only for a local plaintext endpoint (e.g. dev MinIO).

Upload is best-effort: a failed upload logs an error but never fails the local backup. Remote retention is not managed by Accelero — configure a bucket lifecycle policy to expire old objects (the idiomatic S3 approach). BACKUP_KEEP only prunes the local directory.

Restore. POST /api/v1/admin/restore uploads a snapshot (plaintext or .db.age) to replace the database. It's gated behind ALLOW_RESTORE=true (a wrong file is total data loss) and applies on the next restart — Accelero can't safely swap an open SQLite file live, so it validates + stages the upload and swaps it at startup, preserving the previous DB as <DATABASE_PATH>.pre-restore-<timestamp>. See the API reference.

Variable Default Description
ALLOW_RESTORE false Set to true to enable POST /admin/restore. Off by default because a bad restore wipes all data.

Retries

Transient operations in the deploy and reconcile paths — image pulls, git clones, and Docker network creation — are retried with bounded exponential backoff and full jitter. A registry blip or a dropped connection turns into a successful deploy on the second try instead of a failed one.

Variable Default Description
RETRY_MAX_ATTEMPTS 3 Total attempts including the first. 1 disables retrying. Clamped to >=1.
RETRY_BASE_DELAY 1s Backoff before the second attempt; doubles each subsequent attempt. Go duration (500ms, 2s).
RETRY_MAX_DELAY 30s Ceiling on any single backoff sleep, so exponential growth can't produce absurd waits.

Backoff for attempt n is a uniform random draw in [0, min(RETRY_BASE_DELAY × 2^(n-1), RETRY_MAX_DELAY)] — full jitter spreads retries from concurrent deploys so they don't thundering-herd a recovering registry.

Failures that a retry cannot fix are not retried — they fail fast:

  • Image pulls: unauthorized, permission denied, or image-not-found.
  • Git clones: authentication required / failed, repository not found, branch/reference not found.

Everything else (connection resets, timeouts, upstream 5xx, DNS hiccups) is treated as transient and retried up to the budget.

Auto-deploy circuit breaker

Where retries handle a transient failure within one operation, the circuit breaker handles a stack that keeps failing across reconcile cycles (a bad image reference, revoked credentials, a malformed compose). Such a stack is left in error status but the reconciler keeps retrying it, so it self-heals once the underlying problem is fixed (typically a git push) — the breaker throttles those retries so a persistently-broken stack isn't redeployed every cycle.

Variable Default Description
CIRCUIT_BREAKER_THRESHOLD 5 Consecutive reconcile-triggered deploy failures before a stack's breaker trips open. 0 disables the breaker (stacks retry every cycle).
CIRCUIT_BREAKER_COOLDOWN 10m How long the breaker stays open before allowing one half-open trial deploy. Go duration.

Lifecycle for a failing stack:

  1. Closed — auto-deploys run normally. Each consecutive failure increments a counter.
  2. Open — after CIRCUIT_BREAKER_THRESHOLD consecutive failures the breaker trips; auto-deploys are skipped (counted in accelero_auto_deploys_skipped_total, one stack.circuit_breaker.opened audit entry) until the cooldown elapses.
  3. Half-open — after the cooldown, one trial deploy is allowed. Success closes the breaker (stack returns to active); failure re-opens it for another cooldown.

Manual deploys are never gated by the breaker — an operator triggering POST /stacks/{id}/deploy always runs. Trips are exposed as accelero_circuit_breaker_tripped_total{stack}.

Native TLS

Variable Default Description
TLS_CERT_FILE (unset — plain HTTP) Path to a PEM-encoded certificate (or certificate chain).
TLS_KEY_FILE (unset) Path to the matching PEM-encoded private key.
TLS_HSTS_MAX_AGE 31536000 Strict-Transport-Security: max-age=<n> value, in seconds. Set to 0 to disable.

Both TLS_CERT_FILE and TLS_KEY_FILE must be set together — only one set is a fatal startup error rather than a silent downgrade. Unset = plain HTTP, the default. Many deployments terminate TLS at a reverse proxy (Caddy, nginx, Traefik) and don't need Accelero doing it twice; this setting exists for the cases that do — air-gapped hosts, single-binary deploys, dev environments.

HSTS is set on every response only when TLS is on. Accelero deliberately omits the includeSubDomains and preload directives — they affect every other service sharing the same origin and are effectively un-undoable for preload. Configure those at your edge proxy if you want them.

Generate a self-signed certificate for local development:

openssl req -x509 -nodes -newkey rsa:2048 \
  -keyout key.pem -out cert.pem -days 365 -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

Security response headers

Variable Default Description
CONTENT_SECURITY_POLICY default-src 'none'; frame-ancestors 'none' Value of the Content-Security-Policy header. Set to an explicit empty string to omit the header.

On every response Accelero always sets three defensive headers — they have no downside for a JSON API:

  • X-Content-Type-Options: nosniff — stops a browser second-guessing a declared Content-Type (e.g. rendering a JSON error as HTML).
  • X-Frame-Options: DENY — clickjacking defence, honoured by clients that ignore CSP.
  • Referrer-Policy: no-referrer — Accelero URLs carry stack ids/names; don't leak them via the Referer header.

The Content-Security-Policy header is configurable. The default is locked down (default-src 'none') because Accelero serves no browser UI today — nothing legitimate needs to load. Setting CONTENT_SECURITY_POLICY= (explicitly empty) omits the CSP header while keeping the three companions above, which is what you want behind an edge proxy that sets its own policy. Leaving the variable unset keeps the secure default.

Unlike HSTS, these headers are valid over both HTTP and HTTPS, so they are always sent.

Webhook signature verification

Variable Default Description
WEBHOOK_SECRET (unset — API-key auth on /webhook) Shared HMAC-SHA256 secret for the root-level POST /webhook endpoint. When set, callers must sign the raw request body and pass the hex digest as X-Hub-Signature-256: sha256=<hex> — the GitHub webhook format. Replaces the API-key check for /webhook so external senders (GitHub, Gitea, Gogs, CI) can authenticate without smuggling the API key into their webhook config.

POST /api/v1/webhook still requires the API key regardless — that route stays a private operator surface. Rejected signatures bump accelero_webhook_signature_rejected_total{reason} and return 401 Unauthorized with a generic {"error":"invalid webhook signature"} body (no reason detail leaks to the caller). The middleware caps request bodies at 1 MiB; larger bodies return 413 Request Entity Too Large.

Known limitation: HMAC alone does not prevent replay. An attacker who captures a valid webhook payload can re-POST it later. Replay protection (timestamp in body + window check, or nonce tracking) is left to the caller or an upstream proxy for now.

Compute a signature with openssl:

BODY='{"stack":"default"}'
printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"

At-rest encryption

Variable Default Description
ACCELERO_ENCRYPTION_KEY (unset — encryption disabled) Base64-encoded 32-byte master key, inline. When set, repo_token and docker_password are encrypted before being written to SQLite (AES-256-GCM) and decrypted on read. Generate with openssl rand -base64 32.
ACCELERO_ENCRYPTION_KEY_FILE (unset) Path to a file whose contents are the base64-encoded 32-byte key. Preferred in production — env vars leak through docker inspect, ps, systemd unit files, and shell history, while a file mounted as a Docker/K8s secret doesn't. Trailing whitespace/newlines are trimmed. Empty file = startup error (likely a broken secret mount).

| ACCELERO_ENCRYPTION_KEY_VAULT | (unset) | The master key wrapped by HashiCorp Vault Transit (a vault:v1:… ciphertext). Accelero asks Vault to unwrap it at startup and keeps the plaintext in memory only. See below. |

Setting more than one of these is a fatal configuration error — the sources are mutually exclusive so a key rotation via one can't silently be ignored.

Vault Transit as the key source

Rather than storing the master key anywhere, you can store only its Vault-wrapped form. Accelero calls Transit's decrypt endpoint once at startup to recover the key, so the master key is never at rest in plaintext — and rotating the Transit key (the KEK) never requires re-encrypting Accelero's database.

Variable Default Description
ACCELERO_ENCRYPTION_KEY_VAULT (unset — source disabled) The vault:v1:… ciphertext of the master key. Setting it selects this source.
VAULT_ADDR Required. e.g. https://vault.internal:8200.
VAULT_TOKEN Required. Token needing only update on <mount>/decrypt/<key>.
ACCELERO_VAULT_TRANSIT_KEY Required. The Transit key that wrapped the master key.
ACCELERO_VAULT_TRANSIT_MOUNT transit Transit mount path.
ACCELERO_VAULT_NAMESPACE (unset) Vault Enterprise namespace.

One-time setup:

vault secrets enable transit
vault write -f transit/keys/accelero

# Generate a master key and wrap it. Keep ONLY the ciphertext.
MASTER_KEY=$(openssl rand -base64 32)
vault write -field=ciphertext transit/encrypt/accelero plaintext="$MASTER_KEY"
# → vault:v1:AbCdEf...   ← this is ACCELERO_ENCRYPTION_KEY_VAULT

Then run Accelero with ACCELERO_ENCRYPTION_KEY_VAULT plus VAULT_ADDR, VAULT_TOKEN, and ACCELERO_VAULT_TRANSIT_KEY. Startup logs which source was used.

This fails closed: if ACCELERO_ENCRYPTION_KEY_VAULT is set but the other required variables are missing, or Vault is unreachable, or the token can't decrypt, Accelero exits rather than starting with encryption silently disabled. Vault must be reachable at every start (the unwrap is not cached to disk). AWS KMS and GCP KMS sources are planned and will follow the same shape.

Without any of these, these fields are stored as plaintext — fine for local development, strongly discouraged in shared/production environments. After setting the key for the first time on an existing deployment, call POST /api/v1/admin/encrypt-existing to migrate legacy plaintext rows. See at-rest encryption for the full behaviour, including the fail-closed policy when the key is removed later.

Multi-host

By default Accelero manages the single Docker daemon at DOCKER_SOCK. That daemon is the default host. You can additionally register remote daemons and point individual stacks at them, so one Accelero instance drives several hosts.

Endpoint Effect
POST /api/v1/hosts Register a host {name, endpoint, tls_ca?, tls_cert?, tls_key?}. The endpoint is pinged before it's stored, so a bad address fails immediately instead of at first deploy. Audited host.create.
GET /api/v1/hosts List hosts. TLS material is never returned — only tls_enabled.
DELETE /api/v1/hosts/{id} Remove a host. Refused with 409 while any stack still targets it. Audited host.delete.

All /hosts endpoints are admin-only (reads included — the endpoint list is infrastructure detail).

Endpoints must be unix://… or tcp://host:port. For a TLS-protected daemon, supply all three of tls_ca / tls_cert / tls_key as PEM strings (a partial triple is rejected); tls_key is encrypted at rest with the same cipher as other secrets and never leaves via the API. ssh:// endpoints are not supported yet.

Point a stack at a host with host_id on create or update, using the host's name or id:

curl -sX POST $ACCELERO/api/v1/hosts -H "X-API-KEY: $ADMIN_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"edge-1","endpoint":"tcp://10.0.0.5:2376",
       "tls_ca":"-----BEGIN CERTIFICATE-----\n...","tls_cert":"...","tls_key":"..."}'

curl -sX POST $ACCELERO/api/v1/stacks -H "X-API-KEY: $ADMIN_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"web","repo_url":"...","compose_path":"docker-compose.yaml","host_id":"edge-1"}'

An empty host_id means the default host, so existing stacks are unaffected — they keep deploying to DOCKER_SOCK. Setting host_id: "" on update moves a stack back to the default host.

Everything stack-scoped follows the stack's host. Deploys, reconciliation, drift, /preview, and all container introspection (/containers, logs + logs/stream, stats + stats/stream, exec, events, restart) run against the host the stack is deployed to. If a stack's host is unreachable, those endpoints return 502 rather than silently reading the wrong daemon.

The root resource browsers span every host. /images, /volumes, and /networks query the default host plus every registered host, and each result carries a host field naming where it lives (default for the DOCKER_SOCK daemon). ?stack= still narrows as before. If one host is unreachable it's skipped with a logged warning and the rest are returned — you only get a 500 if every host fails. The periodic Docker resource cleanup likewise prunes on every host.

!!! note "Volume browse/write is default-host-only" GET /volumes/{name}/browse and POST /volumes/{name}/files still operate on the default host, because the helper container that reads the volume is launched there. Volume names are only unique per host, so allowing a remote name here could silently act on a same-named volume on the default host. Remote volumes therefore appear in GET /volumes (tagged with their host) but can't be browsed yet.

Approval gates

A stack created (or updated) with requires_approval: true holds every deploy behind a manual approval step instead of running it immediately. This applies to all triggers — manual POST /deploy, the legacy webhook, and reconcile auto-deploy.

When a deploy is triggered on such a stack, Accelero creates a deployment in the pending_approval state, emits an approval.requested audit event (delivered as a notification), and does not execute. At most one approval is open per stack at a time — repeated triggers (e.g. the reconcile loop firing every cycle) return the existing one rather than spawning duplicates or re-notifying.

An operator then acts via the API:

Endpoint Effect
POST /api/v1/stacks/{id}/deployments/{deployId}/approve Runs the held deploy (202; rollout is async). Audited approval.granted.
POST /api/v1/stacks/{id}/deployments/{deployId}/reject Cancels it. Optional body {"reason": "..."}. Audited approval.rejected.
GET /api/v1/approvals Lists all pending approvals across every stack, oldest first.
Variable Default Description
APPROVAL_TIMEOUT 24h How long a pending approval waits before being auto-rejected (approval.timed_out). Go duration. 0 disables expiry — approvals wait indefinitely.

Each lifecycle event (approval.requested / granted / rejected / timed_out) is written to the audit log and, if notifications are configured, delivered to the webhook/Slack sinks. deploy.start is deliberately not emitted for a gated stack, so you don't get a contradictory "deploy started" + "awaiting approval" pair.

Notifications

Accelero can push notable lifecycle events to external destinations. Set any of the URLs below to enable that sink; sinks fire independently and with none set, notifications are off (zero overhead).

Variable Default Description
NOTIFY_WEBHOOK_URL (unset — disabled) Generic webhook. Receives the full event as JSON via POST.
NOTIFY_SLACK_WEBHOOK_URL (unset — disabled) Slack incoming webhook. Receives a {"text": "<message>"} payload.
NOTIFY_DISCORD_WEBHOOK_URL (unset — disabled) Discord webhook. Receives a {"content": "<message>"} payload.
NOTIFY_TEAMS_WEBHOOK_URL (unset — disabled) Microsoft Teams incoming webhook. Receives a MessageCard ({"@type":"MessageCard", ..., "text":"<message>"}), the portable form across connector versions.

Email (SMTP). The email sink is enabled when NOTIFY_SMTP_HOST, NOTIFY_EMAIL_FROM, and NOTIFY_EMAIL_TO are all set. It sends one plain-text message per event.

Variable Default Description
NOTIFY_SMTP_HOST (unset — disabled) SMTP server hostname. Setting it (with FROM + TO) enables the email sink.
NOTIFY_SMTP_PORT 587 Port. 465 uses implicit TLS; any other port uses STARTTLS when the server advertises it (else plaintext).
NOTIFY_SMTP_USERNAME / NOTIFY_SMTP_PASSWORD Credentials. When a username is set, PLAIN auth is used. Omit both for an unauthenticated relay.
NOTIFY_EMAIL_FROM (unset) Envelope + header From address.
NOTIFY_EMAIL_TO (unset) Comma-separated recipient list.

Events notified: deploy started, completed, failed, rolled back; drift detected; auto-deploy triggered. Read-only, secret-CRUD, and other audit operations are deliberately not notified (avoids channel spam).

Delivery semantics: best-effort and asynchronous — each sink fires in its own goroutine with a 10s timeout. A failing or slow sink logs a warning and is never allowed to block or fail a deploy. There are no retries and no ordering guarantees.

Generic webhook payload (NOTIFY_WEBHOOK_URL):

{
  "type": "deploy.failed",
  "stack": "web",
  "outcome": "failure",
  "message": "❌ Deploy failed for stack `web`: git clone failed: ...",
  "fields": { "changes": "3" },
  "time": "2026-07-21T10:04:05Z"
}

type is the audit operation (deploy.start, deploy.complete, deploy.failed, deploy.rolled_back, drift.detected, drift.auto_deployed); fields mirrors the audit metadata. Implemented in internal/notify by wrapping the shared audit recorder, so the deployer and reconciler need no changes — anything that records one of these audit events is notified automatically.

Legacy Single-Stack Variables

These variables are supported for backward compatibility. If set, Accelero creates a "default" stack from them on first startup. For new deployments, use the REST API to create stacks instead.

Variable Description
REPO_URL Git repository URL
REPO_USERNAME Git authentication username
REPO_TOKEN Git authentication token
REPO_BRANCH Git branch to clone
COMPOSE_PATH Path to docker-compose.yaml in the repo
SERVICE_NAMES Comma-separated service filter (empty = all)
DOCKER_USERNAME Docker registry username
DOCKER_PASSWORD Docker registry password
DOCKER_REGISTRY Docker registry address

Sample Environment File

A ready-to-fill template lives at .env.example in the repository. Copy it to .env and fill in the values:

cp .env.example .env
# edit .env
docker run --env-file .env ...

Do not confuse .env.example (configures the Accelero daemon itself) with a .env inside your GitOps repo next to docker-compose.yaml — that one feeds ${VAR} interpolation in your compose. See Variable Interpolation.

Docker Compose Example

services:
  accelero:
    image: arbianshkodra/accelero:latest
    ports:
      - "8000:8000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - accelero-data:/data
    environment:
      - API_KEY=your-secure-key
      - LOG_FORMAT=json
    restart: unless-stopped

volumes:
  accelero-data: