configs/bin/box
bin/boxRaw
1#!/usr/bin/env bash
2#
3# Launch Incus containers to work in there.
4#
5# Steps to do before first run on a new machine:
6#
7# sudo incus admin init
8#
9# then download the ubuntu image:
10# sudo incus launch images:ubuntu/24.04 test
11#
12# - create a managed network bridge:
13# incus network create incusbr0 ipv4.address=auto ipv4.nat=true ipv6.address=none
14# incus profile device set default eth0 network incusbr0
15#
16#
17
18set -e
19
20if [ -z "${1:-}" ]; then
21 echo "Error: no container name provided."
22 echo "Usage: box <container_name> <profile_name(optional)>"
23 exit 1
24fi
25
26# Choose which container manager to use:
27MNGR="sudo incus"
28
29# dev user with same permission levels as host user
30DEV_USER="dev"
31HOST_USER="${SUDO_USER:-$USER}"
32HOST_UID=$(id -u "$HOST_USER")
33HOST_GID=$(id -g "$HOST_USER")
34HOST_HOME=$(getent passwd "$HOST_USER" | cut -d: -f6)
35
36CONTAINER_NAME="$1"
37HOST_SHARE_DIR="$HOST_HOME/container_data/$CONTAINER_NAME"
38CONTAINER_SHARE_DIR="/mnt/shared"
39IMAGE="images:ubuntu/24.04"
40
41PROFILE_NAME=${2:-"default"} # if $2 is empty, use "default"
42PROFILES_FILE="$HOST_HOME/bin/box_profiles.sh"
43
44# EXPOSE_PORTS="7000 7001 7002 7003 7004 7005 7006 7007 7008 7009 7010"
45EXPOSE_PORTS=""
46
47
48if [ -z "$CONTAINER_NAME" ]; then
49 echo "error: no container name provided."
50 echo "Usage: ./box.sh <container_name> [profile_name]"
51 exit 1
52fi
53
54if [ ! -f "$PROFILES_FILE" ]; then
55 echo "error: $PROFILES_FILE not found."
56 exit 1
57fi
58
59source "$PROFILES_FILE"
60
61if ! declare -f "profile_$PROFILE_NAME" > /dev/null; then
62 echo "error: profile '$PROFILE_NAME' not found in $PROFILES_FILE."
63 echo "Available profiles are:"
64 grep -oP '(?<=profile_).*(?=\(\))' "$PROFILES_FILE"
65 exit 1
66fi
67
68if $MNGR info "$CONTAINER_NAME" > /dev/null 2>&1; then
69 echo "error: container '$CONTAINER_NAME' already exists."
70 exit 1
71fi
72
73echo "profile selected: [$PROFILE_NAME]"
74
75mkdir -p "$HOST_SHARE_DIR"
76
77echo "launch Incus container: $CONTAINER_NAME..."
78# security.nesting=true allows Docker to run inside this container
79$MNGR launch "$IMAGE" "$CONTAINER_NAME" -c security.nesting=true
80
81echo "wait for container"
82for _ in $(seq 1 30); do
83 if $MNGR exec "$CONTAINER_NAME" -- true > /dev/null 2>&1; then
84 break
85 fi
86 sleep 1
87done
88
89if ! $MNGR exec "$CONTAINER_NAME" -- true > /dev/null 2>&1; then
90 echo "error: container did not become ready."
91 $MNGR delete -f "$CONTAINER_NAME"
92 exit 1
93fi
94
95echo "configure non-root development user"
96$MNGR exec "$CONTAINER_NAME" \
97 --env DEV_USER="$DEV_USER" \
98 --env HOST_UID="$HOST_UID" \
99 --env HOST_GID="$HOST_GID" \
100 -- bash -s <<'IN_CONTAINER'
101
102set -e
103
104# Find whether the desired UID already exists.
105EXISTING_USER=$(getent passwd "$HOST_UID" | cut -d: -f1 || true)
106
107if [ -n "$EXISTING_USER" ] && [ "$EXISTING_USER" != "$DEV_USER" ]; then
108 echo "renaming existing UID $HOST_UID user $EXISTING_USER -> $DEV_USER"
109
110 usermod -l "$DEV_USER" "$EXISTING_USER"
111
112elif [ -z "$EXISTING_USER" ]; then
113 echo "creating user $DEV_USER with UID $HOST_UID"
114
115 useradd \
116 -m \
117 -u "$HOST_UID" \
118 -s /bin/bash \
119 "$DEV_USER"
120fi
121
122# Move the user home to /home/dev.
123OLD_HOME=$(getent passwd "$DEV_USER" | cut -d: -f6)
124NEW_HOME="/home/$DEV_USER"
125
126if [ "$OLD_HOME" != "$NEW_HOME" ]; then
127 if [ -e "$NEW_HOME" ]; then
128 # In case the target directory already exists, copy the old home
129 # contents and point the user to the new location.
130 if [ -d "$OLD_HOME" ]; then
131 cp -a "$OLD_HOME"/. "$NEW_HOME"/
132 fi
133
134 usermod -d "$NEW_HOME" "$DEV_USER"
135 else
136 usermod -d "$NEW_HOME" -m "$DEV_USER"
137 fi
138fi
139
140usermod -s /bin/bash "$DEV_USER"
141
142# Match host primary GID as well.
143GROUP_NAME=$(getent group "$HOST_GID" | cut -d: -f1 || true)
144
145if [ -z "$GROUP_NAME" ]; then
146 groupadd -g "$HOST_GID" hostusers
147 GROUP_NAME=hostusers
148fi
149
150usermod -g "$GROUP_NAME" "$DEV_USER"
151
152# Make sure home belongs to dev.
153mkdir -p "/home/$DEV_USER"
154chown -R "$HOST_UID:$HOST_GID" "/home/$DEV_USER"
155chmod 755 "/home/$DEV_USER"
156
157echo "configured user:"
158id "$DEV_USER"
159
160IN_CONTAINER
161
162echo "configure shared directory (between container & host)"
163$MNGR config device add "$CONTAINER_NAME" project_storage disk \
164 source="$HOST_SHARE_DIR" \
165 path="$CONTAINER_SHARE_DIR"
166
167# shift=true maps the shared directory permissions between host and container
168$MNGR config device set "$CONTAINER_NAME" project_storage shift=true
169
170echo "configure port forwarding"
171for PORT in $EXPOSE_PORTS; do
172 $MNGR config device add "$CONTAINER_NAME" "proxy${PORT}" proxy \
173 listen="tcp:0.0.0.0:${PORT}" \
174 connect="tcp:127.0.0.1:${PORT}"
175done
176
177echo "wait for network"
178NETWORK_READY=false
179for _ in $(seq 1 30); do
180 if $MNGR exec "$CONTAINER_NAME" -- \
181 sh -c 'ip -4 route show default | grep -q "^default "'; then
182 NETWORK_READY=true
183 break
184 fi
185
186 sleep 1
187done
188if [ "$NETWORK_READY" != "true" ]; then
189 echo "error: container did not get a default IPv4 route."
190 echo "network state:"
191 $MNGR exec "$CONTAINER_NAME" -- ip addr || true
192 $MNGR exec "$CONTAINER_NAME" -- ip route || true
193 exit 1
194fi
195
196echo "check internet"
197INTERNET_READY=false
198for _ in $(seq 1 10); do
199 if $MNGR exec "$CONTAINER_NAME" -- ping -c 1 -W 2 8.8.8.8 > /dev/null 2>&1; then
200 INTERNET_READY=true
201 break
202 fi
203
204 sleep 1
205done
206if [ "$INTERNET_READY" != "true" ]; then
207 echo "error: container has a network route but no internet access."
208 echo "network state:"
209 $MNGR exec "$CONTAINER_NAME" -- ip addr || true
210 $MNGR exec "$CONTAINER_NAME" -- ip route || true
211 exit 1
212fi
213
214echo "apt update & apt install"
215{
216 # Load the profile functions inside the container.
217 cat "$PROFILES_FILE"
218
219 cat <<'IN_CONTAINER'
220
221set -e
222
223export DEBIAN_FRONTEND=noninteractive
224export NEEDRESTART_MODE=a # 'a' for automatic restart
225
226apt-get update
227apt-get install -y sudo
228apt-get upgrade -y
229
230# give user 'dev' sudo access
231usermod -aG sudo "$DEV_USER"
232
233printf '%s\n' "$DEV_USER ALL=(ALL) NOPASSWD:ALL" \
234 > "/etc/sudoers.d/$DEV_USER"
235
236chmod 0440 "/etc/sudoers.d/$DEV_USER"
237
238if declare -f "profile_$PROFILE_NAME" > /dev/null; then
239 "profile_$PROFILE_NAME"
240else
241 echo "error: profile function $PROFILE_NAME not found."
242 exit 1
243fi
244
245# profile_docker might have created the docker group
246if getent group docker > /dev/null; then
247 usermod -aG docker "$DEV_USER"
248fi
249
250IN_CONTAINER
251
252} | $MNGR exec "$CONTAINER_NAME" \
253 --env DEV_USER="$DEV_USER" \
254 --env PROFILE_NAME="$PROFILE_NAME" \
255 -- bash -s
256
257echo "configure development environment as $DEV_USER"
258$MNGR exec "$CONTAINER_NAME" \
259 -- su -l "$DEV_USER" -c 'bash -s' <<'IN_DEV'
260
261set -e
262
263cd "$HOME"
264
265if [ ! -d "$HOME/configs/.git" ]; then
266 git clone https://github.com/arnaucube/configs.git "$HOME/configs"
267fi
268
269cd "$HOME/configs"
270
271# yes | bash install-new-server.sh
272
273IN_DEV
274
275echo "configure npm global packages as $DEV_USER"
276$MNGR exec "$CONTAINER_NAME" \
277 -- su -l "$DEV_USER" -c 'bash -s' <<'IN_DEV'
278
279set -e
280
281mkdir -p "$HOME/.local"
282
283# install global npm packages under the dev home instead of /usr/lib
284npm config set prefix "$HOME/.local"
285
286PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'
287
288if ! grep -qxF "$PATH_LINE" "$HOME/.bashrc" 2>/dev/null; then
289 echo "$PATH_LINE" >> "$HOME/.bashrc"
290fi
291
292export PATH="$HOME/.local/bin:$PATH"
293
294IN_DEV
295
296echo "configure shared directory permissions"
297
298# Make existing files in the shared directory belong to the host/dev user.
299sudo chown -R "$HOST_UID:$HOST_GID" "$HOST_SHARE_DIR"
300
301# directories: rwxr-xr-x
302find "$HOST_SHARE_DIR" \
303 -type d \
304 -exec chmod 755 {} +
305
306# normal files: rw-r--r--
307find "$HOST_SHARE_DIR" \
308 -type f \
309 ! -perm /111 \
310 -exec chmod 644 {} +
311
312# preserve executable files as executable
313find "$HOST_SHARE_DIR" \
314 -type f \
315 -perm /111 \
316 -exec chmod 755 {} +
317
318echo "configure Incus shell to use non-root user"
319
320# This alias is global for the current Incus client.
321# Containers entered with `incus shell` are expected to have the dev user.
322$MNGR alias remove shell > /dev/null 2>&1 || true
323$MNGR alias add shell "exec @ARGS@ -- su -l $DEV_USER"
324
325echo "check configuration"
326$MNGR exec "$CONTAINER_NAME" \
327 -- su -l "$DEV_USER" -c '
328 echo "user: $(whoami)"
329 echo "home: $HOME"
330 echo "uid/gid: $(id)"
331 echo "working directory: $(pwd)"
332 '
333
334echo "check shared directory permissions"
335$MNGR exec "$CONTAINER_NAME" \
336 -- su -l "$DEV_USER" -c '
337 touch /mnt/shared/.box-permission-test
338 rm /mnt/shared/.box-permission-test
339 '
340
341echo "setup complete"
342echo "----------------------------------------------------"
343echo "Shared Directory (Host): $HOST_SHARE_DIR"
344echo "Shared Directory (Container): $CONTAINER_SHARE_DIR"
345echo "----------------------------------------------------"
346echo "To enter the container, run: $MNGR shell $CONTAINER_NAME"
347echo "To enter as root, run: $MNGR exec $CONTAINER_NAME -- bash"
348echo "To stop the container, run: $MNGR stop $CONTAINER_NAME"
349echo "To remove the container, run: $MNGR delete $CONTAINER_NAME"