Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions .devcontainer/codespaces.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 2 additions & 30 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand Down
9 changes: 1 addition & 8 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
136 changes: 52 additions & 84 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,24 @@ 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
```

##### 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:
Expand All @@ -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?
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
73 changes: 12 additions & 61 deletions src/define.sh
Original file line number Diff line number Diff line change
@@ -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
Loading