site logo

Marico's space

将 Docker 镜像从 ECR 部署到 AWS Lightsail

服务器技术 2026-07-27 17:34:57 9

Lightsail 挺适合托管单个小型容器的:定价简单、带宽包含在内、不用折腾 VPC 和安全组那些破事。唯一的坑是,从亚马逊 ECR(弹性容器注册表)拉取私有镜像这件事上,标准 Lightsail 实例不像 EC2 那样能直接认证。这篇把完整流程捋一遍。

整体链路是这样的:

docker build ──push──> ECR (私有仓库) ──pull──> Lightsail 实例 ──run──> 容器

前置条件

  • 一个 AWS 账号,本地安装好 AWS CLI
  • 本地安装 Docker(用来构建),服务器上也要装(用来运行)
  • 一个能跑通的 Dockerfile。如果部署的是 Next.js 应用,standalone 输出的镜像比较合适

1. 创建 ECR 仓库

ECR 就是私有的 Docker 仓库,一个镜像对应一个仓库:

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

输出里有个 repositoryUri,格式类似:

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

下面会反复用到这个 URI,先导成环境变量省得老敲:

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

2. 本地构建镜像

先看 Dockerfile。这是一个 Next.js 应用的 multi-stage 构建,第一阶段装依赖、跑构建,第二阶段只复制追踪后的运行时文件,生成一个精简镜像,以非 root 用户运行:

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 只打包运行 server 需要的追踪文件。
# public 和 .next/static 默认不包含,必须手动复制进来。
# --chown 让非 root 用户可写,这样 Next.js 才能往 /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 # 预先创建缓存目录并设置好权限,Next.js 运行时需要往这里写。
RUN mkdir -p .next/cache && chown -R node:node .next USER node
EXPOSE 3000
CMD ["node", "server.js"]

前提是 next.config 里设置了 output: "standalone"。没设置的话 .next/standalone 目录不存在,后面的 COPY 步骤会报错。

然后按照服务器架构构建镜像。多数 Lightsail 实例是 x86_64 架构,如果你在 M 系列 Mac 上开发,必须指定平台交叉编译,不然镜像在服务器上跑不起来:

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

给镜像打上 ECR 的标签,准备推送。latest 标签够用了:

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

3. 推送到 ECR

ECR 用短效 token 做 docker login,AWS CLI 可以直接拉取然后 pipe 给 Docker:

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

然后 push:

docker push ${ECR_URI}:latest

注意花括号的写法:${ECR_URI}:latest,不是 $ECR_URI:latest。某些 shell 里裸变量形式会把 :latest 吞掉,看到的输出会是类似 ...showcaseatest 这种鬼样子,然后 push 报 repository does not exist。用 ${ECR_URI} 明确变量边界就能避开。

4. 创建 Lightsail 实例

创建一个实例(Amazon Linux 2023 装 Docker 比较省事),SSH 进去装 Docker:

sudo dnf install -y docker
sudo systemctl enable --now docker
sudo usermod -aG docker ec2-user
# 登出再登入,让用户组权限生效

5. 难点:Lightsail 访问私有 ECR 仓库的认证问题

EC2 上,你可以给实例挂一个 IAM 角色(实例配置文件),AWS CLI 自动从实例元数据服务拿凭证,服务器上不用存任何密钥。标准 Lightsail 实例不支持实例配置文件角色,所以这套干净的方案用不了。

实际做法是建一个只有 ECR 只读权限的 IAM 用户,把访问密钥放到服务器上:

  1. 在 IAM 控制台创建新用户(比如 lightsail-ecr-pull
  2. 附加 AWS 托管策略 AmazonEC2ContainerRegistryReadOnly
  3. 创建访问密钥,会拿到 access key ID 和 secret。这个就是实例用来认证的凭证

然后在实例上用这个用户的密钥配置 CLI:

aws configure # 填入那个只读用户的 access key + secret

总结一下:Lightsail 上没法像 EC2 实例角色那样彻底省掉 IAM 用户,只能尽量缩小它的权限范围。AmazonEC2ContainerRegistryReadOnly 只允许拉取镜像,所以万一密钥泄露影响面也有限。定期轮换一下更稳妥。

6. 在实例上拉取并运行

先登录 ECR(跟推送时一样的 token 流程,在实例上执行):

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

别忘了还要在 Lightsail 的防火墙里开放端口。Lightsail 实例有自己的 IPv4 防火墙,默认只开放 SSH(22) 和 HTTPS(443),80 端口是关着的。所以上面的容器虽然在跑,但外部根本访问不到。去控制台实例的 Networking 标签页,在 IPv4 Firewall 下加一条 HTTP / TCP / 80 的规则。(这是独立于系统层防火墙的,Lightsail 这层不过的话后面根本到不了。)

现在用实例公网 IP 访问一下浏览器,应该能看到应用跑起来了。

ECR 登录 token 12 小时过期。不过这只影响拉取操作,运行中的容器不受影响。发布新版本之前重新跑一遍 get-login-password | docker login 就行。

7. 挂个静态 IP

还有一个问题:实例重启后公网 IP 会变。Lightsail 默认给的公网 IP 是动态的,stop 再 start 就变了,你域名的 A 记录就失效了。去控制台 Networking → Create static IP,挂到你的实例上,然后把域名的 A 记录 指过去。静态 IP挂载在运行中的实例上是免费的(只有预留了但没挂才计费),没必要省这点。

8. 发布新版本

更新流程就是:构建 → 推送 → 拉取 → 替换。

# 本地
docker build --platform linux/amd64 -t ${ECR_URI}:latest .
docker push ${ECR_URI}:latest # 在实例上
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

这种方案 vs 直接在服务器上构建,值不值?

走 ECR 推送的核心价值在于构建不在服务器上发生——可以在 CI、本地开发机,或者任何比小实例资源更充足的机器上构建,服务器只负责拉取一个成品。如果你能接受直接在服务器上 git clone 然后 docker build,那 ECR(和 IAM 用户)都可以省掉。选哪个取决于你想让构建发生在哪里,不只是托管在哪。

还有个成本因素:最便宜的 Lightsail 套餐(512MB / 1vCPU)往往跑不动 docker build——Next.js 构建直接 OOM 被 kill 掉。在别处构建好、从 ECR 拉取成品镜像就完全绕开了这个问题:拉取和运行预构建镜像比现场编译轻量多了,这就是 ECR 能让你用这些便宜实例跑的原因,不用为了撑过构建阶段而升配。