#!/usr/bin/env bash print_help () { echo "Usage: $0 [OPTION[=VALUE]]... [COMMAND [ARGS]...]..." echo echo "Run the docker-wine container with behaviour determined by the following" echo "OPTIONS:" echo " --cache Use the cached image pulled from Docker Hub and don't" echo " attempt to pull the latest version" echo " --local Use locally built docker-wine image instead of pulling" echo " image from Docker Hub" echo " --local=VALUE Specify an alternate locally built image and use instead" echo " of pulling image from Docker Hub" echo " --rm Start the container in non-persistent mode. i.e. Without" echo " mounting the user's home to a volume or path on the" echo " host" echo " --tag=VALUE Specify an image tag to use (default is latest)" echo " --name=VALUE Name of the docker container instantiated" echo " (default is 'wine')" echo " --arm64 Start the containter using linux/arm64 platform for ARM" echo " and Apple Silicon (M1) native image" echo " --as-root Start the container as root" echo " --as-me Start the container using your current username, UID and" echo " GID (default when alternate --home value specified)" echo " --notty Start container attached with no tty" echo " --nordp Use a version of the docker-wine image that does not" echo " include RDP server packages for a reduced image size" echo " --rdp Shortcut for --rdp=interactive" echo " --rdp=OPTION Runs docker-wine container with Remote Desktop Protocol" echo " server" echo " Valid values for OPTION are:" echo " no Don't use RDP server (default)" echo " start Start the RDP server as a detached" echo " daemon" echo " stop Stop the detached RDP server by" echo " stopping the container" echo " restart Restart the detached RDP server by" echo " stopping and starting the container" echo " interactive Start the RDP server and also run an" echo " interactive bash session" echo " --rdp-port=VALUE Bind RDP to a different TCP port (default is 3389)" echo " --shm-size=VALUE Set the shared memory size (default is '1g' for 1 GB)" echo " --xvfb[=OPTIONAL] Start xvfb" echo " OPTIONAL consists of comma separated values of:" echo " SERVER_NR Server number to use, eg. :1" echo " SCREEN Screen number to use, eg. 0" echo " RESOLUTION Screen resolution, eg. 320x240x8" echo " If OPTIONAL is left blank, defaults to:" echo " :95,0,320x240x8" echo " --sound=OPTION Select a pulseaudio configuration for sound output when" echo " running in X11 forwarding mode" echo " Valid values for OPTION are:" echo " default Use the default pulseaudio config for" echo " host OS (unix is default for Linux," echo " dummy is default for macOS)" echo " unix Use UNIX socket '/tmp/pulse-socket' to" echo " connect to the host machine's" echo " pulseaudio server (Linux only)" echo " dummy Run pulseaudio server in container with" echo " dummy (null) output" echo " none Alias for dummy" echo " --home-volume=VALUE Use an alternate volume to winehome for storing" echo " persistent user data. Valid values can specify either" echo " a docker volume or local path" echo " e.g." echo " --home=my_new_volume" echo " --home=/tmp/my_user" echo " --home=VALUE Specify an alternate path for the user's home within the" echo " container (default is /home/\$USER)" echo " --force-owner Allow the user to take ownership of a home volume that" echo " belongs to another user (NOTE: Use with caution!)" echo " --nosudo Disable sudo for the user" echo " --password=VALUE Specify a password for the user in plain text (default" echo " is the user's username)" echo " --password-prompt Prompt to set a password for the user" echo " --secure-password=VALUE Provide an encrypted password for the user" echo " --device=VALUE Bind device(s) to container. Uses standard docker" echo " syntax and multiple statements are allowed" echo " --env=VALUE Specify additional environment variable(s) to be passed" echo " to the container. Uses standard docker syntax and" echo " multiple statements are allowed" echo " --mount=VALUE Specify additional directory bindings. Uses" echo " standard docker syntax and multiple statements are" echo " allowed" echo " --network=VALUE Specify the network to connect the container to. Uses" echo " standard docker syntax" echo " --volume=VALUE Specify additional volume(s) to be mounted. Uses" echo " standard docker syntax and multiple statements are" echo " allowed" echo " --workdir=VALUE Specify alternate WORKDIR (default is \$HOME)" echo " --help Display this help screen and exit" echo echo "e.g." echo " $0" echo " $0 wine notepad" echo " $0 wineboot --init" echo " $0 --local --volume=my_vol:/some/path:ro" echo " $0 --local --as-me wine notepad" echo " $0 --as-root --rdp" echo " $0 --rdp=start --password=pa55w0rd" } add_run_arg () { RUN_ARGS+=("$1") } add_run_args_for_as_me () { USER_HOME="${HOME}" WORKDIR="${USER_HOME}" add_run_arg --env="USER_NAME=$(whoami)" add_run_arg --env="USER_UID=$(id -u)" add_run_arg --env="USER_GID=$(id -g)" add_run_arg --env="USER_HOME=${USER_HOME}" } encrypt_password () { local password="$1" local encrypted_password if [ -z "${password}" ]; then echo "ERROR: Password cannot be left blank" exit 1 fi encrypted_password="$(openssl passwd -1 -salt "$(openssl rand -base64 6)" "${password}")" # Add encrypted password to run args add_run_arg --env="USER_PASSWD=${encrypted_password}" } add_run_arg_timezone () { local tz if [ -f "/etc/timezone" ]; then tz="$(cat /etc/timezone)" elif [ -f "/etc/localtime" ]; then tz="$(readlink /etc/localtime | awk -F/ '{print $(NF-1)"/"$NF}')" else tz="UTC" fi add_run_arg --env="TZ=${tz}" } configure_xquartz () { # Return 0 (true) if this function makes any changes local changes_made=1 # Check XQuartz installed if ! [ -f /opt/X11/bin/xquartz ]; then local answer local attempts local max_attempts=5 # Prompt to allow install echo "XQuartz needs to be installed for X11 forwarding to operate. If necessary, Homebrew will also be installed to perform the installation of XQuartz." for (( attempts = 0; attempts < max_attempts; attempts++ )); do read -r -p "Do you want to continue? [y/N] " answer # Default is No [ -z "${answer}" ] && answer="n" case "${answer}" in [Yy]|[Yy][Ee][Ss]) install_xquartz || exit 1 changes_made=0 break ;; [Nn]|[Nn][Oo]) echo "Unable to start container with X11 forwarding. Please install XQuartz or alternatively use Remote Desktop. e.g. $0 --rdp" exit 0 ;; *) echo "Invalid response. Please use y or n" ;; esac done # Fail after too many attempts if [ "${attempts}" -ge "${max_attempts}" ]; then echo "ERROR: Too many invalid responses" exit 1 fi fi # Configure XQuartz if [ -e ~/Library/Preferences/org.xquartz.X11.plist ] ; then xquartz_properties=org.xquartz.X11 else xquartz_properties=org.macosforge.xquartz.X11; fi if [ "$(defaults read $xquartz_properties app_to_run)" != "/usr/bin/true" ]; then defaults write $xquartz_properties app_to_run /usr/bin/true changes_made=0 fi if [ "$(defaults read $xquartz_properties nolisten_tcp)" != "0" ]; then defaults write $xquartz_properties nolisten_tcp 0 changes_made=0 fi if [ "$(defaults read $xquartz_properties enable_iglx)" != "1" ]; then # Enable GLX (OpenGL) defaults write $xquartz_properties enable_iglx -bool true changes_made=0 fi return $changes_made } install_xquartz() { # Return 0 if XQuartz is successfully installed local installed=1 # Install Homebrew if ! command -v brew >/dev/null 2>&1; then /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" # Confirm installed if ! command -v brew >/dev/null 2>&1; then echo "ERROR: Failed to install Homebrew, unable to proceed with XQuartz installation" exit 1 fi fi # Install XQuartz if ! [ -f /opt/X11/bin/xquartz ]; then brew install --cask xquartz # Confirm installed [ -f /opt/X11/bin/xquartz ] && installed=0 fi return $installed } add_x11_key () { local display="$1" # Check for .Xauthority which is required for authenticating as the current user on the host's X11 server if [ -z "${XAUTHORITY:-${HOME}/.Xauthority}" ]; then echo "ERROR: No valid .Xauthority file found for X11" exit 1 fi # Get the hex key for the display from host user's .Xauthority file and store in ~/.docker-wine.Xkey xauth list "${display}" | head -n1 | awk '{print $3}' > ~/.docker-wine.Xkey # Lock down permissions chmod 600 ~/.docker-wine.Xkey # Add .Xkey to the run args add_run_arg --volume="${HOME}/.docker-wine.Xkey:/root/.Xkey:ro" } configure_sound () { local os="$1" case "${os}" in linux) if [ "${SOUND}" == "default" ]; then SOUND="unix" fi ;; macos) if [ "${SOUND}" == "default" ]; then SOUND="dummy" fi ;; *) echo "ERROR: '${os}' is not a valid OS string for configuring sound" exit 1 ;; esac case "${SOUND}" in unix) configure_pulseaudio_unix_socket ;; dummy|none) add_run_arg --env="DUMMY_PULSEAUDIO=yes" ;; *) echo "ERROR: '${SOUND}' is not a valid option for configuring sound" exit 1 ;; esac } configure_pulseaudio_unix_socket () { # Use audio if pulseaudio is installed if command -v pulseaudio >/dev/null 2>&1; then # One-off setup for creation of UNIX socket for pulseaudio to allow access for other users if [ ! -f "${HOME}/.config/pulse/default.pa" ]; then echo "INFO: Creating pulseaudio config file ${HOME}/.config/pulse/default.pa" mkdir -p "${HOME}/.config/pulse" echo -e ".include /etc/pulse/default.pa\nload-module module-native-protocol-unix auth-anonymous=1 socket=/tmp/pulse-socket" > "${HOME}/.config/pulse/default.pa" fi # Restart pulseaudio daemon to create the UNIX socket if [ ! -e "/tmp/pulse-socket" ]; then echo "INFO: No socket found for pulseaudio so restarting service..." pulseaudio -k pulseaudio --start sleep 1 fi # Add the pulseaudio UNIX socket to run args if [ -e "/tmp/pulse-socket" ]; then add_run_arg --volume="/tmp/pulse-socket:/tmp/pulse-socket" else echo "INFO: pulseaudio socket /tmp/pulse-socket doesn't exist, so sound will not function" fi else echo "INFO: pulseaudio not installed so running without sound" fi } run_container () { local mode case "$1" in interactive) mode="-it" ;; detached) mode="--detach" ;; *) echo "ERROR: '$1' is not a valid container run mode" exit 1 ;; esac # Add common docker run args add_run_arg --rm add_run_arg --hostname="$(hostname)" add_run_arg --name="${CONTAINER_NAME}" add_run_arg --shm-size="${SHM_SIZE}" add_run_arg --workdir="${WORKDIR}" add_run_arg_timezone # Append -nordp to image tag if NO_RDP is set to 'yes' if [ "${NO_RDP}" == "yes" ]; then # Only append if image tag specified does not already end in -nordp if ! echo "${IMAGE_TAG}" | grep -q -E "\-nordp$"; then IMAGE_TAG="${IMAGE_TAG}-nordp" fi fi # Grab the latest image from docker hub or use the locally built version if [ "${USE_LOCAL_IMAGE}" == "no" ]; then [ "${DOCKER_PULL_IMAGE}" == "yes" ] && docker pull "${DOCKER_IMAGE}:${IMAGE_TAG}" else DOCKER_IMAGE="${LOCAL_IMAGE}" fi # Add volume for user home only if not using --rm option if [ "${NO_USER_VOLUME}" == "no" ]; then add_run_arg --volume="${USER_VOLUME}:${USER_HOME}" # Create the docker volume to store user's home only if using default winehome if [ "${USER_VOLUME}" == "winehome" ] && ! docker volume ls -qf "name=winehome" | grep -q "^winehome$"; then echo "INFO: Creating Docker volume container 'winehome'..." docker volume create winehome fi fi # NOTTY rules them all if [ "${NOTTY}" == "yes" ] ; then docker run "${RUN_ARGS[@]}" "${DOCKER_IMAGE}:${IMAGE_TAG}" "${CMD_ARGS[@]}" else docker run "${mode}" "${RUN_ARGS[@]}" "${DOCKER_IMAGE}:${IMAGE_TAG}" "${CMD_ARGS[@]}" fi } # Set default values CONTAINER_NAME="wine" DOCKER_IMAGE="scottyhardy/docker-wine" DOCKER_PULL_IMAGE="yes" HOST_RDP_PORT="3389" IMAGE_TAG="latest" LOCAL_IMAGE="docker-wine" NO_RDP="no" NO_USER_VOLUME="no" NOTTY="no" SHM_SIZE="1g" SOUND="default" USE_LOCAL_IMAGE="no" USE_RDP_SERVER="no" USER_HOME="/home/wineuser" USER_VOLUME="winehome" WORKDIR="${USER_HOME}" USE_XVFB="no" XVFB_RESOLUTION="320x240x8" XVFB_SCREEN="0" XVFB_SERVER=":95" # Array to store all of the `docker run` arguments RUN_ARGS=() while [ $# -gt 0 ]; do case "$1" in --cache) DOCKER_PULL_IMAGE="no" ;; --local) USE_LOCAL_IMAGE="yes" ;; --local=*) USE_LOCAL_IMAGE="yes" LOCAL_IMAGE="${1#*=}" ;; --rm) NO_USER_VOLUME="yes" ;; --tag=*) IMAGE_TAG="${1#*=}" ;; --name=*) CONTAINER_NAME="${1#*=}" ;; --arm64) add_run_arg --platform="linux/arm64" IMAGE_TAG="arm64" ;; --as-root) add_run_arg --env="RUN_AS_ROOT=yes" WORKDIR="/" ;; --as-me) add_run_args_for_as_me ;; --notty) NOTTY="yes" ;; --nordp) NO_RDP="yes" ;; --rdp) USE_RDP_SERVER="interactive" ;; --rdp=*) USE_RDP_SERVER="${1#*=}" ;; --rdp-port=*) HOST_RDP_PORT="${1#*=}" ;; --shm-size=*) SHM_SIZE="${1#*=}" ;; --xvfb) USE_XVFB="yes" ;; --xvfb=*) USE_XVFB="yes" IFS=, read -r XVFB_SERVER XVFB_SCREEN XVFB_RESOLUTION <<< "${1#*=}" ;; --sound=*) SOUND="${1#*=}" ;; --home-volume=*) USER_VOLUME="${1#*=}" # Start container as self to prevent unintentionally changing ownership of a user's local filesystem by wineuser add_run_args_for_as_me ;; --home=*) USER_HOME="${1#*=}" add_run_arg --env="USER_HOME=${USER_HOME}" ;; --force-owner) add_run_arg --env="FORCED_OWNERSHIP=yes" ;; --nosudo) add_run_arg --env="USER_SUDO=no" ;; --password=*) encrypt_password "${1#*=}" ;; --password-prompt) read -r -s -p "Password: " PASSWD echo encrypt_password "${PASSWD}" ;; --secure-password=*) add_run_arg --env="USER_PASSWD=${1#*=}" ;; --device=*|--env=*|--mount=*|--network=*|--volume=*) add_run_arg "$1" ;; --workdir=*) WORKDIR="${1#*=}" ;; --help) print_help exit 0 ;; -*) echo "ERROR: '$1' is not a valid option" echo print_help exit 1 ;; *) break ;; esac shift done # Collect remaining command line args to pass to the container to run CMD_ARGS=("$@") # Sanity checks if ! docker system info >/dev/null 2>&1; then echo "ERROR: Docker is not running or not installed, unable to proceed" exit 1 fi if ! echo "${USE_RDP_SERVER}" | grep -q -E "^(no|start|stop|restart|interactive)$"; then echo "ERROR: '${USE_RDP_SERVER}' is not a valid value for --rdp option" exit 1 fi if [ "${USE_RDP_SERVER}" != "no" ] && [ -n "${CMD_ARGS[0]}" ]; then echo "ERROR: Commands cannot be passed to container when using --rdp option" exit 1 fi if [ "${USE_RDP_SERVER}" != "no" ] && [ "${NO_RDP}" != "no" ]; then echo "ERROR: Cannot combine conflicting options --rdp and --nordp" exit 1 fi # Run xvfb and send everything into the void if [ "${USE_XVFB}" == "yes" ] ; then add_run_arg --env="USE_XVFB=yes" add_run_arg --env="XVFB_SERVER=${XVFB_SERVER}" add_run_arg --env="XVFB_SCREEN=${XVFB_SCREEN}" add_run_arg --env="XVFB_RESOLUTION=${XVFB_RESOLUTION}" add_run_arg --env="DISPLAY=${XVFB_SERVER}" run_container "interactive" # Run in RDP mode elif [ "${USE_RDP_SERVER}" != "no" ]; then add_run_arg --env="RDP_SERVER=yes" add_run_arg --publish="${HOST_RDP_PORT}:3389/tcp" case "${USE_RDP_SERVER}" in interactive) CMD_ARGS=("/bin/bash") run_container "interactive" ;; start) run_container "detached" ;; stop) docker kill "${CONTAINER_NAME}" ;; restart) docker kill "${CONTAINER_NAME}" run_container "detached" ;; *) echo "ERROR: '${USE_RDP_SERVER}' is not a valid value for --rdp option" exit 1 ;; esac # Run in X11 forwarding mode else # Set CMD_ARGS to /bin/bash if no commands specified [ -z "${CMD_ARGS[0]}" ] && CMD_ARGS=("/bin/bash") # Run in X11 forwarding mode on macOS if [ "$(uname)" == "Darwin" ]; then # Advise to reboot if need to configure XQuartz if configure_xquartz; then echo "INFO: XQuartz configuration updated. Please reboot to enable X11 forwarding to operate." exit 0 fi # Ensure XQuartz is running so ~/.Xauthority file is updated with new X11 key if ! ps -e | awk '{print $4}' | grep -q "/opt/X11/bin/Xquartz"; then open -a xquartz fi # Store the X11 key for the display in ~/.docker-wine.Xkey and add to run args add_x11_key ":0" # Configure sound output configure_sound "macos" # Add macOS run args add_run_arg --env="DISPLAY=host.docker.internal:0" run_container "interactive" # Run in X11 forwarding mode on Linux elif [ "$(uname)" == "Linux" ]; then # Store the X11 key for the display in ~/.docker-wine.Xkey and add to run args add_x11_key "$DISPLAY" # Configure sound output configure_sound "linux" # Add Linux run args add_run_arg --env="DISPLAY" add_run_arg --volume="/tmp/.X11-unix:/tmp/.X11-unix:ro" run_container "interactive" else echo "ERROR: '$(uname)' OS is not supported" exit 1 fi fi