Always Free Tier

Oracle Cloud ARM Server

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.

1 What You Get (Free)

Oracle's Always Free tier includes Ampere ARM instances that never expire. The total allocation per account:

ResourceAllocation
CPU4 OCPUs (ARM Ampere A1)
Memory24 GB
Boot Volume200 GB (2x 47 GB default, or 1x 200 GB)
Network4 Gbps aggregate, public IPv4
Outbound Data10 TB / month
ShapeVM.Standard.A1.Flex
OS OptionsUbuntu, 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.

Why auto-provision? ARM capacity is limited. Oracle returns "Out of host capacity" most of the time. The script retries every 60 seconds, rotating across availability domains, until an instance lands. This can take hours or weeks.

2 Create an Oracle Cloud Account

  1. Go to cloud.oracle.com and click Sign Up
  2. Enter your email, name, and choose a Home Region
Home Region is permanent. You cannot change it after signup. Pick a region close to your users or with good ARM availability. Regions with multiple availability domains (Phoenix, Ashburn, London, Frankfurt, Tokyo) give you more retry targets.
  1. Add a payment method (credit card required for verification — you will not be charged for Always Free resources)
  2. Complete email verification and wait for account activation (usually instant, sometimes up to 30 minutes)
  3. Sign in to the OCI Console at cloud.oracle.com

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

Collect Required OCIDs

You'll need four identifiers from the console. Here's where to find each:

ValueWhere to Find
Tenancy OCIDProfile menu → Tenancy → OCID
User OCIDProfile menu → My Profile → OCID
Compartment OCIDIdentity → Compartments → root compartment OCID (same as tenancy for personal accounts)
Subnet OCIDNetworking → Virtual Cloud Networks → your VCN → Subnets → public subnet OCID
No VCN yet? Create one: Networking → Virtual Cloud Networks → Start VCN Wizard → "Create VCN with Internet Connectivity". This creates a public subnet with an internet gateway — exactly what you need.

3 Generate SSH Keys

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.

4 Install & Configure the OCI CLI

# 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
Upload your API public key. After oci setup config, you must add the generated public key to your OCI user:
OCI Console → Profile → My Profile → API Keys → Add API Key → Paste Public Key
Paste the contents of ~/.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>

Find the ARM Image 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)

Find Availability Domains

# 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

5 The Auto-Provisioning Script

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.

LLM Note: To adapt this script, replace the 6 variables in the 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 &

6 Post-Provision: Resize & Harden

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

Initial Server Setup

# 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
Oracle security lists. In addition to iptables on the instance, you must also open ports in the OCI Console:
Networking → Virtual Cloud Networks → your VCN → Security Lists → Default → Add Ingress Rules.
Add TCP rules for ports 80 and 443 (source: 0.0.0.0/0).

7 Run on a Schedule (macOS LaunchAgent)

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

8 Troubleshooting

ProblemCause & 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.
Pro tips:
  • Provision with 1 OCPU / 6 GB initially (better capacity odds), then resize to 4 / 24 after creation
  • Each Oracle account gets the same free allocation — a second account in a different region doubles your capacity
  • The instance persists even if your free trial credits expire — Always Free resources are not terminated

LLM Quick Reference

For LLMs scripting this automation: The complete provisioning flow is 6 commands. Here they are in order, with all placeholders marked.
# 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.