The goal
The console's VCN wizard does all this in one click, but it doesn't teach you much. You'll build the same thing by hand with the CLI: one public subnet for things reached from the internet, and one private subnet for everything else.
If the CLI isn't working yet, follow OCI CLI setup. Then set a variable for your compartment. Use a lab compartment if you have one:
export C=ocid1.compartment.oc1..your-compartment-id
oci network vcn list --compartment-id "$C" --query 'data[]."display-name"'
For background on these terms, see compartments and OCIDs.
What you're building
VCN 10.0.0.0/16
├─ Public subnet 10.0.1.0/24 → route 0.0.0.0/0 to the internet gateway
└─ Private subnet 10.0.2.0/24 → route 0.0.0.0/0 to the NAT gateway
| Part | What it does |
|---|---|
| VCN | Your private network |
| Subnet | A slice of the VCN's address range |
| Route table | Decides where traffic goes |
| Internet gateway | Two-way internet access, for public IPs |
| NAT gateway | Outbound-only internet, for private instances |
| Security list | Firewall rules for a subnet |
Step 1: create the VCN
VCN=$(oci network vcn create \
--compartment-id "$C" \
--cidr-blocks '["10.0.0.0/16"]' \
--display-name lab-vcn \
--dns-label labvcn \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output)
echo "$VCN"
Pick a range that doesn't overlap with your other VCNs or your office network, since you may need to connect them later. The DNS label can't be changed after creation.
Step 2: create the gateways
IGW=$(oci network internet-gateway create \
--compartment-id "$C" --vcn-id "$VCN" \
--is-enabled true --display-name lab-igw \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output)
NAT=$(oci network nat-gateway create \
--compartment-id "$C" --vcn-id "$VCN" \
--display-name lab-nat \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output)
Check your account's current pricing before leaving these running. For more about each gateway, read VCN gateways and routing.
Step 3: create the route tables
PUB_RT=$(oci network route-table create \
--compartment-id "$C" --vcn-id "$VCN" \
--display-name lab-public-rt \
--route-rules '[{"destination":"0.0.0.0/0","destinationType":"CIDR_BLOCK","networkEntityId":"'"$IGW"'"}]' \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output)
PRIV_RT=$(oci network route-table create \
--compartment-id "$C" --vcn-id "$VCN" \
--display-name lab-private-rt \
--route-rules '[{"destination":"0.0.0.0/0","destinationType":"CIDR_BLOCK","networkEntityId":"'"$NAT"'"}]' \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output)
The quote pattern '"$IGW"' looks odd. It ends the single-quoted JSON, adds the variable and starts the JSON again.
You don't need a route for traffic inside the VCN. It's automatic.
Step 4: create a security list
This allows SSH from your IP only, plus all outbound traffic:
MYIP=$(curl -s https://ifconfig.me)
SL=$(oci network security-list create \
--compartment-id "$C" --vcn-id "$VCN" \
--display-name lab-public-sl \
--egress-security-rules '[{"destination":"0.0.0.0/0","protocol":"all"}]' \
--ingress-security-rules '[{"source":"'"$MYIP"'/32","protocol":"6","tcpOptions":{"destinationPortRange":{"min":22,"max":22}}}]' \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output)
Protocol 6 means TCP, and 17 means UDP. In production, prefer network security groups, which apply to specific instances instead of whole subnets.
Step 5: create the subnets
Unlike some other clouds, OCI has a real setting for this: --prohibit-public-ip-on-vnic.
PUB_SUBNET=$(oci network subnet create \
--compartment-id "$C" --vcn-id "$VCN" \
--cidr-block 10.0.1.0/24 \
--display-name lab-public \
--dns-label pub \
--route-table-id "$PUB_RT" \
--security-list-ids '["'"$SL"'"]' \
--prohibit-public-ip-on-vnic false \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output)
PRIV_SUBNET=$(oci network subnet create \
--compartment-id "$C" --vcn-id "$VCN" \
--cidr-block 10.0.2.0/24 \
--display-name lab-private \
--dns-label priv \
--route-table-id "$PRIV_RT" \
--security-list-ids '["'"$SL"'"]' \
--prohibit-public-ip-on-vnic true \
--wait-for-state AVAILABLE \
--query 'data.id' --raw-output)
These are regional subnets, because we didn't name an availability domain. They span every availability domain in the region, which is what you normally want. OCI reserves 3 addresses in each subnet.
The private subnet shares a security list here to keep the lab short. On a real network, give it its own, with rules for your internal traffic.
Step 6: check your work
oci network subnet list --compartment-id "$C" --vcn-id "$VCN" \
--query 'data[].{name:"display-name", cidr:"cidr-block", private:"prohibit-public-ip-on-vnic", state:"lifecycle-state"}' \
--output table
oci network route-table get --rt-id "$PUB_RT" --query 'data."route-rules"'
To test it, launch an instance in the public subnet by following your first Linux instance. If you can't connect, the OS firewall may be the cause, even with the right rules. See port open but still timing out.
Common problems
| Problem | Likely cause | Fix |
|---|---|---|
NotAuthorizedOrNotFound | No policy, wrong OCID or wrong compartment | See the policy cheat sheet |
LimitExceeded | Service limit reached | See service limits |
| Instance has no public IP | Private subnet, or none was requested | Check the subnet, and assign one |
| Private instance has no internet | No NAT route, or egress rule missing | Check the route table. See lost outbound |
InvalidParameter on JSON | Quoting problem in the shell | echo the JSON string first |
Step 7: clean up
Remove the pieces in this order, or you'll get a Conflict error. Terminate any instances first.
oci network subnet delete --subnet-id "$PUB_SUBNET" --force --wait-for-state TERMINATED
oci network subnet delete --subnet-id "$PRIV_SUBNET" --force --wait-for-state TERMINATED
oci network route-table delete --rt-id "$PUB_RT" --force --wait-for-state TERMINATED
oci network route-table delete --rt-id "$PRIV_RT" --force --wait-for-state TERMINATED
oci network security-list delete --security-list-id "$SL" --force --wait-for-state TERMINATED
oci network internet-gateway delete --ig-id "$IGW" --force --wait-for-state TERMINATED
oci network nat-gateway delete --nat-gateway-id "$NAT" --force --wait-for-state TERMINATED
oci network vcn delete --vcn-id "$VCN" --force --wait-for-state TERMINATED
Caution
--forceskips the confirmation prompt. Check that the variables still hold your lab's OCIDs. In a new terminal they'll be empty or, worse, hold values you set for something else.
Prevent it next time
- Plan your address ranges before building, and leave room to grow.
- Use a separate subnet per tier, and keep databases in private subnets.
- Add a service gateway so private instances can reach OCI services without internet access.
- Build real networks with Terraform and Resource Manager.