Create an Oracle Cloud account and auto-provision a free ARM server (up to 4 OCPUs, 24 GB RAM, 200 GB disk). This guide is written for both humans following along and LLMs scripting the automation.
Oracle's Always Free tier includes Ampere ARM instances that never expire. The total allocation per account:
| Resource | Allocation |
|---|---|
| CPU | 4 OCPUs (ARM Ampere A1) |
| Memory | 24 GB |
| Boot Volume | 200 GB (2x 47 GB default, or 1x 200 GB) |
| Network | 4 Gbps aggregate, public IPv4 |
| Outbound Data | 10 TB / month |
| Shape | VM.Standard.A1.Flex |
| OS Options | Ubuntu, Oracle Linux, CentOS (ARM64) |
You can split the 4 OCPU / 24 GB across up to 4 instances, or use it all on one. This guide provisions a single max instance.
Once signed in, note your Tenancy OCID from the Console:
# OCI Console → Profile (top-right) → Tenancy: <name> # Copy the OCID — looks like: # ocid1.tenancy.oc1..aaaaaaaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
You'll need four identifiers from the console. Here's where to find each:
| Value | Where to Find |
|---|---|
| Tenancy OCID | Profile menu → Tenancy → OCID |
| User OCID | Profile menu → My Profile → OCID |
| Compartment OCID | Identity → Compartments → root compartment OCID (same as tenancy for personal accounts) |
| Subnet OCID | Networking → Virtual Cloud Networks → your VCN → Subnets → public subnet OCID |
Create a dedicated key pair for your Oracle instance:
# Generate an Ed25519 key (recommended) or RSA 4096 ssh-keygen -t ed25519 -f ~/.ssh/oracle_key -C "oracle-arm" -N "" # Or RSA if your OCI image doesn't support Ed25519: # ssh-keygen -t rsa -b 4096 -f ~/.ssh/oracle_key -C "oracle-arm" -N "" # Verify both files exist ls -la ~/.ssh/oracle_key ~/.ssh/oracle_key.pub
The .pub file contents will be passed to the instance at creation. The private key stays on your machine.
# macOS (Homebrew) brew install oci-cli # Linux / other bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)" # Verify oci --version
Run the setup wizard to generate your API key and config:
# Interactive setup — generates ~/.oci/config and API keys oci setup config # You'll be prompted for: # 1. User OCID → from Step 2 # 2. Tenancy OCID → from Step 2 # 3. Home Region → e.g., us-phoenix-1 # 4. API key location → accept default ~/.oci/oci_api_key.pem
oci setup config, you must add the generated public key to your OCI user:~/.oci/oci_api_key_public.pem
Verify it works:
# Should return your tenancy details without error
oci iam tenancy get --tenancy-id <YOUR_TENANCY_OCID>
Get the latest Ubuntu ARM image for your region:
# List Ubuntu ARM images in your region oci compute image list \ --compartment-id <YOUR_TENANCY_OCID> \ --operating-system "Canonical Ubuntu" \ --shape "VM.Standard.A1.Flex" \ --sort-by TIMECREATED \ --sort-order DESC \ --query 'data[0:3].{id:id, name:"display-name", created:"time-created"}' \ --output table # Copy the OCID of the image you want (usually the newest)
# List availability domains for your region oci iam availability-domain list \ --compartment-id <YOUR_TENANCY_OCID> \ --query 'data[*].name' \ --raw-output # Multi-AD regions (Phoenix, Ashburn, etc.) return 3 ADs # Single-AD regions return 1 AD
This script retries oci compute instance launch every 60 seconds, rotating through availability domains, until capacity opens. Save it as ~/bin/oracle-auto-provision.sh.
CONFIGURATION block below. All OCIDs were collected in Steps 2 and 4. The script is self-contained with no external dependencies beyond the OCI CLI.
#!/bin/bash ######################################################################## # Oracle Cloud Always Free ARM Auto-Provisioning Script ######################################################################## # Retries creating an ARM instance every 60s until capacity opens. # Rotates through all availability domains in the region. # # Usage: chmod +x oracle-auto-provision.sh && ./oracle-auto-provision.sh # Monitor: tail -f /tmp/oracle-provision.log # Stop: kill $(cat /tmp/oracle-provision.pid) ######################################################################## export SUPPRESS_LABEL_WARNING=True # ====================== CONFIGURATION ====================== # Replace these 6 values with your own OCIDs from Steps 2-4. COMPARTMENT_ID="ocid1.tenancy.oc1..YOUR_TENANCY_OCID" SUBNET_ID="ocid1.subnet.oc1.REGION.YOUR_SUBNET_OCID" IMAGE_ID="ocid1.image.oc1.REGION.YOUR_IMAGE_OCID" SSH_KEY_FILE="$HOME/.ssh/oracle_key.pub" INSTANCE_NAME="my-arm-server" SHAPE="VM.Standard.A1.Flex" # Shape config — full Always Free allocation OCPUS=4 MEMORY_GB=24 BOOT_VOLUME_GB=200 # Retry interval in seconds RETRY_INTERVAL=60 # Availability domains — run the command from Step 4 to find yours. # Single-AD regions: use just one entry. Multi-AD regions: list all. ADS=( "YOUR-AD-1" # "YOUR-AD-2" # uncomment for multi-AD regions # "YOUR-AD-3" # uncomment for multi-AD regions ) # Optional: email notification on success (requires RESEND_API_KEY env var) NOTIFY_EMAIL="" # e.g., "you@example.com" # =================== END CONFIGURATION ===================== # --- Single-instance lock (prevents duplicate scripts) --- LOCKDIR="/tmp/oracle-provision.lock" PIDFILE="$LOCKDIR/pid" acquire_lock() { if mkdir "$LOCKDIR" 2>/dev/null; then echo $$ > "$PIDFILE" return 0 fi local old_pid old_pid=$(cat "$PIDFILE" 2>/dev/null) if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then echo "Another instance is running (PID $old_pid). Exiting." return 1 fi rm -rf "$LOCKDIR" if mkdir "$LOCKDIR" 2>/dev/null; then echo $$ > "$PIDFILE" return 0 fi return 1 } if ! acquire_lock; then exit 0 fi echo $$ > /tmp/oracle-provision.pid cleanup() { rm -rf "$LOCKDIR" /tmp/oracle-provision.pid; } trap cleanup EXIT log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } AD_COUNT=${#ADS[@]} AD_INDEX=0 log "==========================================" log "Oracle ARM Auto-Provisioning (PID $$)" log "$OCPUS OCPU, ${MEMORY_GB}GB RAM, ${BOOT_VOLUME_GB}GB boot" log "Retry every ${RETRY_INTERVAL}s across $AD_COUNT AD(s)" log "==========================================" attempt=0 while true; do attempt=$((attempt + 1)) ad="${ADS[$AD_INDEX]}" log "Attempt #$attempt — $ad" result=$(oci compute instance launch \ --compartment-id "$COMPARTMENT_ID" \ --availability-domain "$ad" \ --shape "$SHAPE" \ --shape-config "{\"ocpus\":$OCPUS,\"memoryInGBs\":$MEMORY_GB}" \ --image-id "$IMAGE_ID" \ --subnet-id "$SUBNET_ID" \ --display-name "$INSTANCE_NAME" \ --assign-public-ip true \ --boot-volume-size-in-gbs "$BOOT_VOLUME_GB" \ --ssh-authorized-keys-file "$SSH_KEY_FILE" \ 2>&1) || true if echo "$result" | grep -qi "out of.*capacity"; then log " Out of capacity — retrying" elif echo "$result" | grep -qi "LimitExceeded"; then log " Limit exceeded — retrying" elif echo "$result" | grep -qi "ServiceError\|RequestException"; then msg=$(echo "$result" | grep '"message"' | head -1 | \ sed 's/.*"message": "\(.*\)".*/\1/') log " Error: $msg" elif echo "$result" | grep -q '"lifecycle-state"'; then # Success — extract instance ID and wait for RUNNING instance_id=$(echo "$result" | \ python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])" \ 2>/dev/null) log " PROVISIONING! ID: $instance_id" log " Waiting for RUNNING state..." oci compute instance get \ --instance-id "$instance_id" \ --wait-for-state RUNNING \ --wait-interval-seconds 15 > /dev/null 2>&1 || true sleep 10 public_ip=$(oci compute instance list-vnics \ --instance-id "$instance_id" \ --query 'data[0]."public-ip"' \ --raw-output 2>/dev/null) log "" log "==========================================" log "INSTANCE CREATED!" log "IP: $public_ip" log "SSH: ssh -i ~/.ssh/oracle_key ubuntu@$public_ip" log "ID: $instance_id" log "==========================================" # Write details to file for other scripts to consume cat > /tmp/oracle-instance-details.txt <<EOF IP=$public_ip INSTANCE_ID=$instance_id SSH=ssh -i ~/.ssh/oracle_key ubuntu@$public_ip SHAPE=$OCPUS OCPU / ${MEMORY_GB}GB EOF # macOS notification osascript -e "display notification \"IP: $public_ip\" \ with title \"Oracle ARM Instance Created!\" \ sound name \"Glass\"" 2>/dev/null || true # Optional email notification if [ -n "$NOTIFY_EMAIL" ] && [ -n "${RESEND_API_KEY:-}" ]; then curl -s -X POST "https://api.resend.com/emails" \ -H "Authorization: Bearer $RESEND_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"from\": \"noreply@yourdomain.com\", \"to\": [\"$NOTIFY_EMAIL\"], \"subject\": \"Oracle ARM Instance Provisioned!\", \"html\": \"<h2>Oracle ARM Instance Created!</h2><p><b>IP:</b> ${public_ip}</p><p><b>SSH:</b> <code>ssh -i ~/.ssh/oracle_key ubuntu@${public_ip}</code></p>\" }" > /dev/null 2>&1 \ && log " Email sent to $NOTIFY_EMAIL" \ || log " Email send failed" fi exit 0 else log " Unexpected: ${result:0:120}" fi AD_INDEX=$(( (AD_INDEX + 1) % AD_COUNT )) sleep "$RETRY_INTERVAL" done
# Save and make executable chmod +x ~/bin/oracle-auto-provision.sh # Run it (logs to stdout, redirect for background use) ~/bin/oracle-auto-provision.sh 2>&1 | tee /tmp/oracle-provision.log # Or run in background nohup ~/bin/oracle-auto-provision.sh >> /tmp/oracle-provision.log 2>&1 &
If you provisioned with a smaller shape to improve your odds (e.g., 1 OCPU / 6 GB), resize to the full allocation after creation:
# Get your instance OCID oci compute instance list \ --compartment-id <YOUR_COMPARTMENT_OCID> \ --lifecycle-state RUNNING \ --query 'data[*].{id:id, name:"display-name", ocpus:"shape-config".ocpus, mem:"shape-config"."memory-in-gbs"}' \ --output table # Stop the instance oci compute instance action \ --instance-id <INSTANCE_OCID> \ --action STOP # Wait for STOPPED state, then resize oci compute instance update \ --instance-id <INSTANCE_OCID> \ --shape "VM.Standard.A1.Flex" \ --shape-config '{"ocpus": 4, "memoryInGBs": 24}' \ --force # Wait ~30 seconds for resize to apply, then start oci compute instance action \ --instance-id <INSTANCE_OCID> \ --action START
# SSH in ssh -i ~/.ssh/oracle_key ubuntu@<PUBLIC_IP> # Update packages sudo apt update && sudo apt upgrade -y # Set hostname sudo hostnamectl set-hostname my-arm-server # Set timezone sudo timedatectl set-timezone America/Chicago # Enable firewall (allow SSH first!) sudo iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT sudo iptables -I INPUT 2 -p tcp --dport 80 -j ACCEPT sudo iptables -I INPUT 3 -p tcp --dport 443 -j ACCEPT sudo netfilter-persistent save # Install common tools sudo apt install -y htop curl wget git unzip jq
iptables on the instance, you must also open ports in the OCI Console:To have the script auto-start and retry continuously on your Mac:
# Create the LaunchAgent plist cat > ~/Library/LaunchAgents/com.oracle.arm-provision.plist <<'EOF' <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.oracle.arm-provision</string> <key>ProgramArguments</key> <array> <string>/bin/bash</string> <string>-c</string> <string>$HOME/bin/oracle-auto-provision.sh</string> </array> <key>StandardOutPath</key> <string>/tmp/oracle-provision.log</string> <key>StandardErrorPath</key> <string>/tmp/oracle-provision.log</string> <key>RunAtLoad</key> <true/> <!-- Restart every 5 minutes if it exits (covers crashes) --> <key>StartInterval</key> <integer>300</integer> <!-- Don't restart if success flag exists --> <key>WatchPaths</key> <array> <string>/tmp/oracle-instance-details.txt</string> </array> <key>EnvironmentVariables</key> <dict> <key>PATH</key> <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string> <!-- Set RESEND_API_KEY here for email notifications --> </dict> </dict> </plist> EOF # Load it launchctl load ~/Library/LaunchAgents/com.oracle.arm-provision.plist # Check status launchctl list | grep oracle # Stop and unload after provisioning launchctl unload ~/Library/LaunchAgents/com.oracle.arm-provision.plist
| Problem | Cause & Fix |
|---|---|
Out of host capacity |
Normal — ARM is popular. The script will keep retrying. Can take hours to weeks. Try off-peak hours (early morning, weekends) or a different region. |
LimitExceeded |
You've hit the Always Free shape limit. Check if you already have ARM instances: oci compute instance list --compartment-id ... --query 'data[?contains("shape",`A1`)]' |
NotAuthenticated |
API key not uploaded or expired. Re-run oci setup config and upload the public key to the OCI Console. |
Authorization failed |
User OCID or tenancy OCID is wrong in ~/.oci/config. Double-check them in the Console. |
| Script exits immediately | Lock file exists from a previous run. Remove it: rm -rf /tmp/oracle-provision.lock |
| Instance created but no public IP | Subnet may not auto-assign public IPs. Assign one manually: Compute → Instances → your instance → Attached VNICs → Primary VNIC → IPv4 → Edit → check "Public IP". |
| SSH connection refused | Wait 2-3 minutes after creation for cloud-init to finish. Check security lists allow TCP 22 inbound. |
409 Conflict during resize |
Instance still processing a previous change. Wait 30-60 seconds and retry. |
# 1. Generate SSH key ssh-keygen -t ed25519 -f ~/.ssh/oracle_key -C "oracle-arm" -N "" # 2. Install OCI CLI brew install oci-cli # macOS # 3. Configure OCI CLI (interactive — needs USER_OCID, TENANCY_OCID, REGION) oci setup config # 4. Upload API public key to OCI Console (manual step) cat ~/.oci/oci_api_key_public.pem # copy this to Console → API Keys # 5. Find required values # ADs: oci iam availability-domain list --compartment-id $TENANCY --query 'data[*].name' # Image: oci compute image list --compartment-id $TENANCY \ --operating-system "Canonical Ubuntu" --shape "VM.Standard.A1.Flex" \ --sort-by TIMECREATED --sort-order DESC --query 'data[0].id' --raw-output # Subnet: oci network subnet list --compartment-id $TENANCY --query 'data[0].id' --raw-output # 6. Launch (single attempt — wrap in loop for auto-retry) oci compute instance launch \ --compartment-id "$TENANCY" \ --availability-domain "$AD" \ --shape "VM.Standard.A1.Flex" \ --shape-config '{"ocpus":4,"memoryInGBs":24}' \ --image-id "$IMAGE" \ --subnet-id "$SUBNET" \ --display-name "my-arm-server" \ --assign-public-ip true \ --boot-volume-size-in-gbs 200 \ --ssh-authorized-keys-file ~/.ssh/oracle_key.pub # Expected "Out of host capacity" responses are normal. # Retry every 60s, rotating ADs, until success. # On success: response contains "lifecycle-state": "PROVISIONING" # Then wait for RUNNING, extract public IP from list-vnics.