<?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%2F35a6b40f-5d5c-4ffc-b7ef-1cecef605c2b.jpg</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>Research, Plan, Implement: A Workflow That Keeps AI Agents Accurate</title>
      <dc:creator>Shayan Araghi</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/shayan-araghi/research-plan-implement-a-workflow-that-keeps-ai-agents-accurate-npb</link>
      <guid>https://dev.to/shayan-araghi/research-plan-implement-a-workflow-that-keeps-ai-agents-accurate-npb</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Context Rot
&lt;/h2&gt;

&lt;p&gt;Have you ever had to stop an AI agent halfway through a task to correct it? Work with AI agents long enough and you'll see a pattern: the longer a session runs, the worse the output gets.&lt;/p&gt;

&lt;p&gt;Every input you give the agent and every output it produces gets appended to the context window. Nothing leaves. By the time you're fifty messages deep, the agent is re-reading abandoned approaches, stale file contents, and corrections you made an hour ago.&lt;/p&gt;

&lt;p&gt;The fix isn't a better prompt. It's less context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Rule
&lt;/h2&gt;

&lt;p&gt;Keep the context window small. Two habits will keep your AI agent from hallucinating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Delegate to subagents.&lt;/strong&gt; Subagents do the heavy reading in their own context and return only the summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear between phases.&lt;/strong&gt; Once a phase produces a file, you no longer need the context that led to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I aim to stay under 40% context usage in the main agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Research → Plan → Implement
&lt;/h2&gt;

&lt;p&gt;I picked up this workflow from a &lt;a href="https://www.youtube.com/watch?v=rmvDxxNubIg" rel="noopener noreferrer"&gt;HumanLayer talk&lt;/a&gt;, and it's the most reliable setup I've used. There are three phases, each ending in a markdown file, with a context clear between each.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Research&lt;/strong&gt; — the agent writes a research doc, then clears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; — the agent writes a plan doc, then clears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement&lt;/strong&gt; — the agent executes the plan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The main agent never needs to remember the previous phase, because the previous phase wrote it down. All it needs is the conclusion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Research
&lt;/h3&gt;

&lt;p&gt;The research phase answers how something works today. For example: &lt;code&gt;Describe how the payments flow works end to end. Look carefully at the API endpoint implementations.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The main agent spins up parallel subagents to figure it out. From &lt;a href="https://github.com/humanlayer/humanlayer/tree/main/.claude" rel="noopener noreferrer"&gt;HumanLayer's repo&lt;/a&gt;, I found three subagents to be the most useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;codebase-locator&lt;/code&gt; — finds where things live&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;codebase-analyzer&lt;/code&gt; — explains how a component works&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;codebase-pattern-finder&lt;/code&gt; — finds existing patterns to model the new work after&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part about using subagents is that you can point them at a cheaper model. Mine run Sonnet while the orchestrator runs Opus.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plan
&lt;/h3&gt;

&lt;p&gt;The plan phase creates the exact steps needed to build the feature. It lists which files to touch, which lines, and what exactly needs to change. It runs &lt;code&gt;codebase-locator&lt;/code&gt; and &lt;code&gt;codebase-analyzer&lt;/code&gt; in parallel, then writes the plan.&lt;/p&gt;

&lt;p&gt;You can pass in an existing research doc. The agent will reference it but still verify it, in case the code has drifted since it was written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plans are only as good as the requirements you give them.&lt;/strong&gt; Agents don't have the full context, so they fill the gaps by assuming what you need. I've found those assumptions rarely match what the app actually needs. Write out the edge cases and the exact behavior you want.&lt;/p&gt;

&lt;p&gt;I'd also add a fourth subagent: &lt;code&gt;context-locator&lt;/code&gt;, which finds prior research relevant to your task. My research docs overlap a lot, and pulling in older ones produces noticeably better plans.&lt;/p&gt;

&lt;h4&gt;
  
  
  What a plan doc looks like
&lt;/h4&gt;

&lt;p&gt;A real plan runs long — usually a few hundred lines across several phases. Here's the shape of one, for a made-up feature: adding rate limiting to an API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Rate Limiting Implementation Plan&lt;/span&gt;

