---
name: oci-deployment
description: Deploy applications to Oracle Cloud Infrastructure (OCI) — compute instances, container instances, port security, and Docker deployments.
trigger:
  - "deploy to OCI"
  - "OCI compute instance"
  - "OCI security list"
  - "OCI port open"
  - "Oracle Cloud deployment"
  - "OCI Docker"
---

# OCI Deployment Guide

Deploying to Oracle Cloud Infrastructure requires three layers: (1) compute instance setup, (2) security list (firewall) configuration, (3) application deployment (Docker or native).

## Core Concepts

- **Compute Instance**: Your VM (e.g., Ampere A1 ARM64, VM.Standard.E2.1)
- **Security List (Seclist)**: Firewall rules controlling inbound/outbound traffic
- **VCN (Virtual Cloud Network)**: Network where your instance lives
- **OCI CLI**: Command-line tool for managing OCI resources

## Layer 1: Security List (Port Opening) 🔧

**This is the #1 cause of "cannot access my app" issues.** Even if your app runs fine locally on the instance, OCI's Security List blocks all inbound traffic by default.

### Steps to Open Ports

1. **Login to OCI Console**: https://cloud.oracle.com
2. **Navigate to your Instance**:
   - Menu → **Compute** → **Instances**
   - Click your instance name
3. **Find the Security List**:
   - Scroll to **"Primary VNIC"** section
   - Click the blue link under **"Security Lists"** (format: `seclist-xxx`)
4. **Add Inbound Rules**:
   - Click **"Add Inbound Rule"**
   - Add rules for each port your app uses:

   **Rule for Backend API (e.g., port 8000)**:
   ```
   Source CIDR: 0.0.0.0/0  (or specific IP for security)
   IP Protocol: TCP
   Destination Port: 8000
   Description: Backend API
   ```

   **Rule for AI Service (e.g., port 8001)**:
   ```
   Source CIDR: 0.0.0.0/0
   IP Protocol: TCP
   Destination Port: 8001
   Description: AI Service
   ```

   **Rule for Web Frontend (e.g., port 8080)**:
   ```
   Source CIDR: 0.0.0.0/0
   IP Protocol: TCP
   Destination Port: 8080
   Description: Web Frontend
   ```

5. **Save** and wait 10-30 seconds for rules to propagate

### Verification

From your local machine (not the instance), test if ports are open:

```bash
# Test backend port
curl -I http://<INSTANCE_PUBLIC_IP>:8000/health

# If connection times out, security list is not configured
# If "Connection refused", app is not running on the instance
# If you get HTTP response, success!
```

## Layer 2: Nginx Reverse Proxy (Domain-based Access) 🌐

When you want to use a domain name (e.g., `app.example.com`) instead of IP:PORT, configure Nginx as a reverse proxy.

### Installation

```bash
sudo apt update && sudo apt install -y nginx
nginx -v  # Verify installation
```

### Create Site Configuration

**File**: `/etc/nginx/sites-available/your-domain.com`

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name your-domain.com;

    # Logs
    access_log /var/log/nginx/your-domain.com.access.log;
    error_log /var/log/nginx/your-domain.com.error.log;

    # Reverse proxy to backend
    location / {
        proxy_pass http://localhost:8000;  # Your app's port
        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;
        
        # WebSocket support (if needed)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Health check endpoint (optional)
    location /nginx-health {
        access_log off;
        return 200 "OK\n";
        add_header Content-Type text/plain;
    }
}
```

### Enable Site

```bash
# Create symlink
sudo ln -sf /etc/nginx/sites-available/your-domain.com /etc/nginx/sites-enabled/

# Remove default site (important! It can intercept requests)
sudo rm -f /etc/nginx/sites-enabled/default

# Test configuration
sudo nginx -t

# Restart Nginx
sudo systemctl restart nginx
sudo systemctl status nginx  # Verify it's running
```

### Verification

```bash
# Test from the instance itself
curl -s http://localhost/health
curl -s http://your-domain.com/health

