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
Dockerfilethat produces a runnable image. If you’re deploying a Next.js app, astandaloneoutput 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.configsetsoutput: "standalone". Without it, the.next/standalonedirectory won’t exist and theCOPYsteps 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 withrepository 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:
- In the IAM console, create a new IAM user (e.g.
lightsail-ecr-pull). - Attach the AWS-managed
AmazonEC2ContainerRegistryReadOnlypolicy to the user. - 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.
AmazonEC2ContainerRegistryReadOnlylimits 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 loginstep 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.
답글 남기기