diff --git a/.devcontainer/codespaces.yml b/.devcontainer/codespaces.yml index ea2480b..14bf120 100644 --- a/.devcontainer/codespaces.yml +++ b/.devcontainer/codespaces.yml @@ -4,17 +4,10 @@ services: container_name: chromeos environment: VERSION: "stable" - GPU: "N" - FORCE_HOST_CURSOR: "Y" - KEEP_AWAKE: "N" - RAM_SIZE: "half" - DISK_SIZE: "max" - CPU_CORES: "max" devices: + - /dev/dri - /dev/kvm - /dev/net/tun - device_cgroup_rules: - - "c 226:* rwm" cap_add: - NET_ADMIN ports: diff --git a/Dockerfile b/Dockerfile index 902e2ef..a3f0eaf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ # syntax=docker/dockerfile:1 FROM scratch AS runner -COPY --from=qemux/qemu:7.46 / / +COPY --from=qemux/qemu:7.47 / / ARG VERSION_ARG="0.0" + ARG DEBCONF_NOWARNINGS="yes" ARG DEBIAN_FRONTEND="noninteractive" ARG DEBCONF_NONINTERACTIVE_SEEN="true" @@ -23,38 +24,9 @@ EOF COPY --chmod=755 ./src /run/ -RUN <<'EOF' - set -eu - - sed -i \ - 's@if ! enabled "$GPU" || isAmdCpu || \[\[ "$ARCH" != "amd64" \]\]; then@if ! enabled "$GPU" || [[ "$ARCH" != "amd64" ]]; then@' \ - /run/display.sh - - sed -i \ - 's| -device usb-tablet||' \ - /run/config.sh - - sed -i \ - 's@USB_OPTS="-device $USB"@& \&\& { [[ "${TABLET:-Y}" =~ ^[Yy] ]] \&\& USB_OPTS+=" -device usb-tablet" || USB_OPTS+=" -device usb-mouse"; }@' \ - /run/config.sh - - grep -q 'usb-mouse' /run/config.sh || { - echo "patch failed: TABLET conditional not injected into config.sh" >&2 - exit 1 - } - - ! grep -q 'isAmdCpu' /run/display.sh || { - echo "patch failed: AMD GPU gate not removed from display.sh" >&2 - exit 1 - } - - bash -n /run/display.sh /run/config.sh -EOF - VOLUME /storage EXPOSE 5900 8006 -ENV GPU="Y" ENV RAM_SIZE="4G" ENV CPU_CORES="2" ENV DISK_SIZE="64G" diff --git a/compose.yml b/compose.yml index 714ae44..14bf120 100644 --- a/compose.yml +++ b/compose.yml @@ -4,22 +4,15 @@ services: container_name: chromeos environment: VERSION: "stable" - GPU: "Y" - FORCE_HOST_CURSOR: "Y" - KEEP_AWAKE: "N" devices: + - /dev/dri - /dev/kvm - /dev/net/tun - device_cgroup_rules: - - "c 226:* rwm" cap_add: - NET_ADMIN ports: - 8006:8006 - - 5900:5900/tcp - - 5900:5900/udp volumes: - ./chromeos:/storage - - /dev/dri:/dev/dri:rw restart: always stop_grace_period: 2m diff --git a/readme.md b/readme.md index 1c76ba0..3d0980d 100644 --- a/readme.md +++ b/readme.md @@ -38,23 +38,16 @@ services: container_name: chromeos environment: VERSION: "stable" - GPU: "Y" - FORCE_HOST_CURSOR: "Y" - KEEP_AWAKE: "N" devices: + - /dev/dri - /dev/kvm - /dev/net/tun - device_cgroup_rules: - - "c 226:* rwm" cap_add: - NET_ADMIN ports: - 8006:8006 - - 5900:5900/tcp - - 5900:5900/udp volumes: - ./chromeos:/storage - - /dev/dri:/dev/dri:rw restart: always stop_grace_period: 2m ``` @@ -62,7 +55,7 @@ services: ##### Docker CLI: ```bash -docker run -it --rm --name chromeos -e "VERSION=stable" -p 8006:8006 --device=/dev/kvm --device=/dev/net/tun --device-cgroup-rule="c 226:* rwm" --cap-add NET_ADMIN -v "${PWD:-.}/chromeos:/storage" -v /dev/dri:/dev/dri --stop-timeout 120 docker.io/dockurr/chromeos +docker run -it --rm --name chromeos -e "VERSION=stable" -p 8006:8006 --device=/dev/dri --device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN -v "${PWD:-.}/chromeos:/storage" --stop-timeout 120 docker.io/dockurr/chromeos ``` ##### Kubernetes: @@ -85,9 +78,6 @@ kubectl apply -f https://raw.githubusercontent.com/dockur/chromeos/master/kubern > [!NOTE] > Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported. -> [!IMPORTANT] -> For best performance, run on a host with a GPU and `/dev/dri/` exposed. GPU acceleration uses the QEMU egl-headless path: Intel and AMD render nodes go through the open-source Mesa driver, Nvidia through its proprietary driver (see the FAQ). Without a usable GPU it falls back to software rendering, which works but is slow. - ## FAQ 💬 ### How do I use it? @@ -179,77 +169,6 @@ kubectl apply -f https://raw.githubusercontent.com/dockur/chromeos/master/kubern CPU_CORES: "4" ``` -### How do I password-protect the viewer? - - By default the viewer on port 8006 is open to anyone who can reach it. Set `PROTECT` to require a login (HTTP basic auth). The default credentials are `Docker` / `admin`, so override them with `USERNAME` and `PASSWORD`: - - ```yaml - environment: - PROTECT: "Y" - USERNAME: "admin" - PASSWORD: "your-password" - ``` - -### How do I let the host reclaim unused memory? - - By default the VM holds the full `RAM_SIZE` for its entire lifetime. Set `BALLOONING` to enable dynamic memory ballooning, which lets the host reclaim guest RAM that isn't in use: - - ```yaml - environment: - BALLOONING: "Y" - ``` - - The target can be tuned with `BALLOONING_MIN_MEM` (default `33%`) and `BALLOONING_RAM_THRESHOLD` (default `80.0`). - -### How does GPU acceleration work? - - The container expects the host's `/dev/dri/` to be bind-mounted in. At startup, the entrypoint scans for a usable render node and hands it to QEMU as the VirGL backend (`-display egl-headless,rendernode=...` + `virtio-vga-gl`). Both the `volumes: - /dev/dri:/dev/dri:rw` mount and the `device_cgroup_rules: - "c 226:* rwm"` rule in the example compose are required for this. Intel and AMD render nodes work out of the box; for Nvidia see below. If no usable render node is found, the container falls back to software rendering. - - The `GPU` setting accepts: - - | Value | Effect | - |-------|--------| - | `Y` / `auto` | Auto-detect (default); prefers a ready Nvidia node, otherwise Intel/AMD | - | `N` | Off — software rendering (3–15 fps) | - | `intel` / `amd` / `nvidia` | Force a specific vendor, useful on multi-GPU hosts | - - For finer control, set `RENDERNODE` to a specific node (e.g. `/dev/dri/renderD128`). On a miss the container logs exactly what was found and what to fix, then falls back to software rendering. - -### How do I use an Nvidia GPU? - - Nvidia cards render through the same egl-headless path, with two extra requirements: - - - The host must load `nvidia-drm` with `modeset=1` (add `options nvidia_drm modeset=1` to a file in `/etc/modprobe.d/`, run `update-initramfs -u`, then reboot). Without it the card is invisible to the GBM/EGL backend the container uses. - - Run the container with the Nvidia runtime and the `graphics` capability so the driver's EGL libraries are injected: - - ```yaml - services: - chromeos: - image: dockurr/chromeos - runtime: nvidia - environment: - GPU: "Y" - NVIDIA_VISIBLE_DEVICES: "all" - NVIDIA_DRIVER_CAPABILITIES: "all" - device_cgroup_rules: - - "c 226:* rwm" - volumes: - - /dev/dri:/dev/dri:rw - ``` - - Or with the CLI: add `--gpus all -e NVIDIA_DRIVER_CAPABILITIES=all`. The render node is auto-detected by vendor, so no card-specific configuration is needed. If both an Nvidia and an Intel/AMD GPU are present, the Nvidia card is preferred once its EGL libraries are available; force the choice either way with `GPU: "nvidia"` / `GPU: "intel"`, or pin a node with `RENDERNODE`. - -### How does the cursor work? - - ChromeOS Flex sees the input device as a touchscreen and doesn't render a cursor. noVNC has an optional "Show dot when no cursor" setting, but the dot is small and easy to miss. By default the container overrides this with a CSS rule so the browser's normal cursor shows through: - - ```yaml - environment: - FORCE_HOST_CURSOR: "Y" - ``` - - Set it to `"N"` to disable the override. - ### How do I right-click? ChromeOS treats the input device as a touchscreen, so right-click events are ignored. To open a context menu, **left-click and hold for about half a second**. The touch UI interprets a long-press as a context-menu gesture. @@ -261,7 +180,6 @@ kubectl apply -f https://raw.githubusercontent.com/dockur/chromeos/master/kubern ```yaml environment: TABLET: "N" - FORCE_HOST_CURSOR: "N" ``` This swaps the tablet for a `usb-mouse`, so ChromeOS shows its own cursor and right-click works. The trade-off is pointer tracking: ChromeOS scales the relative movements noVNC sends, so the cursor drifts away from the real pointer position over distance and clicks land off-target. This mode suits a direct VNC client more than the browser viewer; for noVNC, the default tablet mode is recommended. @@ -299,6 +217,41 @@ kubectl apply -f https://raw.githubusercontent.com/dockur/chromeos/master/kubern Trade-off: slight blurring on photos and gradients (invisible on UI text). Most useful when accessing the container over WAN or on bandwidth-constrained networks. +### How do I password-protect the noVNC viewer? + + By default the viewer on port 8006 is open to anyone who can reach it. Set `PROTECT` to require a login: + + ```yaml + environment: + PROTECT: "Y" + USERNAME: "admin" + PASSWORD: "password" + ``` + +### How do I enable GPU acceleration? + + To enable hardware-accelerated graphics using an Intel or AMD GPU, add the following device to your compose file: + + ```yaml + devices: + - /dev/dri + ``` + + For NVIDIA GPUs, the `NVIDIA Container Toolkit` must be installed on the host and the GPU must be exposed to the container: + + ```yaml + environment: + NVIDIA_DRIVER_CAPABILITIES: "graphics" + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: + - gpu + ``` + ### How do I enable developer mode? Add `DEV_MODE: "Y"` to your compose file: @@ -369,6 +322,21 @@ kubectl apply -f https://raw.githubusercontent.com/dockur/chromeos/master/kubern - /dev/bus/usb ``` +### How do I let the host reclaim unused memory? + + By default the VM holds the full `RAM_SIZE` for its entire lifetime. Set `BALLOONING` to enable dynamic memory ballooning, which lets the host reclaim guest RAM that isn't in use: + + ```yaml + environment: + BALLOONING: "Y" + ``` + + The target can be tuned with `BALLOONING_MIN_MEM` (default `33%`) and `BALLOONING_RAM_THRESHOLD` (default `80.0`). + +### Are these all available options? + + No. For a complete overview of all supported settings, see the [environment variables](https://github.com/qemus/qemu/blob/master/docs/environment.md) page. + ### How do I verify that KVM is available? First, make sure your platform and container runtime meet the [requirements](#requirements-️) listed above. diff --git a/src/define.sh b/src/define.sh index cc7fb66..d371c63 100755 --- a/src/define.sh +++ b/src/define.sh @@ -1,83 +1,34 @@ #!/usr/bin/env bash set -Eeuo pipefail -: "${LOSSY:="N"}" : "${TABLET:="Y"}" -: "${FORCE_HOST_CURSOR:="Y"}" +: "${KEEP_AWAKE:="N"}" +: "${FORCE_HOST_CURSOR:="N"}" -BOOT_DESC=" $APP (${VERSION,,})" - -gpu="${GPU:-}" -gpu_vendor="" - -case "${gpu,,}" in - ""|n|no|0|false|off) GPU="" ;; - y|yes|1|true|on|auto) GPU="Y" ;; - intel) GPU="Y"; gpu_vendor="0x8086" ;; - amd) GPU="Y"; gpu_vendor="0x1002" ;; - nvidia) GPU="Y"; gpu_vendor="0x10de" ;; - *) info "Unknown GPU value \"$gpu\"; treating it as auto."; GPU="Y" ;; -esac - -if [ -n "$GPU" ] && [ -z "${RENDERNODE:-}" ]; then - compgen -G "/usr/lib/*/libEGL_nvidia.so.0" >/dev/null && nvidia_egl=1 || nvidia_egl= - for node in /dev/dri/renderD*; do - { exec 3<"$node"; } 2>/dev/null || continue - exec 3<&- - dev="/sys/class/drm/${node##*/}/device" - vid=$(cat "$dev/vendor" 2>/dev/null) - if [ -n "$gpu_vendor" ] && [ "$vid" != "$gpu_vendor" ]; then - continue - fi - if [ "$vid" != "0x10de" ]; then - : "${RENDERNODE:=$node}" - elif [ -z "$nvidia_egl" ]; then - info "Nvidia GPU at $node has no graphics capability; run the container with \"--gpus all -e NVIDIA_DRIVER_CAPABILITIES=all\"." - elif ! compgen -G "$dev/drm/card*" >/dev/null; then - info "Nvidia GPU at $node needs nvidia-drm modeset=1 on the host; add \"options nvidia_drm modeset=1\" and reboot." - else - RENDERNODE="$node"; break - fi - done -fi - -if [ -n "$GPU" ] && { [ -z "${RENDERNODE:-}" ] || [ ! -c "${RENDERNODE:-/dev/null}" ]; }; then - info "No usable ${gpu_vendor:+$gpu }GPU render node found; falling back to software rendering." - GPU="" -fi - -if [ -n "$GPU" ]; then - case "$(cat "/sys/class/drm/${RENDERNODE##*/}/device/vendor" 2>/dev/null)" in - 0x8086) gpu_name="Intel" ;; - 0x1002) gpu_name="AMD" ;; - 0x10de) gpu_name="Nvidia" ;; - *) gpu_name="GPU" ;; - esac - info "Hardware rendering on $gpu_name render node $RENDERNODE." -fi - -LOSSY_OPT="" -[[ "${LOSSY^^}" =~ ^Y ]] && LOSSY_OPT=",lossy=on" -export LOSSY_OPT - -# Show the browser's cursor over the noVNC canvas — ChromeOS hides its own cursor in touchscreen mode (which we are, since usb-tablet sends absolute coords). +# Show the browser's cursor over the noVNC canvas — ChromeOS hides its own cursor +# in touchscreen mode (which we are, since usb-tablet sends absolute coords). CSS_MARKER='/* chromeos-flex */' CSS_RULE='#noVNC_container, #noVNC_container * { cursor: default !important; }' BASE_CSS='/usr/share/novnc/app/styles/base.css' if [ -f "$BASE_CSS" ]; then + sed -i "\|$CSS_MARKER|,+1d" "$BASE_CSS" 2>/dev/null || true - if [[ "${FORCE_HOST_CURSOR^^}" =~ ^[Yy] ]]; then + + if enabled "$TABLET" || enabled "$FORCE_HOST_CURSOR"; then printf '\n%s\n%s\n' "$CSS_MARKER" "$CSS_RULE" >> "$BASE_CSS" fi + fi -if [[ "${TABLET^^}" =~ ^Y ]] && [ -x /run/mouse_fix.sh ]; then +if enabled "$TABLET" && [ -x /run/mouse_fix.sh ]; then nohup /run/mouse_fix.sh >/dev/null 2>&1 & disown +else + MOUSE="usb-mouse" fi -if [[ "${KEEP_AWAKE:-N}" =~ ^[Yy] ]] && [ -x /run/keep_awake.sh ]; then +if enabled "$KEEP_AWAKE" && [ -x /run/keep_awake.sh ]; then nohup /run/keep_awake.sh >/dev/null 2>&1 & disown fi diff --git a/src/entry.sh b/src/entry.sh index 42390e0..498ec16 100644 --- a/src/entry.sh +++ b/src/entry.sh @@ -1,17 +1,20 @@ #!/usr/bin/env bash set -Eeuo pipefail +: "${PLATFORM:="x64"}" : "${APP:="ChromeOS Flex"}" : "${SUPPORT:="https://github.com/dockur/chromeos"}" +: "${GPU:="Y"}" : "${AUDIO:="N"}" : "${LOSSY:="N"}" : "${SHUTDOWN:="Y"}" : "${TIMEOUT:="105"}" -: "${PLATFORM:="x64"}" +: "${RAM_MINIMUM:="2G"}" : "${BOOT_MODE:="uefi"}" : "${RAM_MINIMUM:="2G"}" : "${DISK_MINIMUM:="16G"}" +: "${MOUSE:="usb-tablet"}" cd /run