All documents
Step by step

Spinning up a GPU, and wiring it to FastAPI

From an empty AWS account to a resident's question answered by our own model, in nine steps. Written from a real attempt, including the two things that stopped it.

Document
OPS-GPU-1
Version
1.0
Date
31 August 2026
Status
Current
Author
Abad Naseer

0Before you start

This takes you from an AWS account with nothing in it to a resident's question answered by a model on hardware we control, with the application falling back to Gemini whenever that hardware is off. It is one ordered list. Follow it top to bottom.

Every step below was carried out for real in the client's own account on 30 August 2026. Where something failed, the failure is written down here rather than smoothed over, because two of them will happen to anybody who repeats this.

The single most important sentence in this document: Ollama has no authentication of any kind. Anything that can reach port 11434 can use the model, read what is being asked and run the bill up. Section 3 is not optional and it is not last.

What you need in front of you:

ThingWhy
An AWS account on a paid planSection 1 explains why the Free Tier plan blocks this outright
An access key with EC2 rightsTo make the machine and, later, to start and stop it
The public addresses of the application machinesThey are the only addresses allowed near the model
Python with boto3The scripts under deploy/gpu/ use it

1Clear the two things that block everything

Both of these are account level, both take time, and neither can be worked around by anybody except the account owner. Start them first and do the rest while they clear.

The Free Tier plan

A new AWS account is on the Free Tier plan, which is not the same thing as the free tier allowance. On that plan, launching anything outside a small list of types is refused outright:

InvalidParameterCombination: The specified instance type is not
eligible for Free Tier.

On the account this was tried in, the only launchable types were t3.micro, t3.small, t4g.micro, t4g.small, c7i-flex.large and m7i-flex.large. No GPU type is on that list, so no quota increase can help until the account is moved to a paid plan in Billing. This surprises people because the error names the instance type, and the instance type is not the problem.

The G quota

Separately, a new account has a quota of 0 running on-demand G instances. A g6.xlarge needs 4 vCPUs of it. Request the increase in Service Quotas, in the region you will actually use, against "Running On-Demand G and VT instances". Approval is usually hours and can be two days.

$ .venv/bin/python deploy/gpu/quota.py
  current   0
  requested 4
  status    PENDING

Both must clear. Clearing one and not the other looks identical from the command line, which is why they are listed as two steps rather than one.

2Make the machine

  • 1Pick the AMI
    Deep Learning Base OSS Nvidia Driver GPU AMI, Ubuntu 22.04. It brings the NVIDIA driver with it, which is the part that is tedious to install by hand.
  • 2Pick the type
    g6.xlarge. One L4, which is the smallest card that answers a retrieval prompt in a few seconds rather than a few minutes.
  • 3100GB of gp3 storage
    The model, the driver and the AMI together do not fit comfortably in less.
  • 4Set shutdown behaviour to stop, not terminate
    This is the one setting that is unrecoverable if it is wrong. The machine shuts itself down when it is idle, and with terminate set, the idle timer deletes it.
  • 5Tag it AutoStop = true
    The safety net in section 5 finds the machine by this tag rather than by an id written into it.
  • 6Paste the user data
    deploy/gpu/userdata.sh. It installs Ollama, pulls the model and installs the idle timer, unattended.

It does not have to be in the same account as the applications, and in this case it is not: the applications live in one account and the GPU in another. That rules out private networking between them, so the model is reached over the public internet and the firewall in section 3 is the only thing in front of it.

3Shut the door

Before the model is installed, not after. A machine with Ollama running and an open 11434 is a free GPU for whoever scans for it, and they scan continuously.

PortFromNever
11434The application addresses, as /320.0.0.0/0
22Your own address, as /320.0.0.0/0
Anything elseNobodyEver
security group  sg-06ad9397831595b43   ollama-gpu
  11434  from 35.91.251.211/32, 54.188.207.85/32, 54.254.25.0/32
  22     from the operator's own address
  and nothing else

Three addresses because there are three agents and each one asks the model directly. Adding a fourth agent means adding a fourth rule, which is a small piece of friction worth keeping.

4Install the model

If the user data ran, this is already done and the check at the end of the section confirms it. By hand:

curl -fsSL https://ollama.com/install.sh | sh
systemctl enable --now ollama

Then pull the model through the HTTP API, not through the command line:

curl -s http://127.0.0.1:11434/api/pull \
  -d '{"name": "llama3.1:8b"}'

This is the bug that cost an afternoon. Running ollama pull from cloud-init dies with panic: $HOME is not defined: the CLI reads $HOME to find its model directory and cloud-init has none. The first unattended build finished with Ollama running and no model in it, which from the outside is indistinguishable from a download that failed. The HTTP API has no such dependency. The idle check in section 5 had the same problem and was moved to the API for the same reason.

