ЁЯПл The SchoolтА║ЁЯУо CI/CDтА║ЁЯУж рдзрдбрд╛ 07 тАФ image build рдЖрдгрд┐ push рдХрд░рд╛: рдлреЛрдЯреЛрдХреЙрдкреА рдорд╢реАрди
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУж рдзрдбрд╛ 07 тАФ image build рдЖрдгрд┐ push рдХрд░рд╛: рдлреЛрдЯреЛрдХреЙрдкреА рдорд╢реАрди

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 07 ┬╖ рдорд╛рдЧреЗ: lesson-06-secrets-oidc ┬╖ рдкреБрдвреЗ: lesson-08-environments-gates


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ06, рдЖрдгрд┐ рдлреЛрдЯреЛрдХреЙрдкреА рдбреЗрд╕реНрдХ: рддрдкрд╛рд╕рд▓реЗрд▓рд╛ рдЧреГрд╣рдкрд╛рда рдбрдмрд╛ рдмрдирддреЛ, рддреНрдпрд╛рд▓рд╛ commit рдЪрд╛ рдард╕рд╛ рдорд┐рд│рддреЛ, рдЖрдгрд┐ рддреЛ рд▓реЙрдХрд░рдордзреНрдпреЗ рдареЗрд╡рд▓рд╛ рдЬрд╛рддреЛ. рдЦрд▒реНрдпрд╛ files:

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рддрдкрд╛рд╕рдгреА рдбреЗрд╕реНрдХрдиреЗ тЬЕ рдЧреГрд╣рдкрд╛рдард╛рд╡рд░ рд╢рд┐рдХреНрдХрд╛ рдорд╛рд░рд▓рд╛. рдЖрддрд╛ рдлреЛрдЯреЛрдХреЙрдкреА рдорд╢реАрди ЁЯН▒ рд╕реВрдЪрдирд╛ рдлрд▓рдХрд╛рдВрд╡рд░ рдЬрд╛рдгрд╛рд░реА рдкреНрд░рдд рдХрд╛рдврддреЗ, рдЖрдгрд┐ рдпрд╛ рдбреЗрд╕реНрдХрдЪреНрдпрд╛ рддреАрди рд╕рд╡рдпреА рдЖрд╣реЗрдд.

рдкреНрд░рдд рдХрд╛рдврддрд╛рдирд╛рдЪ рддреНрдпрд╛рд╡рд░ рдард╕рд╛ рд▓рд┐рд╣рд╛. Commit рд▓рд╛ 40 рдЕрдХреНрд╖рд░рд╛рдВрдЪрд╛ рдард╕рд╛ рдЕрд╕рддреЛ, рддреЛ рдореНрд╣рдгрдЬреЗ SHA. рддреНрдпрд╛рдЪреА рдкрд╣рд┐рд▓реА 7 рдЕрдХреНрд╖рд░реЗ рдбрдмреНрдпрд╛рдЪреНрдпрд╛ рдЖрдд APP_VERSION рдореНрд╣рдгреВрди рдЬрд╛рддрд╛рдд, рдореНрд╣рдгрдЬреЗ рдЪрд╛рд▓реВ ре▓рдк "рддреВ рдХреЛрдгрддреА рдкреНрд░рдд рдЖрд╣реЗрд╕?" рдпрд╛рдЪреЗ рдЙрддреНрддрд░ рджреЗрдК рд╢рдХрддреЛ. рдкреВрд░реНрдг SHA label рд╡рд░ рдЬрд╛рддреЛ тАФ image рдЪрд╛ tag тАФ рдореНрд╣рдгрдЬреЗ рд▓реЙрдХрд░рдЪреА рдлрд│реА рддреНрдпрд╛рдЪ рдкреНрд░рд╢реНрдирд╛рдЪреЗ рдЙрддреНрддрд░ рдирд╛рд╡рд╛рдиреЗ рджреЗрддреЗ. latest рдирд╛рд╣реА, main рдирд╛рд╣реА: рддреЗ рдЖрдард╡рдбреНрдпрд╛-рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ bytes рдХрдбреЗ рдирд┐рд░реНрджреЗрд╢ рдХрд░рддрд╛рдд.

