DevConda — blog-workspace

Push a Spring Boot Docker Image to Amazon ECR

Part 1 system architecture at a glance

When should you read this?

You already deploy a Spring Boot jar to one EC2. That still works. It gets awkward when another runtime (Fargate, another region) or a rollback by version should run the same build without another scp of a jar. Package the app as a Docker image and store it in Amazon ECR (AWS storage for container images). Later Fargate or a host pulls that tagged image and runs it.

Part 1 of 4 (ECR -> Fargate -> ALB HTTPS -> Actions). This post ends when a tagged image is in Amazon ECR. Next (part 2): run $REPO:1.0.0 on ECS Fargate — planned slug run-spring-boot-on-ecs-fargate.

It follows the one-box EC2 arc (Nginx + Spring, Actions to EC2, health gate). Those posts ship a jar to one host. Here the next step is build a container image and put it in ECR.

What you are building

Spring Boot repo
  -> mvn/gradle package  ->  app.jar
  -> Dockerfile          ->  local image
  -> docker push         ->  ECR repository (private)
Image ref: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/api:1.0.0
           \________ registry host ________/\__/ \___/
              account + region                 repo  tag

Keep account, region, and repository name stable so part 2 can reuse the same image reference. The first figure shows the jar-to-ECR pipeline; Fargate, ALB, and Actions are later parts in this series.

Part 1: system architecture (request flow) – Spring Boot / docker / ECR / IAM

The second figure contrasts jar-to-one-EC2 delivery with a versioned image stored in ECR that any runtime can pull.

Dockerfile for the jar

Build the jar the way CI already does (mvn -B -DskipTests package or ./gradlew bootJar). Put the Dockerfile where that output lives.

Before COPY . . in a multi-stage build, add a .dockerignore so Docker does not ship .git, old target/ or build/ output, or IDE folders into the build context:

.git
target
build
.idea
*.iml

That keeps docker build fast and avoids stale jars in the image.

Spring Boot often writes more than one jar (for example *-plain.jar). Copy only the executable jar and name it app.jar in the build stage. If several non-plain jars appear, fix the build so one boot jar is produced before relying on head -n 1.

Maven multi-stage:

FROM eclipse-temurin:21-jdk AS build
WORKDIR /workspace
COPY . .
RUN ./mvnw -B -DskipTests package \
  && JAR=$(ls target/*.jar | grep -v plain | head -n 1) \
  && cp "$JAR" app.jar

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /workspace/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]

Gradle multi-stage (same pattern; output under build/libs/):

FROM eclipse-temurin:21-jdk AS build
WORKDIR /workspace
COPY . .
RUN ./gradlew bootJar -x test \
  && JAR=$(ls build/libs/*.jar | grep -v plain | head -n 1) \
  && cp "$JAR" app.jar

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /workspace/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]

If the jar is already on disk as target/app.jar or build/libs/app.jar, a single stage is enough:

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]

The process inside the container must listen on 8080 — the same port Nginx proxies to on EC2. Use the default server.port or set server.port=8080 in config; the mapped host port in docker run must match what the app binds inside the container.

The container layout figure shows the JRE base layer, app.jar, and the 8080 entrypoint.

Spring Boot container layout on port 8080
Spring Boot container layout on port 8080

Create the ECR repository

Pick a region (example ap-northeast-2) and a short repository name (api):

# Create a private ECR repository named api (scan on every push)
aws ecr create-repository \
  --repository-name api \
  --region ap-northeast-2 \
  --image-scanning-configuration scanOnPush=true

If it already exists, RepositoryAlreadyExistsException is fine – reuse it. Save repositoryUri from the response. That value is registry host + repository name (no tag). The tag is added on docker tag / docker push (.../api:1.0.0). The URI figure labels account, region, repository, and tag.

ECR image URI: registry host, repository, tag
ECR image URI: registry host, repository, tag

IAM: ecr:CreateRepository once to create. To push: ecr:GetAuthorizationToken (on *), plus on the repository ecr:BatchCheckLayerAvailability, ecr:PutImage, ecr:InitiateLayerUpload, ecr:UploadLayerPart, ecr:CompleteLayerUpload (and commonly ecr:BatchGetImage). To run describe-images later: ecr:DescribeImages.

Build, tag, push

Log in to the registry for that region (authorization token lasts 12 hours):

# Read the current AWS account ID from STS
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-northeast-2
# ECR repo URI without tag (registry host + /api)
REPO=$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/api

# Get a 12-hour ECR auth token and pipe it into docker login
aws ecr get-login-password --region $REGION \
  | docker login --username AWS --password-stdin $ACCOUNT.dkr.ecr.$REGION.amazonaws.com

Cannot perform an interactive login from a non-TTY device usually means the password pipe broke – rerun get-login-password and confirm Docker is running.

Build with a version tag, not only latest, then smoke-test locally before push:

docker build -t api:1.0.0 .
docker run --rm -p 8080:8080 api:1.0.0

In another shell:

curl -sf http://127.0.0.1:8080/actuator/health || curl -sf http://127.0.0.1:8080/

If Actuator is not enabled, the root URL or any known health endpoint is enough — confirm the container answers before you upload layers to ECR.

Tag and push:

docker tag api:1.0.0 $REPO:1.0.0
docker push $REPO:1.0.0

denied: Your authorization token has expired – log in again. no basic auth credentials – login never succeeded for that registry host. Wrong account or region in $REPO sends the image to the wrong place; match sts get-caller-identity and the ECR console. The push-path figure shows build, tag, push, and registry login.

Build, tag, and push to ECR
Build, tag, and push to ECR

Confirm the image is in ECR

# List images in repo api (tags, push time, size) to confirm 1.0.0 landed
aws ecr describe-images \
  --repository-name api \
  --region ap-northeast-2 \
  --query 'imageDetails[].{tags:imageTags,pushed:imagePushedAt,size:imageSizeInBytes}' \
  --output table

Tag 1.0.0 should appear with a fresh pushed time. The console Images tab should show the same digest. If scanOnPush is on, findings arrive shortly after; fix critical base-image issues before part 2. The verify figure shows CLI describe-images and the console Images tab agreeing on digest.

Verify the image tag in ECR
Verify the image tag in ECR

Next in series

Part 2 runs $REPO:1.0.0 on ECS Fargate in the same account and region: Run Spring Boot on ECS Fargate. Use that exact tag string in the first task definition; do not depend on floating latest.

Minimal checklist

  1. Executable fat jar builds; docker run answers on 8080.
  2. ECR repository api exists in the target region.
  3. docker login to that account’s ECR registry host succeeds.
  4. docker push .../api:1.0.0 completes without auth errors.
  5. describe-images lists tag 1.0.0.

After that, ECR holds the image Fargate will pull. The existing EC2 jar deploy can stay until part 2.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *