diff --git a/.env b/.env new file mode 100644 index 0000000..5f14e87 --- /dev/null +++ b/.env @@ -0,0 +1,19 @@ +UID=1000 # id -u +GID=1000 # id -g +HOST_USER=spyder # Container-Benutzername +HOST_GROUP=spyder # Container-Gruppe + +# Container-interne Pfade +SPYDER_HOME=/home/spyder +SPYDER_WORKSPACE=/home/spyder/workspace + +# Host-Verzeichnisse für Bind-Mounts (können relativ zum Projekt sein) +SPYDER_HOME_VOLUME=./spyder-home +SPYDER_WORKSPACE_VOLUME=./spyder-workspace + +# Display- und Laufzeit-Details des Host-Systems +XDG_RUNTIME_DIR=/run/user/1000 +WAYLAND_DISPLAY=wayland-1 +DISPLAY=:0 + +DEBUG=1 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02ddd7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +spyder-home/ +spyder-workspace/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..412d14b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM mambaorg/micromamba:latest +ARG MAMBA_DOCKERFILE_ACTIVATE=1 + +ENV TZ=Europe/Berlin +ENV LANG=C.UTF-8 + +COPY --chown=$MAMBA_USER:$MAMBA_USER environment.yml /tmp/environment.yml +RUN micromamba install -y -n base -f /tmp/environment.yml && \ + micromamba clean -a -y + +USER root +RUN apt-get update && \ + apt-get install -y --no-install-recommends gosu && \ + rm -rf /var/lib/apt/lists/* + +# Wrapper that starts Spyder safely for desktop sessions +COPY --chmod=0755 start-spyder.sh /usr/local/bin/start-spyder.sh + +# Qt/Wayland & Matplotlib (Qt5) +ENV QT_X11_NO_MITSHM=1 +ENV MPLBACKEND=Qt5Agg + +# Default workspace created dynamically in entrypoint +WORKDIR /root +ENTRYPOINT ["/usr/local/bin/start-spyder.sh"] +CMD ["spyder","--new-instance"] \ No newline at end of file diff --git a/README.md b/README.md index 4f025ad..8343d27 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,136 @@ -# spyder-desktop-docker +# Spyder in Docker on Wayland -Dockerized Spyder IDE with native Wayland support, optional X11 fallback, GPU acceleration, and a ready-to-use Micromamba environment for reproducible Python development. \ No newline at end of file +This stack packages the Spyder IDE inside a Docker container and supports both native Wayland and optional X11 fallback sessions on Linux desktops. + +## Quick Start (local build) + +1. Adjust the variables in `.env` to match your host (UID/GID, display socket paths, and bind mount locations). +2. Ensure the bind mount folders exist (the repository includes `spyder-home/` and `spyder-workspace/` by default). +3. Build and launch Spyder from source with the provided `compose.yaml`: + +```bash +docker compose up +``` + +Use the X11 fallback when necessary: + +```bash +docker compose --profile x11 up +``` + +Spyder exits when you close the IDE window. Stop the stack with `Ctrl+C`. + +## Using a Prebuilt Image + +Build the image locally, tag it, and push it to your registry of choice (example: `ghcr.io/your-account/spyder-wayland:latest`). + +```bash +docker compose build +docker tag spyder-conda ghcr.io/your-account/spyder-wayland:latest +docker push ghcr.io/your-account/spyder-wayland:latest +``` + +Consumers can run the IDE without building locally using the provided `compose.runtime.yaml`: + +```bash +docker compose -f compose.runtime.yaml up +``` + +Override the default image by exporting `SPYDER_IMAGE` in `.env`. + +## Environment Variables + +Key settings live in `.env`: + +- `UID`, `GID`, `HOST_USER`, `HOST_GROUP` – mirror the host user to preserve file ownership. +- `SPYDER_HOME`, `SPYDER_WORKSPACE` – container paths for the Spyder home and project workspace. +- `SPYDER_HOME_VOLUME`, `SPYDER_WORKSPACE_VOLUME` – host-side bind mounts for persistence (can be absolute or relative paths). +- `XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY`, `DISPLAY` – display/socket paths exported from the host session. +- `DEBUG` – placeholder for future debug toggles. + +Update these values to relocate persisted data or point to non-standard display sockets. + +### Example `.env` + +``` +UID=1000 +GID=1000 +HOST_USER=spyder +HOST_GROUP=spyder +SPYDER_HOME=/home/spyder +SPYDER_WORKSPACE=/home/spyder/workspace +SPYDER_HOME_VOLUME=./spyder-home +SPYDER_WORKSPACE_VOLUME=./spyder-workspace +XDG_RUNTIME_DIR=/run/user/1000 +WAYLAND_DISPLAY=wayland-1 +DISPLAY=:0 +SPYDER_IMAGE=ghcr.io/your-account/spyder-wayland:latest +DEBUG=0 +``` + +### Example `docker-compose.yml` + +``` +services: + spyder-wayland: + image: ${SPYDER_IMAGE} + environment: + UID: "${UID}" + GID: "${GID}" + HOST_USER: "${HOST_USER}" + HOST_GROUP: "${HOST_GROUP}" + SPYDER_HOME: "${SPYDER_HOME}" + SPYDER_WORKSPACE: "${SPYDER_WORKSPACE}" + HOME: "${SPYDER_HOME}" + WAYLAND_DISPLAY: "${WAYLAND_DISPLAY}" + XDG_RUNTIME_DIR: "${XDG_RUNTIME_DIR}" + QT_QPA_PLATFORM: "wayland" + QTWEBENGINE_DISABLE_SANDBOX: "1" + QTWEBENGINE_CHROMIUM_FLAGS: "--no-sandbox" + TZ: "Europe/Berlin" + volumes: + - ${SPYDER_HOME_VOLUME}:${SPYDER_HOME} + - ${SPYDER_WORKSPACE_VOLUME}:${SPYDER_WORKSPACE} + - ${XDG_RUNTIME_DIR}:${XDG_RUNTIME_DIR} + working_dir: ${SPYDER_WORKSPACE} + devices: + - "/dev/dri:/dev/dri" + shm_size: "1gb" + + spyder-x11: + profiles: + - "x11" + image: ${SPYDER_IMAGE} + environment: + UID: "${UID}" + GID: "${GID}" + HOST_USER: "${HOST_USER}" + HOST_GROUP: "${HOST_GROUP}" + SPYDER_HOME: "${SPYDER_HOME}" + SPYDER_WORKSPACE: "${SPYDER_WORKSPACE}" + HOME: "${SPYDER_HOME}" + DISPLAY: "${DISPLAY}" + QT_QPA_PLATFORM: "xcb" + QT_X11_NO_MITSHM: "1" + QTWEBENGINE_DISABLE_SANDBOX: "1" + QTWEBENGINE_CHROMIUM_FLAGS: "--no-sandbox" + TZ: "Europe/Berlin" + volumes: + - ${SPYDER_HOME_VOLUME}:${SPYDER_HOME} + - ${SPYDER_WORKSPACE_VOLUME}:${SPYDER_WORKSPACE} + - /tmp/.X11-unix:/tmp/.X11-unix:ro + working_dir: ${SPYDER_WORKSPACE} + devices: + - "/dev/dri:/dev/dri" + shm_size: "1gb" +``` + +## GPU Acceleration + +The configuration keeps GPU acceleration enabled. The container shares `/dev/dri` with the host; if you run into driver issues, temporarily comment out the device mapping and set `QTWEBENGINE_CHROMIUM_FLAGS` or `QT_OPENGL` in `compose.yaml` to fall back to software rendering. + +## Notes + +- The entrypoint (`start-spyder.sh`) creates a user matching the host UID/GID at runtime and cleans up stale Spyder lock files before launching the IDE. +- Python dependencies are managed via `micromamba` using `environment.yml`. +- Shared memory is increased to 1 GB to satisfy QtWebEngine requirements. diff --git a/compose.runtime.yaml b/compose.runtime.yaml new file mode 100644 index 0000000..5b07ee6 --- /dev/null +++ b/compose.runtime.yaml @@ -0,0 +1,56 @@ +# Runtime compose file that pulls a prebuilt Spyder image. +services: + spyder-wayland: + # Default Wayland session using the published image. + image: ${SPYDER_IMAGE:-ghcr.io/your-account/spyder-wayland:latest} + environment: + UID: "${UID}" + GID: "${GID}" + HOST_USER: "${HOST_USER}" + HOST_GROUP: "${HOST_GROUP}" + SPYDER_HOME: "${SPYDER_HOME}" + SPYDER_WORKSPACE: "${SPYDER_WORKSPACE}" + HOME: "${SPYDER_HOME}" + WAYLAND_DISPLAY: "${WAYLAND_DISPLAY}" + XDG_RUNTIME_DIR: "${XDG_RUNTIME_DIR}" + QT_QPA_PLATFORM: "wayland" + QTWEBENGINE_DISABLE_SANDBOX: "1" + QTWEBENGINE_CHROMIUM_FLAGS: "--no-sandbox" + TZ: "Europe/Berlin" + volumes: + - ${SPYDER_HOME_VOLUME}:${SPYDER_HOME} + - ${SPYDER_WORKSPACE_VOLUME}:${SPYDER_WORKSPACE} + - ${XDG_RUNTIME_DIR}:${XDG_RUNTIME_DIR} + working_dir: ${SPYDER_WORKSPACE} + devices: + - "/dev/dri:/dev/dri" + shm_size: "1gb" + restart: "unless-stopped" + + spyder-x11: + profiles: + - "x11" + image: ${SPYDER_IMAGE:-ghcr.io/your-account/spyder-wayland:latest} + environment: + UID: "${UID}" + GID: "${GID}" + HOST_USER: "${HOST_USER}" + HOST_GROUP: "${HOST_GROUP}" + SPYDER_HOME: "${SPYDER_HOME}" + SPYDER_WORKSPACE: "${SPYDER_WORKSPACE}" + HOME: "${SPYDER_HOME}" + DISPLAY: "${DISPLAY}" + QT_QPA_PLATFORM: "xcb" + QT_X11_NO_MITSHM: "1" + QTWEBENGINE_DISABLE_SANDBOX: "1" + QTWEBENGINE_CHROMIUM_FLAGS: "--no-sandbox" + TZ: "Europe/Berlin" + volumes: + - ${SPYDER_HOME_VOLUME}:${SPYDER_HOME} + - ${SPYDER_WORKSPACE_VOLUME}:${SPYDER_WORKSPACE} + - /tmp/.X11-unix:/tmp/.X11-unix:ro + working_dir: ${SPYDER_WORKSPACE} + devices: + - "/dev/dri:/dev/dri" + shm_size: "1gb" + restart: "unless-stopped" diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..8a9b959 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,63 @@ +# Build-time compose file for the Spyder IDE stack with Wayland-native and X11 fallback frontends. +services: + spyder-wayland: + # Preferred Wayland session; shares GPU via /dev/dri. + build: + context: . + dockerfile: Dockerfile + image: spyder-conda + environment: + UID: "${UID}" + GID: "${GID}" + HOST_USER: "${HOST_USER}" + HOST_GROUP: "${HOST_GROUP}" + SPYDER_HOME: "${SPYDER_HOME}" + SPYDER_WORKSPACE: "${SPYDER_WORKSPACE}" + HOME: "${SPYDER_HOME}" + WAYLAND_DISPLAY: "${WAYLAND_DISPLAY}" + XDG_RUNTIME_DIR: "${XDG_RUNTIME_DIR}" + QT_QPA_PLATFORM: "wayland" + QTWEBENGINE_DISABLE_SANDBOX: "1" + QTWEBENGINE_CHROMIUM_FLAGS: "--no-sandbox" + TZ: "Europe/Berlin" + volumes: + - ${SPYDER_HOME_VOLUME}:${SPYDER_HOME} + - ${SPYDER_WORKSPACE_VOLUME}:${SPYDER_WORKSPACE} + - ${XDG_RUNTIME_DIR}:${XDG_RUNTIME_DIR} + working_dir: ${SPYDER_WORKSPACE} + devices: + - "/dev/dri:/dev/dri" + shm_size: "1gb" + restart: "no" + + spyder-x11: + # Optional X11 fallback; enable via `--profile x11`. + profiles: + - "x11" + build: + context: . + dockerfile: Dockerfile + image: spyder-conda + environment: + UID: "${UID}" + GID: "${GID}" + HOST_USER: "${HOST_USER}" + HOST_GROUP: "${HOST_GROUP}" + SPYDER_HOME: "${SPYDER_HOME}" + SPYDER_WORKSPACE: "${SPYDER_WORKSPACE}" + HOME: "${SPYDER_HOME}" + DISPLAY: "${DISPLAY}" + QT_QPA_PLATFORM: "xcb" + QT_X11_NO_MITSHM: "1" + QTWEBENGINE_DISABLE_SANDBOX: "1" + QTWEBENGINE_CHROMIUM_FLAGS: "--no-sandbox" + TZ: "Europe/Berlin" + volumes: + - ${SPYDER_HOME_VOLUME}:${SPYDER_HOME} + - ${SPYDER_WORKSPACE_VOLUME}:${SPYDER_WORKSPACE} + - /tmp/.X11-unix:/tmp/.X11-unix:ro + working_dir: ${SPYDER_WORKSPACE} + devices: + - "/dev/dri:/dev/dri" + shm_size: "1gb" + restart: "no" diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..02aa55b --- /dev/null +++ b/environment.yml @@ -0,0 +1,15 @@ +name: spyder-wayland +channels: + - conda-forge +dependencies: + - python=3.11 + - spyder>=6,<7 + - spyder-kernels>=2.5 + - pyqt # = PyQt5 + - qt-main # Qt 5.15.x + - qt-wayland # Wayland-Plugin für Qt5 + - ipykernel + - numpy + - pandas + - matplotlib + - pip diff --git a/start-spyder.sh b/start-spyder.sh new file mode 100644 index 0000000..7b84bbd --- /dev/null +++ b/start-spyder.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -euo pipefail + +DEFAULT_ARGS=("spyder" "--new-instance") + +TARGET_UID=${UID:-1000} +TARGET_GID=${GID:-1000} +TARGET_USER=${HOST_USER:-spyder} +TARGET_GROUP=${HOST_GROUP:-$TARGET_USER} +TARGET_HOME=${SPYDER_HOME:-/home/${TARGET_USER}} +TARGET_WORKSPACE=${SPYDER_WORKSPACE:-${TARGET_HOME}/workspace} + +if [ "$#" -eq 0 ]; then + set -- "${DEFAULT_ARGS[@]}" +fi + +existing_group=$(getent group "${TARGET_GID}" || true) +if [ -z "${existing_group}" ]; then + # Reconcile group entry so host files stay accessible. + if getent group "${TARGET_GROUP}" >/dev/null 2>&1; then + groupmod --gid "${TARGET_GID}" "${TARGET_GROUP}" + else + groupadd --gid "${TARGET_GID}" "${TARGET_GROUP}" + fi +else + TARGET_GROUP=$(printf '%s' "${existing_group}" | cut -d: -f1) +fi + +existing_user=$(getent passwd "${TARGET_UID}" || true) +if [ -z "${existing_user}" ]; then + # Mirror host UID/GID inside the container. + if id "${TARGET_USER}" >/dev/null 2>&1; then + usermod --uid "${TARGET_UID}" --gid "${TARGET_GID}" --home "${TARGET_HOME}" --shell /bin/bash "${TARGET_USER}" + else + useradd --uid "${TARGET_UID}" --gid "${TARGET_GID}" --home-dir "${TARGET_HOME}" --shell /bin/bash "${TARGET_USER}" + fi +else + TARGET_USER=$(printf '%s' "${existing_user}" | cut -d: -f1) + TARGET_HOME=$(printf '%s' "${existing_user}" | cut -d: -f6) +fi + +install -d -m 0755 "${TARGET_HOME}" "${TARGET_WORKSPACE}" +# Ensure mounts are owned by the runtime user. +chown "${TARGET_UID}:${TARGET_GID}" "${TARGET_HOME}" "${TARGET_WORKSPACE}" + +for lock in \ + "${TARGET_HOME}/.config/spyder/spyder.lock" \ + "${TARGET_HOME}/.local/share/spyder/spyder.lock" \ + "${TARGET_HOME}/.spyder-py3/spyder.lock" +do + # Remove stale Spyder locks that can survive bind mounts. + rm -f "$lock" +done + +export HOME="${TARGET_HOME}" +export USER="${TARGET_USER}" +export QTWEBENGINE_DISABLE_SANDBOX="${QTWEBENGINE_DISABLE_SANDBOX:-1}" + +cd "${TARGET_WORKSPACE}" + +exec gosu "${TARGET_UID}:${TARGET_GID}" micromamba run -n base "$@" \ No newline at end of file