Building AWS Systems, Chapter 1: A Local AWS Lab
The first chapter in our Building AWS Systems series: create a reproducible local AWS lab with Proxmox, Ubuntu Server, Docker, LocalStack, Terraform, AWS CLI, awslocal, Python, and uv.
Chapter 1: Building a Local AWS Lab with Proxmox, Ubuntu, Docker, LocalStack, Terraform and Python
Learn cloud architecture by building real systems locally.
This series is not about memorizing AWS services. It is about rebuilding the engineering intuition required to design, deploy, debug and explain cloud-native systems.
Table of contents
- Why this series exists
- What we are building
- Why use a local AWS lab?
- The architecture of the lab
- Creating the virtual machine in Proxmox
- Installing Ubuntu Server 24.04
- First boot: update the system
- Installing the QEMU Guest Agent
- Installing Docker from the official Docker repository
- Running the first Docker container
- Installing the AWS tooling
- Installing Terraform
- Installing Python tooling with uv
- Installing helper tools
- Creating the project repository
- Running LocalStack with Docker Compose
- Understanding the LocalStack configuration
- Verifying the environment
- Troubleshooting notes from the real setup
- What we built
- Exercises
- Next chapter
1. Why this series exists
Many engineers use AWS professionally for years without creating the infrastructure from scratch very often.
That may sound strange at first, but it is completely normal.
In a real company, when you join a team, the infrastructure usually already exists. The Lambda functions are deployed. The SQS queues are connected. IAM roles and policies are already defined. Terraform modules have been maintained for years. The CI/CD pipelines are already running.
Your daily work may involve adding features, fixing bugs, improving observability, debugging production incidents, writing application code, or modifying existing infrastructure. All of that is valuable experience.
But after enough time, a different problem appears.
You know how to work inside a cloud system, but you may not feel sharp when asked to build the system again from zero.
This series exists to solve that problem.
The goal is not to pass a certification exam. The goal is to rebuild practical AWS muscle memory by creating real systems locally, one component at a time.
We will use:
- Proxmox for virtualization
- Ubuntu Server 24.04 as the Linux environment
- Docker as the container runtime
- Docker Compose to orchestrate local services
- LocalStack to emulate AWS APIs locally
- Terraform for infrastructure as code
- Python for application code
- Rust later, after the fundamentals are in place
By the end of the series, we want to be comfortable not only using AWS services, but also explaining why each service exists, how it behaves under failure, and how it fits into production architectures.
2. What we are building
In this chapter, we are building the foundation for the rest of the course: a local AWS laboratory.
This lab should be:
- repeatable
- isolated
- cheap to run
- easy to reset
- close enough to real AWS to be useful for learning
- simple enough to understand completely
The final result will look like this:
Proxmox host
-> Ubuntu Server 24.04 VM
-> Docker Engine
-> LocalStack container
-> AWS-compatible APIs on port 4566
-> Terraform
-> AWS CLI
-> awslocal
-> Python 3.12
-> uv
Later chapters will build on this foundation.
For example:
Python producer
-> SQS queue running in LocalStack
-> Python consumer
Then:
API request
-> Lambda
-> SQS
-> Lambda worker
-> DynamoDB
And later:
EventBridge
-> SQS
-> Lambda
-> PostgreSQL
-> Redis
-> CloudWatch-style logs
This first chapter is less glamorous than those future systems, but it matters. A weak environment makes every future lesson harder. A strong environment lets us focus on learning AWS instead of debugging random machine setup issues.
3. Why use a local AWS lab?
There are two common ways to learn AWS.
The first is using a real AWS account.
That is valuable, and eventually we should do it. Real AWS teaches us real IAM, real billing, real regional behavior, real service limits, and real deployment constraints.
The second is using a local AWS emulator such as LocalStack.
That is what we will use first.
Why?
Because early learning should be fast and safe.
When you are practicing, you should be able to destroy and recreate infrastructure many times. You should be able to make mistakes without worrying about accidentally leaving a resource running. You should be able to run tests repeatedly. You should be able to work offline or from a home lab.
LocalStack gives us that freedom.
It does not replace AWS completely. It is not supposed to. Some services behave differently. Some advanced features require a paid LocalStack plan. Some production issues only appear in the real cloud.
But for learning the fundamentals of event-driven systems, infrastructure as code, queues, functions, tables, retries and service integration, it is an excellent starting point.
The mental model is:
Learn locally first.
Understand the concepts.
Build muscle memory.
Then deploy selected projects to real AWS.
4. The architecture of the lab
Before installing tools, let’s understand the stack.
Proxmox
Proxmox is the virtualization platform running on the physical machine. It allows us to create a dedicated VM for this lab.
The VM gives us isolation. If we break something, we can restore a snapshot or rebuild the environment without affecting our laptop or other services.
Ubuntu Server 24.04
Ubuntu Server is the operating system inside the VM.
We use a server installation because this machine does not need a desktop environment. It will be accessed through SSH and used as a development server.
Docker
Docker runs containers.
LocalStack itself will run as a Docker container. Later, we may also run PostgreSQL, Redis, Grafana, Prometheus, application containers, or Kubernetes tooling.
Docker Compose
Docker Compose lets us define multi-container environments using a YAML file.
Instead of running long docker run commands manually, we describe our services in docker-compose.yml.
LocalStack
LocalStack exposes AWS-compatible endpoints locally.
For most services, instead of sending requests to AWS, our tools will send requests to:
http://localhost:4566
or, from another machine:
http://<vm-ip>:4566
Terraform
Terraform lets us define infrastructure as code.
We will not manually create queues and tables whenever possible. We will write Terraform files and apply them.
This matters because production infrastructure is rarely created by clicking around in the AWS Console. It is usually declared, reviewed, versioned and applied.
Python and uv
Python will be our primary application language.
uv will help us create virtual environments, install dependencies, run tools and keep projects reproducible.
5. Creating the virtual machine in Proxmox
The VM used for this lab was created with the following approximate resources:
Operating system: Ubuntu Server 24.04
CPU: 8 vCPUs
Memory: 16 GB RAM
Disk: 100 GB
Network: VirtIO
Disk controller: VirtIO SCSI
CPU type: host
These values are not mandatory, but they are comfortable for this type of lab.
Why 8 vCPUs?
LocalStack itself does not require 8 CPUs for simple exercises.
But the lab will grow. Over time, we may run:
- LocalStack
- Python applications
- PostgreSQL
- Redis
- test suites
- Terraform
- Docker builds
- possibly Kubernetes tools such as k3d or k3s
Having extra CPU makes the environment feel responsive.
Why 16 GB RAM?
Docker-based development environments can consume memory quickly.
A minimal lab can run with less, but 16 GB gives us room to experiment without constantly thinking about memory pressure.
Why 100 GB disk?
Docker images, build caches, volumes and package downloads accumulate over time.
A 30 GB disk may work at first, but it becomes annoying quickly. A 100 GB disk is a practical starting point.
Why CPU type host?
In Proxmox, the CPU type controls which CPU features are exposed to the VM.
Using host tells Proxmox to expose the host CPU capabilities directly to the guest VM.
This is useful for performance and compatibility, especially when running modern development tooling, Docker workloads, and possibly Kubernetes-related tools later.
In the Proxmox VM configuration, the CPU line should look similar to this:
cpu: host,flags=+aes
If it says something like this instead:
cpu: x86-64-v2-AES,flags=+aes
then the VM is using a generic CPU model. That can still work, but for this lab host is preferred.
Why VirtIO networking?
VirtIO is a paravirtualized driver designed for virtual machines.
It is usually faster and more efficient than emulated network adapters such as Intel E1000.
For a Docker host, VirtIO is the right choice.
Why disable the Proxmox VM firewall initially?
For a learning lab, it is often easier to start with fewer moving parts.
Docker manages networking and iptables rules internally. Proxmox firewall rules can coexist with Docker, but while learning, they add one more layer to debug.
The recommendation for this first milestone is:
Disable the Proxmox VM firewall initially.
Re-enable it later when you intentionally want to practice network security.
Security matters, but so does controlling the learning curve.
6. Installing Ubuntu Server 24.04
During the Ubuntu installation, choose a minimal server setup.
Recommended options:
Install OpenSSH Server: yes
Install Docker from Ubuntu installer: no
Install Kubernetes/LXD/snaps: no, unless you specifically need them
Why not install Docker from the Ubuntu installer?
Because we want Docker from Docker’s official repository, not the distribution package.
Linux distributions often prioritize stability over the latest upstream version. That is reasonable for many servers, but for a development lab we usually want the official Docker packages.
After installation, SSH into the VM.
Example:
ssh <your-user>@192.168.88.200
Use your own username and IP address.
In our setup, the VM initially received an IP through DHCP, then we discussed assigning a static IP so the lab can always be reached at the same address.
For example:
Subnet: 192.168.88.0/24
Address: 192.168.88.200
Gateway: 192.168.88.1
DNS: 192.168.88.1
If your router uses a different network, adapt these values.
7. First boot: update the system
The first commands executed on the VM were the standard update commands:
sudo apt update
sudo apt upgrade -y
What do they do?
apt update refreshes the local package index. It does not upgrade software by itself. It only asks Ubuntu’s package repositories what versions are available.
apt upgrade -y upgrades installed packages without asking for confirmation for each package.
After a fresh server installation, this is a good first step.
If the kernel or important system packages were upgraded, reboot:
sudo reboot
Then reconnect through SSH.
8. Installing the QEMU Guest Agent
The QEMU Guest Agent is not required for Docker, LocalStack or Terraform.
But it is useful when running Ubuntu inside Proxmox.
It allows Proxmox to communicate with the guest OS. With the guest agent installed and enabled, Proxmox can:
- show the VM IP address
- shut down the VM cleanly
- coordinate better backups and snapshots
- query guest information
Install it inside Ubuntu:
sudo apt install qemu-guest-agent -y
sudo systemctl enable --now qemu-guest-agent
Then, in Proxmox, make sure the VM option is enabled:
VM -> Options -> QEMU Guest Agent -> Enabled
You can check the service status inside Ubuntu:
systemctl status qemu-guest-agent
You can also check whether the VirtIO port exists:
ls /dev/virtio-ports/
A working setup should show something like:
org.qemu.guest_agent.0
Troubleshooting note
During the real setup, we saw an error like this:
Timed out waiting for device dev-virtio\x2dports-org.qemu.guest_agent.0.device
Timed out waiting for device /dev/virtio-ports/org.qemu.guest_agent.0
That usually means the guest agent package is installed inside Ubuntu, but Proxmox is not exposing the guest agent device to the VM.
The fix is usually:
- Shut down the VM.
- Enable QEMU Guest Agent in Proxmox VM options.
- Start the VM again.
- Restart the service if needed.
Again, this is not required for the AWS lab itself. It is a Proxmox quality-of-life feature.
9. Installing Docker from the official Docker repository
Now we install Docker.
First, install base packages required to configure external repositories:
sudo apt install -y \
curl \
git \
unzip \
jq \
make \
ca-certificates \
gnupg \
lsb-release
Some of these tools are needed immediately; others are useful throughout the lab.
curldownloads files and scripts.gitmanages source code.unzipextracts downloaded archives.jqprocesses JSON, which is extremely useful with AWS CLI output.makewill help us create repeatable project commands.ca-certificatesandgnupghelp apt verify repository signatures.lsb-releasehelps scripts detect the Ubuntu codename.
Before installing Docker CE, remove possible conflicting packages:
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
sudo apt remove -y $pkg
done
This step is defensive. On a fresh Ubuntu server, many of these packages may not be installed. That is fine.
Now create the directory for apt keyrings:
sudo install -m 0755 -d /etc/apt/keyrings
Download Docker’s GPG key:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Make it readable by apt:
sudo chmod a+r /etc/apt/keyrings/docker.gpg
Add Docker’s official apt repository:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Update the package index again:
sudo apt update
Install Docker Engine and Compose:
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
Verify Docker:
docker version
If that fails with a permission error, try:
sudo docker version
At this point, Docker works, but your user may not yet have permission to access the Docker daemon directly.
Add your user to the Docker group:
sudo usermod -aG docker $USER
Then either log out and log back in, reboot, or run:
newgrp docker
Now verify Docker Compose:
docker compose version
Important: the modern command is:
docker compose
not:
docker-compose
The older docker-compose command was a separate Python-based tool. The modern Compose plugin is integrated into the Docker CLI.
10. Running the first Docker container
Run Docker’s hello-world test:
docker run hello-world
This command verifies the full Docker path:
- The Docker CLI can talk to the Docker daemon.
- Docker can pull an image from a registry.
- Docker can create a container.
- Docker can start the container.
- The container can write output.
This is a small command, but it validates a lot.
If it works, Docker is ready.
11. Installing the AWS tooling
Even though we will use LocalStack, we still install the official AWS CLI.
Why?
Because LocalStack imitates AWS APIs. We want to learn the same commands and mental models that apply to the real cloud.
Download AWS CLI v2:
cd /tmp
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
Unzip it:
unzip awscliv2.zip
Install it:
sudo ./aws/install
Verify:
aws --version
You should see output similar to:
aws-cli/2.x.x Python/3.x Linux/x86_64
Do not worry if your exact version differs.
Why install awslocal?
The standard AWS CLI sends requests to AWS endpoints by default.
LocalStack runs locally, usually on:
http://localhost:4566
You can point AWS CLI manually at LocalStack using --endpoint-url:
aws --endpoint-url=http://localhost:4566 sqs list-queues
That works, but it becomes repetitive.
awslocal is a convenience wrapper that automatically targets LocalStack.
We will install it after installing uv.
12. Installing Terraform
Terraform will be used throughout this series.
Instead of manually creating AWS resources, we will describe them using code.
Install HashiCorp’s GPG key:
wget -O- https://apt.releases.hashicorp.com/gpg \
| sudo gpg --dearmor \
-o /usr/share/keyrings/hashicorp-archive-keyring.gpg
Add the HashiCorp apt repository:
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com \
$(. /etc/os-release && echo $VERSION_CODENAME) main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list
Update apt:
sudo apt update
Install Terraform:
sudo apt install terraform
Verify:
terraform version
Why install Terraform so early?
Because we want infrastructure as code to be part of the learning process from the beginning.
A common beginner mistake is to learn cloud services through manual clicks first and only later learn Terraform. That approach creates two separate learning paths.
In this course, we will do the opposite.
When we learn SQS, we will create queues with Terraform.
When we learn DynamoDB, we will create tables with Terraform.
When we learn IAM, we will write policies with Terraform.
This makes our work reproducible from day one.
13. Installing Python tooling with uv
Ubuntu 24.04 ships with Python 3.12.
Verify:
python3 --version
Install Python support packages:
sudo apt install -y python3-venv python3-pip
Now install uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
Reload your shell:
source ~/.bashrc
Verify:
uv --version
uv is a modern Python package and project manager. We will use it for virtual environments, dependency management and command execution.
Now install awslocal as a uv-managed tool:
uv tool install awscli-local
Verify:
awslocal --version
Later, once LocalStack is running, we will use commands like:
awslocal sqs list-queues
Instead of:
aws --endpoint-url=http://localhost:4566 sqs list-queues
Both are valid. awslocal is simply more convenient for local work.
14. Installing helper tools
A good lab environment needs more than Docker and Terraform.
Install useful command-line tools:
sudo apt install -y \
git \
make \
curl \
wget \
unzip \
zip \
jq \
tree \
ripgrep \
htop \
btop \
vim \
nano \
ca-certificates \
gnupg \
software-properties-common \
build-essential \
pkg-config \
libssl-dev
Why these tools?
treehelps visualize project structure.ripgrepis a fast code search tool.htopandbtophelp inspect system resources.build-essential,pkg-config, andlibssl-devare common dependencies for compiling Python packages and Rust crates.vimandnanogive us terminal editors.
Install yq for YAML processing:
sudo snap install yq
Alternatively, if you prefer avoiding snaps, install Mike Farah’s yq binary manually:
sudo wget \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
-O /usr/local/bin/yq
sudo chmod +x /usr/local/bin/yq
Install direnv:
sudo apt install direnv
Enable it for Bash:
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
source ~/.bashrc
direnv automatically loads environment variables when entering a project directory. This will be useful later when we define AWS variables for LocalStack.
Install just, a command runner similar to Make:
sudo snap install --edge just
make is universal and we will still use it, but just is convenient for developer workflows.
Finally, configure Git defaults:
git config --global init.defaultBranch main
git config --global pull.rebase false
git config --global core.editor vim
These settings are personal preferences, but they avoid small annoyances later.
15. Creating the project repository
Now create the project directory.
mkdir ~/aws-gym
cd ~/aws-gym
Initialize Git:
git init
git branch -M main
Create the first directory structure:
mkdir -p infrastructure/terraform
mkdir -p app
mkdir -p scripts
mkdir -p volume/localstack
Create the first files:
touch .env
touch Makefile
touch docker-compose.yml
The project structure now looks like this:
aws-gym/
|-- app/
|-- docker-compose.yml
|-- infrastructure/
| `-- terraform/
|-- Makefile
|-- scripts/
`-- volume/
`-- localstack/
Why this structure?
app/will contain application code.infrastructure/terraform/will contain Terraform code.scripts/will contain helper scripts.volume/localstack/will persist LocalStack state when persistence is enabled.docker-compose.ymldefines local services..envstores local environment variables.Makefilewill provide repeatable commands.
A note about naming
For the course, we will standardize on:
infrastructure/terraform
Small naming consistency matters because paths become documentation. Using one clear spelling across examples, commands and future chapters keeps the project easier to follow.
16. Running LocalStack with Docker Compose
Now create the .env file:
cat > .env <<'ENV'
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_DEFAULT_REGION=us-east-1
PERSISTENCE=1
DEBUG=1
ENV
These credentials are fake.
LocalStack does not require real AWS credentials for local development. Many AWS SDKs and tools expect credentials to exist, so we provide dummy values.
Now create docker-compose.yml:
cat > docker-compose.yml <<'COMPOSE'
services:
localstack:
image: localstack/localstack:4.5
container_name: localstack
ports:
- "4566:4566"
environment:
- DEBUG=${DEBUG}
- PERSISTENCE=${PERSISTENCE}
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
volumes:
- ./volume/localstack:/var/lib/localstack
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "bash", "-c", "awslocal sts get-caller-identity"]
interval: 10s
timeout: 5s
retries: 20
COMPOSE
Start LocalStack:
docker compose up
If you want it to run in the background:
docker compose up -d
Check running containers:
docker ps
You should see a container named:
localstack
17. Understanding the LocalStack configuration
Let’s look at the Compose file piece by piece.
The image
image: localstack/localstack:4.5
This tells Docker which image to run.
Pinning a version is better than using latest for learning material. If a future LocalStack release changes behavior, readers following this chapter still get the same baseline.
The container name
container_name: localstack
This gives the container a predictable name.
That makes commands easier:
docker logs localstack
instead of needing to discover a generated container name.
The port mapping
ports:
- "4566:4566"
LocalStack exposes most AWS service endpoints through port 4566.
The left side is the host port. The right side is the container port.
This means:
VM port 4566 -> LocalStack container port 4566
From inside the VM, tools can reach LocalStack at:
http://localhost:4566
From another machine on the network, tools may reach it at:
http://<vm-ip>:4566
assuming firewall rules allow it.
Environment variables
environment:
- DEBUG=${DEBUG}
- PERSISTENCE=${PERSISTENCE}
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
These values come from .env.
DEBUG=1 makes LocalStack more verbose. This is useful while learning.
PERSISTENCE=1 tells LocalStack to persist state under /var/lib/localstack.
AWS_DEFAULT_REGION=us-east-1 gives us a default AWS region.
Volumes
volumes:
- ./volume/localstack:/var/lib/localstack
- /var/run/docker.sock:/var/run/docker.sock
The first mount persists LocalStack data in the project directory.
The second mount gives LocalStack access to the Docker socket. Some LocalStack features need Docker to start additional containers, especially when working with Lambda-like execution.
This is powerful, but it is also something to understand: mounting the Docker socket gives the container significant control over the Docker host. In this isolated lab VM, that is acceptable. On a shared production server, it would require more careful consideration.
Healthcheck
healthcheck:
test: ["CMD", "bash", "-c", "awslocal sts get-caller-identity"]
interval: 10s
timeout: 5s
retries: 20
The healthcheck periodically runs a simple AWS identity command through LocalStack.
If it succeeds, Docker considers the service healthy.
The command:
awslocal sts get-caller-identity
asks the local STS service: who am I?
In real AWS, STS returns information about the current caller identity. In LocalStack, it returns a local/fake identity, which is still useful for verifying connectivity.
18. Verifying the environment
After installing the tooling, run a full verification block:
python3 --version
uv --version
aws --version
awslocal --version
terraform version
docker --version
docker compose version
jq --version
yq --version
git --version
A healthy environment should show versions for all of them.
Now verify LocalStack:
awslocal sts get-caller-identity
Expected output will be JSON, similar to:
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
The exact values do not matter.
What matters is that:
awslocalruns.- It reaches LocalStack.
- LocalStack responds with valid JSON.
You can also test through the standard AWS CLI:
aws --endpoint-url=http://localhost:4566 sts get-caller-identity
This should return similar output.
If both commands work, your local AWS lab is alive.
19. Troubleshooting notes from the real setup
A good technical guide should include mistakes and debugging steps, not only the clean path.
Here are a few notes from the actual setup.
Docker permission denied
Symptom:
permission denied while trying to connect to the Docker daemon socket
Cause:
Your user is not in the docker group, or the current shell session has not picked up the new group membership yet.
Fix:
sudo usermod -aG docker $USER
newgrp docker
Or log out and log back in.
Then retry:
docker run hello-world
docker compose versus docker-compose
Use:
docker compose up
Not:
docker-compose up
The first one is the modern Docker Compose plugin.
QEMU Guest Agent timeout
Symptom:
Timed out waiting for device /dev/virtio-ports/org.qemu.guest_agent.0
Cause:
The guest agent service is installed in Ubuntu, but the Proxmox VM does not expose the guest agent device.
Fix:
Enable QEMU Guest Agent in Proxmox VM options, then reboot the VM.
LocalStack asks for a token
Some LocalStack features may require a token or a paid plan.
For the early parts of this series, we will stay within features that are useful for local AWS learning without depending on paid-only behavior where possible.
If you see token-related output, do not panic. First verify whether the service you are using actually requires it.
20. What we built
In this chapter, we built the foundation for a local AWS learning environment.
We created:
A dedicated Ubuntu Server VM
A Docker-based runtime
A LocalStack AWS-compatible endpoint
A Terraform installation
A Python 3.12 development environment
A uv-based Python toolchain
AWS CLI and awslocal
A project repository for future lessons
This may look like basic setup work, but it is more important than it appears.
We now have a safe environment where we can create AWS-style infrastructure, test applications, break things, inspect behavior, and destroy everything without worrying about cloud bills.
Most importantly, we have established the mindset of the course:
Do not memorize cloud services.
Build systems.
Understand the reason behind every component.
Make the environment reproducible.
Verify everything.
21. Exercises
Before moving to the next chapter, try these exercises.
Exercise 1: Verify all tools
Run:
python3 --version
uv --version
aws --version
awslocal --version
terraform version
docker --version
docker compose version
jq --version
yq --version
git --version
Write down the versions in a NOTES.md file.
Why?
Because future debugging is easier when you know the baseline you started from.
Exercise 2: Restart LocalStack
Start LocalStack:
docker compose up -d
Check it:
docker ps
View logs:
docker logs localstack
Stop it:
docker compose down
Start it again:
docker compose up -d
The goal is to become comfortable managing the local cloud process.
Exercise 3: Compare AWS CLI and awslocal
Run:
awslocal sts get-caller-identity
Then run:
aws --endpoint-url=http://localhost:4566 sts get-caller-identity
Confirm they return equivalent results.
This proves that awslocal is convenience, not magic.
Exercise 4: Inspect the Docker volume
Run:
tree volume
Then create resources in later chapters and inspect the directory again.
This will help you understand what LocalStack persists locally.
Exercise 5: Create a Proxmox snapshot
After the environment is working, create a Proxmox snapshot named:
localstack-ready
This gives you a clean restore point before starting the AWS exercises.
22. Next chapter
In the next chapter, we will create our first real AWS-style resource: an SQS queue.
We will learn:
- what a message queue is
- why distributed systems use queues
- how SQS differs from RabbitMQ and Kafka
- how to create an SQS queue with Terraform
- how to send messages with Python
- how to receive messages with Python
- what visibility timeout means
- why dead-letter queues exist
- how to inspect queue state through LocalStack
The goal of the next chapter is not just to run send-message and receive-message.
The goal is to understand why queues are one of the most important building blocks in cloud architecture.
Once queues make sense, Lambda, EventBridge, retries, idempotency and distributed workflows become much easier to understand.
Appendix: Command summary
This appendix is intentionally repetitive. It exists so the environment can be recreated quickly later.
System update
sudo apt update
sudo apt upgrade -y
sudo reboot
Base packages
sudo apt install -y \
curl \
git \
unzip \
jq \
make \
ca-certificates \
gnupg \
lsb-release
Docker installation
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
sudo apt remove -y $pkg
done
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker
Docker verification
docker version
docker compose version
docker run hello-world
QEMU Guest Agent
sudo apt install qemu-guest-agent -y
sudo systemctl enable --now qemu-guest-agent
systemctl status qemu-guest-agent
AWS CLI
cd /tmp
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
unzip awscliv2.zip
sudo ./aws/install
aws --version
Terraform
wget -O- https://apt.releases.hashicorp.com/gpg \
| sudo gpg --dearmor \
-o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com \
$(. /etc/os-release && echo $VERSION_CODENAME) main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install terraform
terraform version
Python and uv
sudo apt install -y python3-venv python3-pip
python3 --version
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc
uv --version
uv tool install awscli-local
awslocal --version
Extra tooling
sudo apt install -y \
git \
make \
curl \
wget \
unzip \
zip \
jq \
tree \
ripgrep \
htop \
btop \
vim \
nano \
ca-certificates \
gnupg \
software-properties-common \
build-essential \
pkg-config \
libssl-dev
sudo snap install yq
sudo apt install direnv
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
sudo snap install --edge just
Git defaults
git config --global init.defaultBranch main
git config --global pull.rebase false
git config --global core.editor vim
Project repository
mkdir ~/aws-gym
cd ~/aws-gym
git init
git branch -M main
mkdir -p infrastructure/terraform
mkdir -p app
mkdir -p scripts
mkdir -p volume/localstack
touch .env
touch Makefile
touch docker-compose.yml
LocalStack .env
cat > .env <<'ENV'
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_DEFAULT_REGION=us-east-1
PERSISTENCE=1
DEBUG=1
ENV
LocalStack docker-compose.yml
cat > docker-compose.yml <<'COMPOSE'
services:
localstack:
image: localstack/localstack:4.5
container_name: localstack
ports:
- "4566:4566"
environment:
- DEBUG=${DEBUG}
- PERSISTENCE=${PERSISTENCE}
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
volumes:
- ./volume/localstack:/var/lib/localstack
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "bash", "-c", "awslocal sts get-caller-identity"]
interval: 10s
timeout: 5s
retries: 20
COMPOSE
Start LocalStack
docker compose up -d
docker ps
awslocal sts get-caller-identity Want to build something similar?
If you are validating a product, modernizing operations, or shipping a technical MVP, we can help.