-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathinstall-dokploy.sh
More file actions
552 lines (480 loc) · 20.3 KB
/
Copy pathinstall-dokploy.sh
File metadata and controls
552 lines (480 loc) · 20.3 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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
#!/bin/bash
# Docker + Dokploy Installer
# Run this AFTER setup.sh has completed hardening.
# Usage: sudo bash install-dokploy.sh
#
# This script:
# 1. Installs Docker (official APT repo + GPG + log rotation)
# 2. Sets up DOCKER-USER firewall (deny-by-default)
# 3. Installs Dokploy (handles Swarm init internally)
# 4. Re-verifies firewall + SSH after Dokploy install
set -euo pipefail
VERSION="1.0.16"
if [[ "${1:-}" == "--version" || "${1:-}" == "-v" ]]; then
echo "Docker + Dokploy Installer v$VERSION"
exit 0
fi
# === ROOT CHECK ===
if [ "$(id -u)" -ne 0 ]; then
echo "This script requires root privileges."
echo "Re-running with sudo..."
exec sudo bash "$0" "$@"
fi
# === AUTO-SCREEN ===
# If not inside screen, relaunch inside screen so the script survives SSH drops.
if [ -z "${STY:-}" ]; then
if command -v screen &>/dev/null; then
echo "Launching inside screen (reconnect with: screen -r dokploy-install)"
exec screen -S dokploy-install bash "$0" "$@"
else
echo "[WARN] screen not found — if SSH drops, the install will be interrupted."
echo " Consider running: screen -S dokploy-install bash $0"
fi
fi
# === LOAD CONFIG FROM SETUP.SH ===
CONFIG_FILE="/root/.vps_hardening_config"
if [ ! -f "$CONFIG_FILE" ]; then
echo "[ERROR] Config file not found at $CONFIG_FILE"
echo " Run setup.sh first to harden the server."
exit 1
fi
# Safe config parsing — only read expected variables (no arbitrary code execution)
while IFS='=' read -r key value; do
# Skip comments and empty lines
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
# Strip leading/trailing whitespace and quotes
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
case "$key" in
SSH_PORT|NEW_USER|LOG_DAYS|LOG_WEEKS|CURRENT_USER)
declare "$key=$value"
;;
*)
echo "[WARN] Ignoring unknown config key: $key"
;;
esac
done < "$CONFIG_FILE"
# Validate required variables
for var in SSH_PORT NEW_USER LOG_DAYS; do
if [ -z "${!var:-}" ]; then
echo "[ERROR] Missing $var in $CONFIG_FILE -- run setup.sh first"
exit 1
fi
done
# Validate SSH_PORT is a number in valid range
if ! echo "$SSH_PORT" | grep -qE '^[0-9]+$' || [ "$SSH_PORT" -lt 1024 ] || [ "$SSH_PORT" -gt 65535 ]; then
echo "[ERROR] Invalid SSH_PORT=$SSH_PORT in $CONFIG_FILE -- expected a number between 1024 and 65535"
exit 1
fi
# Validate NEW_USER format
if ! echo "$NEW_USER" | grep -qE '^[a-z][a-z0-9_-]*$'; then
echo "[ERROR] Invalid NEW_USER=$NEW_USER in $CONFIG_FILE -- expected lowercase letters, numbers, underscores, hyphens"
exit 1
fi
LOG_FILE="/var/log/vps_setup.log"
TOTAL_STEPS=3
CURRENT_STEP=0
# === CLEANUP TRAP ===
SETUP_PHASE="init"
cleanup_on_error() {
local exit_code=$?
if [ "$exit_code" -ne 0 ]; then
echo ""
printf " \033[1;31m──────────────────────────────────────────────\033[0m\n"
printf " \033[1;31m[ERROR] INSTALL FAILED during phase: %s\033[0m\n" "$SETUP_PHASE"
printf " Check the log: %s\n" "$LOG_FILE"
printf " \033[1;31m──────────────────────────────────────────────\033[0m\n"
# Restore SSH access if we broke something
echo ""
printf " \033[1;33m[!] Verifying SSH is still accessible...\033[0m\n"
if [ -f /run/sshd-hardened.pid ] && kill -0 "$(cat /run/sshd-hardened.pid)" 2>/dev/null; then
printf " \033[1;32m[OK] Standalone sshd still running on port %s\033[0m\n" "$SSH_PORT"
elif systemctl is-active ssh.service &>/dev/null; then
printf " \033[1;32m[OK] ssh.service is active\033[0m\n"
else
printf " \033[1;33m[!] Restarting SSH service...\033[0m\n"
sudo systemctl start ssh.service 2>/dev/null || true
fi
# Re-apply UFW in case Dokploy broke it
if command -v ufw &>/dev/null; then
sudo ufw limit "$SSH_PORT/tcp" 2>/dev/null || true
fi
fi
}
trap cleanup_on_error EXIT
trap '' HUP PIPE
# === UI FUNCTIONS ===
if ! command -v gum &>/dev/null; then
echo "[ERROR] gum not found. Run setup.sh first."
exit 1
fi
progress_bar() {
tty -s 2>/dev/null || return 0
local current=$1
local total=$2
local label="$3"
local filled=$((current * 20 / total))
local empty=$((20 - filled))
local bar
bar="$(printf '%*s' "$filled" '' | tr ' ' '=')$(printf '%*s' "$empty" '' | tr ' ' ' ')"
echo ""
printf " \033[0;90m──────────────────────────────────────────────\033[0m\n"
echo ""
printf " [\033[0;32m%s\033[0m] \033[1;34mStep %s/%s\033[0m -- %s\n" "$bar" "$current" "$total" "$label"
echo ""
}
wait_for_apt() {
local max_wait=120
local waited=0
while sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 ||
sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1 ||
sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; do
if [ "$waited" -eq 0 ]; then
warn "APT is locked by another process (likely unattended-upgrades). Waiting..."
fi
sleep 5
waited=$((waited + 5))
if [ "$waited" -ge "$max_wait" ]; then
error "APT still locked after ${max_wait}s — kill the process or try again later"
fi
done
if [ "$waited" -gt 0 ]; then
log "APT lock released after ${waited}s"
fi
}
run_with_spinner() {
local label="$1"
shift
sudo -v 2>/dev/null || true
if tty -s 2>/dev/null; then
gum spin --spinner dot --title "$label" -- "$@"
else
"$@" > /dev/null 2>&1
fi
}
run_with_log() {
local label="$1"
shift
sudo -v 2>/dev/null || true
printf " \033[1;34m>> %s\033[0m\n" "$label"
local tmpfile
tmpfile=$(mktemp) || { echo "Failed to create temp file"; return 1; }
# Ensure temp file is cleaned up even if script is interrupted
# $tmpfile must expand now, not at signal time
# shellcheck disable=SC2064
trap "rm -f '$tmpfile'; trap - RETURN" RETURN
"$@" > "$tmpfile" 2>&1 &
local pid=$!
tail -f "$tmpfile" 2>/dev/null | while IFS= read -r line; do
printf " \033[0;90m %s\033[0m\n" "$line"
done &
local tail_pid=$!
wait "$pid"
local exit_code=$?
sleep 1
kill "$tail_pid" 2>/dev/null; wait "$tail_pid" 2>/dev/null || true
rm -f "$tmpfile"
return "$exit_code"
}
log() {
if tty -s 2>/dev/null; then
gum style --foreground 2 " [OK] $1" 2>/dev/null || true
echo "" 2>/dev/null || true
fi
echo "[OK] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 2>/dev/null || true
}
warn() {
if tty -s 2>/dev/null; then
gum style --foreground 3 " [!] $1" 2>/dev/null || true
fi
echo "[WARN] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 2>/dev/null || true
}
error() {
if tty -s 2>/dev/null; then
gum style --foreground 1 --bold " [X] $1" 2>/dev/null || true
fi
echo "[ERROR] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 2>/dev/null || true
exit 1
}
# === WELCOME ===
clear 2>/dev/null || true
gum style \
--border double \
--border-foreground 4 \
--padding "1 6" \
--margin "1 2" \
--bold \
--align center \
"DOCKER + DOKPLOY INSTALLER" \
"" \
"Self-hosted PaaS · Deny-by-default firewall" \
"3 steps · about 5-10 minutes"
echo ""
gum style --bold --foreground 6 " WHAT IT DOES"
gum style --foreground 240 " ────────────────────────────────────────────────"
echo ""
printf " $(gum style --foreground 240 '1') Docker: official APT repo + GPG + log rotation\n"
printf " $(gum style --foreground 240 '2') Firewall: DOCKER-USER deny-by-default + allow 80/443 + temporary 3000\n"
printf " $(gum style --foreground 240 '3') Dokploy: self-hosted PaaS at port 3000\n"
echo ""
gum style \
--border rounded \
--border-foreground 3 \
--foreground 3 \
--padding "0 2" \
--margin "0 2" \
"⚠ PROVIDER FIREWALL CHECK" \
"Open TCP 3000 temporarily before continuing." \
"Keep your custom SSH port open." \
"Close TCP 3000 after domain + HTTPS setup in Dokploy."
echo ""
gum style --foreground 6 --bold " STATUS: No Docker or Dokploy changes have been made yet."
echo ""
gum confirm "Start Docker + Dokploy install now?" || { echo "Install cancelled."; exit 0; }
START_TIME=$SECONDS
echo "=== Docker + Dokploy Install - $(date) ===" >> "$LOG_FILE"
# === STEP 1: INSTALL DOCKER ===
CURRENT_STEP=1
progress_bar "$CURRENT_STEP" "$TOTAL_STEPS" "Install Docker (~2-3 min)"
SETUP_PHASE="docker"
wait_for_apt
run_with_spinner "Installing Docker prerequisites" sudo apt-get install -y -qq ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
DOCKER_GPG_TMP=$(mktemp)
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o "$DOCKER_GPG_TMP"
DOCKER_FP=$(gpg --with-colons --import-options show-only --import "$DOCKER_GPG_TMP" 2>/dev/null | awk -F: '/^fpr:/{print $10; exit}')
EXPECTED_DOCKER_FP="9DC858229FC7DD38854AE2D88D81803C0EBFCD88"
if [ "$DOCKER_FP" != "$EXPECTED_DOCKER_FP" ]; then
rm -f "$DOCKER_GPG_TMP"
error "Docker GPG key fingerprint mismatch! Expected: $EXPECTED_DOCKER_FP Got: $DOCKER_FP"
fi
sudo gpg --yes --dearmor -o /etc/apt/keyrings/docker.gpg < "$DOCKER_GPG_TMP" 2>/dev/null
rm -f "$DOCKER_GPG_TMP"
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
run_with_spinner "Updating Docker repository" sudo apt-get update -qq
run_with_log "Installing Docker Engine" sudo apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
log "Docker installed (official APT repo with GPG)"
log "Strict Docker CLI mode enabled (use sudo docker; user not added to docker group)"
sudo mkdir -p /etc/docker
if [ "$LOG_DAYS" -le 90 ]; then
DOCKER_MAX_FILE=3
elif [ "$LOG_DAYS" -le 365 ]; then
DOCKER_MAX_FILE=7
else
DOCKER_MAX_FILE=14
fi
sudo tee /etc/docker/daemon.json > /dev/null << EOF
{
"log-driver": "json-file",
"log-opts": {"max-size": "10m", "max-file": "${DOCKER_MAX_FILE}"},
"no-new-privileges": true
}
EOF
sudo systemctl restart docker
log "Docker log rotation configured"
# NOTE: Docker Swarm is NOT initialized here. Dokploy's installer does
# "docker swarm leave --force" then re-inits Swarm itself. If we init
# Swarm first, the leave+rejoin cycle disrupts iptables and kills SSH.
# Let Dokploy handle Swarm initialization entirely.
# === STEP 2: DOCKER-USER FIREWALL ===
CURRENT_STEP=2
progress_bar "$CURRENT_STEP" "$TOTAL_STEPS" "Configure Docker firewall"
SETUP_PHASE="docker-firewall"
sudo tee /usr/local/bin/docker-firewall.sh > /dev/null << 'FWSCRIPT'
#!/bin/bash
# Persistent DOCKER-USER rules — re-applied after Docker starts on each boot.
# Port 3000 (Dokploy UI) is NOT included here: it is opened temporarily during
# initial setup and should be closed manually after domain + HTTPS is configured.
for cmd in iptables ip6tables; do
"$cmd" -L DOCKER-USER -n &>/dev/null 2>&1 || continue
"$cmd" -F DOCKER-USER
"$cmd" -I DOCKER-USER -j DROP
"$cmd" -I DOCKER-USER -p tcp --dport 443 -j ACCEPT
"$cmd" -I DOCKER-USER -p tcp --dport 80 -j ACCEPT
"$cmd" -I DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
"$cmd" -I DOCKER-USER -i lo -j ACCEPT
done
# Allow Docker bridge networks (172.16.0.0/12) + overlay/Swarm networks (10.0.0.0/8)
iptables -I DOCKER-USER -s 172.16.0.0/12 -j ACCEPT
iptables -I DOCKER-USER -s 10.0.0.0/8 -j ACCEPT
# Allow Docker internal IPv6 networks
ip6tables -I DOCKER-USER -s fd00::/8 -j ACCEPT 2>/dev/null || true
FWSCRIPT
sudo chmod 750 /usr/local/bin/docker-firewall.sh
sudo tee /etc/systemd/system/docker-firewall.service > /dev/null << 'FWSERVICE'
[Unit]
Description=Docker DOCKER-USER firewall rules
After=docker.service
Requires=docker.service
BindsTo=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/docker-firewall.sh
ExecReload=/usr/local/bin/docker-firewall.sh
[Install]
WantedBy=multi-user.target
FWSERVICE
sudo systemctl daemon-reload
sudo systemctl enable docker-firewall
run_with_spinner "Configuring DOCKER-USER firewall rules" sudo systemctl start docker-firewall
# Temporary port 3000 (not in the persistent service — dies on next Docker restart)
sudo iptables -I DOCKER-USER -p tcp --dport 3000 -j ACCEPT
sudo ip6tables -I DOCKER-USER -p tcp --dport 3000 -j ACCEPT 2>/dev/null || true
log "Docker firewall configured (DOCKER-USER: deny-by-default, allow 80, 443, 3000)"
# === STEP 3: INSTALL DOKPLOY ===
CURRENT_STEP=3
progress_bar "$CURRENT_STEP" "$TOTAL_STEPS" "Install Dokploy (~2-5 min)"
SETUP_PHASE="dokploy"
# Pre-install iptables-persistent BEFORE Dokploy so its installer finds it
# already present and skips the install (which would otherwise flush all rules
# and conflict with UFW).
echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections
sudo apt-get install -y -qq iptables-persistent > /dev/null 2>&1
sudo apt-mark hold ufw > /dev/null 2>&1 || true
log "Pre-installed iptables-persistent (prevents Dokploy from flushing rules)"
DOKPLOY_INSTALLER=$(mktemp)
curl -sSL https://dokploy.com/install.sh -o "$DOKPLOY_INSTALLER"
# Basic sanity check — verify this looks like the Dokploy installer
if ! grep -qi "dokploy" "$DOKPLOY_INSTALLER"; then
rm -f "$DOKPLOY_INSTALLER"
error "Dokploy installer content looks suspicious — aborting for safety"
fi
INSTALLER_HASH=$(sha256sum "$DOKPLOY_INSTALLER" | awk '{print $1}')
log "Dokploy installer SHA256: $INSTALLER_HASH"
# Pre-create docker_gwbridge before Dokploy runs "docker swarm init".
# When swarm init finds this bridge already present, it skips recreating it,
# which prevents the network disruption that kills SSH connections.
# See: https://docs.docker.com/engine/swarm/networking/
if ! sudo docker network inspect docker_gwbridge &>/dev/null; then
sudo docker network create \
--subnet 172.18.0.0/16 \
--gateway 172.18.0.1 \
-o com.docker.network.bridge.name=docker_gwbridge \
-o com.docker.network.bridge.enable_icc=false \
-o com.docker.network.bridge.enable_ip_masquerade=true \
docker_gwbridge > /dev/null 2>&1
log "Pre-created docker_gwbridge (prevents SSH disruption during swarm init)"
fi
run_with_log "Installing Dokploy (~2-5 min)" bash "$DOKPLOY_INSTALLER"
rm -f "$DOKPLOY_INSTALLER"
log "Dokploy installed"
sudo apt-mark unhold ufw > /dev/null 2>&1 || true
# === POST-DOKPLOY RECOVERY ===
# Dokploy's install script can break things. Fix everything it might have touched.
SETUP_PHASE="post-dokploy-recovery"
# 1. Reinstall UFW if Dokploy removed it
if ! dpkg -l ufw 2>/dev/null | grep -q "^ii"; then
run_with_spinner "Reinstalling UFW (removed by Dokploy)" sudo apt-get install -y -qq ufw
fi
# 2. Re-apply UFW rules (force reset removes any manual rules added after setup.sh)
# Backup existing UFW rules before reset
sudo cp /etc/ufw/user.rules "/etc/ufw/user.rules.bak.$(date +%s)" 2>/dev/null || true
sudo cp /etc/ufw/user6.rules "/etc/ufw/user6.rules.bak.$(date +%s)" 2>/dev/null || true
warn "UFW rules backed up and will be reset to match hardening config"
sudo ufw disable > /dev/null 2>&1 || true
sudo ufw --force reset > /dev/null
sudo ufw default deny incoming > /dev/null
sudo ufw default allow outgoing > /dev/null
sudo ufw limit "$SSH_PORT/tcp" > /dev/null
sudo ufw allow 80/tcp > /dev/null
sudo ufw allow 443/tcp > /dev/null
sudo ufw allow 3000/tcp > /dev/null
sudo ufw --force enable > /dev/null
log "UFW rules reconfigured after Dokploy"
# 3. Re-apply needrestart SSH protection
sudo mkdir -p /etc/needrestart/conf.d
sudo tee /etc/needrestart/conf.d/99-no-ssh-restart.conf > /dev/null << 'NEEDRESTART'
$nrconf{override_rc}{q(ssh)} = 0;
$nrconf{override_rc}{q(sshd)} = 0;
NEEDRESTART
# 4. Re-apply DOCKER-USER rules
run_with_spinner "Re-applying DOCKER-USER firewall rules" sudo systemctl restart docker-firewall
sudo iptables -I DOCKER-USER -p tcp --dport 3000 -j ACCEPT
sudo ip6tables -I DOCKER-USER -p tcp --dport 3000 -j ACCEPT 2>/dev/null || true
# 5. Verify sshd is still alive — this is the bug that caused ECONNRESET
if [ -f /run/sshd-hardened.pid ] && ! kill -0 "$(cat /run/sshd-hardened.pid)" 2>/dev/null; then
warn "Standalone sshd died during Dokploy install — restarting on port $SSH_PORT"
sudo /usr/sbin/sshd -p "$SSH_PORT" -o "PidFile=/run/sshd-hardened.pid"
log "Standalone sshd restarted on port $SSH_PORT"
elif ! [ -f /run/sshd-hardened.pid ] && ! systemctl is-active ssh.service &>/dev/null; then
warn "No sshd running — starting ssh.service"
sudo systemctl start ssh.service
log "ssh.service started as fallback"
fi
log "Post-Dokploy recovery complete — all services verified"
# Docker/Dokploy or distro defaults can re-apply sysctl values after setup.sh.
# Re-assert the critical runtime values discovered during VPS validation.
sudo sysctl -w net.ipv4.conf.all.log_martians=1 > /dev/null
sudo sysctl -w net.ipv4.conf.default.log_martians=1 > /dev/null
sudo sysctl -w fs.suid_dumpable=0 > /dev/null
sudo sysctl -w fs.protected_fifos=2 > /dev/null
log "Critical sysctl runtime values re-applied"
if [ ! -f /run/sshd-hardened.pid ]; then
sudo rm -f /etc/ssh/sshd_test_config /etc/ssh/sshd_config.d/zz-setup-keepalive.conf
log "Temporary SSH setup files removed"
fi
# Wait for Dokploy to be ready
gum spin --spinner dot --title "Waiting for Dokploy to start..." -- bash -c '
for i in $(seq 1 30); do
curl -s http://localhost:3000 &>/dev/null && exit 0
sleep 2
done
exit 1
' && log "Dokploy is running" || warn "Dokploy did not respond within 60s -- it may still be starting"
# === FINAL SUMMARY ===
PUBLIC_IP=$(curl -s --max-time 10 -4 ifconfig.me 2>/dev/null || \
curl -s --max-time 10 https://api.ipify.org 2>/dev/null || \
echo "YOUR_SERVER_IP")
if echo "$PUBLIC_IP" | grep -q ":"; then
SSH_HOST="[$PUBLIC_IP]"
else
SSH_HOST="$PUBLIC_IP"
fi
USER_HOME=$(getent passwd "$NEW_USER" | cut -d: -f6)
# Update summary file
if [ -n "$USER_HOME" ] && [ -f "$USER_HOME/.vps_setup_summary" ]; then
if ! grep -q "DOKPLOY_URL" "$USER_HOME/.vps_setup_summary"; then
echo "DOKPLOY_URL=http://$PUBLIC_IP:3000" | sudo tee -a "$USER_HOME/.vps_setup_summary" > /dev/null
fi
fi
echo ""
ELAPSED=$(( SECONDS - START_TIME ))
ELAPSED_MIN=$(( ELAPSED / 60 ))
ELAPSED_SEC=$(( ELAPSED % 60 ))
gum style \
--border double \
--border-foreground 2 \
--padding "1 4" \
--margin "0 2" \
--bold \
--align center \
"DOKPLOY READY (${ELAPSED_MIN}m ${ELAPSED_SEC}s)"
echo ""
gum style --bold --foreground 2 " ACCESS"
gum style --foreground 240 " ──────────────────────────────────────────────────"
printf " $(gum style --bold 'SSH') ssh %s@%s -p %s\n" "$NEW_USER" "$SSH_HOST" "$SSH_PORT"
printf " $(gum style --bold 'Dokploy') http://%s:3000\n" "$PUBLIC_IP"
echo ""
gum style --bold --foreground 2 " NEXT STEPS"
gum style --foreground 240 " ──────────────────────────────────────────────────"
printf " $(gum style --bold --foreground 6 '1') Open http://%s:3000 and create your admin account\n" "$PUBLIC_IP"
printf " $(gum style --bold --foreground 6 '2') Configure a domain + HTTPS in Dokploy\n"
printf " $(gum style --bold --foreground 6 '3') Close port 3000 after domain + HTTPS is configured:\n"
printf " sudo ufw delete allow 3000/tcp\n"
printf " sudo iptables -D DOCKER-USER -p tcp --dport 3000 -j ACCEPT\n"
printf " sudo ip6tables -D DOCKER-USER -p tcp --dport 3000 -j ACCEPT 2>/dev/null || true\n"
echo ""
printf '\a'
# If running inside screen, wait for user to read the summary before screen exits
if [ -n "${STY:-}" ]; then
echo ""
read -rp " Press Enter to exit..."
fi