# Runbook: Oracle Cloud Always Free VPS (4 OCPU ARM, 24 GB RAM, 200 GB disk)

You are an AI coding agent running on the user's machine. Guide the user, step by step, to a working Oracle Cloud Always Free VPS (Ampere A1 ARM, up to 4 OCPUs, 24 GB RAM, 200 GB block storage), then connect to it over SSH.

Human-readable guide: https://alexisamz.com/blog/oracle-vps-gratuit (FR) | https://alexisamz.com/en/blog/oracle-vps-free (EN).

## How to run this runbook

- Reply in the user's language (French if the conversation is in French).
- **Go one checkpoint at a time.** Each checkpoint below ends with a question. Ask it, wait for the answer, and only then continue. Never batch several checkpoints into one message.
- **Never assume the user's situation.** Their OS, account state, region, and SSH setup are unknown until they tell you or a command shows it.
- Keep messages short: what we are doing, the one action for the user (or the command you will run), the question.
- Show each command before running anything that changes their system, and ask before overwriting `~/.oci/config` or any SSH file.
- Never print, log, or echo private keys (OCI API `.pem` or SSH private key). Never commit credentials.
- Never ask the user to paste a private key into the chat, and never `cat` a `.pem` file: move or copy it with `mv`/`cp` only. The configuration preview (OCIDs, fingerprint, region) contains no secret and can be pasted.
- If something fails, explain the error in plain words, propose one fix, and ask before applying it.
- No em-dashes: use colons, periods, commas, or bullet points.

## Checkpoint 0: Where are we?

Ask the user, in one message:

1. Which system are you on: macOS, Linux, or Windows?
2. Do you already have an Oracle Cloud account? (yes / no / started but not finished)
3. If yes: which Home Region did you pick? (shown top right of the console, e.g. `Germany Central (Frankfurt)`)

Adapt every following step to the answers. On Windows, prefer PowerShell commands and paths under `$HOME\.oci` and `$HOME\.ssh`; skip `chmod`.

## Checkpoint 1: Oracle Cloud account (browser, done by the user)

Skip if the account already exists. You cannot create the account: it requires identity and card verification in a browser (a temporary authorization, not a charge). Explain this, then give the user these steps:

1. Open https://www.oracle.com/cloud/free/ and click "Start for free".
2. **Home Region: choose carefully, it cannot be changed later.** Pick a region close to you, but avoid the most popular ones: busy regions often have no free ARM capacity and return `Out of host capacity` for days. For users in France, be firm: **France South (Marseille), not Paris.** The author of this guide ran a retry script every 2 to 3 minutes all night in Paris without ever getting a machine, then created a new account in Marseille and got the 4 OCPU / 24 GB server immediately. If a French user already has a Paris account, explain this, and suggest creating a new account in Marseille rather than waiting. Elsewhere, prefer a smaller region near the user over the biggest hub.
3. Finish the card verification and wait for the "account ready" email (can take a few minutes up to an hour).

Ask: "Is your account created and can you log in to the console? Which Home Region did you end up with?"

If they already have an account in a busy region, reassure them: it still works, the script below retries automatically until capacity frees up. It may just take longer.

## Checkpoint 2: API key (browser, done by the user)

Give these steps:

1. In the console, click the profile icon (top right), then "My profile" (or "User settings").
2. In "Resources" (bottom left), click "API keys", then "Add API key".
3. Choose "Generate API key pair", click "Download private key" (a `.pem` file), then "Add".
4. Oracle shows a configuration preview (`user`, `fingerprint`, `tenancy`, `region`). Copy it.

Ask: "Paste the configuration preview here (it contains no secret), and tell me where the downloaded `.pem` file is (usually your Downloads folder)."

## Checkpoint 3: Local setup (you run, after confirmation)

