-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdevbox.sh
More file actions
executable file
·342 lines (312 loc) · 12.9 KB
/
devbox.sh
File metadata and controls
executable file
·342 lines (312 loc) · 12.9 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
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: ./devbox.sh [options]
Options:
--user NAME Devbox username (default: dev)
--pass PASSWORD Devbox user password (default: changeme)
--uid UID UID for container user (default: current host UID; empty in .env auto-detects)
--gid GID GID for container user (default: current host GID; empty in .env auto-detects)
--ssh-port PORT Host SSH port for main devbox (default: 2202)
--docker-gid GID GID of docker socket on NAS (default: 1000)
--projects-dir PATH Projects directory on NAS (default: /volume1/projects)
--workspace-link MODE /workspace compatibility link: on|off (default: on)
--start-dir PATH Auto-cd target on shell login (default: /workspace)
--passwordless-sudo MODE Passwordless sudo: on|off (default: on)
--container NAME Main container name (default: devbox)
--home-dir PATH Persistent home dir on host (default: auto; /volume1/home/<user> if exists)
--playwright Also start playwright profile container
--playwright-ssh-port PORT SSH port for playwright container (default: 2203)
--playwright-container NAME Playwright container name (default: devbox-playwright)
--post-install TARGET Optional post-install script/profile(s): example | dev | ai | migrate | /projects/path/script.sh
Repeat flag or use comma list: --post-install dev --post-install ai / --post-install dev,ai
--env-file PATH Optional env file (default auto-load: ./.env, then ./.env.local)
--recreate Remove existing containers before rebuild
-h, --help Show this help
Examples:
./devbox.sh --user dev --ssh-port 2202
./devbox.sh --playwright --post-install example --user devuser --uid 1000 --gid 1000
USAGE
}
DEVBOX_USER="dev"
DEVBOX_PASS="changeme"
DEVBOX_UID="$(id -u)"
DEVBOX_GID="$(id -g)"
DEVBOX_SSH_PORT="2202"
DOCKER_GID="1000"
DEVBOX_PROJECTS_DIR="/volume1/projects"
DEVBOX_PROJECTS_MOUNT="/projects"
DEVBOX_WORKSPACE_LINK="on"
DEVBOX_START_DIR="/workspace"
DEVBOX_PASSWORDLESS_SUDO="on"
DEVBOX_CONTAINER_NAME="devbox"
DEVBOX_HOME_DIR=""
DEVBOX_PLAYWRIGHT_ENABLED="no"
DEVBOX_PLAYWRIGHT_SSH_PORT="2203"
DEVBOX_PLAYWRIGHT_CONTAINER_NAME="devbox-playwright"
POST_INSTALL_TARGET=""
POST_INSTALL_SET_BY_CLI="no"
POST_INSTALL_TARGETS=()
DEVBOX_ENV_FILE=""
RECREATE="no"
load_env_file() {
local file="$1"
[ -z "$file" ] && return 0
[ -f "$file" ] || return 0
set -a
# shellcheck disable=SC1090
. "$file"
set +a
}
# Auto-load local environment values if present.
# CLI flags still override these defaults later.
# Load order: .env then .env.local (override).
[ -f "./.env" ] && load_env_file "./.env"
[ -f "./.env.local" ] && load_env_file "./.env.local"
# Pre-parse optional env file so explicit CLI flags can still override it.
ARGS=("$@")
for ((i=0; i<${#ARGS[@]}; i++)); do
if [ "${ARGS[$i]}" = "--env-file" ] && [ $((i + 1)) -lt ${#ARGS[@]} ]; then
DEVBOX_ENV_FILE="${ARGS[$((i + 1))]}"
break
fi
done
[ -n "$DEVBOX_ENV_FILE" ] && load_env_file "$DEVBOX_ENV_FILE"
while [ $# -gt 0 ]; do
case "$1" in
--user) DEVBOX_USER="${2:-}"; shift 2 ;;
--pass) DEVBOX_PASS="${2:-}"; shift 2 ;;
--uid) DEVBOX_UID="${2:-}"; shift 2 ;;
--gid) DEVBOX_GID="${2:-}"; shift 2 ;;
--ssh-port) DEVBOX_SSH_PORT="${2:-}"; shift 2 ;;
--docker-gid) DOCKER_GID="${2:-}"; shift 2 ;;
--projects-dir) DEVBOX_PROJECTS_DIR="${2:-}"; shift 2 ;;
--workspace-link) DEVBOX_WORKSPACE_LINK="${2:-}"; shift 2 ;;
--start-dir) DEVBOX_START_DIR="${2:-}"; shift 2 ;;
--passwordless-sudo) DEVBOX_PASSWORDLESS_SUDO="${2:-}"; shift 2 ;;
--container) DEVBOX_CONTAINER_NAME="${2:-}"; shift 2 ;;
--home-dir) DEVBOX_HOME_DIR="${2:-}"; shift 2 ;;
--playwright) DEVBOX_PLAYWRIGHT_ENABLED="yes"; shift ;;
--playwright-ssh-port) DEVBOX_PLAYWRIGHT_SSH_PORT="${2:-}"; shift 2 ;;
--playwright-container) DEVBOX_PLAYWRIGHT_CONTAINER_NAME="${2:-}"; shift 2 ;;
--post-install)
if [ "${POST_INSTALL_SET_BY_CLI}" != "yes" ]; then
POST_INSTALL_TARGETS=()
POST_INSTALL_SET_BY_CLI="yes"
fi
IFS=',' read -r -a _post_items <<< "${2:-}"
for _post_item in "${_post_items[@]}"; do
_post_item="${_post_item#"${_post_item%%[![:space:]]*}"}"
_post_item="${_post_item%"${_post_item##*[![:space:]]}"}"
[ -n "${_post_item}" ] && POST_INSTALL_TARGETS+=("${_post_item}")
done
shift 2
;;
--env-file) DEVBOX_ENV_FILE="${2:-}"; shift 2 ;; # already loaded in pre-parse
--recreate) RECREATE="yes"; shift ;;
-h|--help) usage; exit 0 ;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done
if [ "${POST_INSTALL_SET_BY_CLI}" != "yes" ] && [ -n "${POST_INSTALL_TARGET}" ]; then
IFS=',' read -r -a _post_items <<< "${POST_INSTALL_TARGET}"
for _post_item in "${_post_items[@]}"; do
_post_item="${_post_item#"${_post_item%%[![:space:]]*}"}"
_post_item="${_post_item%"${_post_item##*[![:space:]]}"}"
[ -n "${_post_item}" ] && POST_INSTALL_TARGETS+=("${_post_item}")
done
fi
if [ "${DEVBOX_WORKSPACE_LINK}" != "on" ] && [ "${DEVBOX_WORKSPACE_LINK}" != "off" ]; then
echo "Error: --workspace-link must be 'on' or 'off'." >&2
exit 1
fi
if [ "${DEVBOX_PASSWORDLESS_SUDO}" != "on" ] && [ "${DEVBOX_PASSWORDLESS_SUDO}" != "off" ]; then
echo "Error: --passwordless-sudo must be 'on' or 'off'." >&2
exit 1
fi
# Keep .env portable across hosts: blank UID/GID means "use current host user/group".
if [ -z "${DEVBOX_UID}" ]; then
DEVBOX_UID="$(id -u)"
fi
if [ -z "${DEVBOX_GID}" ]; then
DEVBOX_GID="$(id -g)"
fi
# Resolve host directory for persistent user settings.
# Priority:
# 1) explicit DEVBOX_HOME_DIR / --home-dir
# 2) /volume1/home/<user> when present
# 3) /home/<user> when present
# 4) fallback isolated path under projects
if [ -z "${DEVBOX_HOME_DIR}" ]; then
if [ -d "/volume1/home/${DEVBOX_USER}" ]; then
DEVBOX_HOME_DIR="/volume1/home/${DEVBOX_USER}"
elif [ -d "/home/${DEVBOX_USER}" ]; then
DEVBOX_HOME_DIR="/home/${DEVBOX_USER}"
else
DEVBOX_HOME_DIR="/volume1/projects/.devbox-home/${DEVBOX_USER}"
fi
fi
if docker compose version >/dev/null 2>&1; then
COMPOSE=(docker compose)
elif docker-compose version >/dev/null 2>&1; then
COMPOSE=(docker-compose)
else
echo "Error: docker compose or docker-compose is required on the NAS host." >&2
exit 1
fi
resolve_post_install_script() {
local target="$1"
if [ -z "$target" ]; then
echo ""
return 0
fi
case "$target" in
example) echo "${DEVBOX_PROJECTS_MOUNT}/devbox/scripts/post-install-example.sh" ;;
dev) echo "${DEVBOX_PROJECTS_MOUNT}/devbox/scripts/post-install-dev.sh" ;;
ai) echo "${DEVBOX_PROJECTS_MOUNT}/devbox/scripts/post-install-ai.sh" ;;
migrate) echo "${DEVBOX_PROJECTS_MOUNT}/devbox/scripts/post-install-migrate.sh" ;;
*)
if [[ "$target" = /* ]]; then
echo "$target"
else
echo "${DEVBOX_PROJECTS_MOUNT}/$target"
fi
;;
esac
}
run_post_install() {
local container="$1"
local script_path="$2"
[ -z "$script_path" ] && return 0
docker exec -u 0 "$container" sh -lc "test -f '$script_path'" \
|| { echo "Post-install script not found in container: $script_path" >&2; return 1; }
echo "Running post-install in $container: $script_path"
docker exec -u 0 "$container" sh -lc "bash '$script_path'"
}
configure_container_runtime() {
local container="$1"
docker exec -u 0 "$container" sh -lc \
"set -e; \
mkdir -p '/home/${DEVBOX_USER}'; \
ln -sfn '${DEVBOX_PROJECTS_MOUNT}' '/home/${DEVBOX_USER}/projects'; \
if [ '${DEVBOX_WORKSPACE_LINK}' = 'on' ]; then [ -e /workspace ] && [ ! -L /workspace ] && rm -rf /workspace || true; ln -sfn '/home/${DEVBOX_USER}/projects' /workspace; else [ -L /workspace ] && rm -f /workspace || true; fi; \
PROFILE='/home/${DEVBOX_USER}/.bashrc'; \
touch \"\$PROFILE\"; \
sed -i '/### DEVBOX AUTO-CD ###/,/### \\/DEVBOX AUTO-CD ###/d' \"\$PROFILE\"; \
cat >> \"\$PROFILE\" <<EOF
### DEVBOX AUTO-CD ###
if [ -n \"\\\$PS1\" ]; then
if [ -d \"${DEVBOX_START_DIR}\" ]; then
cd \"${DEVBOX_START_DIR}\"
elif [ -d \"/workspace\" ]; then
cd \"/workspace\"
fi
fi
### /DEVBOX AUTO-CD ###
EOF
LOGIN_PROFILE='/home/${DEVBOX_USER}/.profile'; \
touch \"\$LOGIN_PROFILE\"; \
sed -i '/### DEVBOX SOURCE BASHRC ###/,/### \\/DEVBOX SOURCE BASHRC ###/d' \"\$LOGIN_PROFILE\"; \
cat >> \"\$LOGIN_PROFILE\" <<'EOF'
### DEVBOX SOURCE BASHRC ###
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
### /DEVBOX SOURCE BASHRC ###
EOF
if [ '${DEVBOX_PASSWORDLESS_SUDO}' = 'on' ]; then echo '${DEVBOX_USER} ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-devbox-nopasswd; chmod 0440 /etc/sudoers.d/90-devbox-nopasswd; else rm -f /etc/sudoers.d/90-devbox-nopasswd; fi"
}
export DEVBOX_USER DEVBOX_PASS DEVBOX_UID DEVBOX_GID DEVBOX_SSH_PORT DOCKER_GID
export DEVBOX_PROJECTS_DIR DEVBOX_PROJECTS_MOUNT DEVBOX_WORKSPACE_LINK DEVBOX_START_DIR DEVBOX_PASSWORDLESS_SUDO DEVBOX_CONTAINER_NAME DEVBOX_HOME_DIR
export DEVBOX_PLAYWRIGHT_SSH_PORT DEVBOX_PLAYWRIGHT_CONTAINER_NAME
POST_INSTALL_SCRIPTS=()
if [ "${#POST_INSTALL_TARGETS[@]}" -gt 0 ]; then
for _post_target in "${POST_INSTALL_TARGETS[@]}"; do
_post_script="$(resolve_post_install_script "$_post_target")"
[ -n "${_post_script}" ] && POST_INSTALL_SCRIPTS+=("${_post_script}")
done
fi
mkdir -p "${DEVBOX_PROJECTS_DIR}"
mkdir -p "${DEVBOX_HOME_DIR}" "${DEVBOX_HOME_DIR}/.ssh"
chmod 700 "${DEVBOX_HOME_DIR}/.ssh" || true
# One-time migration: populate SSH settings in mounted home when empty.
TARGET_SSH_DIR="${DEVBOX_HOME_DIR}/.ssh"
HOST_USER_NAME="$(id -un)"
HOST_SSH_CANDIDATE_A="/volume1/home/${HOST_USER_NAME}/.ssh"
HOST_SSH_CANDIDATE_B="${HOME}/.ssh"
if [ -z "$(find "${TARGET_SSH_DIR}" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]; then
if [ -d "${HOST_SSH_CANDIDATE_A}" ] && [ "${HOST_SSH_CANDIDATE_A}" != "${TARGET_SSH_DIR}" ]; then
echo "Migrating SSH settings: ${HOST_SSH_CANDIDATE_A} -> ${TARGET_SSH_DIR}"
cp -a "${HOST_SSH_CANDIDATE_A}/." "${TARGET_SSH_DIR}/"
elif [ -d "${HOST_SSH_CANDIDATE_B}" ] && [ "${HOST_SSH_CANDIDATE_B}" != "${TARGET_SSH_DIR}" ]; then
echo "Migrating SSH settings: ${HOST_SSH_CANDIDATE_B} -> ${TARGET_SSH_DIR}"
cp -a "${HOST_SSH_CANDIDATE_B}/." "${TARGET_SSH_DIR}/"
fi
fi
if [ "${RECREATE}" = "yes" ]; then
if [ "${DEVBOX_PLAYWRIGHT_ENABLED}" = "yes" ]; then
"${COMPOSE[@]}" --profile playwright down --remove-orphans
else
"${COMPOSE[@]}" down --remove-orphans
fi
# Compose down can miss stale containers if project metadata changed.
docker rm -f "${DEVBOX_CONTAINER_NAME}" >/dev/null 2>&1 || true
if [ "${DEVBOX_PLAYWRIGHT_ENABLED}" = "yes" ]; then
docker rm -f "${DEVBOX_PLAYWRIGHT_CONTAINER_NAME}" >/dev/null 2>&1 || true
fi
fi
if [ "${DEVBOX_PLAYWRIGHT_ENABLED}" = "yes" ]; then
"${COMPOSE[@]}" --profile playwright up -d --build
else
"${COMPOSE[@]}" up -d --build
fi
configure_container_runtime "${DEVBOX_CONTAINER_NAME}"
if [ "${DEVBOX_PLAYWRIGHT_ENABLED}" = "yes" ]; then
configure_container_runtime "${DEVBOX_PLAYWRIGHT_CONTAINER_NAME}"
fi
# Ensure mounted dirs are writable for the selected user
docker exec -u 0 "${DEVBOX_CONTAINER_NAME}" sh -lc \
"chown -R '${DEVBOX_USER}' '${DEVBOX_PROJECTS_MOUNT}' '/home/${DEVBOX_USER}' || true"
if [ "${DEVBOX_PLAYWRIGHT_ENABLED}" = "yes" ]; then
docker exec -u 0 "${DEVBOX_PLAYWRIGHT_CONTAINER_NAME}" sh -lc \
"chown -R '${DEVBOX_USER}' '${DEVBOX_PROJECTS_MOUNT}' '/home/${DEVBOX_USER}' || true"
fi
if [ "${#POST_INSTALL_SCRIPTS[@]}" -gt 0 ]; then
for _post_script in "${POST_INSTALL_SCRIPTS[@]}"; do
run_post_install "${DEVBOX_CONTAINER_NAME}" "${_post_script}"
if [ "${DEVBOX_PLAYWRIGHT_ENABLED}" = "yes" ]; then
run_post_install "${DEVBOX_PLAYWRIGHT_CONTAINER_NAME}" "${_post_script}"
fi
done
fi
echo
echo "Container is ready."
echo "+------------------------------------------------------------+"
echo "| User: ${DEVBOX_USER} (UID:${DEVBOX_UID} GID:${DEVBOX_GID})"
echo "| Main SSH: ${DEVBOX_SSH_PORT}"
echo "| Projects mount: ${DEVBOX_PROJECTS_DIR} -> ${DEVBOX_PROJECTS_MOUNT}"
echo "| Home mount: ${DEVBOX_HOME_DIR} -> /home/${DEVBOX_USER}"
echo "| /workspace link: ${DEVBOX_WORKSPACE_LINK}"
echo "| Login start dir: ${DEVBOX_START_DIR}"
echo "| Passwordless sudo: ${DEVBOX_PASSWORDLESS_SUDO}"
if [ "${DEVBOX_PLAYWRIGHT_ENABLED}" = "yes" ]; then
echo "| Playwright SSH: ${DEVBOX_PLAYWRIGHT_SSH_PORT}"
fi
if [ "${#POST_INSTALL_SCRIPTS[@]}" -gt 0 ]; then
echo "| Post-install:"
for _post_script in "${POST_INSTALL_SCRIPTS[@]}"; do
echo "| - ${_post_script}"
done
fi
if [ "${DEVBOX_PASS}" = "changeme" ]; then
echo "| Password: changeme (default)."
else
echo "| Password: value from --pass/.env (custom)."
fi
echo "| Please change the password after first login."
echo "+------------------------------------------------------------+"