This repository provisions and runs a complete OCI-based application stack with Terraform, deploys a containerized app runtime, and automates image publishing through GitHub Actions.
The system is intentionally split into clear layers:
- Infrastructure as Code (Terraform): networking, compute, load balancer, and bastion access.
- Application Runtime (Containers): Flask app + NGINX reverse proxy.
- Delivery Automation (CI): container builds and pushes to GHCR on
main.
flowchart LR
U[User / Browser] -->|HTTP :80| LB[OCI Public Load Balancer]
subgraph VCN[VCN 172.16.0.0/20]
direction LR
subgraph PUB[Public Subnet 172.16.1.0/24]
LB
end
subgraph PRIV[Private Subnet 172.16.2.0/24]
VM[OCI Compute VM]
BASTION[OCI Bastion]
end
end
LB -->|Backend :8080 /health| VM
BASTION -->|Managed SSH :22| VM
VM -->|Docker Compose| NGINX[nginx container :80]
NGINX -->|proxy_pass| APP[Flask app container :8080]
VM -->|egress| NAT[NAT Gateway]
PUB -->|0.0.0.0/0 route| IGW[Internet Gateway]
GH[GitHub Actions CI] -->|build + push| GHCR[GHCR app/nginx images]
Terraform root for deployment is terraform/environments/dev, and it composes three modules:
terraform/modules/networkterraform/modules/appterraform/modules/lb
Creates core network primitives:
- VCN (
172.16.0.0/20) - Internet Gateway for public subnet egress/ingress patterns
- NAT Gateway for private subnet outbound internet access
- Public route table (
0.0.0.0/0 -> IGW) - Private route table (
0.0.0.0/0 -> NAT) - Public subnet (
172.16.1.0/24) - Private subnet (
172.16.2.0/24, no public IP on VNIC) - Security lists
Important security behavior:
- Public security list allows inbound HTTP (
TCP/80) from anywhere. - Private security list allows:
TCP/8080from public subnet CIDR only (for LB to app traffic)TCP/22from private subnet CIDR (to support managed access path)
- Egress is open (
all) for outbound updates/pulls.
Tagging model:
- Uses
local.common_tags(project,environment,managed_by,owner) for consistent resource metadata.
Creates one OCI compute instance inside the private subnet:
- Shape defaults:
VM.Standard.A1.Flex - CPU/memory defaults:
4 OCPUs,24 GB - No public IP assigned
- Image selected dynamically from latest Oracle Linux 8 image for the shape
- Cloud-init style startup bootstrap via
setup.sh
Bootstrap behavior (setup.sh):
- System update
- Docker Engine and compose plugin install
- Docker service enabled
opcadded to docker group- Clones your app repo and runs
docker compose up -d
Creates external entrypoint and access tooling:
- OCI flexible public load balancer (
100-1000 Mbps) - Backend set using HTTP health checker on
/healthport8080 - Backend target = app VM private IP (
:8080) - HTTP listener on port
80 - OCI Bastion and managed SSH session targeting the app instance (
opc, port 22)
This environment is orchestrator glue:
- Calls all modules
- Passes compartment, region, keys, and subnet outputs between modules
Flow of data:
networkoutputs subnet IDs.appconsumes private subnet ID and outputsinstance_id+private_ip.lbconsumes app outputs and network outputs.
Credential and input model:
- Provider is configured explicitly with OCI identity variables.
terraform.tfvarsis expected locally for real values.terraform.tfvars.exampleis provided as a non-secret template.
docker-compose.yml defines two services:
- Built from
app/Dockerfile - Flask server listens on
0.0.0.0:8080 - Health endpoint:
/health - Compose healthcheck probes local app health
- Built from
nginx/Dockerfile - Publishes
80:80 - Reverse proxies
/toapp:8080 - Own health endpoint:
/healthz - Depends on healthy app service before startup
Traffic inside the VM:
- Inbound HTTP hits NGINX.
- NGINX forwards to Flask app container.
- Flask serves
index.htmland health status JSON.
Current workflow executes on push to main:
- Logs in to GHCR with
GITHUB_TOKEN - Builds
appimage and pushes:- Immutable tag:
${GITHUB_SHA} - Mutable environment tag:
prod
- Immutable tag:
- Builds
nginximage and pushes same tag strategy
Why dual tags are useful:
- SHA tag gives reproducible rollback/reference.
prodtag gives stable deploy pointer for runtime consumers.
- Intended public ingress is HTTP on load balancer (
TCP/80). - App compute instance stays in private subnet with no public IP.
- App port
8080is not globally open; only LB subnet CIDR is allowed. - Administrative SSH is designed through OCI Bastion managed sessions.
- Sensitive
.tfvarsshould remain untracked. - Keep provider credentials in local files and/or environment, not committed files.
terraform.tfvars.exampleshould contain placeholders only.
- Fill local
terraform/environments/dev/terraform.tfvars. terraform initterraform planterraform apply
- Instance startup script installs Docker and starts compose stack.
- Health checks at both app and proxy layers support LB and local readiness behavior.
- Push to
maintriggers CI image publishing to GHCR. - Infrastructure and runtime can evolve independently:
- Terraform changes for topology/security/capacity
- App/NGINX changes for application behavior
terraform/environments/dev/main.tf: module compositionterraform/environments/dev/providers.tf: OCI provider setupterraform/modules/network/main.tf: VCN/subnets/routes/security liststerraform/modules/app/main.tf: compute instance declarationterraform/modules/app/setup.sh: bootstrap and compose startupterraform/modules/lb/main.tf: public LB + bastiondocker-compose.yml: runtime service topologynginx/nginx.conf: reverse proxy behaviorapp/test.py: Flask app and health endpoint.github/workflows/ci.yml: image build/push automation
If you want production hardening next, the highest-impact upgrades are:
- TLS termination on LB (HTTPS listener + cert management)
- Replace boot-time
git clonewith pinned image deployment strategy - Add CI validation stages (
terraform fmt/validate, app tests, compose checks) - Narrow security-list egress and SSH scope further
- Add observability (structured logs + metrics + alerts)