-
Notifications
You must be signed in to change notification settings - Fork 116
Expand file tree
/
Copy pathdappnode_install.sh
More file actions
executable file
·1518 lines (1295 loc) · 51.6 KB
/
dappnode_install.sh
File metadata and controls
executable file
·1518 lines (1295 loc) · 51.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
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
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
# This installer is written for bash. It's safe to *run it from zsh* (it will execute via bash
# thanks to the shebang), but users sometimes invoke it as `zsh ./script.sh` or `source ./script.sh`.
# - If sourced, bail out (sourcing would pollute the current shell and can break it).
# - If invoked by a non-bash shell, re-exec with bash before hitting bash-specific builtins.
##############################
# Logging / Errors #
##############################
# Note: LOGFILE and LOGS_DIR are set during bootstrap_filesystem based on DAPPNODE_DIR
# Early definition allows these functions to be used throughout the script.
log() {
# LOGFILE is created after dir bootstrap; until then we just print to stdout.
if [[ -n "${LOGFILE:-}" && -d "${LOGS_DIR:-}" ]]; then
printf '%s\n' "[INFO] $*" | tee -a "$LOGFILE"
else
printf '%s\n' "[INFO] $*"
fi
}
warn() {
# LOGFILE is created after dir bootstrap; until then we just print to stdout.
if [[ -n "${LOGFILE:-}" && -d "${LOGS_DIR:-}" ]]; then
printf '%s\n' "[WARN] $*" | tee -a "$LOGFILE"
else
printf '%s\n' "[WARN] $*"
fi
}
error() {
# LOGFILE is created after dir bootstrap; until then we just print to stdout.
if [[ -n "${LOGFILE:-}" && -d "${LOGS_DIR:-}" ]]; then
printf '%s\n' "[ERROR] $*" | tee -a "$LOGFILE"
else
printf '%s\n' "[ERROR] $*"
fi
}
die() {
# LOGFILE is created after dir bootstrap; until then we just print to stdout.
if [[ -n "${LOGFILE:-}" && -d "${LOGS_DIR:-}" ]]; then
printf '%s\n' "[ERROR] $*" | tee -a "$LOGFILE"
else
printf '%s\n' "[ERROR] $*"
fi
exit 1
}
##############################
# Script Guards #
##############################
if (return 0 2>/dev/null); then
die "This script must be executed, not sourced. Run: bash $0"
fi
if [ -z "${BASH_VERSION:-}" ]; then
exec /usr/bin/env bash "$0" "$@"
fi
set -Eeuo pipefail
# Optional env inputs (avoid unbound-variable errors under `set -u`)
: "${UPDATE:=false}"
: "${STATIC_IP:=}"
: "${LOCAL_PROFILE_PATH:=}"
: "${MINIMAL:=false}"
: "${LITE:=false}"
: "${PACKAGES:=}"
: "${RESOLVE_FROM_HOST:=false}"
# Enable alias expansion in non-interactive bash scripts.
# Required so commands like `dappnode_wireguard` (defined as aliases in `.dappnode_profile`) work.
shopt -s expand_aliases
# Ensure array is always defined (avoid `set -u` edge cases)
DNCORE_COMPOSE_ARGS=()
usage() {
cat <<'EOF'
Usage: dappnode_install.sh [options]
Options:
--update Clean existing downloaded artifacts before installing (equivalent: UPDATE=true)
--static-ip <ipv4> Set a static IP (equivalent: STATIC_IP=...)
--local-profile-path <path> Use a local .dappnode_profile instead of downloading (equivalent: LOCAL_PROFILE_PATH=...)
--ipfs-endpoint <url> Override IPFS gateway endpoint (equivalent: IPFS_ENDPOINT=...)
--profile-url <url> Override profile download URL (equivalent: PROFILE_URL=...)
--minimal Install only BIND DAPPMANAGER NOTIFICATIONS PREMIUM (equivalent: MINIMAL=true)
--lite Install reduced package set: BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM (equivalent: LITE=true)
--packages <list> Override package selection (comma or space separated), e.g. BIND,IPFS,VPN
--resolve-from-host Configure host DNS to resolve .dappnode.private domains (Linux only) (equivalent: RESOLVE_FROM_HOST=true)
-h, --help Show this help
Environment variables (also supported):
UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL, LITE, PACKAGES, RESOLVE_FROM_HOST
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--update)
UPDATE=true
shift
;;
--static-ip)
[[ $# -ge 2 ]] || die "--static-ip requires an IPv4 argument"
STATIC_IP="$2"
shift 2
;;
--local-profile-path)
[[ $# -ge 2 ]] || die "--local-profile-path requires a path argument"
LOCAL_PROFILE_PATH="$2"
shift 2
;;
--ipfs-endpoint)
[[ $# -ge 2 ]] || die "--ipfs-endpoint requires a URL argument"
IPFS_ENDPOINT="$2"
shift 2
;;
--profile-url)
[[ $# -ge 2 ]] || die "--profile-url requires a URL argument"
PROFILE_URL="$2"
shift 2
;;
--minimal)
MINIMAL=true
shift
;;
--lite)
LITE=true
shift
;;
--packages)
[[ $# -ge 2 ]] || die "--packages requires a package list argument"
PACKAGES="$2"
shift 2
;;
--packages=*)
PACKAGES="${1#*=}"
shift
;;
--resolve-from-host)
RESOLVE_FROM_HOST=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
*)
die "Unknown option: $1 (use --help)"
;;
esac
done
}
validate_install_mode() {
if [[ "${MINIMAL}" == "true" && "${LITE}" == "true" ]]; then
die "--minimal and --lite are mutually exclusive"
fi
}
require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd"
}
require_downloader() {
if command -v curl >/dev/null 2>&1; then
return 0
fi
if command -v wget >/dev/null 2>&1; then
return 0
fi
die "Missing required downloader: install curl or wget"
}
check_prereqs() {
if ! command -v docker >/dev/null 2>&1; then
die "Docker is not installed. Install Docker first, then re-run this installer."
fi
# Docker CLI may exist while the daemon is stopped/unreachable.
if ! docker info >/dev/null 2>&1; then
die "Docker is installed but not running (or not reachable). Start Docker and try again."
fi
require_downloader
# Ensure compose is available (Docker Desktop / modern docker engine)
if ! docker compose version >/dev/null 2>&1; then
die "Docker Compose not available (expected: 'docker compose'). Update Docker or install the compose plugin."
fi
}
# Wait until dappmanager publishes INTERNAL_IP via its local HTTP endpoint.
# Runs the curl inside the provided container and exits with error on timeout.
# Usage: wait_for_internal_ip <container_name> [timeout_seconds] [initial_sleep_seconds] [final_sleep_seconds]
wait_for_internal_ip() {
local container_name="$1"
local timeout_seconds="${2:-120}"
local initial_sleep_seconds="${3:-10}"
local final_sleep_seconds="${4:-2}"
local internal_ip_url="http://127.0.0.1/global-envs/INTERNAL_IP"
local hostname_url="http://127.0.0.1/global-envs/HOSTNAME"
log "Waiting for dappmanager to publish INTERNAL_IP and HOSTNAME..."
sleep "$initial_sleep_seconds"
local start_seconds internal_http_code internal_value internal_result
local hostname_http_code hostname_value hostname_result
start_seconds=$SECONDS
internal_http_code=""
internal_value=""
hostname_http_code=""
hostname_value=""
local retry_count=0
while true; do
if (( SECONDS - start_seconds >= timeout_seconds )); then
die "Timed out after ${timeout_seconds}s waiting for INTERNAL_IP and HOSTNAME from dappmanager (expected HTTP 200 with non-empty values). Last seen: INTERNAL_IP code=${internal_http_code:-?} value=${internal_value:-<empty>}; HOSTNAME code=${hostname_http_code:-?} value=${hostname_value:-<empty>}"
fi
# Must be executed inside the dappmanager container.
# Return format is:
# <body>\n<http_code>
# Parse in bash (not inside container sh) to avoid shell portability issues.
internal_result="$(
docker exec -i "$container_name" sh -lc "curl -sS -w '\n%{http_code}' '$internal_ip_url' 2>/dev/null || true" 2>/dev/null || true
)"
internal_http_code="$(printf '%s\n' "$internal_result" | tail -n 1 | tr -d '\r')"
internal_value="$(printf '%s\n' "$internal_result" | head -n 1 | tr -d '\r' | xargs)"
hostname_result="$(
docker exec -i "$container_name" sh -lc "curl -sS -w '\n%{http_code}' '$hostname_url' 2>/dev/null || true" 2>/dev/null || true
)"
hostname_http_code="$(printf '%s\n' "$hostname_result" | tail -n 1 | tr -d '\r')"
hostname_value="$(printf '%s\n' "$hostname_result" | head -n 1 | tr -d '\r' | xargs)"
if [[ "$internal_http_code" == "200" && -n "$internal_value" && "$internal_value" != "null" && "$hostname_http_code" == "200" && -n "$hostname_value" && "$hostname_value" != "null" ]]; then
sleep "$final_sleep_seconds" # Extra buffer to ensure values are fully propagated before we proceed
log "INTERNAL_IP is ready: $internal_value"
log "HOSTNAME is ready: $hostname_value"
return 0
fi
retry_count=$((retry_count + 1))
if (( retry_count % 5 == 1 )); then
log "INTERNAL_IP/HOSTNAME not ready yet (INTERNAL_IP code=${internal_http_code:-?}, HOSTNAME code=${hostname_http_code:-?}). Retrying..."
fi
sleep 2
done
}
# Print VPN access credentials (Wireguard + OpenVPN) after core has started.
# Works on both Linux and macOS as long as the relevant containers are running.
print_vpn_access_credentials() {
local localhost_flag=()
local has_wireguard=false
local has_vpn=false
local pkg
if $IS_MACOS; then
localhost_flag=(--localhost)
fi
for pkg in "${PKGS[@]}"; do
if [[ "$pkg" == "WIREGUARD" ]]; then
has_wireguard=true
elif [[ "$pkg" == "VPN" ]]; then
has_vpn=true
fi
done
if [[ "$has_wireguard" != "true" && "$has_vpn" != "true" ]]; then
log "No VPN package selected (VPN/WIREGUARD). Skipping credentials output."
return 0
fi
log ""
log "Waiting for VPN initialization..."
wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 20 10
log ""
log "##############################################"
log "# DAppNode VPN Access Credentials #"
log "##############################################"
log ""
log "Your DAppNode is ready! Connect using your preferred VPN client."
log "Choose either Wireguard (recommended) or OpenVPN and import the"
log "credentials below into your VPN app to access your DAppNode."
log ""
if [[ "$has_wireguard" == "true" ]]; then
log "--- Wireguard ---"
docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials "${localhost_flag[@]}" 2>&1 || \
warn "Wireguard credentials not yet available. Try later with: dappnode_wireguard${localhost_flag:+ ${localhost_flag[*]}}"
fi
if [[ "$has_wireguard" == "true" && "$has_vpn" == "true" ]]; then
log ""
fi
if [[ "$has_vpn" == "true" ]]; then
log "--- OpenVPN ---"
docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get dappnode_admin "${localhost_flag[@]}" 2>&1 || \
warn "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin${localhost_flag:+ ${localhost_flag[*]}}"
fi
log ""
log "Import the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode"
}
# Build docker compose "-f <file>" args from downloaded compose files.
# This avoids depending on alias expansion or profile-generated strings.
build_dncore_compose_args() {
DNCORE_COMPOSE_ARGS=()
local file
while IFS= read -r file; do
[[ -n "$file" ]] || continue
DNCORE_COMPOSE_ARGS+=( -f "$file" )
done < <(find "${DAPPNODE_CORE_DIR}" -name 'docker-compose-*.yml' -print 2>/dev/null | sort)
}
##################
# OS DETECTION #
##################
OS_TYPE="$(uname -s)"
IS_MACOS=false
IS_LINUX=false
if [[ "$OS_TYPE" == "Darwin" ]]; then
IS_MACOS=true
elif [[ "$OS_TYPE" == "Linux" ]]; then
IS_LINUX=true
else
die "Unsupported operating system: $OS_TYPE"
fi
#############
# VARIABLES #
#############
# Dirs - macOS uses $HOME/dappnode, Linux uses /usr/src/dappnode
if $IS_MACOS; then
DAPPNODE_DIR="$HOME/dappnode"
else
DAPPNODE_DIR="/usr/src/dappnode"
fi
DAPPNODE_CORE_DIR="${DAPPNODE_DIR}/DNCORE"
LOGS_DIR="$DAPPNODE_DIR/logs"
# Files
CONTENT_HASH_FILE="${DAPPNODE_CORE_DIR}/packages-content-hash.csv"
LOGFILE="${LOGS_DIR}/dappnode_install.log"
DAPPNODE_PROFILE="${DAPPNODE_CORE_DIR}/.dappnode_profile"
# Linux-only paths
if $IS_LINUX; then
MOTD_FILE="/etc/motd"
UPDATE_MOTD_DIR="/etc/update-motd.d"
fi
# Get URLs
IPFS_ENDPOINT=${IPFS_ENDPOINT:-"https://ipfs-gateway.dappnode.net"}
# PROFILE_URL env is used to fetch the core packages versions that will be used to build the release in script install method
PROFILE_URL=${PROFILE_URL:-"https://github.com/dappnode/DAppNode/releases/latest/download/dappnode_profile.sh"}
DAPPNODE_ACCESS_CREDENTIALS="${DAPPNODE_DIR}/scripts/dappnode_access_credentials.sh"
DAPPNODE_ACCESS_CREDENTIALS_URL="https://github.com/dappnode/DAppNode/releases/latest/download/dappnode_access_credentials.sh"
# Other
# Architecture detection (cross-platform)
if $IS_MACOS; then
ARCH="$(uname -m)"
[[ "$ARCH" == "x86_64" ]] && ARCH="amd64"
# arm64 is already correct for Apple Silicon
else
ARCH="$(dpkg --print-architecture)"
fi
##############################
# Cross-platform Helpers #
##############################
# Download a file: download_file <destination> <url>
download_file() {
local dest="$1"
local url="$2"
log "Downloading from $url to $dest"
mkdir -p "$(dirname "$dest")"
if command -v curl >/dev/null 2>&1; then
curl -fsSL -o "$dest" "$url"
return
fi
wget -q --show-progress --progress=bar:force -O "$dest" "$url"
}
# Download content to stdout: download_stdout <url>
download_stdout() {
local url="$1"
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$url"
return
fi
wget -q -O- "$url"
}
# Normalize IPFS refs and (if needed) infer the missing :<version> from dappnode_package.json
# Accepts:
# - /ipfs/<cid>:<version>
# - /ipfs/<cid> (version inferred)
# - ipfs/<cid>[:<version>] (leading slash normalized)
normalize_ipfs_version_ref() {
local raw_ref="$1"
local comp="$2"
local ref="$raw_ref"
if [[ "$ref" == ipfs/* ]]; then
ref="/$ref"
fi
# If it already has :<version>, we're done
if [[ "$ref" == /ipfs/*:* ]]; then
echo "$ref"
return 0
fi
# If it's an IPFS ref without a :<version>, infer it from the manifest in the CID
if [[ "$ref" == /ipfs/* ]]; then
local cid_path="$ref"
local manifest_url="${IPFS_ENDPOINT%/}${cid_path}/dappnode_package.json"
local manifest
manifest="$(download_stdout "$manifest_url" 2>/dev/null || true)"
if [[ -z "$manifest" ]]; then
error "Could not fetch IPFS manifest for ${comp} from: $manifest_url"
error "Provide ${comp}_VERSION as /ipfs/<cid>:<version> (example: /ipfs/Qm...:0.2.11)"
return 1
fi
local inferred_version
inferred_version="$(
echo "$manifest" |
tr -d '\r' |
grep -m1 '"version"' |
sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^\"]+)".*/\1/'
)"
if [[ -z "$inferred_version" || "$inferred_version" == "$manifest" ]]; then
error "Could not infer version for ${comp} from IPFS manifest: $manifest_url"
error "Provide ${comp}_VERSION as /ipfs/<cid>:<version>"
return 1
fi
echo "${cid_path}:${inferred_version}"
return 0
fi
# Not an IPFS ref; return as-is
echo "$raw_ref"
}
# Cross-platform in-place sed (macOS requires '' after -i)
sed_inplace() {
if $IS_MACOS; then
sed -i '' "$@"
else
sed -i "$@"
fi
}
##############################
# Compose Patching Helpers #
##############################
# Remove journald logging from compose files (not supported on macOS Docker Desktop)
remove_logging_section() {
local file="$1"
sed_inplace '/logging/d;/journald/d' "$file"
}
# Replace Linux paths with macOS paths in compose files
patch_compose_paths() {
local file="$1"
sed_inplace "s|/usr/src/dappnode|${DAPPNODE_DIR}|g" "$file"
}
# Patch dappmanager compose for macOS: inject env vars the container needs
# to know the host core-dir path and to skip host-only operations,
# and fix the DNCORE volume mount to use the macOS host path.
patch_dappmanager_compose_for_macos() {
local file="$1"
# Replace the host side of the DNCORE volume mount with the actual DAPPNODE_CORE_DIR value
# e.g. /usr/src/dappnode/DNCORE/:/usr/src/app/DNCORE/ -> $HOME/dappnode/DNCORE/:/usr/src/app/DNCORE/
sed_inplace "s|[^[:space:]]*:/usr/src/app/DNCORE/|${DAPPNODE_CORE_DIR}/:/usr/src/app/DNCORE/|" "$file"
local envs_to_add=()
# DAPPNODE_CORE_DIR: lets the container know the host's DNCORE path
if ! grep -q "DAPPNODE_CORE_DIR" "$file"; then
envs_to_add+=(" - DAPPNODE_CORE_DIR=${DAPPNODE_CORE_DIR}")
fi
# DISABLE_HOST_SCRIPTS: tells the container to skip host-only scripts
if ! grep -q "DISABLE_HOST_SCRIPTS" "$file"; then
envs_to_add+=(" - DISABLE_HOST_SCRIPTS=${DISABLE_HOST_SCRIPTS}")
fi
[[ ${#envs_to_add[@]} -gt 0 ]] || return 0
local tmp="${file}.tmp"
local insert_file="${file}.envinsert"
# macOS ships BSD awk, which can error with "newline in string" if a -v argument contains
# literal newlines. Write the insertion block to a temp file and have awk read it.
printf '%s\n' "${envs_to_add[@]}" >"$insert_file"
awk -v insfile="$insert_file" '
/^[[:space:]]*environment:[[:space:]]*$/ {
print
while ((getline line < insfile) > 0) print line
close(insfile)
next
}
{ print }
' "$file" >"$tmp" && mv "$tmp" "$file"
rm -f "$insert_file" || true
}
bootstrap_filesystem() {
# Clean if update
if [[ "${UPDATE}" == "true" ]]; then
log "Cleaning for update..."
rm -f "${LOGFILE}" || true
rm -f "${DAPPNODE_CORE_DIR}"/docker-compose-*.yml || true
rm -f "${DAPPNODE_CORE_DIR}"/dappnode_package-*.json || true
rm -f "${DAPPNODE_CORE_DIR}"/*.tar.xz || true
rm -f "${DAPPNODE_CORE_DIR}"/*.txz || true
rm -f "${DAPPNODE_CORE_DIR}/.dappnode_profile" || true
rm -f "${CONTENT_HASH_FILE}" || true
fi
# Create necessary directories
mkdir -p "${DAPPNODE_DIR}"
mkdir -p "${DAPPNODE_CORE_DIR}"
mkdir -p "${DAPPNODE_DIR}/scripts"
mkdir -p "${DAPPNODE_CORE_DIR}/scripts"
mkdir -p "${DAPPNODE_DIR}/config"
mkdir -p "${LOGS_DIR}"
# Ensure the log file path exists before first use by helpers.
touch "${LOGFILE}" || true
}
# Generic helper: returns 0 if a process is bound to the given port, 1 if not.
# Usage: is_port_listening <port> [tcp|udp]
# tcp (default): matches TCP sockets in LISTEN state
# udp: matches any process bound to the UDP port
is_port_listening() {
local port="$1"
local proto="${2:-tcp}"
if [[ "$proto" == "udp" ]]; then
lsof -i "udp:${port}" -P -n 2>/dev/null | grep -q .
else
lsof -i "tcp:${port}" -P -n 2>/dev/null | grep -q "(LISTEN)"
fi
}
# Check if ports 80/443 are occupied by something other than our own HTTPS container.
# Sets HTTPS_PORTS_BLOCKED=true/false.
check_https_ports_conflict() {
if ! command -v lsof >/dev/null 2>&1; then
warn "lsof not found; assuming ports 80/443 are in use (HTTPS will be skipped)"
HTTPS_PORTS_BLOCKED=true
return
fi
if ! is_port_listening 80 && ! is_port_listening 443; then
HTTPS_PORTS_BLOCKED=false
return
fi
# Port 80 or 443 is in use; check if it's our own HTTPS container
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^DAppNodeCore-https.dnp.dappnode.eth$"; then
# Our own HTTPS container already holds the port — not a conflict
HTTPS_PORTS_BLOCKED=false
else
# Port 80 or 443 is used by something else
HTTPS_PORTS_BLOCKED=true
fi
}
# Check that ports required by VPN/Wireguard are not already in use by another process.
# Must be called after PKGS is populated. Exits with a helpful error on conflict.
check_vpn_ports_conflict() {
if ! command -v lsof >/dev/null 2>&1; then
return # cannot check; proceed and let the container report a bind error
fi
local pkg
for pkg in "${PKGS[@]}"; do
case "$pkg" in
WIREGUARD)
if is_port_listening 51820 udp; then
error "Port 51820/UDP is already in use on this host."
error "This port is required by the Wireguard package and must be free before installing."
error "Free up port 51820 and re-run the installer, or — if you do not need VPN"
error "connectivity — consider using --minimal instead (advanced users only)."
exit 1
fi
;;
VPN)
local vpn_blocked=()
is_port_listening 1194 udp && vpn_blocked+=(1194/UDP)
is_port_listening 8092 tcp && vpn_blocked+=(8092/TCP)
if [[ ${#vpn_blocked[@]} -gt 0 ]]; then
error "Port(s) ${vpn_blocked[*]} are already in use on this host."
error "These ports are required by the OpenVPN package and must be free before installing."
error "Free up the port(s) and re-run the installer, or — if you do not need VPN"
error "connectivity — consider using --minimal instead (advanced users only)."
exit 1
fi
;;
esac
done
}
# Determine packages to be installed
determine_packages() {
# Explicit package list override from flag/env always has top priority.
# It supersedes MINIMAL/LITE and any OS/port-based package determination.
if [[ -n "${PACKAGES//[[:space:],]/}" ]]; then
local raw token normalized
local custom_pkgs=()
raw="${PACKAGES//,/ }"
for token in $raw; do
normalized="$(echo "$token" | tr '[:lower:]' '[:upper:]')"
case "$normalized" in
HTTPS|BIND|IPFS|VPN|WIREGUARD|DAPPMANAGER|WIFI|NOTIFICATIONS|PREMIUM)
;;
*)
die "Unknown package in --packages/PACKAGES: '$token'. Allowed: HTTPS,BIND,IPFS,VPN,WIREGUARD,DAPPMANAGER,WIFI,NOTIFICATIONS,PREMIUM"
;;
esac
local exists=false
local pkg
for pkg in "${custom_pkgs[@]}"; do
if [[ "$pkg" == "$normalized" ]]; then
exists=true
break
fi
done
if [[ "$exists" == "false" ]]; then
custom_pkgs+=("$normalized")
fi
done
[[ ${#custom_pkgs[@]} -gt 0 ]] || die "--packages/PACKAGES was provided but no valid packages were found"
# DAPPMANAGER is required for a functional install; ensure it's present on explicit overrides.
local has_dappmanager=false
local pkg
for pkg in "${custom_pkgs[@]}"; do
if [[ "$pkg" == "DAPPMANAGER" ]]; then
has_dappmanager=true
break
fi
done
if [[ "$has_dappmanager" == "false" ]]; then
custom_pkgs+=("DAPPMANAGER")
log "--packages/PACKAGES did not include DAPPMANAGER; appending it automatically"
fi
if [[ "${MINIMAL}" == "true" || "${LITE}" == "true" ]]; then
log "Custom packages provided; overriding --minimal/--lite and MINIMAL/LITE"
fi
MINIMAL=false
LITE=false
PKGS=("${custom_pkgs[@]}")
log "Packages override enabled via --packages/PACKAGES"
log "Packages to be installed: ${PKGS[*]}"
log "PKGS: ${PKGS[*]}"
for comp in "${PKGS[@]}"; do
local ver_var
ver_var="${comp}_VERSION"
log "$ver_var = ${!ver_var-}"
done
return 0
fi
# Global override: new minimal install, regardless of OS.
if [[ "${MINIMAL}" == "true" ]]; then
PKGS=(BIND DAPPMANAGER NOTIFICATIONS PREMIUM)
log "Minimal mode enabled; overriding packages"
log "Packages to be installed: ${PKGS[*]}"
log "PKGS: ${PKGS[*]}"
for comp in "${PKGS[@]}"; do
local ver_var
ver_var="${comp}_VERSION"
log "$ver_var = ${!ver_var-}"
done
return 0
fi
# Global override: lite install (former minimal behavior), regardless of OS.
if [[ "${LITE}" == "true" ]]; then
PKGS=(BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM)
log "Lite mode enabled; overriding packages"
log "Packages to be installed: ${PKGS[*]}"
log "PKGS: ${PKGS[*]}"
for comp in "${PKGS[@]}"; do
local ver_var
ver_var="${comp}_VERSION"
log "$ver_var = ${!ver_var-}"
done
return 0
fi
# Default mode (no --packages/--minimal/--lite): install full package set.
# HTTPS is included only when ports 80/443 are available.
check_https_ports_conflict
if [ "$HTTPS_PORTS_BLOCKED" == "true" ]; then
PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM)
else
PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM)
fi
log "Packages to be installed: ${PKGS[*]}"
# Debug: print all PKGS and their version variables
log "PKGS: ${PKGS[*]}"
for comp in "${PKGS[@]}"; do
local ver_var
ver_var="${comp}_VERSION"
log "$ver_var = ${!ver_var-}"
done
}
valid_ip() {
local ip="$1"
if [[ ! "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then
return 1
fi
local IFS='.'
# shellcheck disable=SC2206
local octets=( $ip )
[[ ${#octets[@]} -eq 4 ]] || return 1
[[ ${octets[0]} -le 255 && ${octets[1]} -le 255 && ${octets[2]} -le 255 && ${octets[3]} -le 255 ]]
}
configure_static_ip() {
if [[ -z "${STATIC_IP}" ]]; then
return 0
fi
if valid_ip "$STATIC_IP"; then
echo "$STATIC_IP" >"${DAPPNODE_DIR}/config/static_ip"
else
die "The static IP provided (${STATIC_IP}) is not valid."
fi
}
ensure_profile_loaded() {
# If LOCAL_PROFILE_PATH is set, use it as the profile source instead of downloading
if [[ -n "${LOCAL_PROFILE_PATH}" ]]; then
log "Using local profile: ${LOCAL_PROFILE_PATH}"
cp "$LOCAL_PROFILE_PATH" "$DAPPNODE_PROFILE"
elif [[ ! -f "$DAPPNODE_PROFILE" ]]; then
download_file "${DAPPNODE_PROFILE}" "${PROFILE_URL}"
fi
# shellcheck disable=SC1090
source "${DAPPNODE_PROFILE}"
}
resolve_packages() {
# The indirect variable expansion used in ${!ver##*:} allows us to use versions like 'dev:development'
# If such variable with 'dev:'' suffix is used, then the component is built from specified branch or commit.
# you can also specify an IPFS version like /ipfs/<cid>:<version> (the exact version is required).
determine_packages
check_vpn_ports_conflict
for comp in "${PKGS[@]}"; do
ver="${comp}_VERSION"
log "Processing $comp: ${!ver-}"
raw_version_ref="${!ver-}"
if [[ "$raw_version_ref" == /ipfs/* || "$raw_version_ref" == ipfs/* ]]; then
resolved_ref="$(normalize_ipfs_version_ref "$raw_version_ref" "$comp")" || exit 1
printf -v "${comp}_VERSION" '%s' "$resolved_ref"
raw_version_ref="$resolved_ref"
log "Using IPFS for ${comp}: ${raw_version_ref%:*} (version ${raw_version_ref##*:})"
DOWNLOAD_URL="${IPFS_ENDPOINT%/}${raw_version_ref%:*}"
version_for_filenames="${raw_version_ref##*:}"
else
version_for_filenames="${raw_version_ref##*:}"
DOWNLOAD_URL="https://github.com/dappnode/DNP_${comp}/releases/download/v${version_for_filenames}"
fi
comp_lower="$(echo "$comp" | tr '[:upper:]' '[:lower:]')"
printf -v "${comp}_URL" '%s' "${DOWNLOAD_URL}/${comp_lower}.dnp.dappnode.eth_${version_for_filenames}_linux-${ARCH}.txz"
printf -v "${comp}_YML" '%s' "${DOWNLOAD_URL}/docker-compose.yml"
printf -v "${comp}_MANIFEST" '%s' "${DOWNLOAD_URL}/dappnode_package.json"
printf -v "${comp}_YML_FILE" '%s' "${DAPPNODE_CORE_DIR}/docker-compose-${comp_lower}.yml"
printf -v "${comp}_FILE" '%s' "${DAPPNODE_CORE_DIR}/${comp_lower}.dnp.dappnode.eth_${version_for_filenames}_linux-${ARCH}.txz"
printf -v "${comp}_MANIFEST_FILE" '%s' "${DAPPNODE_CORE_DIR}/dappnode_package-${comp_lower}.json"
done
}
dappnode_core_build() {
for comp in "${PKGS[@]}"; do
ver="${comp}_VERSION"
if [[ ${!ver} == dev:* ]]; then
if $IS_MACOS; then
error "Development builds (dev:*) are not supported on macOS."
exit 1
fi
log "Cloning & building DNP_${comp}..."
if ! dpkg -s git >/dev/null 2>&1; then
apt-get install -y git
fi
local tmpdir
tmpdir="$(mktemp -d)"
pushd "$tmpdir" >/dev/null || {
error "Error on pushd"
exit 1
}
git clone -b "${!ver##*:}" https://github.com/dappnode/DNP_"${comp}"
# Change version in YAML to the custom one
local docker_ver comp_lower
docker_ver="$(echo "${!ver##*:}" | sed 's/\//_/g')"
comp_lower="$(echo "$comp" | tr '[:upper:]' '[:lower:]')"
sed_inplace "s~^\(\s*image\s*:\s*\).*~\1${comp_lower}.dnp.dappnode.eth:${docker_ver}~" "DNP_${comp}/docker-compose.yml"
docker compose -f ./DNP_"${comp}"/docker-compose.yml build
cp "./DNP_${comp}/docker-compose.yml" "${DAPPNODE_CORE_DIR}/docker-compose-${comp_lower}.yml"
cp "./DNP_${comp}/dappnode_package.json" "${DAPPNODE_CORE_DIR}/dappnode_package-${comp_lower}.json"
rm -rf "./DNP_${comp}"
popd >/dev/null || {
error "Error on popd"
exit 1
}
rm -rf "$tmpdir"
fi
done
}
dappnode_core_download() {
for comp in "${PKGS[@]}"; do
ver="${comp}_VERSION"
if [[ ${!ver} != dev:* ]]; then
local file_var="${comp}_FILE"
local url_var="${comp}_URL"
local yml_file_var="${comp}_YML_FILE"
local yml_var="${comp}_YML"
local manifest_file_var="${comp}_MANIFEST_FILE"
local manifest_var="${comp}_MANIFEST"
# Download DAppNode Core Images if needed
log "Downloading ${comp} tar..."
[ -f "${!file_var}" ] || download_file "${!file_var}" "${!url_var}" || exit 1
# Download DAppNode Core docker-compose yml files if needed
log "Downloading ${comp} yml..."
[ -f "${!yml_file_var}" ] || download_file "${!yml_file_var}" "${!yml_var}" || exit 1
# Download DAppNode Core manifest files if needed
log "Downloading ${comp} manifest..."
[ -f "${!manifest_file_var}" ] || download_file "${!manifest_file_var}" "${!manifest_var}" || exit 1
# macOS: patch compose files for Docker Desktop compatibility
if $IS_MACOS; then
remove_logging_section "${!yml_file_var}"
patch_compose_paths "${!yml_file_var}"
# Inject macOS-specific env vars into the dappmanager compose
if [[ "$comp" == "DAPPMANAGER" ]]; then
patch_dappmanager_compose_for_macos "${!yml_file_var}"
fi
fi
fi
done
}
dappnode_core_load() {
for comp in "${PKGS[@]}"; do
ver="${comp}_VERSION"
if [[ ${!ver} != dev:* ]]; then
local comp_lower image file_var
comp_lower="$(echo "$comp" | tr '[:upper:]' '[:lower:]')"
image="${comp_lower}.dnp.dappnode.eth:${!ver##*:}"
file_var="${comp}_FILE"
if [[ -z "$(docker images -q "$image" 2>/dev/null)" ]]; then
docker load -i "${!file_var}" 2>&1 | tee -a "$LOGFILE"
fi
fi
done
}
customMotd() {
generateMotdText
if [ -d "${UPDATE_MOTD_DIR}" ]; then
# Ubuntu configuration
modifyMotdGeneration
fi
}
# Debian distros use /etc/motd plain text file
generateMotdText() {
local welcome_message
# Check and create the MOTD file if it does not exist
if [ ! -f "${MOTD_FILE}" ]; then
touch "${MOTD_FILE}"
fi
# Write the ASCII art and welcome message as plain text
cat <<'EOF' >"${MOTD_FILE}"
___ _
| \ __ _ _ __ _ __ _ _ ___ __| |___
| |) / _` | '_ \ '_ \ ' \/ _ \/ _` / -_)
|___/\__,_| .__/ .__/_||_\___/\__,_\___|
|_| |_|
EOF
welcome_message="\nChoose a way to connect to your DAppNode, then go to http://my.dappnode\n\n- Wifi\t\tScan and connect to DAppNodeWIFI. Get wifi credentials with dappnode_wifi\n\n- Local Proxy\tConnect to the same router as your DAppNode. Then go to http://dappnode.local\n\n- Wireguard\tDownload Wireguard app on your device. Get your dappnode wireguard credentials with dappnode_wireguard\n\n- Open VPN\tDownload Open VPN app on your device. Get your openVPN creds with dappnode_openvpn\n\n\nTo see a full list of commands available execute dappnode_help\n"
printf "%b" "$welcome_message" >>"${MOTD_FILE}"
}
# Ubuntu distros use /etc/update-motd.d/ to generate the motd
modifyMotdGeneration() {
local disabled_motd_dir
disabled_motd_dir="${UPDATE_MOTD_DIR}/disabled"
mkdir -p "${disabled_motd_dir}"
# Move all the files in /etc/update-motd.d/ to /etc/update-motd.d/disabled/
# Except for the files listed in "files_to_keep"
files_to_keep="00-header 50-landscape-sysinfo 98-reboot-required"
local file base_file
for file in "${UPDATE_MOTD_DIR}"/*; do
base_file="$(basename "${file}")"
if [ -f "${file}" ] && ! echo "${files_to_keep}" | grep -qw "${base_file}"; then
mv "${file}" "${disabled_motd_dir}/"
fi
done
}
addSwap() {
# Is swap enabled?
IS_SWAP=$(swapon --show | wc -l)
# if not then create it
if [ "$IS_SWAP" -eq 0 ]; then
log 'Swap not found. Adding swapfile.'
#RAM=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
#SWAP=$(($RAM * 2))
SWAP=8388608
fallocate -l "${SWAP}k" /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap defaults 0 0' >>/etc/fstab
else
log 'Swap found. No changes made.'
fi
}
# Add .dappnode_profile sourcing to the user's default shell configuration
add_profile_to_shell() {
local user_home
local shell_configs
if $IS_MACOS; then
user_home="$HOME"
# macOS defaults to zsh, but some users still run bash.
shell_configs=(".zshrc" ".zprofile" ".bashrc" ".bash_profile")
else
# Linux: determine user home from /etc/passwd
local user_name
user_name=$(grep 1000 /etc/passwd | cut -f 1 -d:)
if [ -n "$user_name" ]; then
user_home="/home/$user_name"
else
user_home="/root"
fi
shell_configs=(".profile" ".bashrc")
fi
for config_file in "${shell_configs[@]}"; do
local config_path="${user_home}/${config_file}"
local source_line
# .profile may be evaluated by /bin/sh (dash on Debian/Ubuntu) where `source` is not valid.
# Use POSIX '.' there; use `source` elsewhere (bash/zsh).
if [ "$config_file" = ".profile" ]; then