1. Check Python 3: `python3 --version` (Windows: `py --version`). If missing, tell the user how to install it for their OS and wait.
2. Check for an SSH key: list `~/.ssh/*.pub`. If none, propose `ssh-keygen -t ed25519 -C "oracle-vps"` and run it only after a yes.
3. Create `~/.oci/`, copy the `.pem` there as `oci_api_key.pem` (macOS/Linux: `chmod 700 ~/.oci && chmod 600 ~/.oci/oci_api_key.pem`).
4. Write `~/.oci/config` from the pasted preview, with the user's real region and an absolute `key_file` path:
   ```ini
   [DEFAULT]
   user=<USER_OCID>
   fingerprint=<FINGERPRINT>
   tenancy=<TENANCY_OCID>
   region=<THEIR_HOME_REGION>
   key_file=<ABSOLUTE_PATH>/.oci/oci_api_key.pem
   ```
   If the file already exists, show the diff and ask before changing it.
5. Install the SDK: `python3 -m pip install --user oci`.
6. Test the credentials: `python3 -c "import oci; c=oci.config.from_file(); print(oci.identity.IdentityClient(c).get_user(c['user']).data.name)"`.

Ask: "Your Oracle credentials work (I can see the user `<name>`). How do you want to create the server?
   1. I run the script for you and report progress.
   2. I save the script as `oracle_vps.py` and you run it yourself with `python3 oracle_vps.py`."

Either way, explain that the script first asks for the biggest free machine (4 OCPU / 24 GB), then automatically tries 2 / 12 and 1 / 6 if the region has no room, and loops until it gets one.

## Checkpoint 4: Network, launch, and SSH (single script)

Save this as `oracle_vps.py` and run it with `python3 oracle_vps.py`. It is one script on purpose: network, launch, retry, and IP retrieval share state. It is idempotent: it reuses an existing VCN, public subnet, and instance named `vps-arm-free`.

Target: `VM.Standard.A1.Flex`, 4 OCPUs, 24 GB RAM, 200 GB boot volume, Canonical Ubuntu 24.04 aarch64.

