<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shayan Araghi</title>
    <description>The latest articles on DEV Community by Shayan Araghi (@shayan-araghi).</description>
    <link>https://dev.to/shayan-araghi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4046161%2F39d3350c-f9f8-4763-b8cf-7cbfb3b0aca0.png</url>
      <title>DEV Community: Shayan Araghi</title>
      <link>https://dev.to/shayan-araghi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shayan-araghi"/>
    <language>en</language>
    <item>
      <title>Deploying to AWS Lightsail with a Docker image from ECR</title>
      <dc:creator>Shayan Araghi</dc:creator>
      <pubDate>Mon, 27 Jul 2026 00:10:19 +0000</pubDate>
      <link>https://dev.to/shayan-araghi/deploying-to-aws-lightsail-with-a-docker-image-from-ecr-55bn</link>
      <guid>https://dev.to/shayan-araghi/deploying-to-aws-lightsail-with-a-docker-image-from-ecr-55bn</guid>
      <description>&lt;p&gt;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 &lt;strong&gt;private&lt;/strong&gt; image from &lt;a href="https://aws.amazon.com/ecr/" rel="noopener noreferrer"&gt;Amazon ECR&lt;/a&gt;, because a standard Lightsail instance can't authenticate to ECR the way EC2 can. This post walks the whole path.&lt;/p&gt;

&lt;p&gt;The pipeline we're building:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build ──push──&amp;gt; ECR (private repo) ──pull──&amp;gt; Lightsail instance ──run──&amp;gt; container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account and the &lt;a href="https://docs.aws.amazon.com/cli/" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; installed locally.&lt;/li&gt;
&lt;li&gt;Docker installed locally (to build) and on the Lightsail box (to run).&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Dockerfile&lt;/code&gt; that produces a runnable image. If you're deploying a Next.js app, a &lt;code&gt;standalone&lt;/code&gt; output image works well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Create the ECR repository
&lt;/h2&gt;

&lt;p&gt;ECR is a private Docker registry. Create one repository per image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr create-repository &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-name&lt;/span&gt; project-name &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;repositoryUri&lt;/code&gt; in the output — it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/project-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll use that URI everywhere below. Export it to save typing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ECR_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/project-name
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Build the image locally
&lt;/h2&gt;

&lt;p&gt;First, the &lt;code&gt;Dockerfile&lt;/code&gt;. This is a multi-stage build for a Next.js app using &lt;code&gt;output: "standalone"&lt;/code&gt; — 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:24-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:24-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT=3000&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; HOSTNAME=0.0.0.0&lt;/span&gt;

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

&lt;span class="c"&gt;# Pre-create the cache dir owned by node; Next.js writes here at runtime.&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .next/cache &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; node:node .next

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This assumes &lt;code&gt;next.config&lt;/code&gt; sets &lt;code&gt;output: "standalone"&lt;/code&gt;. Without it, the &lt;code&gt;.next/standalone&lt;/code&gt; directory won't exist and the &lt;code&gt;COPY&lt;/code&gt; steps will fail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now build for the architecture your Lightsail instance runs. Most Lightsail plans are &lt;strong&gt;x86_64&lt;/strong&gt;, so if you're on an Apple Silicon Mac you must cross-build or the image won't run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; project-name &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tag it with the ECR URI so it can be pushed. A &lt;code&gt;latest&lt;/code&gt; tag is all you strictly need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker tag project-name:latest &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ECR_URI&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Push to ECR
&lt;/h2&gt;

&lt;p&gt;ECR uses short-lived tokens for &lt;code&gt;docker login&lt;/code&gt;. The AWS CLI can fetch one and pipe it straight into Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ECR_URI&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker push &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ECR_URI&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  4. Create the Lightsail instance
&lt;/h2&gt;

&lt;p&gt;Create an instance (Amazon Linux 2023 keeps the Docker install simple), SSH in, and install Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker ec2-user
&lt;span class="c"&gt;# log out and back in so the group change applies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. The wrinkle: authenticating Lightsail to a private ECR repo
&lt;/h2&gt;

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

&lt;p&gt;The practical path is an IAM &lt;strong&gt;user&lt;/strong&gt; with read-only ECR access, whose access keys live on the instance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the IAM console, &lt;strong&gt;create a new IAM user&lt;/strong&gt; (e.g. &lt;code&gt;lightsail-ecr-pull&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Attach the AWS-managed &lt;strong&gt;&lt;code&gt;AmazonEC2ContainerRegistryReadOnly&lt;/code&gt;&lt;/strong&gt; policy to the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create an access key&lt;/strong&gt; for that user — you'll get an access key ID and a secret. This is what the instance uses to authenticate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then configure the CLI on the instance with that user's keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure   &lt;span class="c"&gt;# paste the pull-only user's access key + secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  6. Pull and run on the instance
&lt;/h2&gt;

&lt;p&gt;Log Docker in to ECR (same token dance as the push, run on the instance):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull &amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/project-name:latest

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; web &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 80:3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/project-name:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Now open the instance's public IP in a browser — you should see the app.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The ECR login token expires after 12 hours. That only affects &lt;strong&gt;pulling&lt;/strong&gt;, so it's a non-issue for a running container. When you deploy a new version, just re-run the &lt;code&gt;get-login-password | docker login&lt;/code&gt; step first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  7. Give it a static IP
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;Networking → Create static IP&lt;/strong&gt;, attach it to your instance, then point your domain's &lt;strong&gt;A record&lt;/strong&gt; at that static IP. A static IP is &lt;strong&gt;free while it's attached&lt;/strong&gt; to a running instance (you're only billed if you reserve one and leave it unattached), so there's no reason not to.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Deploying a new version
&lt;/h2&gt;

&lt;p&gt;The update loop is: build → push → pull → replace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# locally&lt;/span&gt;
docker build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ECR_URI&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:latest &lt;span class="nb"&gt;.&lt;/span&gt;
docker push &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ECR_URI&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:latest

&lt;span class="c"&gt;# on the instance&lt;/span&gt;
aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; &amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com
docker pull &amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/project-name:latest
docker stop web &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;rm &lt;/span&gt;web
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; web &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="nt"&gt;-p&lt;/span&gt; 80:3000 &amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/project-name:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Is this worth it over building on the box?
&lt;/h2&gt;

&lt;p&gt;Pushing through ECR shines when you want the &lt;strong&gt;build to happen off the server&lt;/strong&gt; — 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 &lt;code&gt;git clone&lt;/code&gt; and &lt;code&gt;docker build&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;There's a cost angle too: the cheapest Lightsail tiers (the 512 MB / 1 vCPU plans) often &lt;strong&gt;can't run &lt;code&gt;docker build&lt;/code&gt; at all&lt;/strong&gt; — 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.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>docker</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
