Add Claude Desktop container with per-start sign-in
Dockerfile installs Claude Desktop from Anthropic's apt repo (key fingerprint verified) and runs it as a non-root user matching the host UID/GID. run.sh forwards the host X display with a scoped xauth cookie and checks that a window can be drawn before starting the app. No keyring and no config volume: Electron uses --password-store=basic, the container runs with --rm, so the user logs in on every start. Chromium's sandbox can't create namespaces under Docker's default seccomp profile, so the app runs with --no-sandbox; the container's own seccomp and namespaces remain the boundary to the host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the Claude Desktop container, forwarding the host's X server.
|
||||
# Before launching, verify that a window from the container can actually be
|
||||
# drawn on the host display.
|
||||
#
|
||||
# Usage: ./run.sh [--build] [--check-only]
|
||||
# --build rebuild the image even if it exists
|
||||
# --check-only run the display check and exit without starting Claude
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="${IMAGE:-claude-re-setup:latest}"
|
||||
CONTAINER="${CONTAINER:-claude-desktop}"
|
||||
RUNTIME="${RUNTIME:-docker}"
|
||||
|
||||
build=0
|
||||
check_only=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--build) build=1 ;;
|
||||
--check-only) check_only=1 ;;
|
||||
-h|--help) sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "Unknown argument: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
info() { echo "==> $*"; }
|
||||
|
||||
command -v "$RUNTIME" >/dev/null || die "'$RUNTIME' not found (set RUNTIME=podman to use podman)"
|
||||
command -v xauth >/dev/null || die "xauth not found on the host (dnf install xorg-x11-xauth)"
|
||||
|
||||
# --- Display -----------------------------------------------------------------
|
||||
# On Wayland sessions this is XWayland, which GNOME/KDE start on demand.
|
||||
[[ -n "${DISPLAY:-}" ]] || die "DISPLAY is not set; run this from a graphical session"
|
||||
|
||||
display_num="${DISPLAY#*:}"
|
||||
display_num="${display_num%%.*}"
|
||||
[[ -S "/tmp/.X11-unix/X${display_num}" ]] \
|
||||
|| die "X socket /tmp/.X11-unix/X${display_num} not found for DISPLAY=$DISPLAY"
|
||||
|
||||
# Hand the container a copy of this display's cookie, rewritten to match any
|
||||
# hostname (family ffff). This avoids opening the server up with 'xhost +'.
|
||||
xauth_dir="${XDG_RUNTIME_DIR:-/tmp}/claude-re-setup"
|
||||
xauth_file="$xauth_dir/Xauthority"
|
||||
mkdir -p "$xauth_dir"
|
||||
chmod 700 "$xauth_dir"
|
||||
: > "$xauth_file"
|
||||
xauth nlist "$DISPLAY" | sed -e 's/^..../ffff/' | xauth -f "$xauth_file" nmerge - \
|
||||
|| die "could not export the X cookie for $DISPLAY"
|
||||
[[ -s "$xauth_file" ]] || die "no X cookie found for $DISPLAY (is XAUTHORITY set?)"
|
||||
# The container user has the same UID as us, so 0600 is enough.
|
||||
chmod 600 "$xauth_file"
|
||||
|
||||
# --- Image -------------------------------------------------------------------
|
||||
if (( build )) || ! "$RUNTIME" image inspect "$IMAGE" >/dev/null 2>&1; then
|
||||
info "Building $IMAGE"
|
||||
"$RUNTIME" build \
|
||||
--build-arg USER_UID="$(id -u)" \
|
||||
--build-arg USER_GID="$(id -g)" \
|
||||
-t "$IMAGE" "$(dirname "$(readlink -f "$0")")"
|
||||
fi
|
||||
|
||||
# label=disable: the X socket and cookie are host files that SELinux would
|
||||
# otherwise block, and relabeling /tmp/.X11-unix with :z would break the host.
|
||||
run_args=(
|
||||
--rm
|
||||
--security-opt label=disable
|
||||
--shm-size 1g
|
||||
-e DISPLAY=":${display_num}"
|
||||
-e XAUTHORITY=/tmp/.Xauthority
|
||||
-v /tmp/.X11-unix:/tmp/.X11-unix:ro
|
||||
-v "$xauth_file:/tmp/.Xauthority:ro"
|
||||
)
|
||||
|
||||
# --- Display check -----------------------------------------------------------
|
||||
# 1. xdpyinfo proves the container can connect and authenticate to the server.
|
||||
# 2. xmessage opens a real window; xwininfo confirms the server mapped it.
|
||||
info "Checking that the container can draw on DISPLAY=$DISPLAY"
|
||||
check_script='
|
||||
set -e
|
||||
xdpyinfo >/dev/null 2>&1 || { echo "FAIL: cannot connect to X server at $DISPLAY"; exit 1; }
|
||||
echo "ok: connected to X server ($(xdpyinfo | sed -n "s/^vendor string: *//p"))"
|
||||
|
||||
title="claude-re-setup-x11-check"
|
||||
xmessage -title "$title" -center -timeout 3 "Display check from the container: OK" &
|
||||
for _ in $(seq 1 30); do
|
||||
if xwininfo -name "$title" 2>/dev/null | grep -q "Map State: IsViewable"; then
|
||||
echo "ok: test window is mapped and viewable"
|
||||
wait || true
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
kill %1 2>/dev/null || true
|
||||
echo "FAIL: test window was created but never became viewable"
|
||||
exit 1
|
||||
'
|
||||
if ! "$RUNTIME" run "${run_args[@]}" "$IMAGE" bash -c "$check_script"; then
|
||||
die "display check failed; see the output above"
|
||||
fi
|
||||
|
||||
(( check_only )) && { info "Display check passed"; exit 0; }
|
||||
|
||||
# --- Claude Desktop ----------------------------------------------------------
|
||||
if "$RUNTIME" container inspect "$CONTAINER" >/dev/null 2>&1; then
|
||||
die "container '$CONTAINER' already exists; stop it with: $RUNTIME stop $CONTAINER"
|
||||
fi
|
||||
|
||||
# No config volume: the container is removed on stop (--rm), so the sign-in
|
||||
# is discarded and Claude asks for a login on every start.
|
||||
info "Starting Claude Desktop in container '$CONTAINER'"
|
||||
"$RUNTIME" run -d "${run_args[@]}" \
|
||||
--name "$CONTAINER" \
|
||||
"$IMAGE" >/dev/null
|
||||
info "Running. Logs: $RUNTIME logs -f $CONTAINER Stop: $RUNTIME stop $CONTAINER"
|
||||
Reference in New Issue
Block a user