рдареЗрд╡рдгреНрдпрд╛рдЖрдзреА рдкреНрд░рдд рдиреАрдЯ рдкрд╛рд╣рд╛. Tests runner рдЪреНрдпрд╛ Node рд╡рд░ рдЪрд╛рд▓рд▓реНрдпрд╛; рдбрдмреНрдпрд╛рдЪреЗ рд╕реНрд╡рддрдГрдЪреЗ Node, рд╕реНрд╡рддрдГрдЪреНрдпрд╛ files, рд╕реНрд╡рддрдГрдЪрд╛ port рдЕрд╕рддреЛ. рдореНрд╣рдгреВрди рдбреЗрд╕реНрдХ рдбрдмрд╛ рд╕реБрд░реВ рдХрд░рддреЛ, /healthz рдЙрддреНрддрд░ рджреЗрдИрдкрд░реНрдпрдВрдд рд╡рд┐рдЪрд╛рд░рдд рд░рд╛рд╣рддреЛ, рдЖрдгрд┐ / рдЕрдкреЗрдХреНрд╖рд┐рдд version рдЫрд╛рдкрддреЗ рдХрд╛ рддреЗ рддрдкрд╛рд╕рддреЛ. рддреЛ рдбрдмреНрдпрд╛рдЪрд╛ smoke test рдЖрд╣реЗ, рдХреЛрдбрдЪрд╛ рдирд╛рд╣реА.

рдиреАрдЯ рд╣рд╕реНрддрд╛рдВрддрд░рдг рдХрд░рд╛. рдлреЛрдЯреЛрдХреЙрдкреА рдорд╢реАрди рдЖрдгрд┐ рдлрд╛рдЗрд▓рд┐рдВрдЧ рдбреЗрд╕реНрдХ рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ рдЦреЛрд▓реНрдпрд╛рдВрдордзреНрдпреЗ рдЖрд╣реЗрдд, рдореНрд╣рдгреВрди рдкреНрд░рдд рдзрдбрд╛ 05 рдЪреНрдпрд╛ рдкрд╛рдХрд┐рдЯрд╛рддреВрди рдкреНрд░рд╡рд╛рд╕ рдХрд░рддреЗ; рдлрд╛рдЗрд▓рд┐рдВрдЧ рдбреЗрд╕реНрдХ рдзрдбрд╛ 06 рдЪреНрдпрд╛ рдбреЗ рдкрд╛рд╕рдиреЗ рд▓реЙрдХрд░ ЁЯПж рдЙрдШрдбрддреЛ; рд▓реЙрдХрд░ рдкреНрд░рддреНрдпреЗрдХ рдирд╡реНрдпрд╛ рдбрдмреНрдпрд╛рдЪрд╛ X-ray рдХрд╛рдврддреЛ (scan on push).

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    build["ЁЯН▒ docker build<br/>--build-arg APP_VERSION=abc1234"]
    smoke["ЁЯзк smoke-test the BOX<br/>curl /healthz, grep version abc1234"]
    env["ЁЯУО image.tar.gz<br/>docker save, then gzip"]
    push["ЁЯЪЪ push job, another machine<br/>docker load, ЁЯкк badge, ЁЯОл ECR login"]
    ecr["ЁЯПж ECR hello-courier:abc1234тАж<br/>tag = full SHA, scan on push"]
    build -->|"1 run it"| smoke
    smoke -->|"2 only if it answers"| env
    env -->|"3 download by name"| push
    push -->|"4 docker push"| ecr

тЭУ рдХрд╛рдп

ЁЯза рдПрдХрдЪ рдард╕рд╛, рдЖрдд рдЖрдгрд┐ рдмрд╛рд╣реЗрд░

