Skip to content

Commit 1f1a2be

Browse files
feat(build): Add local Docker installer script (#2702)
* add local Docker installer script with preflight checks * fixed review comment * fixed review comment * fix install script * update exception handling * fixed bugbot review
1 parent f249aa5 commit 1f1a2be

1 file changed

Lines changed: 240 additions & 0 deletions

File tree

install-local.sh

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
REPO_URL="${CONVOY_REPO_URL:-https://github.com/frain-dev/convoy.git}"
6+
INSTALL_DIR="${CONVOY_INSTALL_DIR:-$HOME/convoy}"
7+
MAX_WAIT_SECONDS="${CONVOY_MAX_WAIT_SECONDS:-180}"
8+
9+
log() {
10+
printf "\n==> %s\n" "$1"
11+
}
12+
13+
warn() {
14+
printf "\n[WARN] %s\n" "$1"
15+
}
16+
17+
die() {
18+
printf "\n[ERROR] %s\n" "$1"
19+
exit 1
20+
}
21+
22+
command_exists() {
23+
command -v "$1" >/dev/null 2>&1
24+
}
25+
26+
is_port_in_use() {
27+
local port="$1"
28+
if command_exists lsof; then
29+
lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
30+
return $?
31+
fi
32+
33+
# Fallback when lsof is unavailable: check host binding via Python.
34+
if command_exists python3; then
35+
python3 - "$port" <<'PY'
36+
import socket
37+
import sys
38+
39+
port = int(sys.argv[1])
40+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
41+
try:
42+
s.bind(("0.0.0.0", port))
43+
except OSError as e:
44+
# Permission denied on privileged ports (e.g. 80) is not proof that
45+
# another process is listening; treat as unknown.
46+
if getattr(e, "errno", None) == 13:
47+
sys.exit(2) # unknown
48+
sys.exit(0) # in use
49+
else:
50+
sys.exit(1) # free
51+
finally:
52+
s.close()
53+
PY
54+
return $?
55+
fi
56+
57+
# Last resort when lsof/python3 are unavailable.
58+
warn "Could not reliably check port $port (missing lsof/python3); continuing."
59+
return 1
60+
}
61+
62+
check_prereqs() {
63+
log "Checking prerequisites"
64+
65+
if ! command_exists git; then
66+
die "Git is not installed. Install Git first and run this script again."
67+
fi
68+
69+
if ! command_exists curl; then
70+
die "curl is not installed. Install curl first and run this script again."
71+
fi
72+
73+
if ! command_exists docker; then
74+
cat <<'EOF'
75+
[ERROR] Docker is not installed.
76+
77+
Install Docker Desktop (macOS/Windows):
78+
https://docs.docker.com/desktop/
79+
80+
Install Docker Engine (Linux):
81+
https://docs.docker.com/engine/install/
82+
EOF
83+
exit 1
84+
fi
85+
86+
if ! docker compose version >/dev/null 2>&1; then
87+
die "Docker Compose v2 is not available. Install/enable docker compose and try again."
88+
fi
89+
90+
if ! docker info >/dev/null 2>&1; then
91+
cat <<'EOF'
92+
[ERROR] Docker daemon is not running.
93+
94+
Start Docker Desktop, wait until it is fully running, then run this script again.
95+
EOF
96+
exit 1
97+
fi
98+
}
99+
100+
check_ports() {
101+
# These are the host ports published by configs/local/docker-compose.yml.
102+
local required_ports=(80 5433 6432)
103+
local conflicts=()
104+
local unknown=()
105+
local p
106+
local rc
107+
108+
log "Checking required ports"
109+
110+
for p in "${required_ports[@]}"; do
111+
if is_port_in_use "$p"; then
112+
rc=0
113+
else
114+
rc=$?
115+
fi
116+
if [ "$rc" -eq 0 ]; then
117+
conflicts+=("$p")
118+
elif [ "$rc" -eq 2 ]; then
119+
unknown+=("$p")
120+
fi
121+
done
122+
123+
if [ "${#conflicts[@]}" -gt 0 ]; then
124+
cat <<EOF
125+
[ERROR] Required ports are already in use: ${conflicts[*]}
126+
127+
Stop conflicting services/containers, then retry.
128+
Helpful checks:
129+
docker ps --format 'table {{.Names}}\t{{.Ports}}'
130+
lsof -nP -iTCP -sTCP:LISTEN
131+
132+
If you previously started Convoy local stack:
133+
docker compose -f "$INSTALL_DIR/configs/local/docker-compose.yml" down
134+
EOF
135+
exit 1
136+
fi
137+
138+
if [ "${#unknown[@]}" -gt 0 ]; then
139+
warn "Could not reliably check privileged ports: ${unknown[*]} (permission denied without lsof)."
140+
fi
141+
}
142+
143+
prepare_repo() {
144+
log "Preparing Convoy repository"
145+
146+
if [ -d "$INSTALL_DIR/.git" ]; then
147+
printf "Found existing repo at %s. Pull latest changes? [Y/n]: " "$INSTALL_DIR"
148+
if ! read -r pull_choice; then
149+
pull_choice=""
150+
fi
151+
pull_choice="${pull_choice:-Y}"
152+
if [[ "$pull_choice" =~ ^[Yy]$ ]]; then
153+
git -C "$INSTALL_DIR" pull --ff-only
154+
fi
155+
else
156+
if [ -d "$INSTALL_DIR" ] && [ "$(ls -A "$INSTALL_DIR" 2>/dev/null)" ]; then
157+
die "Install directory '$INSTALL_DIR' exists and is not a git repo. Use an empty path or set CONVOY_INSTALL_DIR to another directory."
158+
fi
159+
git clone "$REPO_URL" "$INSTALL_DIR"
160+
fi
161+
}
162+
163+
ensure_local_config() {
164+
local config_path="$INSTALL_DIR/configs/local/convoy.json"
165+
166+
if [ -f "$config_path" ]; then
167+
return
168+
fi
169+
170+
log "Ensuring local Convoy config exists"
171+
172+
# Recover deleted tracked file from git if available.
173+
if [ -d "$INSTALL_DIR/.git" ] && git -C "$INSTALL_DIR" ls-files --error-unmatch "configs/local/convoy.json" >/dev/null 2>&1; then
174+
git -C "$INSTALL_DIR" checkout -- "configs/local/convoy.json"
175+
fi
176+
177+
if [ ! -f "$config_path" ]; then
178+
die "Missing $config_path. Restore it from the repository or create it before running installer."
179+
fi
180+
}
181+
182+
start_stack() {
183+
local compose_dir="$INSTALL_DIR/configs/local"
184+
185+
[ -d "$compose_dir" ] || die "Missing compose directory: $compose_dir"
186+
187+
if [ "${CONVOY_SKIP_PULL:-0}" != "1" ]; then
188+
log "Pulling latest images"
189+
docker compose -f "$compose_dir/docker-compose.yml" pull
190+
fi
191+
192+
log "Starting Convoy stack"
193+
docker compose -f "$compose_dir/docker-compose.yml" up -d
194+
}
195+
196+
wait_for_health() {
197+
local elapsed=0
198+
local health_url="http://localhost/healthz"
199+
200+
log "Waiting for Convoy health endpoint ($health_url)"
201+
202+
until curl -fsS "$health_url" >/dev/null 2>&1; do
203+
if [ "$elapsed" -ge "$MAX_WAIT_SECONDS" ]; then
204+
die "Timed out waiting for health after ${MAX_WAIT_SECONDS}s. Check logs with: docker compose -f \"$INSTALL_DIR/configs/local/docker-compose.yml\" logs"
205+
fi
206+
207+
sleep 3
208+
elapsed=$((elapsed + 3))
209+
done
210+
211+
log "Convoy is healthy"
212+
}
213+
214+
print_next_steps() {
215+
cat <<EOF
216+
217+
🎉 Convoy is set up.
218+
219+
Useful commands:
220+
docker compose -f "$INSTALL_DIR/configs/local/docker-compose.yml" ps
221+
docker compose -f "$INSTALL_DIR/configs/local/docker-compose.yml" logs -f web agent
222+
docker compose -f "$INSTALL_DIR/configs/local/docker-compose.yml" down
223+
224+
Open:
225+
http://localhost
226+
227+
EOF
228+
}
229+
230+
main() {
231+
check_prereqs
232+
check_ports
233+
prepare_repo
234+
ensure_local_config
235+
start_stack
236+
wait_for_health
237+
print_next_steps
238+
}
239+
240+
main "$@"

0 commit comments

Comments
 (0)