&lt;span class="gu"&gt;## Current State Analysis&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**All routes are unthrottled**&lt;/span&gt; — &lt;span class="sb"&gt;`src/api/router.ts:34-58`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Redis is already available**&lt;/span&gt; for session storage, so no new infra is needed
  — &lt;span class="sb"&gt;`src/lib/redis.ts:12`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Auth middleware runs before routing**&lt;/span&gt;, which is where a limiter would slot
  in — &lt;span class="sb"&gt;`src/middleware/auth.ts:20-45`&lt;/span&gt;

&lt;span class="gs"&gt;**Key gaps:**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; No rate-limiting library installed (verified: absent from &lt;span class="sb"&gt;`package.json`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; No per-user identifier available on unauthenticated routes

&lt;span class="gu"&gt;## Desired End State&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Authenticated requests are limited to 100/min per user; unauthenticated to
  20/min per IP.
&lt;span class="p"&gt;-&lt;/span&gt; Exceeding the limit returns &lt;span class="sb"&gt;`429`&lt;/span&gt; with a &lt;span class="sb"&gt;`Retry-After`&lt;/span&gt; header.
&lt;span class="p"&gt;-&lt;/span&gt; Limits are configurable per route without code changes.

&lt;span class="gu"&gt;### Key Discoveries&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; The existing Redis client is created per-request, which will not work for a
  shared counter — it needs a singleton — &lt;span class="sb"&gt;`src/lib/redis.ts:12-19`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Health-check endpoints must stay unthrottled or the load balancer will mark
  instances unhealthy — &lt;span class="sb"&gt;`deploy/lb-config.yaml:22`&lt;/span&gt;

&lt;span class="gu"&gt;## What We're NOT Doing&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; No distributed quota syncing across regions.
&lt;span class="p"&gt;-&lt;/span&gt; No admin UI for adjusting limits (config file only).
&lt;span class="p"&gt;-&lt;/span&gt; No billing-tier-based limits — that's a follow-up.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## Phase 1: Shared Redis Client&lt;/span&gt;

&lt;span class="gu"&gt;### Changes Required&lt;/span&gt;

&lt;span class="gu"&gt;#### 1. Convert the Redis client to a singleton&lt;/span&gt;
&lt;span class="gs"&gt;**File**&lt;/span&gt;: &lt;span class="sb"&gt;`src/lib/redis.ts`&lt;/span&gt;
&lt;span class="gs"&gt;**Changes**&lt;/span&gt;: Export one shared connection instead of constructing per request.

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;ts
&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RedisClient&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRedis&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;RedisClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;

&lt;span class="gu"&gt;#### 2. Update existing call sites&lt;/span&gt;
&lt;span class="gs"&gt;**Files**&lt;/span&gt;: &lt;span class="sb"&gt;`src/middleware/auth.ts:28`&lt;/span&gt;, &lt;span class="sb"&gt;`src/api/session.ts:15`&lt;/span&gt;
&lt;span class="gs"&gt;**Changes**&lt;/span&gt;: Replace &lt;span class="sb"&gt;`new RedisClient(...)`&lt;/span&gt; with &lt;span class="sb"&gt;`getRedis()`&lt;/span&gt;.

&lt;span class="gu"&gt;### Success Criteria&lt;/span&gt;

&lt;span class="gu"&gt;#### Automated Verification:&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Unit tests pass: &lt;span class="sb"&gt;`npm test`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Type check passes: &lt;span class="sb"&gt;`npm run typecheck`&lt;/span&gt;

&lt;span class="gu"&gt;#### Manual Verification:&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Sessions still persist across requests after the singleton change.

&lt;span class="gs"&gt;**Implementation Note**&lt;/span&gt;: Pause after Phase 1 for confirmation — this touches
session handling, so a regression here breaks login.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things in there matter more than they look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File paths carry line numbers.&lt;/strong&gt; &lt;code&gt;src/api/router.ts:34-58&lt;/code&gt; means the agent actually read the file instead of guessing at its shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gaps are verified, not assumed.&lt;/strong&gt; "verified: absent from &lt;code&gt;package.json&lt;/code&gt;" tells you it checked rather than inferred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Discoveries catch the things that break you.&lt;/strong&gt; The per-request Redis client and the health-check exemption are exactly the details you'd discover painfully in review — or in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What We're NOT Doing" is the scope fence.&lt;/strong&gt; This is the section that stops an agent from cheerfully building an admin UI you never asked for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Success criteria split automated from manual.&lt;/strong&gt; The agent can run the first list itself and knows to hand you the second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pause points are explicit.&lt;/strong&gt; Risky phases say so and stop for confirmation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Implement
&lt;/h3&gt;

&lt;p&gt;The implement phase reads the plan in full before touching anything. If it can't see how the pieces fit together, it should stop and validate rather than guess. It then executes the plan, checking in when something is ambiguous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the Files. Actually Read Them.
&lt;/h2&gt;

&lt;p&gt;This is the step people skip.&lt;/p&gt;

&lt;p&gt;I've lost count of the research and plan docs that assumed the wrong thing. Catching those errors early saves you the bug fixes you'd otherwise do later.&lt;/p&gt;

&lt;p&gt;It's the same rule as any development cycle: catch errors early. Read every research and plan file before moving to the next phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Skip the Research Phase
&lt;/h2&gt;

&lt;p&gt;The instinct is to always run research → plan. That's not always the best process.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small feature?&lt;/strong&gt; Skip the research and write the plan. A research doc just gives &lt;code&gt;context-locator&lt;/code&gt; more to look through and burns tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to understand an unfamiliar area?&lt;/strong&gt; Start with research. Future features in that area can reuse the doc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large feature?&lt;/strong&gt; Write a research doc per area of your app — how payments work, what the end-to-end user flow looks like, and so on. Then a task like &lt;code&gt;Add a new payment method&lt;/code&gt; already knows where to look.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to Store the Files
&lt;/h2&gt;

&lt;p&gt;For a monorepo, I use &lt;code&gt;thoughts/shared/&lt;/code&gt; with &lt;code&gt;research/&lt;/code&gt; and &lt;code&gt;plans/&lt;/code&gt; subfolders, and I commit every file I create to keep the context alongside the code.&lt;/p&gt;

&lt;p&gt;The exception is large teams, where a lot of people may be committing their files. In that case, I'd keep them local and share when needed.&lt;/p&gt;

&lt;p&gt;Let me know how this works for you!&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/humanlayer/humanlayer/tree/main/.claude" rel="noopener noreferrer"&gt;HumanLayer GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=rmvDxxNubIg" rel="noopener noreferrer"&gt;No Vibes Allowed: Solving Hard Problems in Complex Codebases — Dex Horthy, HumanLayer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>agents</category>
    </item>
    <item>
      <title>A Second Brain Your AI Agent Can Read: Obsidian + Claude</title>
      <dc:creator>Shayan Araghi</dc:creator>
      <pubDate>Sat, 08 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/shayan-araghi/a-second-brain-your-ai-agent-can-read-4kf6</link>
      <guid>https://dev.to/shayan-araghi/a-second-brain-your-ai-agent-can-read-4kf6</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;There are so many details we need to remember in life. Recently I've been&lt;br&gt;
working on a legacy project that was written years ago with no maintainers left.&lt;br&gt;
I've had to continuously dig through thousands of lines of my own notes just to&lt;br&gt;
remember how a particular piece of the application works. Sifting through notes&lt;br&gt;
to find one detail takes time I'd rather spend architecting and building.&lt;/p&gt;

&lt;p&gt;AI agents change that. They make it much easier to store and retrieve&lt;br&gt;
information you wrote down days, months, or years ago.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is Obsidian
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://obsidian.md/" rel="noopener noreferrer"&gt;Obsidian&lt;/a&gt; is a note-taking app that stores everything as&lt;br&gt;
plain markdown files. It lets you organize and structure your notes in a&lt;br&gt;
flexible way, and because the files are plain text on disk, anything that can&lt;br&gt;
read a directory can read your notes, including an AI agent.&lt;/p&gt;

&lt;p&gt;For projects, I use Obsidian as my personal documentation: how a system works,&lt;br&gt;
why a decision was made, what the process flows are. Think of it as a second&lt;br&gt;
brain. Everything lives in one place.&lt;/p&gt;

&lt;p&gt;The part that makes it work with an agent isn't the app, it's the linking.&lt;br&gt;
Obsidian lets you create bidirectional links between notes with&lt;br&gt;
&lt;code&gt;[[Note Name]]&lt;/code&gt;. That turns your vault from a pile of files into a graph you can&lt;br&gt;
hand an agent a single entry point to.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I structure my files
&lt;/h2&gt;

&lt;p&gt;Obsidian is flexible enough that there are a dozen reasonable ways to set this&lt;br&gt;
up. Here's mine, but you can adapt it to whatever fits your style.&lt;/p&gt;

&lt;p&gt;I keep two folders. &lt;code&gt;MOC&lt;/code&gt; stands for &lt;em&gt;Map of Content&lt;/em&gt;, and holds one file per&lt;br&gt;
topic named &lt;code&gt;&amp;lt;Topic Name&amp;gt; - MOC&lt;/code&gt;. &lt;code&gt;Permanent Notes&lt;/code&gt; holds everything else. Every&lt;br&gt;
time I create a note related to a topic, I link it in both directions:&lt;br&gt;
&lt;code&gt;[[&amp;lt;Permanent note name&amp;gt;]]&lt;/code&gt; in the MOC, and &lt;code&gt;[[&amp;lt;Topic Name&amp;gt; - MOC]]&lt;/code&gt; in the&lt;br&gt;
note.&lt;/p&gt;

&lt;p&gt;The following files are examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vault/
├── MOC/
│   └── Legacy Payments API - MOC.md
└── Permanent Notes/
    ├── Payments API - Endpoints.md
    ├── Payments API - Webhook Retry Logic.md
    ├── Payments API - Settlement Flow.md
    └── Payments API - Architecture Decisions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the MOC itself stays deliberately thin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Legacy Payments API - MOC&lt;/span&gt;

Legacy service handling card settlement.

&lt;span class="gu"&gt;## Reference&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; [[Payments API - Endpoints]]
&lt;span class="p"&gt;-&lt;/span&gt; [[Payments API - Webhook Retry Logic]]

&lt;span class="gu"&gt;## Design&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; [[Payments API - Settlement Flow]]
&lt;span class="p"&gt;-&lt;/span&gt; [[Payments API - Architecture Decisions]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the whole trick. Instead of pointing an agent at a folder and hoping it&lt;br&gt;
reads the right things, you point it at one file that tells it exactly which&lt;br&gt;
notes matter and how they relate. You control the context window instead of&lt;br&gt;
letting the file system decide it for you.&lt;/p&gt;
&lt;h2&gt;
  
  
  Connecting your agent to the vault
&lt;/h2&gt;

&lt;p&gt;There's no plugin or integration to set up here. Because Obsidian notes are just&lt;br&gt;
markdown files on disk, any agent that can read your file system can read your&lt;br&gt;
vault.&lt;/p&gt;

&lt;p&gt;My workflow is simple: I open Claude at the vault root and tell it to read a&lt;br&gt;
specific MOC file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read MOC/Legacy Payments API - MOC.md and the notes it links to.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or paste the full path in directly if you'd rather not think about where you&lt;br&gt;
launched from.&lt;/p&gt;

&lt;p&gt;The reason to open at the root rather than inside &lt;code&gt;MOC/&lt;/code&gt; is that the links point&lt;br&gt;
outward. Your MOC lives in one folder and every note it references lives in&lt;br&gt;
another, so an agent scoped to &lt;code&gt;MOC/&lt;/code&gt; has to reach outside its working directory&lt;br&gt;
to follow them. Depending on your setup that means approving access every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using an agent with the MOC
&lt;/h2&gt;

&lt;p&gt;When I start researching a topic, I create the MOC first and give the agent the&lt;br&gt;
path to it.&lt;/p&gt;

&lt;p&gt;On the legacy project, that meant creating &lt;code&gt;Legacy Payments API - MOC&lt;/code&gt;, then&lt;br&gt;
working through the codebase asking Claude to document the API endpoints, the&lt;br&gt;
business logic, and the process flows — with each one landing as its own&lt;br&gt;
permanent note and each one linked back to the MOC. I wrote my architecture&lt;br&gt;
documents into the vault too, so I can reference them later instead of&lt;br&gt;
reconstructing the reasoning from scratch.&lt;/p&gt;

&lt;p&gt;Once the MOC has some substance, it becomes the entry point for everything else.&lt;br&gt;
When I need a plan for a new feature, the prompt is roughly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read &lt;code&gt;MOC/Legacy Payments API - MOC.md&lt;/code&gt; and every note it links to. Then write&lt;br&gt;
an implementation plan for adding partial refunds to the settlement flow. Flag&lt;br&gt;
anything in the existing notes that contradicts the change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tips to work efficiently
&lt;/h2&gt;

&lt;p&gt;Two things went wrong for me before this settled into something useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents write far too much.&lt;/strong&gt; Ask for documentation on a service and you'll get&lt;br&gt;
a small book. It becomes too dense for a human to actually read. The more text&lt;br&gt;
the agent has to go through, the more likely it is to hallucinate something that&lt;br&gt;
isn't in there.&lt;/p&gt;

&lt;p&gt;The fix is the most important habit when working with AI agents: proofread what&lt;br&gt;
the agent wrote before you save it. It's tempting to trust Claude to write notes&lt;br&gt;
and then never look at them, but a proofreading pass lets you cut the sections&lt;br&gt;
you'll never need. Every paragraph you delete now is context the agent doesn't&lt;br&gt;
have to process later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notes drift out of sync.&lt;/strong&gt; New information surfaces in conversation and never&lt;br&gt;
makes it into the vault, so the next time you read the note it's wrong. Keeping&lt;br&gt;
notes short is what makes them maintainable. If you can scan the section headers&lt;br&gt;
of a note in ten seconds, you'll actually update it. Long notes are far less&lt;br&gt;
likely to get updated.&lt;/p&gt;

&lt;h3&gt;
  
  
  What doesn't belong in the vault
&lt;/h3&gt;

&lt;p&gt;The instinct when an agent is doing the writing is to let it document&lt;br&gt;
everything, because it's free. But every line it writes is a line you have to&lt;br&gt;
proofread and a line it has to read on every future run.&lt;/p&gt;

&lt;p&gt;The thing to cut first is code detail. You don't need a note listing every&lt;br&gt;
parameter on every method, or a walkthrough of what each function does line by&lt;br&gt;
line. That information is already documented by the code itself.&lt;/p&gt;

&lt;p&gt;Keep the vault at the level the code can't tell you. Why the retry logic backs&lt;br&gt;
off the way it does. What the endpoints are actually doing at a high level. The&lt;br&gt;
reasoning behind an architecture decision nobody wrote down at the time. That's&lt;br&gt;
the stuff you can't reconstruct from source.&lt;/p&gt;

&lt;p&gt;Two more things I've learned to leave out.&lt;/p&gt;

&lt;p&gt;The first is the agent's own reasoning. When Claude works through a problem it'll&lt;br&gt;
often narrate how it got there, what it ruled out, what it checked, and why it&lt;br&gt;
landed where it did. That's useful to read in the moment, but it becomes noise&lt;br&gt;
in a document.&lt;/p&gt;

&lt;p&gt;The second is anything that has an authoritative source somewhere else — framework&lt;br&gt;
docs, library references, the ticket describing the requirement, the Slack thread&lt;br&gt;
where a decision got made. Copying those into the vault creates a second copy&lt;br&gt;
that starts going stale immediately. Link to them instead.&lt;/p&gt;

&lt;p&gt;The main idea is to keep your notes concise and at a high level to stop the AI&lt;br&gt;
agent from hallucinating.&lt;/p&gt;

&lt;p&gt;A decent test: if the information already lives somewhere authoritative — the&lt;br&gt;
repo, the ticket, the docs — you don't need to duplicate it. Write down the part&lt;br&gt;
that only exists in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Documentation is hard to write and harder to keep current, and on a legacy&lt;br&gt;
project with no maintainers that gap is where all your time goes. Pairing an&lt;br&gt;
agent with Obsidian doesn't remove the work. You still proofread and keep things&lt;br&gt;
in sync. What it changes is the kind of work. Instead of scanning your notes to&lt;br&gt;
&lt;em&gt;find&lt;/em&gt; information, you're scanning them to &lt;em&gt;verify&lt;/em&gt; it, and that's a much&lt;br&gt;
simpler job.&lt;/p&gt;

&lt;p&gt;Let me know how it works for you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>documentation</category>
      <category>programming</category>
    </item>
    <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>
