Repository navigation
Expand file tree
/
Copy pathvps.sh
More file actions
executable file
·130 lines (115 loc) · 6 KB
/
Copy pathvps.sh
File metadata and controls
executable file
·130 lines (115 loc) · 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/env bash
# Reach the production VPS and its database over SSH. Nothing secret is ever stored on this machine: credentials
# are read from deploy/.env.prod on the VPS for each command and shown masked unless a connection needs them.
#
# Configuration (environment or .env in the repository root):
# VPS_SSH user@host or an alias from ~/.ssh/config (required)
# VPS_APP_DIR repository checkout on the VPS (default: ~/<repo name>)
# VPS_DB_LOCAL_PORT local end of the database tunnel (default: 15432)
#
# Commands:
# ssh shell in the app directory
# env deploy/.env.prod with secret values masked
# ps | logs [service] production compose status / logs
# deploy-status image tag per replica, what /version answers, and whether backups are fresh
# backup-status age of the last successful off-host backup; non-zero when stale or missing
# tunnel forward localhost:$VPS_DB_LOCAL_PORT → Postgres on the VPS until Ctrl-C
# psql [sql] interactive psql through the tunnel, or run one statement and exit
# datagrip open the tunnel in the background and print a ready-to-paste JDBC URL
# (DataGrip: New → Data Source from URL). --no-tunnel / --no-clipboard for scripting.
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
compose_file="deploy/compose.prod.yml"
env_file="deploy/.env.prod"
# .env fills in whatever the environment did not provide; never overrides it.
if [[ -f "$root/.env" ]]; then
while IFS='=' read -r key value; do
[[ "$key" =~ ^[A-Z_][A-Z0-9_]*$ ]] || continue
[[ -z "${!key:-}" ]] && export "$key=$value"
done < <(grep -E '^[A-Z_]+=' "$root/.env" || true)
fi
usage() { sed -n '2,21p' "$0" >&2; exit 2; }
cmd="${1:-}"; shift || true
[[ -n "$cmd" ]] || usage
if [[ -z "${VPS_SSH:-}" ]]; then
echo "VPS_SSH is not set. Put VPS_SSH=user@host (or an ~/.ssh/config alias) in .env or the environment." >&2
exit 2
fi
app_dir="${VPS_APP_DIR:-~/$(basename "$root")}"
local_port="${VPS_DB_LOCAL_PORT:-15432}"
remote() { ssh "$VPS_SSH" "cd $app_dir && $*"; }
remote_env() { ssh "$VPS_SSH" "cat $app_dir/$env_file"; }
prod() { remote "docker compose -f $compose_file --env-file $env_file $*"; }
env_value() { # KEY DEFAULT
local v; v="$(printf '%s\n' "$REMOTE_ENV" | grep -E "^$1=" | head -n1 | cut -d= -f2- || true)"
# One pair of surrounding quotes is compose syntax (KEY="a,b"), not part of the value.
if [[ "$v" =~ ^\"(.*)\"$ || "$v" =~ ^\'(.*)\'$ ]]; then v="${BASH_REMATCH[1]}"; fi
printf '%s' "${v:-$2}"
}
load_remote_env() { REMOTE_ENV="$(remote_env)"; }
tunnel_running() { pgrep -f "ssh .*-L ${local_port}:127.0.0.1:" >/dev/null; }
open_tunnel() { # background, once
if tunnel_running; then echo "tunnel already open on localhost:${local_port}" >&2; return; fi
ssh -f -N -o ExitOnForwardFailure=yes -L "${local_port}:127.0.0.1:$1" "$VPS_SSH"
echo "tunnel: localhost:${local_port} → ${VPS_SSH}:127.0.0.1:$1 (pid $(pgrep -f "ssh .*-L ${local_port}:127.0.0.1:" | head -n1)); close with: pkill -f 'ssh .*-L ${local_port}:'" >&2
}
case "$cmd" in
ssh)
exec ssh -t "$VPS_SSH" "cd $app_dir && exec \$SHELL -l" ;;
env)
remote_env | sed -E 's/^([A-Z0-9_]*(PASSWORD|TOKEN|SECRET|KEY)[A-Z0-9_]*=).+$/\1****/' ;;
ps)
prod ps ;;
logs)
prod logs -f --tail=200 "${1:-}" ;;
deploy-status)
prod "ps --format '{{.Service}} {{.Image}} {{.Status}}'" | grep -E '^app ' || true
load_remote_env
domain="$(env_value DOMAIN "")"
[[ -n "$domain" ]] && { echo "https://${domain}/version →"; curl -fsS --max-time 10 "https://${domain}/version" || true; echo; }
profiles="$(env_value COMPOSE_PROFILES "")"
if [[ ",${profiles// /}," != *",backup,"* ]]; then
echo "backups: DISABLED (COMPOSE_PROFILES lacks backup; docs/deployment.md §6)"
elif status_out="$(prod "--profile backup run --rm -T backup status" 2>/dev/null)"; then
echo "backups: ok — ${status_out//$'\n'/; }"
else
status_out="${status_out//$'\n'/; }"
echo "backups: STALE — ${status_out:-no answer; run make vps-backup-status}"
fi ;;
backup-status)
prod "--profile backup run --rm -T backup status" ;;
tunnel)
load_remote_env
port="$(env_value POSTGRES_HOST_PORT 5432)"
echo "localhost:${local_port} → ${VPS_SSH}:127.0.0.1:${port} (Ctrl-C to close)"
exec ssh -N -o ExitOnForwardFailure=yes -L "${local_port}:127.0.0.1:${port}" "$VPS_SSH" ;;
psql)
load_remote_env
port="$(env_value POSTGRES_HOST_PORT 5432)"
db="$(env_value POSTGRES_DB app)"; user="$(env_value POSTGRES_USER app)"; pass="$(env_value POSTGRES_PASSWORD "")"
open_tunnel "$port"
export PGPASSWORD="$pass"
if command -v psql >/dev/null; then
psql_cmd=(psql -h localhost -p "$local_port" -U "$user" -d "$db")
else
psql_cmd=(docker run --rm -it --network host -e PGPASSWORD postgres:17-alpine psql -h localhost -p "$local_port" -U "$user" -d "$db")
fi
if [[ $# -gt 0 ]]; then "${psql_cmd[@]}" -c "$*"; else "${psql_cmd[@]}"; fi ;;
datagrip)
want_tunnel=true; want_clip=true
for a in "$@"; do case "$a" in --no-tunnel) want_tunnel=false ;; --no-clipboard) want_clip=false ;; esac; done
load_remote_env
port="$(env_value POSTGRES_HOST_PORT 5432)"
db="$(env_value POSTGRES_DB app)"; user="$(env_value POSTGRES_USER app)"; pass="$(env_value POSTGRES_PASSWORD "")"
$want_tunnel && open_tunnel "$port"
url="jdbc:postgresql://localhost:${local_port}/${db}?user=${user}&password=${pass}"
if $want_clip; then
if command -v wl-copy >/dev/null; then printf '%s' "$url" | wl-copy && echo "(copied to clipboard)" >&2
elif command -v xclip >/dev/null; then printf '%s' "$url" | xclip -selection clipboard && echo "(copied to clipboard)" >&2
elif command -v pbcopy >/dev/null; then printf '%s' "$url" | pbcopy && echo "(copied to clipboard)" >&2
fi
fi
echo "DataGrip → New → Data Source from URL → paste:"
echo "$url" ;;
*) usage ;;
esac