Custom IAM policies attach to new ECR repositories

Self-hosted deployments can automatically apply custom IAM policies to newly created ECR repositories, ensuring worker nodes in separate AWS accounts can pull images.
Self-hosted environments often operate with isolated AWS accounts, placing the Elastic Container Registry (ECR) in a shared platform account while worker nodes run in separate cluster accounts. By default, newly created ECR repositories restrict read access to the registry owner, causing worker nodes to fail with a 403 Forbidden error during initial deployments.
Custom IAM policies can now be attached to new ECR repositories automatically. By providing a JSON policy through a new environment variable, cross-account pull access is granted immediately upon repository creation in the webapp. Existing repositories are also reconciled during subsequent deployments to ensure access configurations stay synchronized and self-heal from partial configuration failures.
View Original GitHub Description
Summary
Self-hosters that operate the webapp's ECR account separately from the account running the EKS workers (e.g., a shared platform account that hosts the registry plus per-team accounts that host clusters) currently hit a 403 Forbidden the first time any project is deployed:
Failed to pull image "<acct-A>.dkr.ecr.<region>.amazonaws.com/<namespace>/proj_…:…":
unexpected status from HEAD request to .../v2/.../manifests/sha256:…: 403 Forbidden
ensureEcrRepositoryExists in apps/webapp/app/v3/getDeploymentImageRef.server.ts calls CreateRepository and PutLifecyclePolicy, but never SetRepositoryPolicy — so the new repo inherits the AWS default (only the registry-owner account can read/pull). Workers in the cluster account get 403 every single deploy. The only workarounds today are running a one-off post-create script or pre-creating every repo by hand.
Proposed change
Add an optional env var:
DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY (V4 mirror: V4_DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY)
Raw IAM policy JSON. When set, the webapp calls SetRepositoryPolicy immediately after CreateRepository so every new repo carries that policy from creation. Operators control the principal/actions; we don't bake in any opinions about cross-account boundaries.
Example value (for the typical self-host case — grant pull to the cluster account):
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowClusterAccountPull",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::<cluster-account-id>:root"},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
}]
}
Why env var (not a chart-level field)
- Mirrors the shape of the sibling vars (
DEPLOY_REGISTRY_ECR_TAGS,DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN, etc.) which are already operator-supplied viawebapp.extraEnvVarsin self-host setups. - Cloud is unaffected — the env var is optional, unset by default; existing behavior unchanged.
- Existing repos are unaffected — only newly-created repos get the policy.
RepositoryCreationTemplatefrom the AWS provider isn't an alternative here: it only applies to repos created via pull-through-cache or replication, not toecr:CreateRepositoryAPI calls.
Implementation
apps/webapp/app/env.server.ts— declareDEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICYand the V4 fallback.apps/webapp/app/v3/registryConfig.server.ts— propagateecrDefaultRepositoryPolicytoRegistryConfig.apps/webapp/app/v3/getDeploymentImageRef.server.ts—createEcrRepositoryaccepts the policy; if set, callsSetRepositoryPolicyafterPutLifecyclePolicy.docs/self-hosting/env/webapp.mdx— documentation row added under Deploy & Registry.
Verification
Verified end-to-end against a self-hosted Trigger.dev on EKS where the ECR account is separate from the cluster account:
- Without the env var (current
main): the new project's first run pod stays inImagePullBackOffwith403 Forbidden. - With the env var set to a JSON granting
ecr:BatchGetImage/GetDownloadUrlForLayer/BatchCheckLayerAvailabilityto the cluster account: a freshtrigger.dev deploy --env prodfollowed by ahello-worldrun completes in ~5s end-to-end on the first try.
Manually also confirmed that existing repos are untouched (the call only fires inside createEcrRepository, which only runs when DescribeRepositories returned RepositoryNotFoundException).
Out of scope
- Chart values surface for this — operators already pass the existing ECR vars via
webapp.extraEnvVars, so this follows the same pattern. Happy to add a first-class chart field in a follow-up if that's the preferred direction. - IAM-policy validation in the webapp — we forward the JSON verbatim to AWS and surface AWS's error messages on misuse, matching how
DEPLOY_REGISTRY_ECR_TAGSis handled today.
This is a draft pending CI / CodeRabbit pass — happy to iterate on direction (e.g., split into per-action env vars, or extend the chart values schema) if any of the above choices feels off.