# Running EC2 Scripts Locally with LocalStack

## The short version

I wanted a practice "server" on my own laptop where I could run bash scripts without paying for real Amazon cloud. I started with just my MacBook, Docker and a tool called LocalStack, which pretends to be Amazon's cloud on your own computer. I used it to create a fake EC2 server, and the dashboard said it was running, but I found out it was only a label with no real machine behind it, so my script could not run on it. I tried a setting that should have made LocalStack build real machines, but none appeared, possibly because of my free plan. In the end I used Docker directly to start a real Ubuntu Linux machine, ran my script on it, and it printed "hello from EC2". Now I have a working place to practise scripts.

## Setup: what I had running

LocalStack runs as one Docker container on your machine and answers AWS API calls on port 4566. I used a MacBook Air with Docker Desktop, the AWS CLI, the `awslocal` wrapper and the LocalStack CLI (version 2026.8.2).

Start LocalStack in the background and confirm the container is healthy:

```bash
localstack start -d
docker ps --format '{{.Names}}  {{.Status}}'
```

You should see one container named `localstack-main` with a `(healthy)` status. The LocalStack web app at app.localstack.cloud then shows your instance under Resource Browser, including an EC2 page.

The AWS CLI refuses to send any request without credentials, even to LocalStack. LocalStack does not check them, so dummy values are enough. Set them once per terminal:

```bash
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_PAGER=""
```

The last line stops the CLI from opening long output in a pager, which otherwise traps you on an `(END)` or `:` prompt until you press `q`. Note that `:q` is a vim command and does nothing in a terminal pager.

## Step 1: Create mock EC2 instances

A fresh LocalStack has no images of your own. Register one, then launch an instance from it. I also wrote a small user-data script to see whether it would run at launch:

```bash
awslocal ec2 describe-images --owners self

awslocal ec2 register-image \
  --name "localstack-practice" \
  --architecture x86_64 \
  --root-device-name /dev/sda1 \
  --virtualization-type hvm

cat > myscript.sh <<'EOF'
#!/bin/bash
echo "hello from EC2" > /tmp/hello.txt
EOF

aws --endpoint-url=http://localhost:4566 ec2 run-instances \
  --image-id ami-ubuntu-practice \
  --instance-type t2.micro \
  --user-data file://myscript.sh \
  --region us-east-1
```

The first command returned an empty list, and `register-image` returned an ID (mine was `ami-390225de4ea68f2eb`). `run-instances` printed a full instance record with `State` set to `pending`, and the Resource Browser listed the instance as `running` shortly after.

That looks like success, and for the EC2 API it is: you can describe, stop, start and terminate these instances, and tools such as Terraform or the AWS CLI will treat them as real. But at this point nothing is running anywhere. The instance is a database record, so the user-data script never executed and there is no machine to log in to.

To see this for yourself, run `docker ps`. If the only container is `localstack-main`, your instances are mock records.

## Step 2: Try to make the instances real

LocalStack can back an EC2 instance with a Docker container if you set `EC2_VM_MANAGER=docker`. LocalStack looks for a local Docker image tagged `localstack-ec2/<ami-name>:<ami-id>`, so I pulled Ubuntu and tagged it:

```bash
docker pull ubuntu:24.04
docker tag ubuntu:24.04 localstack-ec2/ubuntu-practice:ami-ubuntu-practice
```

Then I restarted LocalStack with the setting and checked that it reached the container and that the Docker socket was mounted:

```bash
localstack stop
localstack start -d -e EC2_VM_MANAGER=docker

docker inspect localstack-main | grep EC2_VM_MANAGER
docker inspect localstack-main --format '{{json .Mounts}}' | grep docker.sock
```

Both checks passed. The restart also wiped my earlier instances, so I had to launch again.

My first two launches used `ami-ubuntu-practice` and `ami-000001`. Neither was a registered image, which my `describe-images` output confirmed, so they were never going to work. I then registered a proper image, tagged Ubuntu with the ID it returned, and launched from that:

```bash
AMI_ID=$(awslocal ec2 register-image \
  --name ubuntu-practice \
  --architecture x86_64 \
  --root-device-name /dev/sda1 \
  --virtualization-type hvm \
  --query ImageId --output text)

docker tag ubuntu:24.04 localstack-ec2/ubuntu-practice:$AMI_ID

awslocal ec2 run-instances \
  --image-id $AMI_ID \
  --instance-type t2.micro \
  --user-data file://myscript.sh
```

The API returned success, but `docker ps` still showed only `localstack-main`, and the LocalStack logs recorded `RunInstances => 200` with no Docker activity. My license showed as `freemium`, and I suspect Docker-backed EC2 needs a higher tier, but I could not confirm that. If you have a paid LocalStack plan, this route may work for you, so check the logs with `docker logs localstack-main` for the reason.

