The landscape of cloud computing has evolved dramatically, making robust infrastructure accessible even to the leanest startups and individual developers. Oracle Cloud Infrastructure (OCI) stands out with its compelling "Always Free" tier, a set of resources that remain perpetually free, unlike time-limited trials. By 2026, the strategic importance of leveraging these resources for actual production workloads, albeit specific ones, will be undeniable. This masterclass delves into how to meticulously architect and deploy production-grade applications on OCI's Always Free resources, transforming cost centers into innovation hubs.
For many organizations, especially those in their nascent stages or those experimenting with new services, the "Always Free" paradigm is a game-changer. It eliminates the dreaded "bill shock" and provides a stable foundation for applications that might not require hyper-scale immediately but demand reliability and performance. We'll explore the often-underestimated capabilities of OCI's free offerings, demonstrating how careful planning and smart architecture can yield a resilient, performant, and absolutely free production environment.
Prerequisites
Before embarking on building your production workload, ensure you have the following in place:
- Oracle Cloud Infrastructure Account: You need an active OCI account. If you don't have one, sign up for the Free Tier at oracle.com/cloud/free/. This will grant you access to both the 30-day Free Trial with $300 credit and the Always Free resources.
- Basic Understanding of Cloud Concepts: Familiarity with Virtual Cloud Networks (VCNs), subnets, security lists, compute instances, and databases is beneficial.
- OCI CLI Installed and Configured: While the OCI Console is user-friendly, automating deployments and managing resources programmatically is crucial for production environments. Ensure you have the OCI CLI installed and configured on your local machine or a cloud shell instance.
# Install OCI CLI (example for Ubuntu/Debian)
sudo apt update
sudo apt install python3-pip
pip3 install oci-cli --user
# Configure OCI CLI
oci setup config
# Follow the prompts to provide your User OCID, Tenancy OCID, region, and generate an API key.
# This will create a ~/.oci/config file and a private key file.
Verify your configuration by running a simple command:
oci identity availability-domain list --compartment-id <YOUR_TENANCY_OCID>
Replace <YOUR_TENANCY_OCID> with your actual tenancy OCID, which can be found in the OCI Console under Identity & Security -> Tenancy Details.
Detailed Steps with Commands
Our goal is to build a simple, highly available (within Always Free constraints) web application that uses an Autonomous Database. We will provision two Always Free Ampere A1 Compute instances behind a simple Nginx reverse proxy for basic load distribution, connected to an Always Free Autonomous Database.
1. Setting up your Virtual Cloud Network (VCN) and Network Security
A VCN is the fundamental building block for your network in OCI. We'll create a VCN with a public subnet for our compute instances and a private subnet for the Autonomous Database (though ADB can be public, best practice for production is private). We'll use Security Lists to control traffic.
Create a VCN:
# Define variables
VCN_DISPLAY_NAME="ProdVCN-AlwaysFree"
VCN_CIDR_BLOCK="10.0.0.0/16"
COMPARTMENT_ID="ocid1.compartment.oc1..<YOUR_COMPARTMENT_OCID>" # Replace with your compartment OCID
REGION="us-ashburn-1" # Or your preferred region
echo "Creating VCN..."
oci network vcn create \
--compartment-id "${COMPARTMENT_ID}" \
--display-name "${VCN_DISPLAY_NAME}" \
--cidr-block "${VCN_CIDR_BLOCK}" \
--dns-label "prodvcn" \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output > vcn_id.txt
VCN_ID=$(cat vcn_id.txt)
echo "VCN created with ID: ${VCN_ID}"
Create a Public Subnet:
PUBLIC_SUBNET_DISPLAY_NAME="PublicSubnet-AlwaysFree"
PUBLIC_SUBNET_CIDR="10.0.1.0/24"
AVAILABILITY_DOMAIN=$(oci identity availability-domain list --compartment-id "${COMPARTMENT_ID}" --query 'data[0].name' --raw-output)
echo "Creating Public Subnet..."
oci network subnet create \
--compartment-id "${COMPARTMENT_ID}" \
--vcn-id "${VCN_ID}" \
--display-name "${PUBLIC_SUBNET_DISPLAY_NAME}" \
--cidr-block "${PUBLIC_SUBNET_CIDR}" \
--availability-domain "${AVAILABILITY_DOMAIN}" \
--prohibit-public-ip-on-vnic false \
--dns-label "publicsubnet" \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output > public_subnet_id.txt
PUBLIC_SUBNET_ID=$(cat public_subnet_id.txt)
echo "Public Subnet created with ID: ${PUBLIC_SUBNET_ID}"
Create an Internet Gateway and Route Table:
echo "Creating Internet Gateway..."
oci network internet-gateway create \
--compartment-id "${COMPARTMENT_ID}" \
--vcn-id "${VCN_ID}" \
--display-name "ProdVCN-IGW" \
--is-enabled true \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output > igw_id.txt
IGW_ID=$(cat igw_id.txt)
echo "Internet Gateway created with ID: ${IGW_ID}"
echo "Creating Route Table for Public Subnet..."
oci network route-table create \
--compartment-id "${COMPARTMENT_ID}" \
--vcn-id "${VCN_ID}" \
--display-name "PublicRouteTable" \
--route-rules "[{\"cidrBlock\":\"0.0.0.0/0\", \"networkEntityId\":\"${IGW_ID}\"}]" \
--query 'data.id' --raw-output > public_rt_id.txt
PUBLIC_RT_ID=$(cat public_rt_id.txt)
echo "Public Route Table created with ID: ${PUBLIC_RT_ID}"
# Update public subnet to use this route table
oci network subnet update \
--subnet-id "${PUBLIC_SUBNET_ID}" \
--route-table-id "${PUBLIC_RT_ID}" \
--wait-for-state AVAILABLE
echo "Public Subnet updated with Route Table."
Configure Security Lists for Public Subnet:
Allow SSH (port 22) and HTTP/HTTPS (ports 80, 443) inbound.
echo "Updating Default Security List for Public Subnet..."
# Get the default security list ID for the VCN
DEFAULT_SECURITY_LIST_ID=$(oci network security-list list \
--compartment-id "${COMPARTMENT_ID}" \
--vcn-id "${VCN_ID}" \
--query 'data[? "display-name"==`Default Security List for ProdVCN-AlwaysFree`].id' --raw-output)
# Add ingress rules for SSH, HTTP, HTTPS
oci network security-list update \
--security-list-id "${DEFAULT_SECURITY_LIST_ID}" \
--ingress-security-rules "[ \
{\"protocol\": \"6\", \"source\": \"0.0.0.0/0\", \"sourceType\": \"CIDR_BLOCK\", \"tcpOptions\": {\"destinationPortRange\": {\"max\": 22, \"min\": 22}}}, \
{\"protocol\": \"6\", \"source\": \"0.0.0.0/0\", \"sourceType\": \"CIDR_BLOCK\", \"tcpOptions\": {\"destinationPortRange\": {\"max\": 80, \"min\": 80}}}, \
{\"protocol\": \"6\", \"source\": \"0.0.0.0/0\", \"sourceType\": \"CIDR_BLOCK\", \"tcpOptions\": {\"destinationPortRange\": {\"max\": 443, \"min\": 443}}} \
]"
echo "Security List updated for Public Subnet (SSH, HTTP, HTTPS)."
2. Provisioning Always Free Compute Instances (Ampere A1 Flex)
OCI's Always Free tier offers 4 OCPUs and 24 GB of RAM total across all Ampere A1 Flex instances in your tenancy. We'll use two instances, each with 2 OCPUs and 12 GB RAM, for a simple load-balanced setup.
First, generate an SSH key pair if you don't have one:
ssh-keygen -t rsa -b 2048 -f ~/.ssh/oci_key -N ""
# This creates ~/.ssh/oci_key (private) and ~/.ssh/oci_key.pub (public)
Launch Instance 1 (Web Server 1):
INSTANCE_1_NAME="WebAppServer-01"
SSH_PUBLIC_KEY=$(cat ~/.ssh/oci_key.pub)
IMAGE_ID=$(oci compute image list --compartment-id "${COMPARTMENT_ID}" --operating-system "Oracle Linux" --operating-system-version "8" --shape "VM.Standard.A1.Flex" --query 'data[0].id' --raw-output)
echo "Launching Instance 1: ${INSTANCE_1_NAME}..."
oci compute instance launch \
--compartment-id "${COMPARTMENT_ID}" \
--availability-domain "${AVAILABILITY_DOMAIN}" \
--shape "VM.Standard.A1.Flex" \
--shape-config '{"ocpus": 2, "memoryInGBs": 12}' \
--display-name "${INSTANCE_1_NAME}" \
--image-id "${IMAGE_ID}" \
--subnet-id "${PUBLIC_SUBNET_ID}" \
--ssh-authorized-keys-file ~/.ssh/oci_key.pub \
--wait-for-state RUNNING \
--query 'data.id' --raw-output > instance_1_id.txt
INSTANCE_1_ID=$(cat instance_1_id.txt)
echo "Instance 1 launched with ID: ${INSTANCE_1_ID}"
# Get Public IP of Instance 1
INSTANCE_1_PUBLIC_IP=$(oci compute instance list-vnics --instance-id "${INSTANCE_1_ID}" --query 'data[0]."public-ip"' --raw-output)
echo "Instance 1 Public IP: ${INSTANCE_1_PUBLIC_IP}"
Launch Instance 2 (Web Server 2 / Nginx Proxy):
INSTANCE_2_NAME="WebAppServer-02-Nginx"
echo "Launching Instance 2: ${INSTANCE_2_NAME}..."
oci compute instance launch \
--compartment-id "${COMPARTMENT_ID}" \
--availability-domain "${AVAILABILITY_DOMAIN}" \
--shape "VM.Standard.A1.Flex" \
--shape-config '{"ocpus": 2, "memoryInGBs": 12}' \
--display-name "${INSTANCE_2_NAME}" \
--image-id "${IMAGE_ID}" \
--subnet-id "${PUBLIC_SUBNET_ID}" \
--ssh-authorized-keys-file ~/.ssh/oci_key.pub \
--wait-for-state RUNNING \
--query 'data.id' --raw-output > instance_2_id.txt
INSTANCE_2_ID=$(cat instance_2_id.txt)
echo "Instance 2 launched with ID: ${INSTANCE_2_ID}"
# Get Public IP of Instance 2
INSTANCE_2_PUBLIC_IP=$(oci compute instance list-vnics --instance-id "${INSTANCE_2_ID}" --query 'data[0]."public-ip"' --raw-output)
echo "Instance 2 Public IP: ${INSTANCE_2_PUBLIC_IP}"
# Get Private IP of Instance 1 (needed for Nginx config)
INSTANCE_1_PRIVATE_IP=$(oci compute instance list-vnics --instance-id "${INSTANCE_1_ID}" --query 'data[0]."private-ip"' --raw-output)
echo "Instance 1 Private IP: ${INSTANCE_1_PRIVATE_IP}"
3. Configuring an Always Free Autonomous Database (ADB)
OCI offers Always Free Autonomous Transaction Processing (ATP) or Autonomous Data Warehouse (ADW) with 1 OCPU and 20 GB of storage. We'll create an ATP instance.
ADB_DISPLAY_NAME="ProdAppDB"
ADB_ADMIN_PASSWORD="MyStrongPassword123#" # Choose a strong password
echo "Creating Autonomous Database: ${ADB_DISPLAY_NAME}..."
oci db autonomous-database create \
--compartment-id "${COMPARTMENT_ID}" \
--display-name "${ADB_DISPLAY_NAME}" \
--db-name "prodappdb" \
--admin-password "${ADB_ADMIN_PASSWORD}" \
--cpu-core-count 1 \
--data-storage-size-in-tbs 0.02 \
--is-free-tier true \
--db-workload "OLTP" \
--is-auto-scaling-enabled false \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output > adb_id.txt
ADB_ID=$(cat adb_id.txt)
echo "Autonomous Database created with ID: ${ADB_ID}"
Note on ADB Access: For simplicity, we created a public ADB. In a true production scenario, you would create a private subnet for ADB and configure a Service Gateway for private access from your compute instances. For Always Free, a public ADB with strict Access Control Lists (ACLs) is often used.
Configure Network Access Control List (ACL) for ADB:
Allow access only from your compute instances' public IPs.
echo "Updating ADB ACL to allow access from compute instances..."
oci db autonomous-database update \
--autonomous-database-id "${ADB_ID}" \
--whitelisted-ips "[\"${INSTANCE_1_PUBLIC_IP}/32\", \"${INSTANCE_2_PUBLIC_IP}/32\"]" \
--wait-for-state AVAILABLE
echo "ADB ACL updated. Only instances ${INSTANCE_1_PUBLIC_IP} and ${INSTANCE_2_PUBLIC_IP} can connect."
Download Wallet and Connect:
You'll need the wallet to connect to ADB securely. Download it from the OCI Console for your ADB instance (Database Connection -> Download Wallet) or via CLI.
# This command downloads the wallet as a zip file.
# You'll need to securely transfer this to your compute instances.
oci db autonomous-database generate-wallet \
--autonomous-database-id "${ADB_ID}" \
--file "wallet_ProdAppDB.zip" \
--password "MySecureWalletPassword123" # A password for the wallet zip file
echo "ADB Wallet downloaded to wallet_ProdAppDB.zip"
4. Deploying a Simple Application (Python Flask)
We'll deploy a basic Python Flask application that connects to the ADB. This involves setting up the environment, deploying the code, and configuring a systemd service.
SSH into Instance 1 and Deploy Application:
# On your local machine:
# Transfer the wallet to Instance 1
scp -i ~/.ssh/oci_key wallet_ProdAppDB.zip opc@${INSTANCE_1_PUBLIC_IP}:~/
# SSH into Instance 1
ssh -i ~/.ssh/oci_key opc@${INSTANCE_1_PUBLIC_IP}
# Inside Instance 1 (WebAppServer-01):
sudo yum update -y
sudo yum install python3 python3-pip unzip -y
# Unzip the ADB wallet
mkdir -p ~/db_wallet
unzip ~/wallet_ProdAppDB.zip -d ~/db_wallet/
# Install Python libraries
pip3 install flask cx_Oracle
# Create a simple Flask application (app.py)
cat <<EOF > ~/app.py
import os
import cx_Oracle
from flask import Flask, jsonify
app = Flask(__name__)
# OCI ADB connection details
TNS_ADMIN = os.path.expanduser('~/db_wallet')
os.environ['TNS_ADMIN'] = TNS_ADMIN
# The service name will be like 'prodappdb_high' or 'prodappdb_medium'
# You can find the exact service name from the tnsnames.ora file inside the wallet.
# For Always Free, typically it's db_name_high, db_name_medium, db_name_low
DB_SERVICE_NAME = "prodappdb_high"
DB_USER = "ADMIN"
DB_PASSWORD = "${ADB_ADMIN_PASSWORD}" # Replace with actual ADB admin password
@app.route('/')
def hello_world():
return 'Hello from WebAppServer-01!'
@app.route('/db_test')
def db_test():
try:
connection = cx_Oracle.connect(DB_USER, DB_PASSWORD, DB_SERVICE_NAME)
cursor = connection.cursor()
cursor.execute("SELECT SYSDATE FROM DUAL")
row = cursor.fetchone()
cursor.close()
connection.close()
return jsonify({"message": "Successfully connected to ADB!", "sysdate": str(row[0])})
except cx_Oracle.Error as e:
error_obj, = e.args
return jsonify({"error": f"Database connection failed: {error_obj.message}"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
EOF
# Create a systemd service file for Flask
cat <<EOF > /etc/systemd/system/flaskapp.service
[Unit]
Description=Flask Application
After=network.target
[Service]
User=opc
WorkingDirectory=/home/opc
ExecStart=/usr/bin/python3 /home/opc/app.py
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd, enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable flaskapp
sudo systemctl start flaskapp
sudo systemctl status flaskapp
# Exit Instance 1
exit
Repeat the same steps for Instance 2, but change the hello_world message to "Hello from WebAppServer-02!" and ensure the DB_SERVICE_NAME and DB_PASSWORD are correct.
SSH into Instance 2 (Nginx Proxy) and Configure Nginx:
# On your local machine:
# Transfer the wallet to Instance 2 (if you want this instance to also be able to connect to DB directly)
scp -i ~/.ssh/oci_key wallet_ProdAppDB.zip opc@${INSTANCE_2_PUBLIC_IP}:~/
# SSH into Instance 2
ssh -i ~/.ssh/oci_key opc@${INSTANCE_2_PUBLIC_IP}
# Inside Instance 2 (WebAppServer-02-Nginx):
sudo yum update -y
sudo yum install nginx -y
# Configure Nginx as a reverse proxy
cat <<EOF > /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load balancing for our Flask apps
upstream backend_servers {
server ${INSTANCE_1_PRIVATE_IP}:5000; # Private IP of Instance 1
server 127.0.0.1:5000; # Local Flask app (Instance 2)
}
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
# Load balance requests to backend_servers
location / {
proxy_pass http://backend_servers;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}
EOF
# Enable and start Nginx
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx
# Exit Instance 2
exit
Now, accessing http://${INSTANCE_2_PUBLIC_IP} in your browser should show "Hello from WebAppServer-01!" or "Hello from WebAppServer-02!", demonstrating basic round-robin load distribution. Navigating to http://${INSTANCE_2_PUBLIC_IP}/db_test should show a successful database connection message.
5. Object Storage for Static Assets and Backups
OCI Always Free provides 20 GB of Object Storage. This is excellent for static website assets, application backups, or log storage.
BUCKET_NAME="prod-app-static-assets-2026"
echo "Creating Object Storage Bucket: ${BUCKET_NAME}..."
oci os bucket create \
--compartment-id "${COMPARTMENT_ID}" \
--name "${BUCKET_NAME}" \
--namespace $(oci os ns get --query 'data' --raw-output) \
--access-type "NoPublicAccess" \
--wait-for-state AVAILABLE
echo "Bucket ${BUCKET_NAME} created."
# Example: Upload a file (e.g., a backup or static image)
echo "This is a test file for Object Storage." > test_file.txt
oci os object put \
--bucket-name "${BUCKET_NAME}" \
--name "app_backup_2026_01_01.txt" \
--file "test_file.txt"
echo "Uploaded test_file.txt to bucket."
Security
Operating a production workload, even on Always Free resources, demands a robust security posture. While OCI provides a secure foundation, your configuration choices are paramount.
1. Identity and Access Management (IAM)
- Least Privilege: Grant users and groups only the permissions absolutely necessary for their roles. Avoid giving administrative access unless strictly required.
- Multi-Factor Authentication (MFA): Enable MFA for all OCI console users, especially administrators.
- API Keys: For programmatic access (CLI/SDK), use API keys associated with specific users or, ideally, dynamic groups and instance principals for applications running on OCI compute.
- Compartments: Organize your resources into logical compartments to enforce security policies and resource quotas effectively.
# Example IAM Policy for a 'Devs' group to manage compute in a specific compartment
oci iam policy create \
--compartment-id "ocid1.tenancy.oc1..<YOUR_TENANCY_OCID>" \
--name "ManageComputeInProdCompartment" \
--description "Allows Devs to manage compute instances in ProdAppCompartment." \
--statements '["Allow group Devs to manage instance-family in compartment ProdAppCompartment"]'
2. Network Security
- Security Lists/Network Security Groups (NSGs): Use these to control ingress and egress traffic at the subnet or VNIC level. Always adhere to the principle of least privilege, opening only necessary ports from trusted sources. For our setup, we opened SSH (22), HTTP (80), and HTTPS (443) to 0.0.0.0/0, which is acceptable for a public-facing web server, but you might restrict SSH to your office IP ranges.
- Private Subnets: For databases and internal application components, always prefer private subnets. Access them via Bastion Service, VPN, or OCI's Service Gateway/NAT Gateway. Our ADB example used a public endpoint for simplicity, but a private endpoint with a Service Gateway for database access is the production standard.
- Web Application Firewall (WAF): For critical public-facing applications, consider OCI WAF (not free) or a cloud-agnostic WAF solution to protect against common web exploits (e.g., SQL injection, XSS).
3. Instance Security
- SSH Key Management: Use strong SSH keys and protect your private keys. Never use password-based SSH authentication.
- Operating System Patching: Regularly update and patch your compute instances to protect against known vulnerabilities.
CVE Example: A common vulnerability like CVE-2021-3450 (Apache HTTP Server 2.4.49/2.4.50 path traversal) could be exploited if your Nginx or backend serves static files directly from a vulnerable Apache. Regular OS updates and application patching prevent such exposures.