# Test from external machine (requires OCI Security List port 80 open)
curl -I http://your-domain.com/health
```

### Common Pitfalls
 
- ⚠️ **Default site interferes**: Always remove `/etc/nginx/sites-enabled/default` if you're using a custom domain config.
- ⚠️ **DNS not propagated**: Ensure your domain's A record points to your OCI instance public IP.
- ⚠️ **Port 80 not open in OCI Security List**: Nginx listens on 80, but OCI blocks it by default. See Layer 1 for opening ports.
- ⚠️ **502 Bad Gateway**: Backend app not running on the specified port. Verify with `curl http://localhost:8000/health`.
- ⚠️ **Mixed Content Error**: When your frontend is served over HTTPS but calls HTTP APIs, browsers block the request. 
  **Symptoms**: Browser console shows `Mixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure resource 'http://...'`.
  **Solution**: Ensure all API calls in frontend code use `https://your-domain.com` instead of `http://IP:PORT`.
  **Example fix in JavaScript**:
  ```javascript
  // ❌ Wrong (causes Mixed Content error)
  const API_BASE = 'http://192.18.149.172:8000';
  
  // ✅ Correct (HTTPS)
  const API_BASE = 'https://house.openmenu.app';
  ```
- ⚠️ **Frontend API URL management**: When deploying with domains and HTTPS, always update frontend code to use domain names instead of IP addresses. Check all hardcoded API endpoints in:
  - JavaScript/TypeScript files: `const API_BASE = ...`
  - Flutter/Dart files: `Uri.parse('http://...')`
  - Any configuration files that reference backend URLs

---

## Layer 4: SSL Configuration with Let's Encrypt 🔒

When you want HTTPS (required for production), use Certbot to automatically obtain and renew SSL certificates.

### Prerequisites

1. **Domain DNS configured**: Ensure your domain (e.g., `app.example.com`) A record points to your OCI instance public IP.
2. **Port 80 open in OCI Security List**: Certbot uses HTTP-01 challenge to verify domain ownership.
3. **Nginx already configured**: Complete Layer 2 first (HTTP reverse proxy working).

### Installation

```bash
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
```

### Obtain Certificate

**Automatic Nginx configuration** (recommended - Certbot will modify your Nginx config automatically):

```bash
sudo certbot --nginx -d your-domain.com \
  --non-interactive \
  --agree-tos \
  --email your-email@example.com \
  --no-eff-email \
  --redirect
```

**What this does**:
- ✅ Obtains SSL certificate from Let's Encrypt
- ✅ Saves certificate to `/etc/letsencrypt/live/your-domain.com/`
- ✅ Modifies your Nginx config to add SSL listeners (443)
- ✅ Sets up HTTP → HTTPS 301 redirect
- ✅ Configures automatic renewal cron job

### Verify HTTPS

```bash
# Test HTTPS access
curl -s https://your-domain.com/health

# Test HTTP → HTTPS redirect
curl -sI http://your-domain.com/health | grep -E "(HTTP|Location)"
# Should show: HTTP/1.1 301 Moved Permanently
# Location: https://your-domain.com/health
```

### Certificate Information

- **Certificate path**: `/etc/letsencrypt/live/your-domain.com/fullchain.pem`
- **Private key path**: `/etc/letsencrypt/live/your-domain.com/privkey.pem`
- **Expiry**: 90 days from issuance
- **Auto-renewal**: Certbot installs a cron job or systemd timer for automatic renewal

Check certificate status:
```bash
sudo certbot certificates
```

### Multiple Subdomains

Repeat for each subdomain:

```bash
# First subdomain
sudo certbot --nginx -d house.openmenu.app --non-interactive --agree-tos --email admin@example.com --no-eff-email --redirect

# Second subdomain
sudo certbot --nginx -d test.openmenu.app --non-interactive --agree-tos --email admin@example.com --no-eff-email --redirect

# Third subdomain (AI service)
sudo certbot --nginx -d ai.openmenu.app --non-interactive --agree-tos --email admin@example.com --no-eff-email --redirect
```

Each will create a separate certificate in `/etc/letsencrypt/live/`.

### Common Pitfalls
 
- ⚠️ **Port 80 must be open in OCI Security List**: Certbot's HTTP-01 challenge requires port 80 access from Let's Encrypt servers.
- ⚠️ **DNS not propagated**: Wait a few minutes after setting A record before running certbot.
- ⚠️ **Rate limits**: Let's Encrypt has rate limits (50 certificates per domain per week). Test with `--staging` flag first if unsure:
  ```bash
  sudo certbot --nginx -d your-domain.com --staging
  ```
- ⚠️ **Email for renewal notices**: Use a real email address. Let's Encrypt will send expiration notices if auto-renewal fails.
- ⚠️ **Update email later**: 
  ```bash
  sudo certbot update_account --email new-email@example.com
  ```