## Step 3: Run scripts in a real container

When LocalStack would not start a machine, I started one myself with plain Docker. A container running `sleep infinity` stays alive and behaves like a small Linux VM:

```bash
docker run -d --name myvm ubuntu:24.04 sleep infinity
docker exec -i myvm bash < myscript.sh
docker exec myvm cat /tmp/hello.txt
```

The last command printed `hello from EC2`, which proves the script ran inside the container and wrote its file there. The `< myscript.sh` part feeds the script from your Mac into bash inside the container, so you can edit scripts locally and rerun them at will.

This container is not linked to the mock instances in LocalStack. Use LocalStack for the EC2 API and `myvm` for running code.

Day-to-day commands for the container:

| Task | Command |
| --- | --- |
| Run a script file | `docker exec -i myvm bash < myscript.sh` |
| Open an interactive shell | `docker exec -it myvm bash` |
| Copy a file in | `docker cp myscript.sh myvm:/root/myscript.sh` |
| Copy a file out | `docker cp myvm:/tmp/hello.txt ./hello.txt` |
| Stop and start it | `docker stop myvm` and `docker start myvm` |
| Delete it | `docker rm -f myvm` |
| Install tools inside | `docker exec myvm bash -c "apt-get update && apt-get install -y curl nano"` |

Files you write inside the container survive `docker stop` and `docker start`, but are lost when you run `docker rm`. The `ubuntu:24.04` image is minimal, so install anything you need first.

## Troubleshooting: errors I hit

Every one of these cost me time, so here is the cause and the fix in one place.

| What I saw | Cause | Fix |
| --- | --- | --- |
| `NoCredentials: Unable to locate credentials` | The AWS CLI wants credentials even for LocalStack | `export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test` |
| Terminal stuck on `(END)` or `:` | The AWS CLI opened output in a pager | Press `q`, then `export AWS_PAGER=""` |
| `zsh: no such file or directory: container_id` | I typed `<container_id>` literally, and zsh read `<` as a file redirect | Use the real container name from `docker ps` |
| `No such container: NEW_CONTAINER_NAME` | Same mistake with a placeholder name | Replace it with `myvm` or the real name |
| Instance shows `running` but `docker ps` has only `localstack-main` | The instance is a mock record with no machine behind it | Use a plain Docker container (Step 3) |
| Image ID not in `describe-images` | I launched with an AMI name I had never registered | Register it with `register-image` and use the ID it returns |
| Instances vanished after a restart | LocalStack was restarted without persistence | Relaunch them, or script the launch so it is repeatable |

## Takeaways

A `running` state in LocalStack does not mean a machine exists. Check `docker ps` before you assume you can log in or run scripts.

*   Use LocalStack to practice the EC2 API: registering images, launching, describing, stopping and terminating instances.
    
*   Use a plain Docker container to practice what runs on a server: bash scripts, package installs and file handling.
    
*   Docker-backed EC2 in LocalStack may need a paid plan. I could not make it work on a `freemium` license, but did not confirm that was the cause.
    
*   Scripts passed with `--user-data` only run if a real container backs the instance.
    

The next step I would try is a launch script that creates the container and registers a matching mock instance together, so the two stay in sync.

## Getting back to your machine every day: command cheat sheet

After a restart, open Docker Desktop and wait until it says it's running. Your `myvm` container is set to restart on its own, and anything you saved inside it is still there. The only thing that deletes it is `docker rm`.

### Daily routine

```bash
docker ps                            # check myvm is running
docker start myvm                    # only if it isn't listed
docker exec -it myvm bash            # step inside (type exit to leave)
```

### Write and run scripts from your Mac

```bash
nano myscript.sh                     # write or edit the script
docker exec -i myvm bash < myscript.sh
docker exec myvm cat /tmp/hello.txt  # check what it created
```

### Move files

```bash
docker cp myscript.sh myvm:/root/myscript.sh   # Mac to machine
docker cp myvm:/tmp/hello.txt ./hello.txt      # machine to Mac
```

### Install tools in the machine (once)

```bash
docker exec myvm bash -c "apt-get update && apt-get install -y nano curl"
```

### Manage the machine

```bash
docker stop myvm
docker start myvm
docker rm -f myvm                    # deletes it and everything inside
docker run -d --name myvm ubuntu:24.04 sleep infinity   # recreate if deleted
docker update --restart unless-stopped myvm             # auto-start on Docker launch
```

### LocalStack dashboard (optional)

You only need this for practising the EC2 API. Your scripts run in `myvm` without it. Mock instances are wiped each time LocalStack restarts, so launch new ones.

```bash
localstack start -d
export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 AWS_PAGER=""
localstack stop
```
