Wiki — LocalStack Lab (Jupyter-notebook explainer)
A concept explainer disguised as a Jupyter notebook: a macOS-dotted window titled localstack-lab.ipynb with a kernel badge, a sticky toolbar, and a vertical stack of markdown and code cells that teach how LocalStack emulates AWS on localhost. Each code cell has an In[n] gutter prompt and a Run button that types its Out[n] result with a blinking caret; Run All cascades through every cell and Restart resets the whole notebook.
MCP
Code
:root {
--bg: #eef0f2;
--paper: #ffffff;
--line: #dcdfe4;
--ink: #1f2328;
--ink-soft: #5b6470;
--prompt-in: #2a6fd6;
--prompt-out: #b3452c;
--accent: #2a6fd6;
--run: #16a34a;
--code-bg: #f6f8fa;
--out-bg: #fcfcfd;
--kernel: #6b21a8;
--mono: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
--sans: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
font-size: 15.5px;
line-height: 1.6;
padding: 2rem 1rem 5rem;
}
/* ── notebook window ────────────────────── */
.nb {
max-width: 900px;
margin: 0 auto;
background: var(--paper);
border: 1px solid var(--line);
border-radius: 10px;
box-shadow: 0 8px 30px rgba(31, 35, 40, 0.08);
overflow: hidden;
}
.nb-titlebar {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--line);
background: #fafbfc;
}
.nb-titlebar .dots {
display: flex;
gap: 0.4rem;
}
.nb-titlebar .dots i {
width: 11px;
height: 11px;
border-radius: 50%;
display: block;
}
.nb-titlebar .dots i:nth-child(1) { background: #ff5f57; }
.nb-titlebar .dots i:nth-child(2) { background: #febc2e; }
.nb-titlebar .dots i:nth-child(3) { background: #28c840; }
.nb-titlebar .fname {
font-family: var(--mono);
font-size: 0.82rem;
font-weight: 600;
}
.nb-titlebar .fname .unsaved {
color: var(--ink-soft);
font-weight: 400;
}
.nb-titlebar .kernel {
margin-left: auto;
font-family: var(--mono);
font-size: 0.68rem;
color: var(--kernel);
border: 1px solid currentColor;
border-radius: 20px;
padding: 0.15em 0.7em;
display: flex;
align-items: center;
gap: 0.4em;
}
.nb-titlebar .kernel .ring {
width: 9px;
height: 9px;
border: 2px solid currentColor;
border-radius: 50%;
}
/* ── toolbar ────────────────────────────── */
.nb-toolbar {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.45rem 0.9rem;
border-bottom: 1px solid var(--line);
background: var(--paper);
position: sticky;
top: 0;
z-index: 5;
}
.nb-toolbar button,
.nb-toolbar .sep {
font-family: var(--mono);
font-size: 0.72rem;
}
.nb-toolbar button {
border: 1px solid var(--line);
background: #fff;
color: var(--ink);
border-radius: 5px;
padding: 0.32em 0.75em;
cursor: pointer;
transition: background 0.12s, border-color 0.12s;
}
.nb-toolbar button:hover {
background: var(--code-bg);
border-color: #c4cad3;
}
.nb-toolbar button.primary {
background: var(--run);
border-color: var(--run);
color: #fff;
font-weight: 700;
}
.nb-toolbar button.primary:hover {
background: #15803d;
}
.nb-toolbar .sep {
width: 1px;
height: 1.2rem;
background: var(--line);
margin: 0 0.35rem;
}
.nb-toolbar .status {
margin-left: auto;
font-family: var(--mono);
font-size: 0.68rem;
color: var(--ink-soft);
}
.nb-toolbar .status.busy {
color: var(--prompt-out);
}
/* ── cells ──────────────────────────────── */
.cells {
padding: 1rem 0 2rem;
}
.cell {
display: grid;
grid-template-columns: 86px 1fr;
padding: 0.35rem 1rem 0.35rem 0;
border-left: 4px solid transparent;
}
.cell:hover {
border-left-color: #e3e7ec;
}
.cell.active {
border-left-color: var(--accent);
}
.cell .prompt {
font-family: var(--mono);
font-size: 0.72rem;
text-align: right;
padding: 0.55rem 0.8rem 0 0;
user-select: none;
white-space: nowrap;
}
.cell .prompt.in { color: var(--prompt-in); }
.cell .prompt.out { color: var(--prompt-out); }
/* markdown cells */
.cell.md .body {
padding: 0.35rem 0.2rem;
max-width: 46rem;
}
.cell.md h1 {
font-size: 1.7rem;
line-height: 1.2;
margin: 0.6rem 0 0.4rem;
}
.cell.md h2 {
font-size: 1.25rem;
margin: 0.7rem 0 0.3rem;
padding-bottom: 0.25rem;
border-bottom: 1px solid var(--line);
}
.cell.md p {
margin: 0.45rem 0;
color: #30363d;
}
.cell.md ul,
.cell.md ol {
margin: 0.45rem 0 0.45rem 1.4rem;
}
.cell.md li {
margin: 0.25rem 0;
}
.cell.md code {
font-family: var(--mono);
font-size: 0.84em;
background: var(--code-bg);
border: 1px solid var(--line);
padding: 0.08em 0.35em;
border-radius: 4px;
}
.cell.md .note {
border-left: 3px solid var(--accent);
background: #eef4fd;
border-radius: 0 6px 6px 0;
padding: 0.6rem 0.9rem;
margin: 0.7rem 0;
font-size: 0.92rem;
}
.cell.md .note.warn {
border-left-color: #d97706;
background: #fef6e7;
}
.cell.md table {
border-collapse: collapse;
font-size: 0.85rem;
margin: 0.7rem 0;
width: 100%;
}
.cell.md th {
font-family: var(--mono);
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.08em;
text-align: left;
color: var(--ink-soft);
border-bottom: 2px solid var(--ink);
padding: 0.4rem 0.6rem;
}
.cell.md td {
border-bottom: 1px solid var(--line);
padding: 0.45rem 0.6rem;
vertical-align: top;
}
/* code cells */
.cell.code .body {
position: relative;
}
.cell.code .src {
font-family: var(--mono);
font-size: 0.8rem;
line-height: 1.6;
background: var(--code-bg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 0.6rem 4.4rem 0.6rem 0.8rem;
overflow-x: auto;
white-space: pre;
}
.cell.code .src .cm { color: #9199a3; }
.cell.code .src .st { color: #0a7a4b; }
.cell.code .src .kw { color: #a3319f; }
.cell.code .run {
position: absolute;
top: 0.45rem;
right: 0.5rem;
font-family: var(--mono);
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.05em;
color: #fff;
background: var(--run);
border: none;
border-radius: 4px;
padding: 0.35em 0.8em;
cursor: pointer;
}
.cell.code .run:hover { background: #15803d; }
.cell.code .run:disabled {
background: #9aa3ad;
cursor: default;
}
/* output area */
.cell .outwrap {
display: none;
}
.cell.ran .outwrap {
display: grid;
grid-template-columns: 86px 1fr;
grid-column: 1 / -1;
margin-top: 0.4rem;
}
.cell .out {
font-family: var(--mono);
font-size: 0.78rem;
line-height: 1.6;
background: var(--out-bg);
border-left: 2px solid #eceef1;
padding: 0.45rem 0.8rem;
white-space: pre-wrap;
word-break: break-word;
color: #30363d;
min-height: 1.6em;
}
.cell .out .ok { color: #0a7a4b; font-weight: 700; }
.cell .out .dim { color: #9199a3; }
.cell .out .caret {
display: inline-block;
width: 0.55em;
height: 1em;
background: var(--ink);
vertical-align: text-bottom;
animation: blink 0.9s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
/* ── section jump nav ───────────────────── */
.nb-nav {
max-width: 900px;
margin: 0.8rem auto 0;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
font-family: var(--mono);
font-size: 0.68rem;
}
.nb-nav a {
text-decoration: none;
color: var(--ink-soft);
border: 1px solid var(--line);
background: var(--paper);
border-radius: 20px;
padding: 0.3em 0.85em;
transition: color 0.12s, border-color 0.12s;
}
.nb-nav a:hover {
color: var(--accent);
border-color: var(--accent);
}
@media (max-width: 700px) {
body { padding: 1rem 0.4rem 3rem; }
.cell { grid-template-columns: 58px 1fr; }
.cell.ran .outwrap { grid-template-columns: 58px 1fr; }
.cell .prompt { font-size: 0.62rem; padding-right: 0.4rem; }
.nb-toolbar .status { display: none; }
}// Notebook behavior: Run buttons type each cell's output, Run All cascades,
// Restart clears every output. Nav chips scroll manually (fragment navigation
// is blocked inside sandboxed srcdoc iframes).
let execCount = 0;
let running = false;
const status = document.getElementById("kernel-status");
function setBusy(busy, label) {
running = busy;
status.textContent = label || (busy ? "Kernel busy ●" : "Kernel idle");
status.classList.toggle("busy", busy);
}
function typeOutput(cell, { instant = false } = {}) {
return new Promise((resolve) => {
const template = cell.querySelector(".out-template");
const out = cell.querySelector(".out:not(.prompt)");
const runBtn = cell.querySelector(".run");
const inPrompt = cell.querySelector(".prompt.in");
const outPrompt = cell.querySelector(".outwrap .prompt.out");
if (!template || !out) return resolve();
execCount += 1;
const n = execCount;
inPrompt.textContent = `In [${n}]:`;
outPrompt.textContent = `Out [${n}]:`;
cell.classList.add("ran", "active");
runBtn.disabled = true;
const finalHTML = template.innerHTML.trim();
if (instant || cell.dataset.hasRun) {
out.innerHTML = finalHTML;
cell.dataset.hasRun = "1";
runBtn.disabled = false;
cell.classList.remove("active");
return resolve();
}
// Type the plain text, then swap in the syntax-colored HTML.
const text = template.content
? template.content.textContent.trim()
: template.textContent.trim();
out.innerHTML = '<span class="caret"></span>';
let i = 0;
const step = Math.max(1, Math.round(text.length / 90)); // ~90 frames total
const timer = setInterval(() => {
i = Math.min(text.length, i + step);
out.textContent = text.slice(0, i);
out.insertAdjacentHTML("beforeend", '<span class="caret"></span>');
if (i >= text.length) {
clearInterval(timer);
out.innerHTML = finalHTML;
cell.dataset.hasRun = "1";
runBtn.disabled = false;
cell.classList.remove("active");
resolve();
}
}, 16);
});
}
const codeCells = Array.from(document.querySelectorAll(".cell.code"));
codeCells.forEach((cell) => {
cell.querySelector(".run").addEventListener("click", async () => {
if (running) return;
setBusy(true);
await typeOutput(cell);
setBusy(false);
});
});
document.getElementById("run-all").addEventListener("click", async () => {
if (running) return;
setBusy(true);
for (let i = 0; i < codeCells.length; i++) {
setBusy(true, `Kernel busy ● cell ${i + 1}/${codeCells.length}`);
codeCells[i].scrollIntoView({ behavior: "smooth", block: "center" });
await typeOutput(codeCells[i]);
}
setBusy(false);
});
document.getElementById("restart").addEventListener("click", () => {
if (running) return;
execCount = 0;
codeCells.forEach((cell) => {
cell.classList.remove("ran", "active");
delete cell.dataset.hasRun;
cell.querySelector(".out:not(.prompt)").innerHTML = "";
cell.querySelector(".prompt.in").textContent = "In [ ]:";
cell.querySelector(".run").disabled = false;
});
setBusy(false, "Kernel restarted — idle");
});
document.querySelectorAll('.nb-nav a[href^="#"]').forEach((link) => {
link.addEventListener("click", (e) => {
const target = document.querySelector(link.getAttribute("href"));
if (!target) return;
e.preventDefault();
target.scrollIntoView({ behavior: "smooth", block: "start" });
});
});<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LocalStack Lab — Notebook</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="nb-nav">
<a href="#s1">What is LocalStack</a>
<a href="#s2">How it works</a>
<a href="#s3">Starting it</a>
<a href="#s4">awslocal</a>
<a href="#s5">Hands-on: S3 · SQS · Lambda</a>
<a href="#s6">SDK config</a>
<a href="#s7">Tests & CI</a>
<a href="#s8">Limits & tips</a>
</div>
<div class="nb">
<div class="nb-titlebar">
<div class="dots"><i></i><i></i><i></i></div>
<span class="fname">localstack-lab.ipynb <span class="unsaved">— autosaved</span></span>
<span class="kernel"><span class="ring"></span> Shell · LocalStack</span>
</div>
<div class="nb-toolbar">
<button class="primary" id="run-all">▶▶ Run All</button>
<button id="restart">↻ Restart</button>
<span class="sep"></span>
<button disabled>+ Code</button>
<button disabled>+ Markdown</button>
<span class="status" id="kernel-status">Kernel idle</span>
</div>
<div class="cells">
<!-- ═══ 1 · What is LocalStack ═══ -->
<div class="cell md" id="s1">
<div class="prompt"></div>
<div class="body">
<h1>LocalStack Lab 🧪</h1>
<p><strong>LocalStack</strong> is a local AWS cloud emulator: a single Docker container that impersonates dozens of AWS services (S3, SQS, Lambda, DynamoDB, SNS, …) on your machine. Your code talks to it with the <em>same SDKs, same CLI, same APIs</em> it would use against real AWS — but nothing leaves your laptop, nothing costs money, and you can destroy and recreate the whole "cloud" in seconds.</p>
<div class="note">💡 <strong>The mental model:</strong> LocalStack is to AWS what SQLite is to your production database — a faithful-enough stand-in for development and tests, running entirely on localhost.</div>
</div>
</div>
<!-- ═══ 2 · How it works ═══ -->
<div class="cell md" id="s2">
<div class="prompt"></div>
<div class="body">
<h2>How it works</h2>
<p>Everything runs behind <strong>one edge port: <code>4566</code></strong>. The container exposes a single HTTP gateway that inspects each incoming request (its <code>Authorization</code> header, target host, and payload shape) and routes it to an internal emulator for the right service. That's why "configuring LocalStack" is almost always just one change: <em>point your endpoint at <code>http://localhost:4566</code></em>.</p>
<ul>
<li><strong>One container, many services</strong> — emulators start lazily on first use.</li>
<li><strong>Ephemeral by default</strong> — state lives in the container; remove it and your buckets, queues and tables are gone.</li>
<li><strong>Credentials are fake</strong> — any access key works locally (<code>test</code>/<code>test</code> is the convention); they only select the account namespace.</li>
</ul>
</div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src">curl -s http://localhost:4566/_localstack/health | head -12</div>
<template class="out-template">{
<span class="st">"services"</span>: {
<span class="st">"dynamodb"</span>: <span class="st">"available"</span>,
<span class="st">"lambda"</span>: <span class="st">"available"</span>,
<span class="st">"s3"</span>: <span class="st">"running"</span>,
<span class="st">"sqs"</span>: <span class="st">"running"</span>,
<span class="st">"sns"</span>: <span class="st">"available"</span>
},
<span class="st">"edition"</span>: <span class="st">"community"</span>,
<span class="st">"version"</span>: <span class="st">"3.6.0"</span>
}</template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<!-- ═══ 3 · Starting it ═══ -->
<div class="cell md" id="s3">
<div class="prompt"></div>
<div class="body">
<h2>Starting it</h2>
<p>Three equivalent front doors — pick the one that fits your workflow. All of them end with the same container listening on <code>4566</code>.</p>
</div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src"><span class="cm"># Option A — plain docker</span>
docker run --rm -d -p 4566:4566 -p 4510-4559:4510-4559 \
-v /var/run/docker.sock:/var/run/docker.sock \
localstack/localstack</div>
<template class="out-template">Unable to find image 'localstack/localstack:latest' locally
latest: Pulling from localstack/localstack
<span class="dim">6e3729cf69e0: Pull complete</span>
<span class="dim">f3f4b8ddca6f: Pull complete</span>
Status: Downloaded newer image for localstack/localstack:latest
9f1c2ab8c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8
<span class="ok">✔ LocalStack is up — edge on :4566</span></template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src"><span class="cm"># Option B — docker-compose.yml (checked into the repo)</span>
cat docker-compose.yml</div>
<template class="out-template"><span class="kw">services</span>:
<span class="kw">localstack</span>:
image: localstack/localstack
ports:
- <span class="st">"4566:4566"</span>
environment:
- SERVICES=s3,sqs,lambda,dynamodb
- DEBUG=0
volumes:
- <span class="st">"/var/run/docker.sock:/var/run/docker.sock"</span></template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src"><span class="cm"># Option C — the LocalStack CLI (wraps docker for you)</span>
pip install localstack && localstack start -d</div>
<template class="out-template"> __ _______ __ __
/ / ____ _________ _/ / ___// /_____ ______/ /__
/ / / __ \/ ___/ __ `/ /\__ \/ __/ __ `/ ___/ //_/
/ /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,<
/_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_|
<span class="ok">✔ container started on :4566 (detached)</span></template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<!-- ═══ 4 · awslocal ═══ -->
<div class="cell md" id="s4">
<div class="prompt"></div>
<div class="body">
<h2>Talking to it: <code>awslocal</code></h2>
<p>The real AWS CLI works against LocalStack if you pass <code>--endpoint-url=http://localhost:4566</code> on every call. That gets old fast, so the <code>awslocal</code> wrapper does it for you — <strong>same commands, endpoint pre-wired</strong>:</p>
<table>
<tr><th>Real AWS</th><th>LocalStack</th></tr>
<tr><td><code>aws s3 ls</code></td><td><code>awslocal s3 ls</code></td></tr>
<tr><td><code>aws sqs create-queue …</code></td><td><code>awslocal sqs create-queue …</code></td></tr>
<tr><td><code>aws --endpoint-url=http://localhost:4566 …</code></td><td>(what awslocal does under the hood)</td></tr>
</table>
</div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src">pip install awscli-local
awslocal sts get-caller-identity</div>
<template class="out-template">{
<span class="st">"UserId"</span>: <span class="st">"AKIAIOSFODNN7EXAMPLE"</span>,
<span class="st">"Account"</span>: <span class="st">"000000000000"</span>,
<span class="st">"Arn"</span>: <span class="st">"arn:aws:iam::000000000000:root"</span>
}
<span class="dim"># account 000000000000 — the LocalStack default namespace</span></template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<!-- ═══ 5 · Hands-on ═══ -->
<div class="cell md" id="s5">
<div class="prompt"></div>
<div class="body">
<h2>Hands-on: S3, SQS, Lambda</h2>
<p>The point of the emulator is that real workflows just work. Run the cells in order — each one builds on the previous.</p>
</div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src"><span class="cm"># S3 — bucket, upload, list</span>
awslocal s3 mb s3://demo-bucket
echo "hello localstack" > hello.txt
awslocal s3 cp hello.txt s3://demo-bucket/
awslocal s3 ls s3://demo-bucket/</div>
<template class="out-template">make_bucket: demo-bucket
upload: ./hello.txt to s3://demo-bucket/hello.txt
2026-07-30 10:14:02 17 hello.txt
<span class="ok">✔ objects also browsable at http://localhost:4566/demo-bucket/hello.txt</span></template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src"><span class="cm"># SQS — queue, send, receive</span>
awslocal sqs create-queue --queue-name jobs
awslocal sqs send-message \
--queue-url http://localhost:4566/000000000000/jobs \
--message-body '{"task":"resize","id":42}'</div>
<template class="out-template">{
<span class="st">"QueueUrl"</span>: <span class="st">"http://localhost:4566/000000000000/jobs"</span>
}
{
<span class="st">"MD5OfMessageBody"</span>: <span class="st">"8c7e1f…"</span>,
<span class="st">"MessageId"</span>: <span class="st">"c81d4e2e-bcf2-11e6-869b-7df92533d2db"</span>
}</template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src"><span class="cm"># Lambda — zip, create, invoke (LocalStack runs it in a nested container)</span>
zip fn.zip handler.py
awslocal lambda create-function --function-name hello \
--runtime python3.12 --handler handler.lambda_handler \
--zip-file fileb://fn.zip --role arn:aws:iam::000000000000:role/any
awslocal lambda invoke --function-name hello out.json && cat out.json</div>
<template class="out-template"> adding: handler.py (stored 0%)
{
<span class="st">"FunctionName"</span>: <span class="st">"hello"</span>,
<span class="st">"State"</span>: <span class="st">"Pending"</span>,
<span class="st">"Runtime"</span>: <span class="st">"python3.12"</span>
}
{
<span class="st">"StatusCode"</span>: 200,
<span class="st">"ExecutedVersion"</span>: <span class="st">"$LATEST"</span>
}
{<span class="st">"message"</span>: <span class="st">"hello from local lambda 👋"</span>}</template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<!-- ═══ 6 · SDK config ═══ -->
<div class="cell md" id="s6">
<div class="prompt"></div>
<div class="body">
<h2>Pointing your SDK at it</h2>
<p>In application code the trick is identical: <strong>override the endpoint</strong>, keep everything else. Do it behind an env var so the same code runs against LocalStack in dev and real AWS in prod — the flag simply isn't set there.</p>
</div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src"><span class="cm"># Python · boto3</span>
<span class="kw">import</span> boto3, os
s3 = boto3.client(<span class="st">"s3"</span>,
endpoint_url=os.getenv(<span class="st">"AWS_ENDPOINT_URL"</span>), <span class="cm"># http://localhost:4566 in dev, None in prod</span>
region_name=<span class="st">"us-east-1"</span>)
print([b[<span class="st">"Name"</span>] <span class="kw">for</span> b <span class="kw">in</span> s3.list_buckets()[<span class="st">"Buckets"</span>]])</div>
<template class="out-template">['demo-bucket']</template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src"><span class="cm">// Node · AWS SDK v3</span>
<span class="kw">import</span> { S3Client, ListBucketsCommand } <span class="kw">from</span> <span class="st">"@aws-sdk/client-s3"</span>;
<span class="kw">const</span> s3 = <span class="kw">new</span> S3Client({
endpoint: process.env.AWS_ENDPOINT_URL, <span class="cm">// same trick</span>
region: <span class="st">"us-east-1"</span>,
forcePathStyle: <span class="kw">true</span>, <span class="cm">// S3 path-style for localhost</span>
});
console.log(<span class="kw">await</span> s3.send(<span class="kw">new</span> ListBucketsCommand({})));</div>
<template class="out-template">{ Buckets: [ { Name: 'demo-bucket', CreationDate: 2026-07-30T10:14:01.000Z } ] }</template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<!-- ═══ 7 · Tests & CI ═══ -->
<div class="cell md" id="s7">
<div class="prompt"></div>
<div class="body">
<h2>Tests & CI</h2>
<p>This is where LocalStack earns its keep: <strong>integration tests against a real-ish AWS, per test run, for free</strong>.</p>
<ul>
<li><strong>Ephemeral state is a feature</strong> — start a fresh container per suite (testcontainers-style) and every run begins from zero. No shared dev account, no leftover buckets.</li>
<li><strong>CI is just another docker host</strong> — run the container as a service in your pipeline, export <code>AWS_ENDPOINT_URL</code>, run the suite.</li>
<li><strong>Seed with the CLI</strong> — an init script of <code>awslocal</code> commands (or the <code>/etc/localstack/init/ready.d/</code> hook) recreates queues/buckets/tables on boot.</li>
</ul>
</div>
</div>
<div class="cell code">
<div class="prompt in">In [ ]:</div>
<div class="body">
<button class="run">▶ RUN</button>
<div class="src"><span class="cm"># A typical CI job (GitHub Actions flavor)</span>
services:
localstack:
image: localstack/localstack
ports: [<span class="st">"4566:4566"</span>]
steps:
- run: awslocal s3 mb s3://test-bucket <span class="cm"># seed</span>
- run: AWS_ENDPOINT_URL=http://localhost:4566 npm test</div>
<template class="out-template">✓ uploads report to S3 (211 ms)
✓ enqueues resize job to SQS (94 ms)
✓ lambda processes the message (1.2 s)
Test Suites: <span class="ok">3 passed</span>, 3 total
Tests: <span class="ok">17 passed</span>, 17 total</template>
</div>
<div class="outwrap"><div class="prompt out">Out [ ]:</div><div class="out"></div></div>
</div>
<!-- ═══ 8 · Limits & tips ═══ -->
<div class="cell md" id="s8">
<div class="prompt"></div>
<div class="body">
<h2>Limits & tips</h2>
<ul>
<li><strong>Community vs Pro</strong> — the free edition covers the core serverless set (S3, SQS, SNS, Lambda, DynamoDB, Kinesis, …); advanced services and some IAM enforcement live in the paid tiers. Check the health endpoint for what's actually available.</li>
<li><strong>Emulation ≠ AWS</strong> — quotas, exact error messages, IAM edge cases and timing can differ. LocalStack proves your <em>integration logic</em>; keep a thin smoke test against a real sandbox account for the last mile.</li>
<li><strong>State resets on restart</strong> — by design. If you need it to survive, enable the persistence option or re-seed with an init script (the more reproducible habit).</li>
<li><strong>Lambda needs the docker socket</strong> — mounting <code>/var/run/docker.sock</code> lets LocalStack spawn the nested runtime containers your functions run in.</li>
<li><strong>Port collisions</strong> — everything is <code>4566</code>; a second stack needs a different host port mapping.</li>
</ul>
<div class="note warn">⚠️ <strong>Don't ship endpoint overrides to prod.</strong> Guard them behind an env var that only dev/CI sets — code that hardcodes <code>localhost:4566</code> will "work on my machine" right up until deploy day.</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>LocalStack Lab (Jupyter-notebook explainer)
A long-form educational page that borrows the Jupyter notebook interface as its design language. The shell is a white notebook window with macOS traffic-light dots, a mono filename (localstack-lab.ipynb — autosaved), a purple kernel pill, and a sticky toolbar with Run All, Restart, and a live kernel-status readout. Above the window, a row of pill-shaped jump chips navigates between the eight sections.
Content is a vertical stack of two cell types, exactly like a real notebook:
- Markdown cells — rendered prose with headings, lists, tables, and blue/amber note callouts, teaching what LocalStack is, the single-edge-port (
4566) routing model,awslocalvs--endpoint-url, SDK configuration, tests/CI patterns, and community-vs-pro limits. - Code cells — an
In [n]:gutter prompt, a syntax-tinted mono source block (docker, awslocal, boto3, AWS SDK v3, CI YAML), and a green ▶ RUN button. Running a cell assigns the next execution number, then types theOut [n]:result character-by-character with a blinking block caret before swapping in the syntax-colored final output. Already-run cells re-run instantly, the active cell gets Jupyter’s blue left-border highlight, and Run All scrolls cell-to-cell while a busy counter ticks in the toolbar.
All interactions are vanilla JS; output payloads live in per-cell <template> elements so the HTML stays declarative. Jump-chip links are intercepted with scrollIntoView (fragment navigation is blocked inside sandboxed preview iframes). Below 700px the gutter narrows and the status readout hides.
Illustrative UI only — simulated shell outputs; LocalStack is a real open-source project, shown here as generic public knowledge.