- ⚠️ **SSL Handshake Failures (error:0A00006C)**: After enabling SSL, you may see `SSL_do_handshake() failed (SSL: error:0A00006C:SSL routines::bad key share)` in Nginx logs. This is usually caused by TLS 1.3 compatibility issues with certain clients.
  **Solution**: Edit `/etc/letsencrypt/options-ssl-nginx.conf` and change:
  ```bash
  # From:
  ssl_protocols TLSv1.2 TLSv1.3;
  # To:
  ssl_protocols TLSv1.2;
  ```
  Then reload Nginx: `sudo nginx -s reload`
  
  **Alternative**: Broaden cipher suite support in the same file:
  ```bash
  ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384";
  ssl_prefer_server_ciphers on;  # Let server choose cipher
  ```

### OCI Health Check Best Practices

OCI instances have built-in health monitoring. If you see "health check failed" in OCI console, ensure:

1. **OCI monitoring agent is running**:
   ```bash
   sudo systemctl status oracle-cloud-agent
   # If not running:
   sudo systemctl restart oracle-cloud-agent
   ```

2. **Configure HTTP 80 port health check endpoint**: Certbot configures 80 port to return 404 (redirect to HTTPS), which may cause OCI health checks to fail. Create a dedicated health check config:
   ```bash
   # Create file: /etc/nginx/sites-available/oci-health-check
   sudo tee /etc/nginx/sites-available/oci-health-check > /dev/null << 'EOF'
   server {
       listen 80 default_server;
       listen [::]:80 default_server;
       server_name _;
       
       location / {
           access_log off;
           return 200 "OK\n";
           add_header Content-Type text/plain;
       }
       
       location /health {
           access_log off;
           return 200 '{"status":"healthy"}\n';
           add_header Content-Type application/json;
       }
   }
   EOF
   
   sudo ln -sf /etc/nginx/sites-available/oci-health-check /etc/nginx/sites-enabled/
   sudo nginx -t && sudo nginx -s reload
   ```

3. **Verify from instance metadata service**:
   ```bash
   curl -s http://169.254.169.254/opc/v1/instance/ | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'State: {d.get(\"state\")}')"
   ```

### Certbot Auto-Configuration Notes

When you run `sudo certbot --nginx -d your-domain.com`, Certbot will:
- ✅ Obtain SSL certificate
- ✅ Modify your Nginx config to add `listen 443 ssl;` blocks
- ✅ Add SSL certificate paths
- ✅ **Add a second server block for port 80 that returns `301 Moved Permanently` to HTTPS**
- ✅ Set up auto-renewal cron job

**Important**: The auto-added port 80 server block typically returns `404` or `301`. If you need HTTP health checks, either:
- Use the OCI health check config above (listens on 80 as default_server)
- Or modify Certbot's auto-added config: change `return 404;` to `return 200 "OK\n";`

### Verification Checklist

- [ ] `curl -s https://your-domain.com/health` returns 200 OK
- [ ] `curl -sI http://your-domain.com/health` returns 301 redirect to HTTPS
- [ ] Certificate expires in ~90 days (check with `sudo certbot certificates`)
- [ ] Auto-renewal is configured (check with `sudo systemctl status certbot.timer` or `cat /etc/cron.d/certbot`)

---

## Layer 5: Docker Deployment on OCI Instance

### Option A: Docker Compose (Recommended for Multi-Container)

1. **SSH to your instance**:
   ```bash
   ssh ubuntu@<INSTANCE_PUBLIC_IP>
   ```

2. **Install Docker** (if not installed):
   ```bash
   sudo apt-get update
   sudo apt-get install -y docker.io
   sudo systemctl start docker
   sudo systemctl enable docker
   sudo usermod -aG docker $USER
   # Log out and back in for group to take effect
   ```

3. **Copy your files** (from local machine):
   ```bash
   # On your local machine
   scp -r /path/to/your/app ubuntu@<INSTANCE_PUBLIC_IP>:~/app
   ```

4. **Run with docker-compose**:
   ```bash
   cd ~/app
   sudo docker-compose up -d
   ```

5. **Verify**:
   ```bash
   sudo docker ps
   curl http://localhost:8000/health
   ```

### Adding PostgreSQL to Docker Compose

When your app needs persistent database storage (instead of in-memory or host-based PostgreSQL):

**1. Update `docker-compose.yml`**:
```yaml
version: '3.8'

services:
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/dbname
    depends_on:
      - db
    restart: unless-stopped

  # PostgreSQL Database
  db:
    image: postgres:14
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=dbname
    ports:
      - "5432:5432"  # Optional: only if you need external access
    volumes:
      - db_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  db_data:
```

