Deploying to AWS Lightsail with a Docker image from ECR

작성자

카테고리:

← 피드로
DEV Community · Shayan Araghi · 2026-07-27 개발(SW)

Shayan Araghi

Lightsail is a good home for a single small container: flat pricing, bandwidth included, and none of the VPC/security-group ceremony of EC2. The one rough edge is pulling a private image from Amazon ECR, because a standard Lightsail instance can’t authenticate to ECR the way EC2 can. This post walks the whole path.

The pipeline we’re building:

docker build ──push──> ECR (private repo) ──pull──> Lightsail instance ──run──> container

Enter fullscreen mode Exit fullscreen mode

What you’ll need

  • An AWS account and the AWS CLI installed locally.
  • Docker installed locally (to build) and on the Lightsail box (to run).
  • A Dockerfile that produces a runnable image. If you’re deploying a Next.js app, a standalone output image works well.

1. Create the ECR repository

ECR is a private Docker registry. Create one repository per image:

aws ecr create-repository \
  --repository-name project-name \
  --region us-east-1

Enter fullscreen mode Exit fullscreen mode

Note the repositoryUri in the output — it looks like:

<account-id>.dkr.ecr.us-east-1.amazonaws.com/project-name

Enter fullscreen mode Exit fullscreen mode

You’ll use that URI everywhere below. Export it to save typing:

export ECR_URI=<account-id>.dkr.ecr.us-east-1.amazonaws.com/project-name
export AWS_REGION=us-east-1

Enter fullscreen mode Exit fullscreen mode

2. Build the image locally

First, the Dockerfile. This is a multi-stage build for a Next.js app using output: "standalone" — the first stage installs dependencies and builds, the second copies only the traced runtime files into a slim image that runs as a non-root user:

FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

# Standalone output ships only the traced files needed to run the server.
# public and .next/static are not included by default and must be copied in.
# --chown makes the files writable by the non-root user so Next.js can write
# its runtime cache to /app/.next/cache.
COPY --from=builder --chown=node:node /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static

# Pre-create the cache dir owned by node; Next.js writes here at runtime.
RUN mkdir -p .next/cache && chown -R node:node .next

USER node
EXPOSE 3000
CMD ["node", "server.js"]

Enter fullscreen mode Exit fullscreen mode

This assumes next.config sets output: "standalone". Without it, the .next/standalone directory won’t exist and the COPY steps will fail.

Now build for the architecture your Lightsail instance runs. Most Lightsail plans are x86_64, so if you’re on an Apple Silicon Mac you must cross-build or the image won’t run:

docker build --platform linux/amd64 -t project-name .

Enter fullscreen mode Exit fullscreen mode

Tag it with the ECR URI so it can be pushed. A latest tag is all you strictly need:

docker tag project-name:latest ${ECR_URI}:latest

Enter fullscreen mode Exit fullscreen mode

3. Push to ECR

ECR uses short-lived tokens for docker login. The AWS CLI can fetch one and pipe it straight into Docker:

aws ecr get-login-password --region $AWS_REGION \
  | docker login --username AWS --password-stdin ${ECR_URI}

Enter fullscreen mode Exit fullscreen mode

Then push:

docker push ${ECR_URI}:latest

Enter fullscreen mode Exit fullscreen mode

Note the braces: ${ECR_URI}:latest, not $ECR_URI:latest. Depending on your shell, the bare form can mis-parse the :latest — you’ll see the tag swallowed in the output (e.g. a repo name ending in ...showcaseatest) and the push will fail with repository does not exist. ${ECR_URI} makes the variable boundary explicit and avoids it.

4. Create the Lightsail instance

Create an instance (Amazon Linux 2023 keeps the Docker install simple), SSH in, and install Docker:

sudo dnf install -y docker
sudo systemctl enable --now docker
sudo usermod -aG docker ec2-user
# log out and back in so the group change applies

Enter fullscreen mode Exit fullscreen mode

5. The wrinkle: authenticating Lightsail to a private ECR repo

