1
+ #! /usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # The instance id is used to namespace the dev environment to allow for multiple instances to run
5
+ # without conflicts. e.g:
6
+ # npm run dev start
7
+ # UMBREL_DEV_INSTANCE='apps' npm run dev start
8
+ #
9
+ # Will spin up two separate umbrel-dev instances accessible at:
10
+ # http://umbrel-dev.local
11
+ # http://umbrel-dev-apps.local
12
+ INSTANCE_ID_PREFIX=" umbrel-dev"
13
+ INSTANCE_ID=" ${INSTANCE_ID_PREFIX}${UMBREL_DEV_INSTANCE: +-$UMBREL_DEV_INSTANCE } "
14
+
15
+ show_help () {
16
+ cat << EOF
17
+ umbrel-dev
18
+
19
+ Automatically initialize and manage an umbrelOS development environment.
20
+
21
+ Usage: npm run dev <command> [-- <args>]
22
+
23
+ Commands:
24
+ help Show this help message
25
+ start Either start an existing dev environment or create and start a new one
26
+ logs Stream umbreld logs
27
+ shell Get a shell inside the running dev environment
28
+ exec -- <command> Execute a command inside the running dev environment
29
+ client -- <rpc> [<args>] Query the umbreld RPC server via a CLI client
30
+ rebuild Rebuild the operating system image from source and reboot the dev environment into it
31
+ restart Restart the dev environment
32
+ stop Stop the dev environment
33
+ reset Reset the dev environment to a fresh state
34
+ destroy Destroy the dev environment
35
+
36
+ Environment Variables:
37
+ UMBREL_DEV_INSTANCE The instance id of the dev environment. Allows running multiple instances of
38
+ umbrel-dev in different namespaces.
39
+
40
+ Note: umbrel-dev requires a Docker environment that exposes container IPs to the host. This is how Docker
41
+ natively works on Linux and can be done with OrbStack on macOS. On Windows this should work with WSL 2.
42
+
43
+ EOF
44
+ }
45
+
46
+ build_os_image () {
47
+ docker buildx build --load --file packages/os/umbrelos.Dockerfile --tag " ${INSTANCE_ID} " .
48
+ }
49
+
50
+ create_instance () {
51
+ # --privileged is needed for systemd to work inside the container.
52
+ #
53
+ # We mount a named volume namespaced to the instance id at /data to immitate
54
+ # the data partition of a physical install.
55
+ #
56
+ # We mount the monorepo inside the container at /umbrel-dev as readonly. We
57
+ # setup a writeable fs overlay later to allow the container to install dependencies
58
+ # without modifying the hosts source code dir.
59
+ #
60
+ # --label "dev.orbstack.http-port=80" stops OrbStack from trying to guess which port
61
+ # we're trying to expose which causes some weirdness since it often gets it wrong.
62
+ #
63
+ # --label "dev.orbstack.domains=${INSTANCE_ID}.local" makes the instance accessble at
64
+ # umbrel-dev.local on OrbStack installs.
65
+ #
66
+ # /sbin/init kicks of systemd as the container entrypoint.
67
+ docker run \
68
+ --detach \
69
+ --interactive \
70
+ --tty \
71
+ --privileged \
72
+ --name " ${INSTANCE_ID} " \
73
+ --hostname " ${INSTANCE_ID} " \
74
+ --volume " ${INSTANCE_ID} :/data" \
75
+ --volume " ${PWD} :/umbrel-dev:ro" \
76
+ --label " dev.orbstack.http-port=80" \
77
+ --label " dev.orbstack.domains=${INSTANCE_ID} .local" \
78
+ " ${INSTANCE_ID} " \
79
+ /sbin/init
80
+ }
81
+
82
+ start_instance () {
83
+ docker start " ${INSTANCE_ID} "
84
+ }
85
+
86
+ exec_in_instance () {
87
+ docker exec --interactive --tty " ${INSTANCE_ID} " " ${@ } "
88
+ }
89
+
90
+ stop_instance () {
91
+ # We first need to execute poweroff inside the instance so systemd gracefully stops services before we kill the container
92
+ exec_in_instance poweroff
93
+ docker stop " ${INSTANCE_ID} "
94
+ }
95
+
96
+ remove_instance () {
97
+ docker rm --force " ${INSTANCE_ID} "
98
+ }
99
+
100
+ remove_volume () {
101
+ docker volume rm " ${INSTANCE_ID} "
102
+ }
103
+
104
+ get_instance_ip () {
105
+ docker inspect --format ' {{ .NetworkSettings.IPAddress }}' " ${INSTANCE_ID} "
106
+ }
107
+
108
+ # Get the command
109
+ if [ -z ${1+x} ]; then
110
+ command=" "
111
+ else
112
+ command=" $1 "
113
+ fi
114
+
115
+ if [[ " ${command} " = " start" ]] || [[ " ${command} " = " " ]]
116
+ then
117
+ echo " Starting umbrel-dev instance..."
118
+ if ! start_instance > /dev/null
119
+ then
120
+ echo " Instance not found, creating a new one..."
121
+ if ! docker image inspect " ${INSTANCE_ID} " > /dev/null
122
+ then
123
+ build_os_image
124
+ fi
125
+ create_instance
126
+ fi
127
+ echo
128
+ echo " umbrel-dev instance is booting up..."
129
+
130
+ # Stream systemd logs until boot has completed
131
+ docker logs --tail 100 --follow " ${INSTANCE_ID} " 2> /dev/null &
132
+ logs_pid=$!
133
+ exec_in_instance systemctl is-active --wait multi-user.target > /dev/null|| true
134
+ sleep 2
135
+ kill " ${logs_pid} " || true
136
+ wait
137
+
138
+ # Stream umbreld logs until web server is up
139
+ docker exec " ${INSTANCE_ID} " journalctl --unit umbrel --follow --lines 100 --output cat 2> /dev/null &
140
+ logs_pid=$!
141
+ docker exec " ${INSTANCE_ID} " curl --silent --retry 300 --retry-delay 1 --retry-connrefused http://localhost > /dev/null 2>&1 || true
142
+ sleep 0.1
143
+ kill " ${logs_pid} " || true
144
+ wait
145
+
146
+ # Done!
147
+ cat << 'EOF '
148
+
149
+
150
+ ,;###GGGGGGGGGGl#Sp
151
+ ,##GGGlW""^' '`""%GGGG#S,
152
+ ,#GGG" "lGG#o
153
+ #GGl^ '$GG#
154
+ ,#GGb \GGG,
155
+ lGG" "GGG
156
+ #GGGlGGGl##p,,p##lGGl##p,,p###ll##GGGG
157
+ !GGGlW"""*GGGGGGG#""""WlGGGGG#W""*WGGGGS
158
+ "" "^ '" ""
159
+
160
+ EOF
161
+ echo " Your umbrel-dev instance is ready at:"
162
+ echo
163
+ echo " http://${INSTANCE_ID} .local"
164
+ echo " http://$( get_instance_ip) "
165
+
166
+ exit
167
+ fi
168
+
169
+ if [[ " ${command} " = " help" ]]
170
+ then
171
+ show_help
172
+
173
+ exit
174
+ fi
175
+
176
+ if [[ " ${command} " = " shell" ]]
177
+ then
178
+ exec_in_instance bash
179
+
180
+ exit
181
+ fi
182
+
183
+ if [[ " ${command} " = " exec" ]]
184
+ then
185
+ shift
186
+ exec_in_instance " ${@ } "
187
+
188
+ exit
189
+ fi
190
+
191
+ if [[ " ${command} " = " logs" ]]
192
+ then
193
+ exec_in_instance journalctl --unit umbrel --follow --lines 100 --output cat
194
+
195
+ exit
196
+ fi
197
+
198
+ if [[ " ${command} " = " client" ]]
199
+ then
200
+ shift
201
+ exec_in_instance npm --prefix /umbrel-dev/packages/umbreld run start -- client ${@ }
202
+
203
+ exit
204
+ fi
205
+
206
+ if [[ " ${command} " = " rebuild" ]]
207
+ then
208
+ echo " Rebuilding the operating system image from source..."
209
+ build_os_image
210
+ echo " Restarting the dev environment with the new image..."
211
+ stop_instance || true
212
+ remove_instance || true
213
+ create_instance
214
+
215
+ exit
216
+ fi
217
+
218
+ if [[ " ${command} " = " destroy" ]]
219
+ then
220
+ echo " Destroying the dev environment..."
221
+ remove_instance || true
222
+ remove_volume || true
223
+
224
+ exit
225
+ fi
226
+
227
+ if [[ " ${command} " = " reset" ]]
228
+ then
229
+ echo " Resetting the dev environment state..."
230
+ stop_instance || true
231
+ remove_instance || true
232
+ remove_volume || true
233
+ create_instance
234
+
235
+ exit
236
+ fi
237
+
238
+ if [[ " ${command} " = " restart" ]]
239
+ then
240
+ echo " Restarting the dev environment..."
241
+ stop_instance
242
+ start_instance
243
+
244
+ exit
245
+ fi
246
+
247
+ if [[ " ${command} " = " stop" ]]
248
+ then
249
+ echo " Stopping the dev environment..."
250
+ stop_instance
251
+
252
+ exit
253
+ fi
254
+
255
+ # This is a special command that runs directly inside the container to setup the environment
256
+ # It is not intended to be run on the host machine!
257
+ if [[ " ${command} " = " container-init" ]]
258
+ then
259
+ # Check if this is the first boot
260
+ first_boot=false
261
+ if [[ ! -d " /data/umbrel-dev-overlay" ]]
262
+ then
263
+ first_boot=true
264
+ fi
265
+
266
+ # Setup fs overlay so we can write to the source code dir without modifying it on the host
267
+ echo " Setting up fs overlay..."
268
+ mkdir -p /data/umbrel-dev-overlay/upperdir
269
+ mkdir -p /data/umbrel-dev-overlay/workdir
270
+ mount -t overlay overlay -o lowerdir=/umbrel-dev,upperdir=/data/umbrel-dev-overlay/upperdir,workdir=/data/umbrel-dev-overlay/workdir /umbrel-dev || true
271
+
272
+ # If this is the first boot we should nuke node_modules if they exist so we get fresh Linux deps instead
273
+ # of trying to reuse deps installed from the host. (causes issues with macos native deps)
274
+ if [[ " ${first_boot} " = true ]]
275
+ then
276
+ echo " Nuking node_modules inherited from host..."
277
+ rm -rf /umbrel-dev/packages/ui/node_modules || true
278
+ rm -rf /umbrel-dev/packages/umbreld/node_modules || true
279
+ fi
280
+
281
+ # Install dependencies
282
+ echo " Installing dependencies..."
283
+ npm --prefix /umbrel-dev/packages/umbreld install
284
+ npm --prefix /umbrel-dev/packages/ui install
285
+
286
+ # Run umbreld and ui
287
+ echo " Starting umbreld and ui..."
288
+ npm --prefix /umbrel-dev/packages/umbreld run dev &
289
+ CHOKIDAR_USEPOLLING=true npm --prefix /umbrel-dev/packages/ui run dev &
290
+ wait
291
+
292
+ exit
293
+ fi
294
+
295
+ show_help
296
+ exit
0 commit comments