Skip to content

Commit 097adb2

Browse files
committed
Migrate umbrel-dev from Multipass to Docker
Automatically initialize and manage an umbrelOS development environment. Usage: npm run dev <command> [-- <args>] Commands: help Show this help message start Either start an existing dev environment or create and start a new one logs Stream umbreld logs shell Get a shell inside the running dev environment exec -- <command> Execute a command inside the running dev environment client -- <rpc> [<args>] Query the umbreld RPC server via a CLI client rebuild Rebuild the operating system image from source and reboot the dev environment into it restart Restart the dev environment stop Stop the dev environment reset Reset the dev environment to a fresh state destroy Destroy the dev environment Environment Variables: UMBREL_DEV_INSTANCE The instance id of the dev environment. Allows running multiple instances of umbrel-dev in different namespaces. Note: umbrel-dev requires a Docker environment that exposes container IPs to the host. This is how Docker natively works on Linux and can be done with OrbStack on macOS. On Windows this should work with WSL 2.
1 parent d43edc6 commit 097adb2

File tree

6 files changed

+310
-166
lines changed

6 files changed

+310
-166
lines changed

package.json

+2-13
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
{
22
"scripts": {
3-
"vm:provision": "multipass launch --name umbrel-dev --cpus 4 --memory 8G --disk 50G 22.04 && npm run vm:stop && multipass mount --type native $PWD umbrel-dev:/opt/umbrel-mount && multipass exec umbrel-dev -- /opt/umbrel-mount/scripts/vm provision",
4-
"vm:shell": "multipass shell umbrel-dev",
5-
"vm:exec": "multipass exec --working-directory /home/ubuntu umbrel-dev --",
6-
"vm:logs": "multipass exec umbrel-dev -- journalctl --unit umbreld-production --unit umbreld --unit ui --follow --lines 100 --output cat",
7-
"vm:enable-development": "multipass exec umbrel-dev -- /opt/umbrel-mount/scripts/vm enable-development",
8-
"vm:enable-production": "multipass exec umbrel-dev -- /opt/umbrel-mount/scripts/vm enable-production",
9-
"vm:install-deps": "multipass exec umbrel-dev -- /opt/umbrel-mount/scripts/vm install-deps",
10-
"vm:trpc": "npm run --silent vm:exec -- UMBREL_DATA_DIR=./data UMBREL_TRPC_ENDPOINT=http://localhost/trpc npm --prefix /home/ubuntu/umbrel/packages/umbreld run start -- client",
11-
"vm:start": "multipass start umbrel-dev",
12-
"vm:stop": "multipass stop umbrel-dev",
13-
"vm:restart": "multipass restart umbrel-dev",
14-
"vm:destroy": "multipass delete umbrel-dev && multipass purge",
15-
"vm:remount": "multipass mount . umbrel-dev:/opt/umbrel-mount"
3+
"dev": "./scripts/umbrel-dev",
4+
"dev:help": "npm run dev help"
165
}
176
}

packages/umbreld/package.json

+2-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
"private": true,
1515
"scripts": {
1616
"start": "./source/cli.ts",
17-
"start:vm": "sudo FORCE_COLOR=1 npm run start -- --data-directory ./data --port 80 --log-level verbose",
1817
"client": "UMBREL_DATA_DIR=./data UMBREL_TRPC_ENDPOINT=http://localhost:3001/trpc npm run start -- client",
1918
"format": "prettier --write .",
2019
"format:check": "prettier --check .",
@@ -26,9 +25,8 @@
2625
"test:integration": "npm run test -- integration",
2726
"test:coverage": "open ./coverage/index.html",
2827
"test-everything": "npm run format && npm run test -- --run && npm run lint",
29-
"watch": "NODE_ENV=development nodemon --ext js,json,ts --watch source --exec npm run",
30-
"dev": "UMBREL_UI_PROXY=http://localhost:3000 npm run watch -- start -- -- --data-directory ./data --port 3001 --log-level verbose",
31-
"dev:vm": "sudo FORCE_COLOR=1 npm run dev -- --port 80",
28+
"watch": "NODE_ENV=development nodemon --legacy-watch --ext js,json,ts --watch source --exec npm run",
29+
"dev": "FORCE_COLOR=1 UMBREL_UI_PROXY=http://localhost:3000 npm run watch -- start -- -- --data-directory /home/umbrel/umbrel --log-level verbose",
3230
"build": "tsx scripts/build.ts",
3331
"prepare-release": "tsx scripts/prepare-release.ts",
3432
"timestamp-release": "ots-cli.js stamp release/SHA256SUMS",

packages/umbreld/source/modules/cli-client.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import process from 'node:process'
2-
import os from 'node:os'
32

43
import {createTRPCProxyClient, httpLink} from '@trpc/client'
54
import fse from 'fs-extra'
@@ -9,7 +8,7 @@ import * as jwt from './jwt.js'
98
import type {AppRouter} from './server/trpc/index.js'
109

1110
// TODO: Maybe just read the endpoint from the data dir
12-
const dataDir = process.env.UMBREL_DATA_DIR ?? `${os.homedir()}/umbrel`
11+
const dataDir = process.env.UMBREL_DATA_DIR ?? '/home/umbrel/umbrel'
1312
const trpcEndpoint = process.env.UMBREL_TRPC_ENDPOINT ?? `http://localhost/trpc`
1413

1514
async function signJwt() {

packages/umbreld/umbreld

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
# We need to add this shim as the main umbreld entrypoint so we can sets up the environmnet we need
44
# like adding node_modules/.bin to the PATH so we have access to tsx.
55

6+
7+
# Hook to run development mode
8+
if [[ -d "/umbrel-dev" ]]
9+
then
10+
echo "Running in development mode"
11+
cd /umbrel-dev
12+
exec npm run dev container-init
13+
fi
14+
615
# Find the project directory and follow symlinks if necessary
716
project_directory="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))"
817

scripts/umbrel-dev

+296
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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

Comments
 (0)