Public-cloud deployment of a single-user expense tracker: - 4-container stack: Caddy (HTTPS via Let's Encrypt), nginx (React/Vite SPA), Express API, Postgres 16 - Repeatable deployment via prepare-app.sh using only OCI CLI (no web console) - Persistent Postgres volume, auto-restart policies, healthchecks, backup + restore scripts - DuckDNS dynamic DNS for the public hostname; secrets isolated to gitignored .env Author: Gigi Saji Live URL: https://savesave.duckdns.org
110 lines
4.9 KiB
Bash
110 lines
4.9 KiB
Bash
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# remove-app.sh — tear down everything created by prepare-app.sh.
|
|
#
|
|
# Does NOT touch any existing resources outside sk1-* and the named VM.
|
|
# Idempotent: safe to re-run; each step skips if already gone.
|
|
# =============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$SCRIPT_DIR"
|
|
|
|
if [ ! -f .env ]; then
|
|
echo "ERROR: .env not found in $SCRIPT_DIR" >&2
|
|
exit 1
|
|
fi
|
|
set -a; source .env; set +a
|
|
|
|
: "${OCI_VM_NAME:=sk1-expense-tracker}"
|
|
: "${OCI_SSH_PRIVATE_KEY_PATH:=$HOME/.ssh/id_ed25519}"
|
|
OCI_SSH_PRIVATE_KEY_PATH="${OCI_SSH_PRIVATE_KEY_PATH/#\~/$HOME}"
|
|
|
|
command -v oci >/dev/null || { echo "ERROR: oci CLI not installed"; exit 1; }
|
|
|
|
TENANCY_OCID="$(oci iam compartment list --query 'data[0]."compartment-id"' --raw-output 2>/dev/null)"
|
|
COMPARTMENT_ID="${OCI_COMPARTMENT_ID:-$TENANCY_OCID}"
|
|
|
|
VCN_NAME="sk1-vcn"
|
|
SUBNET_NAME="sk1-subnet"
|
|
IG_NAME="sk1-ig"
|
|
SL_NAME="sk1-seclist"
|
|
RT_NAME="sk1-rt"
|
|
|
|
# ---- Find the VM by name ----
|
|
INSTANCE_ID="$(oci compute instance list --compartment-id "$COMPARTMENT_ID" \
|
|
--display-name "$OCI_VM_NAME" \
|
|
--query 'data[?"lifecycle-state" != `TERMINATED`] | [0].id' --raw-output 2>/dev/null || true)"
|
|
|
|
if [ -n "$INSTANCE_ID" ] && [ "$INSTANCE_ID" != "null" ]; then
|
|
echo "[remove] tearing down Compose stack on the VM..."
|
|
VNIC_ID="$(oci compute instance list-vnics --instance-id "$INSTANCE_ID" --query 'data[0].id' --raw-output 2>/dev/null || true)"
|
|
PUBLIC_IP="$(oci network vnic get --vnic-id "$VNIC_ID" --query 'data."public-ip"' --raw-output 2>/dev/null || true)"
|
|
if [ -n "$PUBLIC_IP" ] && [ "$PUBLIC_IP" != "null" ]; then
|
|
ssh -i "$OCI_SSH_PRIVATE_KEY_PATH" -o StrictHostKeyChecking=no \
|
|
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 \
|
|
ubuntu@"$PUBLIC_IP" \
|
|
'cd /opt/sk1 && sg docker -c "docker compose down -v --rmi local --remove-orphans" || true' \
|
|
|| echo "[remove] (could not reach VM via SSH — proceeding to terminate)"
|
|
fi
|
|
|
|
echo "[remove] terminating VM $OCI_VM_NAME ($INSTANCE_ID)..."
|
|
oci compute instance terminate --instance-id "$INSTANCE_ID" --force \
|
|
--preserve-boot-volume false --wait-for-state TERMINATED
|
|
else
|
|
echo "[remove] no running VM named $OCI_VM_NAME — skipping"
|
|
fi
|
|
|
|
# ---- Delete network resources (in reverse dependency order) ----
|
|
VCN_ID="$(oci network vcn list --compartment-id "$COMPARTMENT_ID" --display-name "$VCN_NAME" \
|
|
--query 'data[0].id' --raw-output 2>/dev/null || true)"
|
|
|
|
if [ -n "$VCN_ID" ] && [ "$VCN_ID" != "null" ]; then
|
|
SUBNET_ID="$(oci network subnet list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" \
|
|
--display-name "$SUBNET_NAME" --query 'data[0].id' --raw-output 2>/dev/null || true)"
|
|
if [ -n "$SUBNET_ID" ] && [ "$SUBNET_ID" != "null" ]; then
|
|
echo "[remove] deleting subnet..."
|
|
oci network subnet delete --subnet-id "$SUBNET_ID" --force --wait-for-state TERMINATED || true
|
|
fi
|
|
|
|
SL_ID="$(oci network security-list list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" \
|
|
--display-name "$SL_NAME" --query 'data[0].id' --raw-output 2>/dev/null || true)"
|
|
if [ -n "$SL_ID" ] && [ "$SL_ID" != "null" ]; then
|
|
echo "[remove] deleting security list..."
|
|
oci network security-list delete --security-list-id "$SL_ID" --force --wait-for-state TERMINATED || true
|
|
fi
|
|
|
|
RT_ID="$(oci network route-table list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" \
|
|
--display-name "$RT_NAME" --query 'data[0].id' --raw-output 2>/dev/null || true)"
|
|
if [ -n "$RT_ID" ] && [ "$RT_ID" != "null" ]; then
|
|
echo "[remove] deleting route table..."
|
|
oci network route-table delete --rt-id "$RT_ID" --force --wait-for-state TERMINATED || true
|
|
fi
|
|
|
|
IG_ID="$(oci network internet-gateway list --compartment-id "$COMPARTMENT_ID" --vcn-id "$VCN_ID" \
|
|
--display-name "$IG_NAME" --query 'data[0].id' --raw-output 2>/dev/null || true)"
|
|
if [ -n "$IG_ID" ] && [ "$IG_ID" != "null" ]; then
|
|
echo "[remove] deleting internet gateway..."
|
|
oci network internet-gateway delete --ig-id "$IG_ID" --force --wait-for-state TERMINATED || true
|
|
fi
|
|
|
|
echo "[remove] deleting VCN..."
|
|
oci network vcn delete --vcn-id "$VCN_ID" --force --wait-for-state TERMINATED || true
|
|
else
|
|
echo "[remove] no VCN named $VCN_NAME — skipping"
|
|
fi
|
|
|
|
# ---- Reset DuckDNS A record (best effort) ----
|
|
if [ -n "${DUCKDNS_DOMAIN:-}" ] && [ -n "${DUCKDNS_TOKEN:-}" ]; then
|
|
echo "[remove] clearing DuckDNS A record..."
|
|
curl -fsS "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN%%.duckdns.org}&token=${DUCKDNS_TOKEN}&clear=true" \
|
|
|| echo "(DuckDNS clear failed — non-fatal)"
|
|
fi
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " All sk1 resources removed. Existing VMs and unrelated"
|
|
echo " resources are untouched."
|
|
echo "================================================================"
|