Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
305 lines (281 loc) · 15.1 KB
/
Copy pathinstall.sh
File metadata and controls
305 lines (281 loc) · 15.1 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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
#!/usr/bin/env bash
#
# install.sh - stand up the entire ForgeBase platform on a fresh Ubuntu server
# (22.04/24.04). Idempotent: safe to re-run (also serves as the upgrade path).
#
# One command on a fresh box, then answer the prompts (domain, email) and it
# verifies DNS and provisions Let's Encrypt TLS automatically:
#
# git clone <repo> forgebase && cd forgebase && sudo bash install.sh
#
# Non-interactive (CI / scripted):
# DOMAIN=base.example.com ACME_EMAIL=you@example.com sudo -E bash install.sh
#
# Optional env: PANEL_USER / PANEL_PASS (else admin + random), SKIP_FIREWALL=1,
# SKIP_HARDENING=1, SKIP_CADDY=1 (DNS-less/test), SKIP_DNS_CHECK=1,
# MAX_UPLOAD_MB (default 100), LISTEN (default 127.0.0.1:8080).
# DNS: A records for DOMAIN, db.DOMAIN and a wildcard *.DOMAIN pointing here.
#
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then echo "Run as root (sudo -E bash install.sh)"; exit 1; fi
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
DOMAIN="${DOMAIN:-$(cat /opt/pgforge/domain 2>/dev/null || true)}"
ACME_EMAIL="${ACME_EMAIL:-$(cat /opt/pgforge/acme_email 2>/dev/null || true)}"
# Detect this server's public IP up front (shown in the DNS instructions).
PUBIP="$(curl -fsSL --max-time 8 https://api.ipify.org 2>/dev/null || curl -fsSL --max-time 8 https://ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}')"
# ----------------------------------------------------------------- interactive setup
# When DOMAIN/ACME_EMAIL aren't provided and we have a terminal, walk the operator
# through it step by step and verify DNS is pointed before provisioning TLS.
if [ -t 0 ]; then
echo "=================================================================="
echo " ForgeBase installer"
echo " This server's public IP: ${PUBIP}"
echo "=================================================================="
if [ -z "$DOMAIN" ]; then
read -rp " Domain to host ForgeBase on (e.g. base.example.com): " DOMAIN
DOMAIN="$(echo "$DOMAIN" | tr 'A-Z' 'a-z' | tr -d ' ')"
fi
if [ -z "$ACME_EMAIL" ]; then
read -rp " Email for Let's Encrypt TLS certificates: " ACME_EMAIL
fi
if [ -z "${LEGACY_DOMAIN:-}" ]; then
read -rp " Legacy domain alias to keep serving old links (optional, Enter to skip): " LEGACY_DOMAIN || LEGACY_DOMAIN=""
LEGACY_DOMAIN="$(echo "${LEGACY_DOMAIN:-}" | tr 'A-Z' 'a-z' | tr -d ' ')"
fi
fi
[ -z "$DOMAIN" ] && { echo "Set DOMAIN=<your.domain> (with sudo -E)"; exit 1; }
[ -z "$ACME_EMAIL" ] && { echo "Set ACME_EMAIL=<you@example.com> (with sudo -E)"; exit 1; }
# DNS verification loop: the panel, the db host and every project subdomain need
# to resolve here, and Let's Encrypt will fail to issue certs until they do.
resolve_ip() { # $1 = hostname -> first A record (tries a public resolver, then the system)
{ command -v dig >/dev/null 2>&1 && dig +short A "$1" @1.1.1.1 2>/dev/null | grep -Eo '^[0-9.]+' | head -1; } \
|| getent hosts "$1" 2>/dev/null | awk '{print $1}' | head -1
}
if [ -t 0 ] && [ -z "${SKIP_DNS_CHECK:-}" ]; then
echo
echo " Point these DNS records (type A) at ${PUBIP}:"
echo " ${DOMAIN}"
echo " db.${DOMAIN}"
echo " *.${DOMAIN} (wildcard - one record covers every project)"
echo " Use 'DNS only' (grey cloud) on Cloudflare so TLS is issued directly."
echo
while true; do
read -rp " Press Enter once the records are set (or type 'skip' to continue anyway): " ans || ans=skip
[ "$ans" = "skip" ] && { echo " Skipping DNS check; TLS will fail until DNS resolves."; break; }
apex_ip="$(resolve_ip "$DOMAIN")"
wild_ip="$(resolve_ip "forgebase-dns-check.${DOMAIN}")" # exercises the wildcard
if [ "$apex_ip" = "$PUBIP" ] && { [ "$wild_ip" = "$PUBIP" ] || [ -z "$wild_ip" ]; }; then
echo " DNS OK: ${DOMAIN} -> ${apex_ip}${wild_ip:+, wildcard -> ${wild_ip}}"
break
fi
echo " Not there yet: ${DOMAIN} -> ${apex_ip:-(no answer)} (want ${PUBIP}). DNS can take a few minutes."
done
fi
PANEL_USER="${PANEL_USER:-admin}"
echo "==> Installing ForgeBase for https://${DOMAIN}"
# ----------------------------------------------------------------- packages
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
# zstd: basebackups are tar+zstd since infra rev 11, and pitr-restore.sh runs on
# the HOST, where GNU tar shells out to the zstd binary to read them.
apt-get install -y -qq git curl openssl unzip ca-certificates golang-go fail2ban zstd >/dev/null
systemctl enable --now fail2ban >/dev/null 2>&1 || true
if ! command -v docker >/dev/null 2>&1; then
echo "==> Installing Docker"
curl -fsSL https://get.docker.com | sh >/dev/null
fi
docker compose version >/dev/null 2>&1 || { echo "docker compose plugin missing"; exit 1; }
# PostgREST powers the per-project Data API (auto REST). Static binary.
if [ ! -x /usr/local/bin/postgrest ]; then
echo "==> Installing PostgREST"
PGRST_URL="$(curl -fsSL https://api.github.com/repos/PostgREST/postgrest/releases/latest \
| grep -oE 'https://[^"]*linux-static-x86-64.tar.xz' | head -1)"
if [ -n "$PGRST_URL" ]; then
( cd /tmp && curl -fsSL -o pgrst.tar.xz "$PGRST_URL" && tar -xf pgrst.tar.xz \
&& install -m 0755 postgrest /usr/local/bin/postgrest && rm -f pgrst.tar.xz postgrest )
fi
fi
# Deno powers Edge Functions.
if [ ! -x /usr/local/bin/deno ]; then
echo "==> Installing Deno"
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh >/dev/null 2>&1 || true
fi
# ----------------------------------------------------------------- swap
if ! swapon --show | grep -q '/swapfile'; then
echo "==> Adding 4G swap"
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile >/dev/null && swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
sysctl -w vm.swappiness=10 >/dev/null
# ----------------------------------------------------------------- layout
mkdir -p /opt/pgforge/{bin,certs,pgbouncer,caddy,stack} \
/opt/pgforge-backups/{wal,physical,dumps} \
/opt/pgforge-storage /opt/pgforge-functions
install -m 0644 "$REPO_DIR/server/edge-runner.ts" /opt/pgforge/edge-runner.ts 2>/dev/null || true
install -m 0644 "$REPO_DIR/server/edge-server.ts" /opt/pgforge/edge-server.ts 2>/dev/null || true
printf '%s\n' "$DOMAIN" > /opt/pgforge/domain
printf '%s\n' "$ACME_EMAIL" > /opt/pgforge/acme_email
# Record the git checkout so the in-app self-update can pull + rebuild from here.
# Only meaningful when installed from a git clone (the documented path).
if git -C "$REPO_DIR" rev-parse --git-dir >/dev/null 2>&1; then
printf '%s\n' "$REPO_DIR" > /opt/pgforge/repo_dir
fi
# ----------------------------------------------------------------- secrets
# On upgrade, reuse the existing secrets so credentials + the stored-password
# encryption key (SESSION_SECRET also encrypts every project's DB password) stay
# stable. Read each from the file that owns it, not by parsing the DSN.
gen() { openssl rand -hex "$1"; }
# Panel login password: exactly 12 characters, letters + digits only (no
# symbols - easy to read out / type). `|| true` swallows the SIGPIPE that head
# raises when it closes the stream early (harmless; the 12 chars are captured).
gen_pass() { LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom 2>/dev/null | head -c 12 || true; }
ENVF=/opt/pgforge/pgforged.env
STACK_ENV=/opt/pgforge/stack/.env
readval() { [ -f "$1" ] && sed -n "s/^$2=//p" "$1" | head -1; }
if [ -f "$ENVF" ]; then
PANEL_PASS="$(readval "$ENVF" PANEL_PASS)"
SESSION_SECRET="$(readval "$ENVF" SESSION_SECRET)"
PANEL_USER="$(readval "$ENVF" PANEL_USER)"
POSTGRES_PASSWORD="$(readval "$STACK_ENV" POSTGRES_PASSWORD)"
fi
# Fill any missing value (fresh install, or a partial prior run).
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-$(gen 24)}"
PANEL_PASS="${PANEL_PASS:-$(gen_pass)}"
SESSION_SECRET="${SESSION_SECRET:-$(gen 32)}"
# ----------------------------------------------------------------- postgres TLS
if [ ! -f /opt/pgforge/certs/server.crt ]; then
echo "==> Generating Postgres TLS cert (10y self-signed; sslmode=require)"
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout /opt/pgforge/certs/server.key -out /opt/pgforge/certs/server.crt \
-subj "/CN=${DOMAIN}" 2>/dev/null
fi
chown 999:999 /opt/pgforge/certs/server.key /opt/pgforge/certs/server.crt
chmod 600 /opt/pgforge/certs/server.key
chown -R 999:999 /opt/pgforge-backups/wal /opt/pgforge-backups/physical
# ----------------------------------------------------------------- db stack
echo "==> Postgres 17 + PgBouncer"
cp -r "$REPO_DIR/server/." /opt/pgforge/stack/
rm -rf /opt/pgforge/stack/caddy
printf 'POSTGRES_PASSWORD=%s\n' "$POSTGRES_PASSWORD" > /opt/pgforge/stack/.env
chmod 600 /opt/pgforge/stack/.env
# uid 70 = pgbouncer user inside the edoburu image; it must be able to read the
# auth file. pgforged's rewrites keep the inode, so ownership persists.
touch /opt/pgforge/pgbouncer/userlist.txt
chown 70 /opt/pgforge/pgbouncer/userlist.txt && chmod 600 /opt/pgforge/pgbouncer/userlist.txt
( cd /opt/pgforge/stack
docker compose build -q
docker compose up -d db
echo " waiting for postgres to be healthy..."
for i in $(seq 1 60); do
[ "$(docker inspect -f '{{.State.Health.Status}}' pgforge-db 2>/dev/null)" = healthy ] && break
sleep 2
done
docker compose up -d pgbouncer )
# ----------------------------------------------------------------- caddy
# SKIP_CADDY=1 is for DNS-less test boxes (talk to pgforged directly on :8080).
if [ -z "${SKIP_CADDY:-}" ]; then
echo "==> Caddy (HTTPS)"
sed -e "s/__DOMAIN__/${DOMAIN}/g" -e "s/__ACME_EMAIL__/${ACME_EMAIL}/g" \
"$REPO_DIR/server/caddy/Caddyfile" > /opt/pgforge/caddy/Caddyfile
cp "$REPO_DIR/server/caddy/docker-compose.yml" /opt/pgforge/caddy/docker-compose.yml
( cd /opt/pgforge/caddy && docker compose up -d )
docker exec pgforge-caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile 2>/dev/null || true
else
echo "==> Skipping Caddy (SKIP_CADDY set); pgforged serves plain HTTP on ${LISTEN:-127.0.0.1:8080}"
fi
# ----------------------------------------------------------------- pgforged
echo "==> Building pgforged (Go control plane)"
# Version/build time are passed in from deploy.sh (git SHA on the dev machine) so
# the running commit is visible in the panel footer for rollback tracking.
PGFORGE_VERSION="${PGFORGE_VERSION:-dev}"
PGFORGE_BUILDTIME="${PGFORGE_BUILDTIME:-unknown}"
LDFLAGS="-X main.version=${PGFORGE_VERSION} -X main.buildTime=${PGFORGE_BUILDTIME}"
( cd "$REPO_DIR/pgforge" && go mod tidy && go build -ldflags "$LDFLAGS" -o /opt/pgforge/bin/pgforged . )
# statement_timeout is a generous backstop on the control-plane connection so a
# runaway meta query can't wedge it forever; interactive editor queries get a
# tighter per-request context timeout in pgforged. options= is how lib/pq sets a
# server GUC at connect (%20 = space, %3D = '=').
cat > "$ENVF" <<ENV
PGFORGE_DSN=postgres://postgres:${POSTGRES_PASSWORD}@127.0.0.1:5432/pgforge?sslmode=require&options=-c%20statement_timeout%3D120000
PANEL_USER=${PANEL_USER}
PANEL_PASS=${PANEL_PASS}
SESSION_SECRET=${SESSION_SECRET}
DOMAIN=${DOMAIN}
LISTEN=${LISTEN:-127.0.0.1:8080}
USERLIST_PATH=/opt/pgforge/pgbouncer/userlist.txt
ENV
chmod 600 "$ENVF"
# ----------------------------------------------------------------- services
# The SQL editor is built into pgforged now; the old interim pgweb is retired.
echo "==> systemd services + timers (via apply-infra.sh)"
# Retire a pgweb unit left by an older install, if present.
if [ -f /etc/systemd/system/pgweb.service ]; then
systemctl disable --now pgweb 2>/dev/null || true
rm -f /etc/systemd/system/pgweb.service /opt/pgforge/pgweb.env
fi
# apply-infra.sh installs EVERY operational script and EVERY systemd unit from
# the repo, so this list can never drift from what the repo ships (a
# hand-maintained list here once silently skipped new units).
sh "$REPO_DIR/scripts/apply-infra.sh" --safe
systemctl enable --now pgforged >/dev/null 2>&1
systemctl restart pgforged
# ----------------------------------------------------------------- per-instance mode (opt-in)
# INSTANCES=1 enables copy-on-write branching + scale-to-zero (each project/branch
# becomes its own Postgres instance on a btrfs store, fronted by a cold-start
# proxy). Off by default; the classic shared cluster is used otherwise.
if [ -n "${INSTANCES:-}" ]; then
echo "==> Enabling per-instance mode (CoW branching + scale-to-zero)"
bash "$REPO_DIR/scripts/setup-instances.sh" "$REPO_DIR" \
|| echo " (per-instance setup failed; classic shared-cluster mode still active)"
fi
# ----------------------------------------------------------------- firewall
if [ -z "${SKIP_FIREWALL:-}" ]; then
echo "==> Firewall"
sh "$REPO_DIR/scripts/setup-firewall.sh"
fi
# ----------------------------------------------------------------- legacy alias
# Optional second domain that keeps old links + connection strings working
# (panel redirects its apex to the primary). Stored in the meta DB settings.
if [ -n "${LEGACY_DOMAIN:-}" ] && [ "$LEGACY_DOMAIN" != "$DOMAIN" ]; then
docker exec pgforge-db psql -U pgforge -d pgforge -c "INSERT INTO settings(key,value) VALUES ('domain_secondary','$LEGACY_DOMAIN')
ON CONFLICT (key) DO UPDATE SET value='$LEGACY_DOMAIN';
INSERT INTO settings(key,value) VALUES ('panel_redirect','1')
ON CONFLICT (key) DO UPDATE SET value='1'" >/dev/null 2>&1 || docker exec pgforge-db psql -U postgres -d pgforge -c "INSERT INTO settings(key,value) VALUES ('domain_secondary','$LEGACY_DOMAIN')
ON CONFLICT (key) DO UPDATE SET value='$LEGACY_DOMAIN';
INSERT INTO settings(key,value) VALUES ('panel_redirect','1')
ON CONFLICT (key) DO UPDATE SET value='1'" >/dev/null 2>&1 || true
echo "==> Legacy alias: $LEGACY_DOMAIN (+ *.$LEGACY_DOMAIN) keeps old links working"
fi
# ----------------------------------------------------------------- hardening
install -m 0755 "$REPO_DIR/scripts/harden-server.sh" /opt/pgforge/bin/harden-server.sh
if [ -z "${SKIP_HARDENING:-}" ]; then
echo "==> Server hardening (fail2ban + auto security updates)"
sh "$REPO_DIR/scripts/harden-server.sh" || true
fi
sleep 1
echo
echo "=================================================================="
echo " ForgeBase installed."
if [ -z "${SKIP_CADDY:-}" ]; then
echo " Panel : https://${DOMAIN}"
else
echo " Panel : http://<this-ip>:${LISTEN:-127.0.0.1:8080} (SKIP_CADDY: no TLS)"
fi
echo " user : ${PANEL_USER}"
echo " password : ${PANEL_PASS}"
echo " Postgres : ${DOMAIN}:5432 (TLS) Pooler: ${DOMAIN}:6543"
echo " pgforged : $(systemctl is-active pgforged)"
echo
echo " Save PANEL_PASS into your local .env."
echo
echo " Off-box backups (STRONGLY recommended - full disaster recovery):"
echo " apt install rclone && rclone config"
echo " echo '<remote>:<path>' > /opt/pgforge/backup_remote"
echo " The nightly sync then mirrors dumps, basebackups, WAL and an"
echo " ENCRYPTED disaster kit. Copy /opt/pgforge/recovery.pass (created"
echo " on the first backup run) somewhere OFF this server - it opens the"
echo " kit. Rebuild procedure: docs/DISASTER-RECOVERY.md"
echo "=================================================================="
echo " ForgeBase by FutureForge Studios Private Limited (ffstudios.io)"
echo " Made with care in India."
echo "=================================================================="