```python
import os, sys, time
import oci

# Biggest Always Free config first, then smaller ones if the region has no room
SHAPES = [(4.0, 24.0), (2.0, 12.0), (1.0, 6.0)]
BOOT_GB = 200
NAME = "vps-arm-free"
RETRY_SECONDS = 85  # below this, OCI answers 429 TooManyRequests

config = oci.config.from_file("~/.oci/config")
tenancy = config["tenancy"]
network = oci.core.VirtualNetworkClient(config)
compute = oci.core.ComputeClient(config)
identity = oci.identity.IdentityClient(config)
M = oci.core.models

# SSH public key: ed25519 first, then rsa
pub = next((os.path.expanduser(p) for p in ("~/.ssh/id_ed25519.pub", "~/.ssh/id_rsa.pub")
            if os.path.exists(os.path.expanduser(p))), None)
if not pub:
    sys.exit("No SSH public key found. Run Step 1 first.")
ssh_key = open(pub).read().strip()

# --- Network: VCN + Internet Gateway + route + public subnet
vcns = network.list_vcns(tenancy).data
vcn = vcns[0] if vcns else network.create_vcn(M.CreateVcnDetails(
    cidr_block="10.0.0.0/16", display_name="VCN-ALWAYS-FREE", compartment_id=tenancy)).data
vcn = oci.wait_until(network, network.get_vcn(vcn.id), "lifecycle_state", "AVAILABLE").data

igws = network.list_internet_gateways(tenancy, vcn_id=vcn.id).data
igw = igws[0] if igws else network.create_internet_gateway(M.CreateInternetGatewayDetails(
    compartment_id=tenancy, vcn_id=vcn.id, is_enabled=True, display_name="IGW-ALWAYS-FREE")).data

rt = network.get_route_table(vcn.default_route_table_id).data
if not any(r.destination == "0.0.0.0/0" for r in rt.route_rules):
    network.update_route_table(rt.id, M.UpdateRouteTableDetails(route_rules=rt.route_rules + [
        M.RouteRule(destination="0.0.0.0/0", destination_type="CIDR_BLOCK", network_entity_id=igw.id)]))

subnets = [s for s in network.list_subnets(tenancy, vcn_id=vcn.id).data if not s.prohibit_public_ip_on_vnic]
subnet = subnets[0] if subnets else network.create_subnet(M.CreateSubnetDetails(
    compartment_id=tenancy, vcn_id=vcn.id, cidr_block="10.0.0.0/24", display_name="SUBNET-PUBLIC",
    route_table_id=vcn.default_route_table_id, security_list_ids=[vcn.default_security_list_id])).data
subnet = oci.wait_until(network, network.get_subnet(subnet.id), "lifecycle_state", "AVAILABLE").data

# --- Instance: reuse if it exists, otherwise launch with retry on capacity errors
existing = [i for i in compute.list_instances(tenancy, display_name=NAME).data
            if i.lifecycle_state not in ("TERMINATED", "TERMINATING")]
if existing:
    instance = existing[0]
else:
    images = compute.list_images(tenancy, operating_system="Canonical Ubuntu", operating_system_version="24.04",
                                 shape="VM.Standard.A1.Flex", sort_by="TIMECREATED", sort_order="DESC").data
    image_id = next(i.id for i in images if "Minimal" not in i.display_name)
    ads = [a.name for a in identity.list_availability_domains(tenancy).data]
    details = lambda ad, ocpus, mem: M.LaunchInstanceDetails(
        display_name=NAME, compartment_id=tenancy, availability_domain=ad, shape="VM.Standard.A1.Flex",
        shape_config=M.LaunchInstanceShapeConfigDetails(ocpus=ocpus, memory_in_gbs=mem),
        source_details=M.InstanceSourceViaImageDetails(image_id=image_id, boot_volume_size_in_gbs=BOOT_GB),
        create_vnic_details=M.CreateVnicDetails(subnet_id=subnet.id, assign_public_ip=True),
        metadata={"ssh_authorized_keys": ssh_key})
    instance, attempt = None, 0
    while instance is None:
        for ocpus, mem in SHAPES:
            for ad in ads:
                attempt += 1
                try:
                    instance = compute.launch_instance(details(ad, ocpus, mem)).data
                    print(f"Got {int(ocpus)} OCPU / {int(mem)} GB")
                    break
                except oci.exceptions.ServiceError as e:
                    if e.status == 500 and "capacity" in e.message.lower():
                        print(f"[{attempt}] {int(ocpus)} OCPU / {int(mem)} GB: out of host capacity")
                    elif e.status == 429:
                        print(f"[{attempt}] rate limited")
                    else:
                        raise
            if instance is not None:
                break
        if instance is None:
            time.sleep(RETRY_SECONDS)

print("Waiting for RUNNING...")
instance = oci.wait_until(compute, compute.get_instance(instance.id), "lifecycle_state", "RUNNING",
                          max_wait_seconds=900).data

# --- Public IP
vnic_id = compute.list_vnic_attachments(tenancy, instance_id=instance.id).data[0].vnic_id
ip = network.get_vnic(vnic_id).data.public_ip
key = pub[:-4]
print(f"ssh -i {key} ubuntu@{ip}")
```

While it retries, tell the user what is happening: their region has no free ARM capacity right now, the script tries again every 85 seconds and can safely run for hours. If they only want the full 4 OCPU / 24 GB machine, set `SHAPES = [(4.0, 24.0)]`. If they got a smaller machine, tell them they can resize it later from the console when capacity frees up (Always Free allows up to 4 OCPU / 24 GB in total).

## Checkpoint 5: SSH verification

Run the printed command, accept the host fingerprint, then validate:

```sh
uname -m && nproc && free -h && df -h /
```

Expected: `aarch64`, `4`, about 23 GiB of memory, and about 190 GB of disk. Report these values to the user, then ask what they want to do next (secure the server, install Docker, host a site...).