**2. Create database tables** (after containers start):
```bash
# Execute SQL against the db container
sudo docker-compose exec -T db psql -U user -d dbname << 'EOF'
CREATE TABLE IF NOT EXISTS layouts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    layout_id VARCHAR(255) UNIQUE NOT NULL,
    filename VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Add more tables as needed
EOF
```

**3. Install psycopg2 in your backend**:
```bash
# In backend/requirements.txt
psycopg2-binary>=2.9.6
```

**4. Python database connection pattern**:
```python
# backend/database.py
import psycopg2
from psycopg2.extras import RealDictCursor
import os

DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/dbname")

def get_db_connection():
    return psycopg2.connect(DATABASE_URL, cursor_factory=RealDictCursor)

def save_layout(layout_id, filename):
    conn = get_db_connection()
    cur = conn.cursor()
    cur.execute(
        "INSERT INTO layouts (layout_id, filename) VALUES (%s, %s)",
        (layout_id, filename)
    )
    conn.commit()
    cur.close()
    conn.close()
```

### Common Pitfalls

- ⚠️ **Port conflict with host PostgreSQL**: If you have PostgreSQL installed on the host (not in Docker), port 5432 will be occupied. 
  **Solution**: 
  ```bash
  # Stop host PostgreSQL
  sudo systemctl stop postgresql
  sudo systemctl disable postgresql
  
  # Then start Docker containers
  sudo docker-compose up -d
  ```
  
- ⚠️ **Database permissions**: If you see `permission denied for table layouts`, grant privileges:
  ```bash
  sudo docker-compose exec -T db psql -U user -d dbname -c "
    GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO user;
    GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO user;
  "
  ```

- ⚠️ **Database not persisting**: Ensure you have a `volumes:` section in docker-compose.yml for the db service. Without it, data is lost when the container restarts.

- ⚠️ **Python module not found in Docker**: If your backend Dockerfile doesn't install psycopg2-binary, the container will crash. Ensure `requirements.txt` includes it and Dockerfile runs `pip install -r requirements.txt`.

- ⚠️ **Connection refused**: In docker-compose, services refer to each other by service name (e.g., `db` not `localhost`). Use `DATABASE_URL=postgresql://user:pass@db:5432/dbname` not `localhost`.

### Option B: OCI Container Instances (Serverless)

If you have images in OCI Registry (OCIR):

```bash
# Create container instance
oci container instance create \
  --display-name "my-app" \
  --image "iad.ocir.io/<namespace>/my-app:latest" \
  --ports "8000" \
  --env-variables "KEY=VALUE"
```

## Layer 3: OCI CLI Setup (Optional)

Required only if you want to manage OCI resources from command line.

```bash
# Install OCI CLI
bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)"

# Configure
oci setup config
# Follow prompts:
# - Enter your OCID (from OCI console → Profile → User Settings)
# - Enter your Tenancy OCID
# - Enter your Region (e.g., us-ashburn-1)
```

## Common Pitfalls

- ⚠️ **Port not accessible**: 99% of the time, it's the Security List. **Always check Layer 1 first!** In our sessions, users repeatedly hit "cannot access" errors — every time it was the Security List missing inbound rules.
- **"Address already in use"**: Another process is using the port. 
  ```bash
  # Find and kill the process
  sudo netstat -tulpn | grep <PORT>
  sudo kill <PID>
  # Then restart your container
  sudo docker-compose up -d
  ```
- **Docker permission denied**: Add user to docker group: `sudo usermod -aG docker $USER`, then re-login.
- **OCI CLI not authenticating**: Run `oci setup config` again, ensure API key is uploaded to OCI console.
- **Instances in different VCNs**: Ensure they can route to each other if needed.
- **Verifying port openness**: 
  1. **First verify locally on the instance**: `curl http://localhost:PORT/health` → should return 200
  2. **Then verify from your local machine**: `curl http://PUBLIC_IP:PORT/health` → if timeout, Security List is not configured
  3. **Check Docker port mapping**: `sudo docker ps` → ensure `0.0.0.0:PORT->PORT` is shown

## Verification Checklist

- [ ] Security List has inbound rules for your app ports
- [ ] Application is running on the instance (`curl http://localhost:PORT` works)
- [ ] Public IP access works (`curl http://PUBLIC_IP:PORT` from your local machine)
- [ ] Docker containers are running (`docker ps` shows your containers)
- [ ] Logs look clean (`docker logs <container-name>`)

## Quick Reference

**OCI Console URLs**:
- Instances: https://cloud.oracle.com/compute/instances
- Security Lists: (via Instance → Primary VNIC → Security List link)
- OCIR: https://cloud.oracle.com/artifacts/container-registry
