Skip to content

Configuration

All settings are environment variables — set them in .env (docker-compose loads it automatically). Start from .env.example, which documents every variable with a secure-setup checklist.

Core

Variable Default Description
GIT_REPO_URL / GIT_BRANCH / GIT_PATH / GIT_TOKEN Optional bootstrap repo (repos are usually managed via the UI)
SYNC_INTERVAL 60 Seconds between reconcile passes
AUTO_SYNC true Apply automatically vs surface OutOfSync only
DRY_RUN false Record plans only — never call AWS mutation APIs
RECONCILE_WORKERS 8 Parallel diff workers per pass
KEEP_TASKDEF_REVISIONS 0 Deregister old ACTIVE revisions beyond newest N
AWS_REGION Default region
PORT 8080 HTTP port
DATABASE_URL sqlite in /data Persistence (Postgres in compose; enables HA + audit)
LOG_FORMAT text json for structured logs

Authentication & RBAC

Variable Default Description
AUTH_MODE none github enables OAuth login
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET GitHub OAuth App credentials
GITHUB_ALLOWED_USERS Comma-separated allowlist
GITHUB_ALLOWED_ORG Restrict login to an org's members
SESSION_SECRET random Signs session cookies — set it for stable sessions
PUBLIC_URL http://localhost:8080 External URL: OAuth callback, CSRF, Secure cookies/HSTS
RBAC_ADMINS / RBAC_OPERATORS / RBAC_DEFAULT_ROLE Role assignment
API_TOKENS CI bearer tokens: token:role[,…]

Integrations

Variable Default Description
WEBHOOK_SECRET Enables the GitHub push webhook
SLACK_WEBHOOK_URL Slack notifications
ENCRYPTION_KEY falls back to SESSION_SECRET Encrypts stored AWS profiles
METRICS_TOKEN Bearer token for /metrics

IAM permissions

Minimum policy for the role running Andro-CD:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "ecs:CreateCluster", "ecs:UpdateCluster", "ecs:DeleteCluster",
      "ecs:PutClusterCapacityProviders", "ecs:DescribeClusters",
      "ecs:RegisterTaskDefinition", "ecs:DeregisterTaskDefinition",
      "ecs:DescribeTaskDefinition", "ecs:ListTaskDefinitions", "ecs:TagResource",
      "ecs:CreateService", "ecs:UpdateService", "ecs:DeleteService",
      "ecs:DescribeServices", "ecs:RunTask", "ecs:StopTask",
      "ecs:ListTasks", "ecs:DescribeTasks",
      "iam:PassRole",
      "logs:CreateLogGroup", "logs:DescribeLogStreams",
      "logs:GetLogEvents", "logs:FilterLogEvents",
      "ecr:DescribeImages",
      "ec2:DescribeSubnets",
      "elasticloadbalancing:DescribeTargetGroups", "elasticloadbalancing:CreateTargetGroup",
      "elasticloadbalancing:ModifyTargetGroup", "elasticloadbalancing:DeleteTargetGroup",
      "elasticloadbalancing:DescribeRules", "elasticloadbalancing:CreateRule",
      "elasticloadbalancing:ModifyRule", "elasticloadbalancing:DeleteRule",
      "elasticloadbalancing:AddTags",
      "application-autoscaling:*",
      "scheduler:CreateSchedule", "scheduler:UpdateSchedule",
      "scheduler:DeleteSchedule", "scheduler:GetSchedule",
      "sts:GetCallerIdentity"
    ],
    "Resource": "*"
  }]
}

Trim per use case: drop scheduler:* without ECSScheduledTask, application-autoscaling:* without autoscaling, elasticloadbalancing:*/ec2:DescribeSubnets without managed load balancers (loadBalancer.create), ecs:DeleteService if you never prune, ecs:DeregisterTaskDefinition without KEEP_TASKDEF_REVISIONS, ecs:TagResource without labels, the cluster mutations without ECSCluster.

Tip

Run with DRY_RUN=true first — the recorded plans show exactly which calls the controller would make, so you can verify the policy before granting write access.

Troubleshooting

"The security token included in the request is invalid" : AWS credentials expired or wrong. Refresh and restart the container.

Diff always OutOfSync after enabling resolveImages : Expected — tags now resolve to digests the live task definition doesn't have yet. Sync once; diffs stay quiet until the tag actually moves upstream.

"cannot decrypt stored secret — was ENCRYPTION_KEY/SESSION_SECRET changed?" : The Fernet key changed, so stored AWS profiles can't be read. Restore the old value or delete + re-add the profiles.

Auto-sync doesn't run after a rollback : By design — rollback pauses the app so the reconciler doesn't undo it. Click Sync to return to Git state.

Everything shows [dry-run] and nothing deploys : DRY_RUN=true is set. Unset it, restart, then Sync (or push a commit).

Two replicas — which one applies? : The leader (advisory lock). If both apply, they're not sharing the same Postgres DATABASE_URL.