On EC2 you’d attach an IAM role to the instance (an instance profile) and the AWS CLI would pick up credentials automatically from instance metadata — no keys on the box. Standard Lightsail instances don’t support instance-profile roles, so that clean path isn’t available.

The practical path is an IAM user with read-only ECR access, whose access keys live on the instance:

  1. In the IAM console, create a new IAM user (e.g. lightsail-ecr-pull).
  2. Attach the AWS-managed AmazonEC2ContainerRegistryReadOnly policy to the user.
  3. Create an access key for that user — you’ll get an access key ID and a secret. This is what the instance uses to authenticate.

Then configure the CLI on the instance with that user’s keys:

aws configure   # paste the pull-only user's access key + secret

Enter fullscreen mode Exit fullscreen mode

The takeaway from the earlier question stands: on Lightsail you can’t fully eliminate the IAM user the way an EC2 instance role does — you can only keep what it’s allowed to do small. AmazonEC2ContainerRegistryReadOnly limits it to pulling images, so if the keys leak that’s the whole blast radius. Rotate them periodically.

6. Pull and run on the instance

Log Docker in to ECR (same token dance as the push, run on the instance):

aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin \
    <account-id>.dkr.ecr.us-east-1.amazonaws.com

Enter fullscreen mode Exit fullscreen mode

Pull and run:

docker pull <account-id>.dkr.ecr.us-east-1.amazonaws.com/project-name:latest

docker run -d \
  --name web \
  --restart unless-stopped \
  -p 80:3000 \
  <account-id>.dkr.ecr.us-east-1.amazonaws.com/project-name:latest

Enter fullscreen mode Exit fullscreen mode

One more step before this is reachable: open the port in Lightsail’s firewall. Lightsail instances have their own IPv4 firewall that only allows SSH (22) and HTTPS (443) by default — port 80 is closed, so the container above is running but unreachable. In the console, go to the instance’s Networking tab and, under IPv4 Firewall, add a rule for HTTP / TCP / 80. (This is separate from any OS-level firewall; the Lightsail rule is the one that bites first.)

Now open the instance’s public IP in a browser — you should see the app.

The ECR login token expires after 12 hours. That only affects pulling, so it’s a non-issue for a running container. When you deploy a new version, just re-run the get-login-password | docker login step first.

7. Give it a static IP

One thing is still missing before this is a real deployment: the instance’s public IP changes on reboot. Lightsail’s default public IP is dynamic — stop/start the instance and it changes, breaking any DNS record pointing at it. In the console, go to Networking → Create static IP, attach it to your instance, then point your domain’s A record at that static IP. A static IP is free while it’s attached to a running instance (you’re only billed if you reserve one and leave it unattached), so there’s no reason not to.

8. Deploying a new version

The update loop is: build → push → pull → replace.

# locally
docker build --platform linux/amd64 -t ${ECR_URI}:latest .
docker push ${ECR_URI}:latest

# on the instance
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
docker pull <account-id>.dkr.ecr.us-east-1.amazonaws.com/project-name:latest
docker stop web && docker rm web
docker run -d --name web --restart unless-stopped -p 80:3000 <account-id>.dkr.ecr.us-east-1.amazonaws.com/project-name:latest

Enter fullscreen mode Exit fullscreen mode

Is this worth it over building on the box?

Pushing through ECR shines when you want the build to happen off the server — in CI, on your laptop, anywhere with more resources than a small instance — and the instance only ever pulls a finished artifact. If instead you’re happy to git clone and docker build on the box, you skip ECR (and the IAM user) entirely. Pick based on where you want the build to live, not on hosting alone.

There’s a cost angle too: the cheapest Lightsail tiers (the 512 MB / 1 vCPU plans) often can’t run docker build at all — a Next.js build will exhaust the RAM and get OOM-killed mid-build. Building elsewhere and pulling a finished image from ECR sidesteps that entirely: pulling and running a prebuilt image is far lighter than compiling one, so ECR is what lets you host on these less-expensive instances instead of paying for a bigger box just to survive the build.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다