The model must be the one the application is configured for:

$ curl -s localhost:11434/api/tags
{"models":[{"name":"llama3.1:8b", ...}]}

5Make it switch itself off

Two mechanisms, because they fail differently. This is the whole difference between roughly $580 a month and roughly $8.

Timer on the machineLambda outside it
Knows when the last question wasYes, from Ollama's journalNo, only CPU
Survives the machine wedgingNoYes
Fires at20 idle minutes45 minutes running under 5% CPU

The on-box timer does the day to day work and is precise. The Lambda catches the machine that has hung, or that somebody started from the console and forgot, and that is the failure that actually costs money, because a timer on a hung machine does not run.

systemctl list-timers ollama-idle-off.timer
sudo /usr/local/bin/ollama-idle-off.sh   # says why it is or is not stopping

6Wire it to FastAPI

Four modules, and they are deliberately small. Nothing else in the application knows a GPU exists.

ModuleIts one job
ai_runtimeWhich engine is switched on. A file, not a database row, so it survives a restart and is readable by eye
gpu_instanceWhere the machine is and whether it is answering. Starts and stops it
ollama_serviceTalks to the model
llmThe router. Everything that wants a sentence written asks this and nothing else

The routing is three lines and the third one is the design:

switch says gemini                     -> Gemini
switch says gpu, and the GPU is ready  -> Ollama, on our own hardware
switch says gpu, and it is not         -> Gemini, and the panel says so

A resident does not care whose hardware answered and must never see a broken assistant because a machine was still booting. What matters is that the panel tells the truth, so nobody demonstrates "our own GPU" to a room while Gemini is quietly doing the work. The panel therefore reports two separate facts: what is switched on, and what actually answered the last question.

Two ways to point at the model

By instance id, which is the normal one. The public address changes every time the machine stops and starts, so it is read from the AWS API rather than written down:

GPU_INSTANCE_ID=i-...
AWS_REGION=us-west-2
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

By hand, which is for a model at a fixed address, such as one on a tunnel or a laptop:

OLLAMA_URL=http://10.8.0.1:11434

OLLAMA_URL did not work until 30 August 2026. The readiness check returned "not configured" whenever AWS credentials were absent, so a perfectly good model at a hand written address was never asked, and every question quietly went to Gemini. Both the state and the health check now short-circuit on OLLAMA_URL before they look at AWS at all, with four tests holding it there.

Set one or the other, never both. Then restart the application.

7Prove it, in five checks

In this order. Each one can fail on its own and each failure means something different.

  • 1The firewall is real
    From a machine that is not one of the three, curl port 11434 and get nothing. From each of the three, get an answer. Assumed boundaries are not boundaries.
  • 2The model generates
    A trivial prompt, straight to the model, timed. On a CPU-only stand-in this took 19 seconds, which is the number that told us the card mattered.
  • 3A real question, answered by our model
    Switch the panel to the GPU and ask a resident's question. It must come back grounded and cited, with the panel saying served_by: gpu and fell_back: false.
  • 4The timeout does what it should
    On the CPU stand-in the first three questions exceeded the 45 second timeout and fell back to Gemini with The GPU stopped answering mid-question. Three residents got correct answers and the panel said who wrote them. That is the design working, not a fault, and 45 seconds stays because an L4 answers in a few.
  • 5Stop the machine and ask again
    The one that matters, because it is what happens in front of a client when somebody forgets to press start. Measured: a correct answer in 1.6 seconds from Gemini, with the panel reporting the fallback and naming the reason.

8When it goes wrong

What you seeWhat it is
Launch refused, naming the instance typeAlmost certainly the Free Tier plan, not the type and not the quota. Section 1
Ollama running, no model in itThe $HOME panic in cloud-init. Pull through the HTTP API. Section 4
Everything answered by Gemini, no error anywhereThe application does not think the GPU is ready. Check the switch, then the health check, then the firewall from the application machine itself
Answers time out then arrive from GeminiWorking as designed. If it happens on a real GPU rather than a stand-in, the model is too big for the card
The machine is gone rather than stoppedShutdown behaviour was left at terminate. It cannot be undone. Section 2
A bill nobody expectedA machine started from the console and forgotten, with the timer wedged. That is what the Lambda is for. Section 5

9What it costs

StateRoughly
Running$0.80 an hour
Stopped$8 a month, for the disk
Left running all month$580

The entire gap between the second row and the third is the auto-off, which is why it has two mechanisms rather than one. Compute is not billed while an instance is stopped; the volume is, whether it runs or not.

Written by Abad Naseer. Every measured number in this document is cited from the running system or from the code, and every target says that it is a target. Where the two disagree, the document says so rather than choosing the flattering one.