Someone asks you to "just delete the old app server". Terminating it takes one click. The trouble comes later: a batch job that still called it, a reserved public IP still being billed, a block volume nobody can identify, or a load balancer backend that goes critical and pages the on-call engineer at 3 a.m.
This checklist breaks the work into four stages: confirm, back up, detach, terminate. Each stage has the CLI commands you need. Run it with the change ticket open and paste the outputs in as you go.
Set the variables once:
export INSTANCE_OCID=ocid1.instance.oc1..example
export COMPARTMENT_OCID=ocid1.compartment.oc1..example
export AD=$(oci compute instance get --instance-id "$INSTANCE_OCID" \
--query 'data."availability-domain"' --raw-output)
1. Confirm the instance is really unused
- The owner and change ticket are confirmed. Check the instance tags for the owner and cost center.
oci compute instance get --instance-id "$INSTANCE_OCID" \
--query 'data.{name:"display-name",state:"lifecycle-state",shape:shape,created:"time-created",freeform:"freeform-tags",defined:"defined-tags"}'
- Network traffic over the last two weeks is checked. Steady inbound traffic means something still calls this host. The query needs the Oracle Cloud Agent compute monitoring plugin. If the plugin isn't running, there's no metric data, which doesn't prove the host is idle.
oci monitoring metric-data summarize-metrics-data \
--compartment-id "$COMPARTMENT_OCID" \
--namespace oci_computeagent \
--query-text "NetworksBytesIn[1d]{resourceId = \"$INSTANCE_OCID\"}.sum()" \
--start-time 2026-10-19T00:00:00Z --end-time 2026-11-02T00:00:00Z
- Active connections and logins on the host are checked. Look at who is connected now and who logged in recently.
sudo ss -tnp state established # who is connected right now
last -n 20 # recent interactive logins
sudo crontab -l; ls /etc/cron.d/ # jobs that call out from this host
systemctl list-units --type=service --state=running
- Load balancers, DNS and configs have no references to it. Search for its private IP, public IP and hostname in load balancer backend sets, DNS zones, app config repos and firewall rules.
oci compute instance list-vnics --instance-id "$INSTANCE_OCID" \
--query 'data[].{vnic:id,private:"private-ip",public:"public-ip",subnet:"subnet-id"}' --output table
Tip
For a soft decommission, stop the instance and leave it stopped for an agreed period, often one to two weeks. A stopped VM isn't billed for OCPU and memory, but its boot and block volumes still are. If anything breaks, you just start it again.
oci compute instance action --instance-id "$INSTANCE_OCID" --action SOFTSTOP \
--wait-for-state STOPPED
2. Back up what you might need
- The boot volume has a final backup.
BOOT_VOL=$(oci compute boot-volume-attachment list \
--availability-domain "$AD" --compartment-id "$COMPARTMENT_OCID" \
--instance-id "$INSTANCE_OCID" --query 'data[0]."boot-volume-id"' --raw-output)
oci bv boot-volume-backup create --boot-volume-id "$BOOT_VOL" \
--type FULL --display-name "decom-final-$(date +%F)" \
--wait-for-state AVAILABLE
- Every attached block volume has a final backup.
oci compute volume-attachment list --compartment-id "$COMPARTMENT_OCID" \
--instance-id "$INSTANCE_OCID" \
--query 'data[?"lifecycle-state"==`ATTACHED`].{att:id,vol:"volume-id",dev:device}' --output table
oci bv backup create --volume-id "$VOLUME_OCID" --type FULL \
--display-name "decom-final-$(date +%F)" --wait-for-state AVAILABLE
- The retention period for the final backups is agreed and recorded in the ticket. Manual backups stay until you delete them, and you pay for their storage until then.
- Anything not on a volume has been exported. Examples are secrets in local files (move them to Vault first), certificates, and application data you plan to keep in Object Storage.
Warning
Volume backups are tied to the region. If the data must survive a regional outage or tenancy cleanup, copy the backup to another region, or export it to Object Storage, before you terminate.
3. Detach the instance from everything around it
- Load balancer backends are drained, then removed.
oci lb backend updateneeds all four flags, so pass the current values for weight, backup and offline.
oci lb backend update --load-balancer-id "$LB_OCID" --backend-set-name app-bes \
--backend-name "10.0.1.15:8080" --weight 1 --backup false --offline false --drain true
# Wait for existing sessions to finish, then:
oci lb backend delete --load-balancer-id "$LB_OCID" --backend-set-name app-bes \
--backend-name "10.0.1.15:8080" --force
- DNS records pointing at the instance are deleted. Check the record before you delete it.
oci dns record rrset get --zone-name-or-id example.internal \
--domain app01.example.internal --rtype A
oci dns record rrset delete --zone-name-or-id example.internal \
--domain app01.example.internal --rtype A --force
For a private zone, also pass --scope PRIVATE and --view-id.
- Reserved public IPs are released or reassigned. Ephemeral IPs disappear with the instance. Reserved IPs stay, and so do their charges.
oci network public-ip list --scope REGION --lifetime RESERVED \
--compartment-id "$COMPARTMENT_OCID" \
--query 'data[].{ip:"ip-address",name:"display-name",assigned:"assigned-entity-id"}' --output table
- NSG rules and security list entries for this host's IP are removed. If the IP is reused later, an old rule would quietly grant the new host the same access.
- Monitoring alarms on this instance are deleted or updated. Otherwise they go to "no data" or fire
absent()alerts.
oci monitoring alarm list --compartment-id "$COMPARTMENT_OCID" --all \
--query "data[?contains(query, '$INSTANCE_OCID')].{name:\"display-name\",id:id}" --output table
Alarms that filter on resourceDisplayName won't match the OCID search. Search for the display name too.
- The host is deregistered from external tools. That means backup agents, monitoring, OS Management Hub, CMDB and endpoint security consoles.
4. Terminate
- Volumes you plan to keep are detached first. By default, termination doesn't delete attached block volumes. Newer CLI versions can also delete data volumes that were created at launch, so check the flags in
oci compute instance terminate --helpfor your version. Detaching first removes the doubt.
oci compute volume-attachment detach --volume-attachment-id "$ATTACHMENT_OCID" \
--wait-for-state DETACHED
- The instance is terminated, with the boot volume decision made explicitly.
# Keep the boot volume for a cooling-off period:
oci compute instance terminate --instance-id "$INSTANCE_OCID" \
--preserve-boot-volume true --wait-for-state TERMINATED
# Or delete it together with the instance (the backup from step 2 is your copy):
oci compute instance terminate --instance-id "$INSTANCE_OCID" \
--preserve-boot-volume false --force --wait-for-state TERMINATED
Caution
Termination can't be undone. Before you run it, confirm the instance OCID against the change ticket. Display names are not unique, so never terminate by name in a script.
- Leftovers are deleted after the agreed retention. This includes the preserved boot volume, detached block volumes, final backups and any custom image you made.
oci bv boot-volume list --compartment-id "$COMPARTMENT_OCID" --availability-domain "$AD" \
--query 'data[?"lifecycle-state"==`AVAILABLE`].{name:"display-name",id:id,created:"time-created"}' --output table
Quick reference
| Left behind | Still costs money? | How to find it |
|---|---|---|
| Preserved boot volume | Yes | oci bv boot-volume list |
| Detached block volume | Yes | oci bv volume list |
| Manual volume or boot backups | Yes | oci bv backup list, oci bv boot-volume-backup list |
| Reserved public IP | Check current pricing | oci network public-ip list --lifetime RESERVED |
| Stale DNS record, NSG rule or alarm | No, but a risk | Zone records, NSG rules, oci monitoring alarm list |
Prevent it next time
- Require
owner,environmentandcost-centerdefined tags on every instance with tag defaults. Then step 1 takes minutes instead of detective work. - Add a
decommission-datetag during the stop period. A weekly OCI Search query can then list instances that are past their date. - Use NSGs that reference other NSGs instead of host IPs. Those rules go away with the VNIC instead of lingering.
- Keep this checklist in the change template for "retire server" requests, and attach the CLI output from each stage.