OpenTofu modules for Oracle Cloud's Always Free tier. One apply gives you a 2-core ARM box with 12 GB RAM, 200 GB combined boot/block storage, and optional MySQL, S3 storage, monitoring, and budget alerts.
| Resource | Spec | Default |
|---|---|---|
| Compute | VM.Standard.A1.Flex — 2 OCPUs, 12 GB RAM | On |
| Boot Storage | 50 GB + weekly backups | On |
| Block Storage | 150 GB at /data |
On |
| MySQL HeatWave | 50 GB, private subnet, Oracle-managed version | Off |
| Object Storage | 20 GB combined on Always Free-only accounts | Off |
| Monitoring | Idle-detection alarms (reclaim prevention) | Off |
| Budget Alerts | Email on any spend + at 50% / 80% / 100% | Off |
/setup
Walks you through everything interactively.
1. OCI account — Sign up at cloud.oracle.com/free. Pick your Home Region carefully (can't change it). Keep the account in its Always Free profile if avoiding all billable usage is the goal.
2. Tools
brew install oci-cli opentofu just # macOS3. OCI CLI
oci setup configUpload the generated public key: OCI Console > My Profile > API Keys.
4. Deploy
./generate-env.sh # random passphrase for state encryption — back this up
just setup # auto-discovers OCI config, generates tfvars
just init && just plan && just apply5. Connect
just ssh-allow # whitelist your IP for SSH
just ssh # connect
just ssh-revoke # close SSH when doneManual setup (without just)
./generate-env.sh
cd terraform/environments/oci-prod
cp oci-prod.auto.tfvars.example oci-prod.auto.tfvars
# Fill in: compartment_ocid, tenancy_ocid, user_ocid, availability_domain, ssh_public_key
cd ../../..
just init && just plan && just applyFind these values with:
grep tenancy ~/.oci/config # compartment_ocid
grep user ~/.oci/config # user_ocid
oci iam availability-domain list --query 'data[0].name' --raw # availability_domain
cat ~/.ssh/id_ed25519.pub # ssh_public_keyCore required values are the compartment, availability domain, and SSH key. Add the tenancy OCID for home-region discovery and the user OCID when enabling Object Storage; everything else has defaults:
| Setting | Default | Override |
|---|---|---|
| Compute | 2 OCPUs, 12 GB (ARM) | ocpus, memory_in_gbs |
| OS | Ubuntu 24.04 Minimal aarch64 | operating_system, os_version |
| Storage | 50 GB boot + 150 GB block | boot_volume_size_gb, enable_block_volume |
| Public access | On (ports 80, 443) | enable_public_access = false |
| SSH | Blocked | just ssh-allow |
| MySQL | Off | enable_mysql = true |
| Object Storage | Off | enable_object_storage = true |
| Idle alerts | Off | enable_idle_alerts = true |
| Budget alerts | Off | set alert_email |
enable_public_access = true # ports 80, 443 open (default)
enable_public_access = false # SSH only
additional_tcp_ports = [80, 443] # customize when public
additional_udp_ports = [51820] # e.g. WireGuardOracle reclaims Always Free instances that stay idle for 7 days. All three must hold simultaneously over the window:
- CPU (95th percentile) < 20%
- Memory < 20%
- Network < 20%
Keep at least one metric above 20% and your instance won't be reclaimed. A cron job, a small web server, or any background process is usually enough.
Enable alerts to get daily warnings after the one-day metric window, giving you time to act:
alert_email = "[email protected]"
enable_idle_alerts = trueSee OCI idle instance reclaim docs for full details.
enable_mysql = true # 50 GB HeatWave, private subnet, SSH tunnel access
enable_object_storage = true # 20 GB combined on Always Free-only accountsThe 200 GB free tier is split 50/150 by default. Block volume is auto-formatted and mounted at /data via cloud-init.
# Alternative: single disk
boot_volume_size_gb = 200
enable_block_volume = falseThe instance is in a public subnet because OCI NAT gateways aren't free. MySQL (when enabled) goes in a private subnet with no internet route — access it via SSH tunnel.
SSH is blocked by default. Use just ssh-allow to whitelist your IP, just ssh-revoke to close it.
Cloud-init on first boot opens iptables, hardens SSH, and enables unattended upgrades. Runs once — changes require instance recreation.
The test environment was applied and verified on 2026-07-14:
- Compute is
VM.Standard.A1.Flexat 2 OCPUs and 12 GB RAM. - Storage is 50 GB boot plus 150 GB block volume; the existing block volume was retained and reattached during the compute replacement.
- MySQL HeatWave remains enabled in the private subnet; Oracle reported MySQL version 9.7.1 at verification time.
- The OCI S3 backend (
terraform-state) initialized successfully. - A post-apply
just planreported no changes, andnix develop -c just checkpassed.
The observed MySQL version is informational only: Oracle manages upgrades for the Always Free MySQL service, so the configuration intentionally does not pin a version.
| Module | What it does |
|---|---|
oci-network |
VCN, subnets, internet gateway, security lists |
oci-compute |
ARM instance, backup policy |
oci-storage |
Block volume + attachment |
oci-mysql-heatwave |
Always Free MySQL |
oci-object-storage |
S3-compatible bucket, lifecycle, credentials |
oci-monitoring |
Idle and high-utilization alarms |
oci-budget-alerts |
Spend alerts at configurable thresholds |
| Recipe | Description |
|---|---|
just setup |
Auto-discover OCI config, generate tfvars |
just init |
Initialize providers |
just plan |
Preview changes |
just apply |
Deploy |
just destroy |
Tear down |
just fmt |
Format .tf files |
just validate |
Validate config |
just output |
Show outputs |
just ssh |
SSH to instance |
just ssh-allow |
Whitelist your IP for SSH |
just ssh-revoke |
Revoke SSH access |
just mysql-tunnel |
Tunnel MySQL to localhost:3306 |
just my-ip |
Print your public IP |
State is encrypted at rest (AES-GCM + PBKDF2). The passphrase lives in .env, which is gitignored and loaded by just automatically. Back up .env — losing the passphrase means losing your state.
This environment uses the OCI Object Storage S3 backend configured in terraform/environments/oci-prod/main.tf. Keep it active for locking, versioning, and offsite backup.
OCI Object Storage — S3-compatible, stays in OCI, negligible free tier impact.
backend "s3" {
bucket = "terraform-state"
key = "oci-prod/terraform.tfstate"
region = "us-sanjose-1"
endpoints = { s3 = "https://axeolpvc5niy.compat.objectstorage.us-sanjose-1.oraclecloud.com" }
skip_region_validation = true
skip_credentials_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
use_path_style = true
}The deployed state bucket is terraform-state; create it manually only when setting up a new environment.
Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env (Customer Secret Key from OCI Console).
HCP Terraform — Free up to 500 resources. Locking, versioning, web UI. tofu login + add a cloud {} block.
PostgreSQL — Self-hosted, built-in locking. Add a backend "pg" block.
To migrate: update main.tf, run just init, follow the prompt.
| Resource | Limit | Project Default |
|---|---|---|
| ARM Compute | 2 OCPUs, 12 GB RAM | 2 OCPUs, 12 GB |
| Boot + Block Volume | 200 GB, 5 backups | 50 + 150 GB |
| Object Storage | 20 GB combined across tiers (Always Free-only profile) | Off |
| MySQL HeatWave | 50 GB data + 50 GB backup | Off |
| Outbound Data | 10 TB/month | — |
Oracle has been known to close free-tier accounts without warning. The boot volume backup policy and prevent_destroy rules protect against accidents, but they're inside OCI. If your account goes away, so do your backups.
Keep offsite copies of anything important.

