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