commit  abc1234ef56тАж  (40 chars тАФ git's fingerprint of the code)
   тФЬтФА INSIDE the box:  APP_VERSION=abc1234            (short SHA тАФ what the app prints)
   тФФтФА ON the label:    hello-courier:abc1234ef56тАж     (full SHA тАФ what you deploy by)

Tag рд╡рд┐рд░реБрджреНрдз digest. Commit-SHA tag рдЖрдкрд▓реНрдпрд╛рд▓рд╛ рд╕рд╛рдВрдЧрддреЛ рдХреА image рдХреЛрдгрддреНрдпрд╛ source revision рдкрд╛рд╕реВрди рдмрдирд▓реА; image рдЪрд╛ digest (sha256:тАж, docker image inspect рдЖрдгрд┐ ECR console рдордзреНрдпреЗ рджрд┐рд╕рддреЛ) рдиреЗрдордХреЗ, рди рдмрджрд▓рдгрд╛рд░реЗ image bytes рдУрд│рдЦрддреЛ. рддреЛрдЪ commit рджреЛрдирджрд╛ build рдХрд░рд╛ рдЖрдгрд┐ рддреБрдореНрд╣рд╛рд▓рд╛ рдПрдХ tag рдкрдг рдХрджрд╛рдЪрд┐рдд рджреЛрди digests рдорд┐рд│рддреАрд▓ тАФ tag рдореНрд╣рдгрдЬреЗ рдкрддреНрддрд╛, digest рдореНрд╣рдгрдЬреЗ рдУрд│рдЦ, рдЬрд╕реЗ Docker рд╢рд╛рд│реЗрдЪрд╛ рдзрдбрд╛ 11 рд╕рд╛рдВрдЧрддреЛ. рдпрд╛рдЪ рдХрд╛рд░рдгрд╛рд╕рд╛рдареА рдзрдбрд╛ 12 base images digest рдиреЗ pin рдХрд░рддреЛ.

ЁЯдФ рдХрд╛

Version рдЖрдд рднрд╛рдЬреВрди рдмрд╕рд╡рд▓реЗрд▓реЗ рдирд╕реЗрд▓ рддрд░ "рдХреЛрдгрддреЗ version рдЪрд╛рд▓реВ рдЖрд╣реЗ?" рд╣рд╛ рдЕрдВрджрд╛рдЬрдЪ рдЕрд╕рддреЛ. Smoke test рд╢рд┐рд╡рд╛рдп, рдПрдЦрд╛рджреА file рд╡рд┐рд╕рд░рдгрд╛рд░реА Dockerfile unit tests рдкрд╛рд╕ рдХрд░рддреЗ рдЖрдгрд┐ рдкрд╣рд┐рд▓реНрдпрд╛рдЪ readiness probe рд▓рд╛ рдорд░рддреЗ. SHA tags рд╢рд┐рд╡рд╛рдп, "рдХрд╛рд▓рдЪреЗ рдкрд░рдд рд▓рд╛рд╡рд╛" рдЕрд╢рдХреНрдп рд╣реЛрддреЗ, latest рдЧрдкрдЪреВрдк рдЦреЛрдЯреЗ рдмреЛрд▓рддреЛ, рдЖрдгрд┐ рдирдВрддрд░рдЪреНрдпрд╛ GitOps commits рдХрдбреЗ рдмрджрд▓рдгреНрдпрд╛рд╕рд╛рд░рдЦреЗ рдЕрд░реНрдердкреВрд░реНрдг рдХрд╛рд╣реАрдЪ рдирд╕рддреЗ. Docker рд╢рд╛рд│реЗрдЪрд╛ рдзрдбрд╛ 12 рдиреЗрдордХреНрдпрд╛ рдпрд╛рдЪ рд╣рд╕реНрддрд╛рдВрддрд░рдгрд╛рд╡рд░ рд╕рдВрдкрддреЛ; рддреНрдпрд╛рдЪрд╛ рдзрдбрд╛ 08 tag рд╕реНрд╡рдЪреНрдЫрддреЗрдмрджреНрджрд▓ рд╕рд╛рдВрдЧрддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

рдард╕рд╛ рдЖрдд рднрд╛рдЬреВрди рдмрд╕рд╡рд╛. app/Dockerfile рддреЛ build argument рдореНрд╣рдгреВрди рдШреЗрддреЗ рдЖрдгрд┐ environment рдордзреНрдпреЗ рдЧреЛрдард╡рддреЗ:

ARG APP_VERSION=dev                 # CI passes the commit SHA here (lesson 07)
ENV APP_VERSION=$APP_VERSION

рдбрдмрд╛ build рдХрд░рд╛ рдЖрдгрд┐ рддреНрдпрд╛рдЪрд╛ smoke-test рдХрд░рд╛. ship.yml рдордзрд▓рд╛ build job тАФ ${GITHUB_SHA::7} рдореНрд╣рдгрдЬреЗ bash рдордзреНрдпреЗ "рдкрд╣рд┐рд▓реА 7 рдЕрдХреНрд╖рд░реЗ"; loop рдбрдмреНрдпрд╛рд▓рд╛ рд╕реБрд░реВ рд╡реНрд╣рд╛рдпрд▓рд╛ рд╕реБрдорд╛рд░реЗ рджрд╣рд╛ рд╕реЗрдХрдВрдж рджреЗрддреЛ:

      - name: ЁЯН▒ docker build (version label = short SHA)
        run: docker build --build-arg APP_VERSION=${GITHUB_SHA::7} -t hello-courier:${GITHUB_SHA} app
      - name: ЁЯзк test the BOX, not just the code
        run: |
          docker run -d --rm -p 3000:3000 --name smoke hello-courier:${GITHUB_SHA}
          for i in $(seq 1 20); do curl -fsS localhost:3000/healthz && break; sleep 0.5; done
          curl -fsS localhost:3000/ | grep -q "version ${GITHUB_SHA::7}"
          docker stop smoke
      - name: ЁЯТ╛ keep the image for the push job (same run, different machine)
        run: docker save hello-courier:${GITHUB_SHA} | gzip > image.tar.gz

grep -q рдЪреА рдУрд│ рдЬреБрдирд╛ рдХрд┐рдВрд╡рд╛ рдЪреБрдХреАрдЪреЗ label рдЕрд╕рд▓реЗрд▓рд╛ build рдкрдХрдбрддреЗ: рдбрдмреНрдпрд╛рдиреЗ version dev рдЫрд╛рдкрд▓реЗ, рддрд░ рдХрд╛рд╣реАрд╣реА push рд╣реЛрдгреНрдпрд╛рдЖрдзреАрдЪ job fail рд╣реЛрддреЛ.

Load, log in, tag, push. push job рдирд╡реНрдпрд╛ рдорд╢реАрдирд╡рд░ рдЙрддрд░рддреЛ, рдореНрд╣рдгреВрди рддреЛ рдкрд╛рдХрд┐рдЯрд╛рдкрд╛рд╕реВрди рд╕реБрд░реВ рдХрд░рддреЛ, рдзрдбрд╛ 06 рдЪрд╛ рдмреЕрдЬ рджрд╛рдЦрд╡рддреЛ, рдордЧ:

      - run: docker load < image.tar.gz
      - name: ЁЯОл log in to ECR
        id: ecr
        uses: aws-actions/amazon-ecr-login@v2
      - name: ЁЯП╖я╕П tag with the commit SHA and push
        id: meta
        env:
          IMAGE: ${{ steps.ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:${{ github.sha }}
        run: |
          docker tag hello-courier:${GITHUB_SHA} "$IMAGE"
          docker push "$IMAGE"
          echo "image=$IMAGE" >> "$GITHUB_OUTPUT"

amazon-ecr-login рдбреЗ рдкрд╛рд╕рдЪреЗ рд░реВрдкрд╛рдВрддрд░ docker login рдордзреНрдпреЗ рдХрд░рддреЗ рдЖрдгрд┐ registry hostname output рдХрд░рддреЗ, рдореНрд╣рдгреВрди IMAGE рд╣реЛрддреЗ 123456789012.dkr.ecr.ap-south-1.amazonaws.com/hello-courier:<full sha> тАФ job рдЪрд╛ image output рдореНрд╣рдгреВрди export рдХреЗрд▓реЗрд▓реА, рдзрдбрд╛ 10 рдордзреНрдпреЗ delivery van рд▓рд╛ рдорд┐рд│рдгрд╛рд░реА рдиреЗрдордХреА string. Scan рдЪреЗ рдирд┐рдХрд╛рд▓ ECR console рдордзреНрдпреЗ image рд╢реЗрдЬрд╛рд░реА рджрд┐рд╕рддрд╛рдд (repository рдзрдбрд╛ 06 рдордзреНрдпреЗ scanOnPush=true рд╕рд╣ рдмрдирд╡рд▓реА рд╣реЛрддреА).

ЁЯФБ рд╣реЗрдЪ CircleCI рдордзреНрдпреЗ
            IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${CIRCLE_SHA1}"
            docker build --build-arg APP_VERSION=${CIRCLE_SHA1:0:7} -t "$IMAGE" app
            docker run -d --rm -p 3000:3000 --name smoke "$IMAGE"
            sleep 2 && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
            docker push "$IMAGE"

рдПрдХрдЪ job build рдХрд░рддреЛ, aws ecr get-login-password рдиреЗ log in рдХрд░рддреЛ рдЖрдгрд┐ push рдХрд░рддреЛ, рдореНрд╣рдгреВрди рдкрд╛рдХрд┐рдЯрд╛рдЪреА рдЧрд░рдЬ рдирд╛рд╣реА. setup_remote_docker рд╕рд╣ container рд╡реЗрдЧрд│реНрдпрд╛ engine рд╡рд░ рдЪрд╛рд▓рддреЛ, рдореНрд╣рдгреВрдирдЪ smoke test curl localhost рдРрд╡рдЬреА docker exec smoke wget рд╡рд╛рдкрд░рддреЛ.

ЁЯжК рд╣реЗрдЪ GitLab CI рдордзреНрдпреЗ
    - IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_SHA}"
    - docker build --build-arg APP_VERSION=${CI_COMMIT_SHORT_SHA} -t "$IMAGE" app
    - docker run -d --rm -p 3000:3000 --name smoke "$IMAGE" && sleep 2 && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
    - docker push "$IMAGE"

CI_COMMIT_SHA рд╣рд╛ рдкреВрд░реНрдг SHA рдЖрд╣реЗ; GitLab рдЪрд╛ built-in CI_COMMIT_SHORT_SHA 8 рдЕрдХреНрд╖рд░рд╛рдВрдЪрд╛ рдЖрд╣реЗ, рдореНрд╣рдгреВрди рдпрд╛ рдмреЛрд▓реАрдд version рдЪреА рдУрд│ рдПрдХ рдЕрдХреНрд╖рд░рд╛рдиреЗ рд▓рд╛рдВрдм рджрд┐рд╕рддреЗ. Docker рд╕реНрд╡рддрдГ docker:27-dind service рдордзреВрди рдпреЗрддреЛ.

ЁЯОй рд╣реЗрдЪ Jenkins рдордзреНрдпреЗ
            IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${GIT_COMMIT}"
            docker build --build-arg APP_VERSION=$(echo "$GIT_COMMIT" | cut -c1-7) -t "$IMAGE" app   # POSIX sh: no ${VAR:0:7}
            docker run -d --rm -p 3000:3000 --name smoke "$IMAGE" && sleep 2 \
              && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
            docker push "$IMAGE"

GIT_COMMIT рд╣рд╛ Jenkins рдЪрд╛ рдкреВрд░реНрдг SHA рдЖрд╣реЗ. рд╕рдВрдкреВрд░реНрдг sh block рдзрдбрд╛ 06 рдордзрд▓реНрдпрд╛ withAWS(...) рдЪреНрдпрд╛ рдЖрдд рдмрд╕рддреЛ, рддреНрдпрд╛рдЪ рдкреНрд░рдХрд╛рд░реЗ log in рдХрд░рддреЛ, рдЖрдгрд┐ agent рдХрдбреЗ рд╕реНрд╡рддрдГрдЪрд╛ Docker CLI рдЕрд╕рд╛рд╡рд╛ рд▓рд╛рдЧрддреЛ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

SHA=$(git rev-parse HEAD)                       # 1) build exactly like CI, from your checkout тАФ no AWS needed
docker build --build-arg APP_VERSION=${SHA::7} -t hello-courier:${SHA} app

# 2) smoke-test the BOX тАФ the same lines as ship.yml
docker run -d --rm -p 3000:3000 --name smoke hello-courier:${SHA}
for i in $(seq 1 20); do curl -fsS localhost:3000/healthz && break; sleep 0.5; done
curl -fsS localhost:3000/ | grep -q "version ${SHA::7}" && echo "тЬЕ the box says ${SHA::7}"
docker stop smoke

# 3) the version is baked in, not guessed
docker image inspect hello-courier:${SHA} --format '{{json .Config.Env}}'   # тАж"APP_VERSION=abc1234"тАж

# 4) the same build ran in your fork's mailroom тАФ the build job needs no AWS
RUN=$(gh run list --workflow ship.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run view "$RUN"     # "build & smoke-test the image" тЬЕ ┬╖ "push to ECR" skipped without the lesson 06 variables

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

тПня╕П рдкреБрдвреЗ

рдкреНрд░рдд рддрд┐рдЪреНрдпрд╛ рдард╢рд╛рд╕рд╣ рд▓реЙрдХрд░рдордзреНрдпреЗ рдЖрд╣реЗ. рдореБрдЦреНрдп рдлрд▓рдХрд╛рд╡рд░ рдкреЛрд╣реЛрдЪрдгреНрдпрд╛рдЖрдзреА рддреА рдЖрдзреА рд╕рд░рд╛рд╡ рдлрд▓рдХрд╛рд╡рд░ рдЬрд╛рддреЗ тАФ рдЖрдгрд┐ рдкреНрд░рд╛рдЪрд╛рд░реНрдп рд╕рд╣реА рдХрд░рддрд╛рдд. ЁЯЫС

git checkout lesson-08-environments-gates

ЁЯУж Lesson 07 тАФ Build & push the image: the photocopier

ЁЯУН You are here: Lesson 07 of 12 ┬╖ Previous: lesson-06-secrets-oidc ┬╖ Next: lesson-08-environments-gates


ЁЯУж What's in this branch

Lessons 01тАУ06, plus the photocopier desk: the checked homework becomes a box, gets the commit's fingerprint, and is filed in the locker. Real files:

ЁЯзТ Explain like I'm 5

The checking desk тЬЕ stamped the homework. Now the photocopier ЁЯН▒ makes the copy that goes on the notice boards, and this desk has three habits.

Write the fingerprint on the copy while copying. A commit has a 40-character fingerprint, the SHA. Its first 7 characters go inside the box as APP_VERSION, so the running app can answer "which copy are you?" The full SHA goes on the label тАФ the image tag тАФ so the locker shelf answers the same question by name. Not latest, not main: those point at different bytes from week to week.

Look at the copy before filing it. The tests ran on the runner's Node; the box has its own Node, its own files, its own port. So the desk starts the box, asks /healthz until it answers, and checks that / prints the expected version. That is a smoke test of the box, not of the code.

Hand it over properly. The photocopier and the filing desk are different rooms, so the copy travels in the envelope from lesson 05; the filing desk opens the locker ЁЯПж with the day pass from lesson 06; the locker X-rays each new box (scan on push).

ЁЯЧ║я╕П Diagram

flowchart LR
    build["ЁЯН▒ docker build<br/>--build-arg APP_VERSION=abc1234"]
    smoke["ЁЯзк smoke-test the BOX<br/>curl /healthz, grep version abc1234"]
    env["ЁЯУО image.tar.gz<br/>docker save, then gzip"]
    push["ЁЯЪЪ push job, another machine<br/>docker load, ЁЯкк badge, ЁЯОл ECR login"]
    ecr["ЁЯПж ECR hello-courier:abc1234тАж<br/>tag = full SHA, scan on push"]
    build -->|"1 run it"| smoke
    smoke -->|"2 only if it answers"| env
    env -->|"3 download by name"| push
    push -->|"4 docker push"| ecr

тЭУ What

ЁЯза One fingerprint, inside and out

commit  abc1234ef56тАж  (40 chars тАФ git's fingerprint of the code)
   тФЬтФА INSIDE the box:  APP_VERSION=abc1234            (short SHA тАФ what the app prints)
   тФФтФА ON the label:    hello-courier:abc1234ef56тАж     (full SHA тАФ what you deploy by)

Tag vs digest. The commit-SHA tag tells us which source revision produced the image; the image digest (sha256:тАж, shown by docker image inspect and in the ECR console) identifies the exact immutable image bytes. Build the same commit twice and you get one tag but possibly two digests тАФ the tag is the address, the digest is the identity, as the Docker school's lesson 11 puts it. Lesson 12 pins base images by digest for the same reason.

ЁЯдФ Why

Without the version baked in, "which version is running?" is a guess. Without the smoke test, a Dockerfile that forgets a file passes the unit tests and dies at the first readiness probe. Without SHA tags, "put back yesterday's" is impossible, latest quietly lies, and GitOps commits later have nothing meaningful to change. The Docker school's lesson 12 ends on this exact handoff; its lesson 08 covers tag hygiene.

ЁЯФз How (in this repo)

Bake the fingerprint. app/Dockerfile takes it as a build argument and freezes it into the environment:

ARG APP_VERSION=dev                 # CI passes the commit SHA here (lesson 07)
ENV APP_VERSION=$APP_VERSION

Build and smoke-test the box. The build job in ship.yml тАФ ${GITHUB_SHA::7} is bash for "the first 7 characters"; the loop gives the box roughly ten seconds to start:

      - name: ЁЯН▒ docker build (version label = short SHA)
        run: docker build --build-arg APP_VERSION=${GITHUB_SHA::7} -t hello-courier:${GITHUB_SHA} app
      - name: ЁЯзк test the BOX, not just the code
        run: |
          docker run -d --rm -p 3000:3000 --name smoke hello-courier:${GITHUB_SHA}
          for i in $(seq 1 20); do curl -fsS localhost:3000/healthz && break; sleep 0.5; done
          curl -fsS localhost:3000/ | grep -q "version ${GITHUB_SHA::7}"
          docker stop smoke
      - name: ЁЯТ╛ keep the image for the push job (same run, different machine)
        run: docker save hello-courier:${GITHUB_SHA} | gzip > image.tar.gz

The grep -q line catches a stale or mislabeled build: if the box prints version dev, the job fails before anything is pushed.

Load, log in, tag, push. The push job lands on a fresh machine, so it starts from the envelope, shows the lesson 06 badge, then:

      - run: docker load < image.tar.gz
      - name: ЁЯОл log in to ECR
        id: ecr
        uses: aws-actions/amazon-ecr-login@v2
      - name: ЁЯП╖я╕П tag with the commit SHA and push
        id: meta
        env:
          IMAGE: ${{ steps.ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:${{ github.sha }}
        run: |
          docker tag hello-courier:${GITHUB_SHA} "$IMAGE"
          docker push "$IMAGE"
          echo "image=$IMAGE" >> "$GITHUB_OUTPUT"

amazon-ecr-login turns the day pass into a docker login and outputs the registry hostname, so IMAGE becomes 123456789012.dkr.ecr.ap-south-1.amazonaws.com/hello-courier:<full sha> тАФ exported as the job's image output, the exact string the delivery van receives in lesson 10. Scan results appear next to the image in the ECR console (the repository was created with scanOnPush=true in lesson 06).

ЁЯФБ The same thing in CircleCI
            IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${CIRCLE_SHA1}"
            docker build --build-arg APP_VERSION=${CIRCLE_SHA1:0:7} -t "$IMAGE" app
            docker run -d --rm -p 3000:3000 --name smoke "$IMAGE"
            sleep 2 && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
            docker push "$IMAGE"

One job builds, logs in with aws ecr get-login-password and pushes, so no envelope is needed. With setup_remote_docker the container runs on a separate engine, which is why the smoke test uses docker exec smoke wget rather than curl localhost.

ЁЯжК The same thing in GitLab CI
    - IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_SHA}"
    - docker build --build-arg APP_VERSION=${CI_COMMIT_SHORT_SHA} -t "$IMAGE" app
    - docker run -d --rm -p 3000:3000 --name smoke "$IMAGE" && sleep 2 && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
    - docker push "$IMAGE"

CI_COMMIT_SHA is the full SHA; GitLab's built-in CI_COMMIT_SHORT_SHA is 8 characters, so the version line reads one character longer in this dialect. Docker itself comes from the docker:27-dind service.

ЁЯОй The same thing in Jenkins
            IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${GIT_COMMIT}"
            docker build --build-arg APP_VERSION=$(echo "$GIT_COMMIT" | cut -c1-7) -t "$IMAGE" app   # POSIX sh: no ${VAR:0:7}
            docker run -d --rm -p 3000:3000 --name smoke "$IMAGE" && sleep 2 \
              && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
            docker push "$IMAGE"

GIT_COMMIT is Jenkins' full SHA. The whole sh block sits inside the withAWS(...) from lesson 06, logs in the same way, and the agent needs a Docker CLI of its own.

ЁЯзк Try it

SHA=$(git rev-parse HEAD)                       # 1) build exactly like CI, from your checkout тАФ no AWS needed
docker build --build-arg APP_VERSION=${SHA::7} -t hello-courier:${SHA} app

# 2) smoke-test the BOX тАФ the same lines as ship.yml
docker run -d --rm -p 3000:3000 --name smoke hello-courier:${SHA}
for i in $(seq 1 20); do curl -fsS localhost:3000/healthz && break; sleep 0.5; done
curl -fsS localhost:3000/ | grep -q "version ${SHA::7}" && echo "тЬЕ the box says ${SHA::7}"
docker stop smoke

# 3) the version is baked in, not guessed
docker image inspect hello-courier:${SHA} --format '{{json .Config.Env}}'   # тАж"APP_VERSION=abc1234"тАж

# 4) the same build ran in your fork's mailroom тАФ the build job needs no AWS
RUN=$(gh run list --workflow ship.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run view "$RUN"     # "build & smoke-test the image" тЬЕ ┬╖ "push to ECR" skipped without the lesson 06 variables

тЪая╕П Common mistakes

тПня╕П Next

The copy is in the locker with its fingerprint. Before it reaches the main board it goes on the practice board first тАФ and the principal signs. ЁЯЫС

git checkout lesson-08-environments-gates
тЖР Previoussecrets oidcNext тЖТenvironments gates

This page is the lesson's README from the lesson-07-build-push-image branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.