From ef14c843f8f60f213f803a739e5a2722600296bf Mon Sep 17 00:00:00 2001 From: Lexian-droid Date: Wed, 29 Jul 2026 22:26:13 +0200 Subject: [PATCH] Initial Wine Desktop image and Pterodactyl Egg --- .github/workflows/docker.yml | 52 ++++++++++++++++++++ .gitignore | 6 +++ Dockerfile | 59 +++++++++++++++++++++++ LICENSE | 21 ++++++++ README.md | 82 +++++++++++++++++++++++++++++++ docker-entrypoint.sh | 93 ++++++++++++++++++++++++++++++++++++ egg-wine-desktop.json | 78 ++++++++++++++++++++++++++++++ 7 files changed, 391 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker-entrypoint.sh create mode 100644 egg-wine-desktop.json diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..f00fba7 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,52 @@ +name: Build and publish Docker image + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,format=long + type=ref,event=tag + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d476a34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +.idea/ +.vscode/ +*.swp +*.log +docker-compose.override.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ec03476 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +FROM ghcr.io/ptero-eggs/yolks:wine_latest + +LABEL org.opencontainers.image.title="Wine Desktop" \ + org.opencontainers.image.description="Wine with an XFCE desktop, VNC, and noVNC for Pterodactyl" \ + org.opencontainers.image.source="https://git.lexian.dev/Lexian-droid/OpenClaw-pterodactyl-wine-gui-egg" + +USER root + +ENV DEBIAN_FRONTEND=noninteractive \ + DISPLAY=:0 \ + DISPLAY_WIDTH=1280 \ + DISPLAY_HEIGHT=720 \ + DISPLAY_DEPTH=24 \ + VNC_PORT=5900 \ + NOVNC_PORT=6080 \ + VNC_PASSWORD= \ + XVFB=0 \ + WINEPREFIX=/home/container/.wine + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + xfce4 \ + xfce4-terminal \ + xvfb \ + x11vnc \ + novnc \ + websockify \ + dbus-x11 \ + xauth \ + x11-utils \ + x11-xserver-utils \ + libgl1 \ + libgl1-mesa-dri \ + libegl1 \ + mesa-utils \ + pulseaudio \ + pulseaudio-utils \ + fonts-dejavu \ + fonts-liberation \ + fonts-noto-core \ + fonts-noto-cjk \ + winbind \ + cabextract \ + unzip \ + curl \ + wget \ + && rm -rf /var/lib/apt/lists/* + +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh \ + && mkdir -p /home/container \ + && chown -R container:container /home/container 2>/dev/null || true + +EXPOSE 5900/tcp 6080/tcp + +# Keep the official tini process and inherited CMD. The wrapper prepares the +# desktop and then execs the official /entrypoint.sh command unchanged. +ENTRYPOINT ["/usr/bin/tini", "-g", "--", "/usr/local/bin/docker-entrypoint.sh"] +CMD ["/bin/bash", "/entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f5c44a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Lexian-droid + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a12873 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Wine Desktop + +Wine Desktop extends the official Pterodactyl Wine yolk with a persistent virtual XFCE desktop. It is intended for Windows GUI applications that need a display, VNC access, or browser-based noVNC access inside a Pterodactyl or Pelican server. + +## Features + +- Based on `ghcr.io/ptero-eggs/yolks:wine_latest`. +- XFCE on Xvfb with configurable resolution. +- Direct VNC on TCP `5900` and browser VNC on TCP `6080`. +- Persistent Wine prefix at `/home/container/.wine`. +- PulseAudio, Mesa, fonts, winetricks, winbind, and common archive tools. +- Retains the official yolk's Steam update, Gecko/Mono, winetricks, and startup handling. + +## Architecture + +The image keeps the yolk's `/usr/bin/tini -g --` process and inherited command. Its entrypoint starts Xvfb, D-Bus, XFCE, x11vnc, noVNC, and the Wine prefix once, then executes the inherited `/bin/bash /entrypoint.sh`. The official entrypoint continues to expand `STARTUP`, perform optional updates and winetricks actions, and launch the application. + +The Wine prefix is not deleted or rebuilt when the container restarts. Store the server data in the normal Pterodactyl volume to keep it persistent. + +## Installation + +Build locally: + +```bash +git clone https://git.lexian.dev/Lexian-droid/OpenClaw-pterodactyl-wine-gui-egg.git WineDesktop +cd WineDesktop +docker build -t wine-desktop:latest . +``` + +The image exposes `5900/tcp` and `6080/tcp`. Publish those ports if running Docker directly: + +```bash +docker run --rm -it -p 5900:5900 -p 6080:6080 \ + -e STARTUP='wine /home/container/application.exe' \ + wine-desktop:latest +``` + +For Pterodactyl or Pelican, import `egg-wine-desktop.json` into the nest, select the published image (or add your own registry tag), and create a server with the Wine Desktop Egg. Add allocations for ports 5900 and 6080 when your panel supports additional allocations. + +## Startup and variables + +The Egg's default startup is `wine {{SERVER_EXECUTABLE}}`, with `SERVER_EXECUTABLE` defaulting to `application.exe`. Set it to a path such as `MyApp/MyApp.exe` or use a custom startup command when needed. The underlying yolk continues to receive and evaluate its normal `STARTUP` value. + +Useful variables: + +| Variable | Default | Purpose | +| --- | --- | --- | +| `SERVER_EXECUTABLE` | `application.exe` | Executable used by the Egg startup command | +| `DISPLAY_WIDTH` | `1280` | Virtual screen width | +| `DISPLAY_HEIGHT` | `720` | Virtual screen height | +| `WINEPREFIX` | `/home/container/.wine` | Persistent Wine prefix | +| `VNC_PASSWORD` | empty | Optional direct VNC password, up to 8 characters | +| `VNC_PORT` | `5900` | x11vnc port | +| `NOVNC_PORT` | `6080` | noVNC/websockify port | + +## Connecting + +Direct VNC clients connect to the server allocation on port `5900`. If `VNC_PASSWORD` is blank, VNC authentication is disabled; protect the allocation with your firewall or proxy. + +Browser clients open `http://SERVER:6080/vnc.html` and connect to the displayed VNC endpoint. Use HTTPS or a reverse proxy when exposing noVNC beyond a trusted network. + +## Troubleshooting + +- **Black screen:** confirm ports are allocated and inspect `/tmp/wine-desktop-xfce.log` and `/tmp/wine-desktop-x11vnc.log` in the container. +- **Application does not start:** run the exact command interactively and check that the executable exists under `/home/container`; the official yolk's `STARTUP` is evaluated by Bash. +- **Prefix problems:** stop the server before changing Wine architecture or winetricks settings. Back up `.wine` before manually repairing it. +- **noVNC loads but cannot connect:** ensure both 6080 and 5900 are reachable from the container network; websockify forwards 6080 to localhost:5900. +- **No audio:** applications must support PulseAudio. The image includes the client and server packages, but application-specific audio configuration may still be required. + +## FAQ + +**Does this replace the official Wine yolk?** No. It uses it as the base and invokes its original entrypoint after the desktop is ready. + +**Does it support Steam server updates and winetricks?** Yes. The inherited entrypoint remains responsible for those features. + +**Is the desktop secure by default?** VNC is bound for container access and is intended to be protected by Pterodactyl allocations, firewall rules, or a reverse proxy. Set `VNC_PASSWORD` for direct VNC authentication and do not expose unprotected ports publicly. + +**Can I use this with Pelican?** Yes. The Egg uses the modern `PTDL_v2` format and standard Docker image/startup fields. + +## License + +This project is licensed under the MIT License. The base Wine yolk and its components retain their own licenses. diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..8212c58 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +readonly LOG_PREFIX="[wine-desktop]" +DISPLAY="${DISPLAY:-:0}" +export DISPLAY +DISPLAY_WIDTH="${DISPLAY_WIDTH:-1280}" +DISPLAY_HEIGHT="${DISPLAY_HEIGHT:-720}" +DISPLAY_DEPTH="${DISPLAY_DEPTH:-24}" +VNC_PORT="${VNC_PORT:-5900}" +NOVNC_PORT="${NOVNC_PORT:-6080}" +WINEPREFIX="${WINEPREFIX:-/home/container/.wine}" +export DISPLAY DISPLAY_WIDTH DISPLAY_HEIGHT DISPLAY_DEPTH VNC_PORT NOVNC_PORT WINEPREFIX + +children=() +cleanup() { + trap - EXIT INT TERM + for pid in "${children[@]:-}"; do + kill "$pid" 2>/dev/null || true + done +} +trap cleanup EXIT INT TERM + +log() { echo "${LOG_PREFIX} $*"; } + +if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then + log "Starting Xvfb on ${DISPLAY} (${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}x${DISPLAY_DEPTH})" + Xvfb "$DISPLAY" -screen 0 "${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}x${DISPLAY_DEPTH}" -ac +extension GLX +render -noreset & + children+=("$!") + for _ in {1..50}; do + xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break + sleep 0.1 + done +fi + +if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then + log "X server did not become ready" + exit 1 +fi + +export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp/runtime-${USER:-container}}" +mkdir -p "$XDG_RUNTIME_DIR" +chmod 0700 "$XDG_RUNTIME_DIR" + +if command -v dbus-launch >/dev/null 2>&1; then + eval "$(dbus-launch --sh-syntax)" + children+=("${DBUS_SESSION_BUS_PID:-}") +fi + +if command -v pulseaudio >/dev/null 2>&1 && ! pgrep -x pulseaudio >/dev/null 2>&1; then + log "Starting PulseAudio" + pulseaudio --system --daemonize=yes --disallow-exit --exit-idle-time=-1 >/tmp/wine-desktop-pulseaudio.log 2>&1 || \ + log "PulseAudio could not start; applications may run without audio" +fi + +if ! pgrep -x xfce4-session >/dev/null 2>&1; then + log "Starting XFCE desktop" + startxfce4 >/tmp/wine-desktop-xfce.log 2>&1 & + children+=("$!") +fi + +vnc_args=(-display "$DISPLAY" -rfbport "$VNC_PORT" -forever -shared -nopw) +if [[ -n "${VNC_PASSWORD:-}" ]]; then + password_file="${WINEPREFIX}/.vncpasswd" + mkdir -p "$WINEPREFIX" + x11vnc -storepasswd "$VNC_PASSWORD" "$password_file" >/dev/null + chmod 0600 "$password_file" + vnc_args=(-display "$DISPLAY" -rfbport "$VNC_PORT" -forever -shared -rfbauth "$password_file") +fi +log "Starting x11vnc on TCP ${VNC_PORT}" +x11vnc "${vnc_args[@]}" >/tmp/wine-desktop-x11vnc.log 2>&1 & +children+=("$!") + +novnc_web="/usr/share/novnc" +[[ -d "$novnc_web" ]] || novnc_web="/usr/share/novnc/utils" +log "Starting noVNC on TCP ${NOVNC_PORT}" +websockify --web="$novnc_web" "${NOVNC_PORT}" "localhost:${VNC_PORT}" >/tmp/wine-desktop-novnc.log 2>&1 & +children+=("$!") + +mkdir -p "$WINEPREFIX" +if [[ ! -f "$WINEPREFIX/system.reg" ]]; then + log "Initializing Wine prefix at ${WINEPREFIX}" + wineboot --init >/tmp/wine-desktop-wineboot.log 2>&1 || { + log "Wine prefix initialization failed; see /tmp/wine-desktop-wineboot.log" + exit 1 + } +fi + +# The inherited command remains the official yolk command, normally: +# /bin/bash /entrypoint.sh. This preserves its update, winetricks, startup, +# and signal behavior while tini remains PID 1. +log "Starting inherited Wine command: $*" +exec "$@" diff --git a/egg-wine-desktop.json b/egg-wine-desktop.json new file mode 100644 index 0000000..b806f99 --- /dev/null +++ b/egg-wine-desktop.json @@ -0,0 +1,78 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-07-30T00:00:00+00:00", + "name": "Wine Desktop", + "author": "support@lexian.dev", + "description": "Runs Windows GUI applications inside Wine using a virtual desktop.", + "features": [], + "docker_images": { + "ghcr.io/lexiandev/wine-desktop:latest": "ghcr.io/lexiandev/wine-desktop:latest", + "ghcr.io/ptero-eggs/yolks:wine_latest": "ghcr.io/ptero-eggs/yolks:wine_latest", + "wine-desktop:latest": "wine-desktop:latest" + }, + "file_denylist": [], + "startup": "wine {{SERVER_EXECUTABLE}}", + "config": { + "files": "{}", + "startup": "{\"done\":\"Wine Desktop is ready.\"}", + "logs": "{}", + "stop": "^C" + }, + "scripts": {}, + "variables": [ + { + "name": "Server executable", + "description": "Windows executable path relative to /home/container, for example application.exe or MyApp/MyApp.exe.", + "env_variable": "SERVER_EXECUTABLE", + "default_value": "application.exe", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + }, + { + "name": "Display width", + "description": "Virtual desktop width in pixels.", + "env_variable": "DISPLAY_WIDTH", + "default_value": "1280", + "user_viewable": true, + "user_editable": true, + "rules": "required|integer|min:640|max:7680", + "field_type": "text" + }, + { + "name": "Display height", + "description": "Virtual desktop height in pixels.", + "env_variable": "DISPLAY_HEIGHT", + "default_value": "720", + "user_viewable": true, + "user_editable": true, + "rules": "required|integer|min:480|max:4320", + "field_type": "text" + }, + { + "name": "VNC password", + "description": "Optional password for direct VNC access. Leave blank to disable VNC authentication.", + "env_variable": "VNC_PASSWORD", + "default_value": "", + "user_viewable": true, + "user_editable": true, + "rules": "nullable|string|max:8", + "field_type": "text" + }, + { + "name": "Wine prefix", + "description": "Persistent Wine prefix directory. Keep the default unless you have a specific reason to change it.", + "env_variable": "WINEPREFIX", + "default_value": "/home/container/.wine", + "user_viewable": true, + "user_editable": true, + "rules": "required|string|max:255", + "field_type": "text" + } + ] +}