Skip to content

Container mode (two-repository Docker deploy)

Container mode splits build from deploy into two repositories: a CI repository builds and pushes a config-agnostic deployer image, and a config-only CD repository runs that image against as many targets as you like. One image drives N deployments, and rollback is a retag.

Engine support

Container mode is a feature of the default CodePipeline engine (EngineType.CODEPIPELINE). The CDK_PIPELINES and GitHubActions engines deploy stages directly by replaying the app per stage; they do not build or consume a deployer image.

When to use it

Reach for container mode when you want to build the deployable artifact once and promote that exact artifact through stages — including across repositories or teams — rather than re-synthesizing per stage from source. Because the image bakes the CDK app and its npm dependencies, the CD side needs no source, no npm install, and no registry access at deploy time: it can synth-and-deploy offline against each target's configuration.

Repo 1 — build the deployer image

The CI repository is your normal CDK app. Add a deployerImage to cicd.config.ts and the pipeline renders as Source → BuildImage with no deploy stages — it builds and pushes an image instead of deploying:

import { defineCICD, Repository, BuildImage, ImageTagStrategy } from '@cdklabs/cdk-cicd-wrapper';

export default defineCICD({
  application: 'my-app',
  repository: Repository.github('my-org/my-app'),
  deployerImage: BuildImage.docker({
    dockerfile: 'Dockerfile', // default; the image payload is your app + deps, NOT cdk.out
    // repositoryName: 'my-app-deployer', // reference an existing ECR repo; omit to provision one
    // tagStrategy: ImageTagStrategy.GIT_SHA, // default: tag by resolved commit; or ImageTagStrategy.LATEST
  }),
});

cdk-cicd deploy-ci provisions the CI pipeline. Its single build project:

  1. runs npm ci and cdk-cicd check (CI as a validation gate),
  2. logs in to ECR,
  3. docker builds your Dockerfile and pushes the image, tagged by strategy (GIT_SHA by default — immutable; LATEST is simplest but not immutable).

If you do not name an existing repository, the pipeline provisions one named <application>-deployer; a disposable pipeline (deploy-ci --disposable) empties and deletes it on teardown.

Why the image, not cdk.out

The image bakes code + dependencies but never cdk.out, so the CD side can synth-and-deploy it offline against any target's config. That is what collapses per-target pipeline sprawl: targets become config rows a single image is run against, not pipeline resources.

Repo 2 — deploy the image

The CD repository is a small, app-agnostic config repository (no CDK code). It declares which image to run and where to deploy it, via defineDeployment in a deploy.config.ts:

// deploy.config.ts
import { defineDeployment, Repository } from '@cdklabs/cdk-cicd-wrapper';

export default defineDeployment({
  // The BASE deployer image repository (no tag). The per-stage version is appended at deploy time.
  image: '111111111111.dkr.ecr.eu-west-1.amazonaws.com/my-app-deployer',

  // The config-only source repo this CD pipeline watches (deploy.config.ts + config/<stage>.json).
  // Omit it to use only the local `cdk-cicd deploy --from-image` executor with no CD pipeline.
  repository: Repository.codecommit('my-app-deploy-config'),

  // Targets say WHERE (account/region/role) + gating. The VERSION each runs comes from config/<stage>.json.
  targets: [
    { stage: 'dev', env: { account: '111111111111', region: 'eu-west-1' } },
    { stage: 'int', env: { account: '111111111111', region: 'eu-west-1' }, manualApproval: true },
    {
      stage: 'prod',
      env: { account: '222222222222', regions: ['eu-west-1', 'us-east-1'] },
      manualApproval: true,
      deployment: { deployRole: 'arn:aws:iam::222222222222:role/automation-deployer' },
    },
  ],
});

Each stage's version lives in its own config file in the CD repository (a hash or semver) — not baked in the image:

// config/dev.json          config/int.json           config/prod.json
{ "version": "1.5.0" }      { "version": "1.4.2" }     { "version": "1.4.2" }

The deploy resolves image = <base-repo>:<version from config/<stage>.json>, so dev can run a newer version than prod, and the version is plain config, reviewable in a pull request.

Provision the CD pipeline, or run locally

With a repository set, cdk-cicd deploy-ci provisions the CD pipeline (the deploy-side twin of the CI deploy-ci): a Source stage, then a Deploy stage with every ungated target running in parallel, then a DeployGated stage where each gated target sits behind its own manual-approval action. Each deploy action runs cdk-cicd deploy --from-image --target <stage>, which reads that stage's version at run time, pulls <base-repo>:<version>, and synth-and-deploys the stage.

The same executor runs locally without any pipeline:

npx cdk-cicd deploy --from-image           # every target (gated targets require --yes)
npx cdk-cicd deploy --from-image --target dev

On a runner whose default Docker network cannot reach AWS, add --docker-network host. The local executor is fail-closed: it refuses a target with manualApproval: true unless you pass --yes.

Division of authority: WHAT vs WHERE

The image is authoritative for what to deploy — the app code and its stage definitions are baked in at build time. The deploy.config.ts target is authoritative for where — the account, region(s), and forced role for each stage. That is why each run pins a single --region: the target's environment overrides whatever region set the image's own cicd.config.ts carries, so the CD repository — not the image — decides the deployment topology.

Rollback is a retag

To roll back, point a stage's version file at the previous tag (e.g. config/prod.json's version back to 1.4.1) and re-run. Because the image pins code + deps and config supplies the lookups, the same image

  • same config always synthesizes the same template — a deterministic rollback with no rebuild.

See also