Chapter 49: Deploying your first app
From a running stack to a deployed application in five commands
The stack is running. Time to deploy something.
This chapter is entirely hands-on. We create a trivial application, launch an instance, deploy the app, hit it through the load balancer, read its logs, and tear it down. Every command you'll use for real work starts here.
Create a simple app
mkdir my-app
cat > my-app/start.sh << 'EOF'
#!/bin/bash
python3 -m http.server 3000
EOF
chmod +x my-app/start.shThat's a web server. It serves the current directory on port 3000. Python's http.server is already installed in the base rootfs from the bootstrap step.
Launch an instance
tinyaws instance launch --type smallOutput:
Launched instance i-1 (type: small, cpu: 100%, memory: 512MB)
Status: provisioningThe instance starts in "provisioning" state. Behind the scenes:
- The CLI sends
POST /instancesto the scheduler - The scheduler picks an agent via round-robin
- The agent creates an overlay filesystem on top of the base rootfs
- The agent starts a
systemd-nspawncontainer with cgroup limits, seccomp profile, and network namespace - The container boots systemd, gets an IP on the
tinyaws0bridge - The agent flips the instance status to "running"
Check status:
tinyaws instance listID TYPE STATUS IP AGENT
i-1 small running 10.0.0.2 127.0.0.1:8080If status is still "provisioning", wait a few seconds and check again. First boot of a new overlay takes a moment while systemd initializes.
Deploy
tinyaws deploy ./my-app --service --port 3000Output:
Uploading ./my-app to object store... done (2 files, 89 bytes)
Deploying to instance i-1...
Registered service svc-1 on port 3000What happens behind the scenes:
- The CLI tars
./my-app, uploads it to the object store as a deployment artifact - The CLI tells the agent to deploy the artifact to instance
i-1 - The agent downloads the artifact from the object store into the container
- The agent extracts it and runs
start.sh - The
--serviceflag registers a service (svc-1) with the registry, pointing at the instance's IP and port 3000 - The load balancer discovers
svc-1on its next registry poll and adds it as a backend
Check the service
tinyaws service listID INSTANCE PORT STATUS URL
svc-1 i-1 3000 healthy http://10.0.0.2:3000The service is healthy when the agent can reach http://10.0.0.2:3000 on the health check interval.
Hit it through the load balancer
curl http://localhost:8088/The load balancer on port 8088 routes to healthy backends. With one service registered, every request goes to svc-1.
You can also hit the instance directly, bypassing the load balancer:
curl http://10.0.0.2:3000/Both return the same response — a directory listing from Python's HTTP server.
Read logs
tinyaws service logs svc-1[2026-02-10 14:23:01] Serving HTTP on 0.0.0.0 port 3000
[2026-02-10 14:23:15] 10.0.0.1 - - "GET / HTTP/1.1" 200 -
[2026-02-10 14:23:18] 10.0.0.1 - - "GET / HTTP/1.1" 200 -Logs are captured from the process's stdout and stderr. The 10.0.0.1 source is the bridge IP — requests from the host (including the load balancer) come through the bridge.
For a live tail:
tinyaws service logs svc-1 --followStop the service
tinyaws service stop svc-1This sends SIGTERM to the service process inside the container and deregisters the service from the registry. The load balancer stops routing to it on its next poll.
The instance is still running. You can deploy something else to it, or shell into it:
tinyaws instance shell i-1That drops you into an interactive shell inside the systemd-nspawn container via machinectl shell.
Terminate the instance
tinyaws instance terminate i-1Terminating instance i-1...
Status: terminatedThe agent stops the systemd-nspawn container, removes the overlay filesystem, releases the IP address, and deregisters the instance from the registry. The controller picks up the termination on its next poll and confirms cleanup.
After termination:
tinyaws instance listID TYPE STATUS IP AGENT
(no instances)The full cycle
Five commands:
tinyaws instance launch --type small
tinyaws deploy ./my-app --service --port 3000
curl http://localhost:8088/
tinyaws service stop svc-1
tinyaws instance terminate i-1Launch, deploy, access, stop, terminate. That's the workflow for everything. Larger deployments have more instances and more services, but the commands are the same.
Troubleshooting
Instance stuck in "provisioning"
The agent hasn't flipped the status yet. Common causes:
- Agent not running. Check
ps aux | grep ec2-agent. If it's not there, start it. - Agent not registered. Hit the registry directly:
curl http://localhost:9000/nodes. If the agent isn't listed, it can't reach the registry. CheckREGISTRY_URL. - Base rootfs missing. The agent can't create an overlay if
/var/lib/tinyaws/basedoesn't exist. Runbootstrap-rootfs.sh.
Service not reachable through load balancer
The load balancer routes to services it finds in the registry.
- Service not registered.
curl http://localhost:9000/services— is your service listed? - Load balancer not polling. Check the LB logs. It polls every 5 seconds.
- Wrong port. The
--portflag intinyaws deploymust match the port the app actually listens on.
No healthy backends
The load balancer returns 502 Bad Gateway when it has no healthy backends.
- Agent heartbeat stopped. The agent sends a heartbeat every 10 seconds. If it crashes, the registry marks its services unhealthy after 30 seconds.
- Health check failing. The agent checks
http://<instance-ip>:<port>/— if the app isn't responding, the service is marked unhealthy.
Deploy fails with "no instances available"
The scheduler has no agents registered, or all instances are at capacity.
- Check the registry:
curl http://localhost:9000/nodes - Check the scheduler:
curl http://localhost:9001/status
One instance, one app, one load balancer route. That's the simplest possible deploy. Everything from here builds on this.
When you want the same pipeline with a real workload — Rust compile, model download, Windows laptop talking to an Arch agent — see Chapter 49b: Deploying Kokoro TTS.

