From 8542aaa19785758a2bc1c3c738c7cdeccdafddf5 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter Date: Tue, 21 Apr 2026 14:51:41 +0200 Subject: [PATCH 1/6] update deeplabcut_docker: replace shell script with pure python --- docker/package/deeplabcut_docker.py | 201 +++++++++++++++++++++------- docker/package/deeplabcut_docker.sh | 153 --------------------- 2 files changed, 151 insertions(+), 203 deletions(-) delete mode 100755 docker/package/deeplabcut_docker.sh diff --git a/docker/package/deeplabcut_docker.py b/docker/package/deeplabcut_docker.py index 296f4789d9..d705d41651 100644 --- a/docker/package/deeplabcut_docker.py +++ b/docker/package/deeplabcut_docker.py @@ -1,75 +1,176 @@ #!/usr/bin/env python3 -"""DeepLabCut2.0-2.2 Toolbox (deeplabcut.org) © A. - -& M. Mathis Labs https://github.com/DeepLabCut/DeepLabCut Please see AUTHORS for -contributors. -https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" +"""Helper CLI to run DeepLabCut Docker images (LGPL-3.0).""" import argparse -import pty +import grp +import os +import platform +import pwd +import shlex +import subprocess import sys +from datetime import datetime, timezone + +__version__ = "0.0.12-alpha" + +_IMAGE = "deeplabcut/deeplabcut" +_DEFAULT_CUDA = "12.4" + + +def _docker() -> list[str]: + """Return the docker CLI argv prefix (from DOCKER env or `docker`).""" + return shlex.split(os.environ.get("DOCKER", "docker")) + + +def _log(msg: str) -> None: + """Log a timestamped message to stderr.""" + ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S%z") + print(f"[{ts}]: {msg}", file=sys.stderr) + + +def _check_system() -> None: + """Verify docker group membership on Linux; warn on macOS.""" + if platform.system() == "Linux": + if os.environ.get("DOCKER", "docker").strip() == "sudo docker": + return + if os.geteuid() == 0: + return + try: + docker_gid = grp.getgrnam("docker").gr_gid + except KeyError: + return + if docker_gid not in os.getgroups(): + _log(f'The current user {os.getuid()} is not in the "docker" group.') + _log('Use DOCKER="sudo docker" (with care) or add your user to "docker".') + sys.exit(1) + elif platform.system() == "Darwin": + _log("macOS support is experimental; report issues at") + _log("https://github.com/DeepLabCut/DeepLabCut/issues") + -__version__ = "0.0.11-alpha" +def _remote_tag(mode: str) -> str: + """Get the DockerHub image tag from DLC_VERSION and CUDA_VERSION env vars.""" + cuda = os.environ.get("CUDA_VERSION", _DEFAULT_CUDA) + ver = os.environ.get("DLC_VERSION", "").strip() + if mode == "notebook": + if ver: + return f"{_IMAGE}:{ver}-jupyter-cuda{cuda}" + return f"{_IMAGE}:latest-jupyter" + if ver: + return f"{_IMAGE}:{ver}-core-cuda{cuda}" + return f"{_IMAGE}:latest" -_MOTD = r""" - .--, .--, - ( ( \.---./ ) ) - '.__/o o\__.' - `{= ^ =}´ - > u < - ____________________.""`-------`"".______________________ -\ ___ __ __ _____ __ / -/ / _ \ ___ ___ ___ / / ___ _ / / / ___/__ __ / /_ \ -\ / // // -_)/ -_)/ _ \ / /__/ _ `// _ \/ /__ / // // __/ / -//____/ \__/ \__// .__//____/\_,_//_.__/\___/ \_,_/ \__/ \ -\_________________________________________________________/ - ___)( )(___ `-.___. - (((__) (__))) ~` -Welcome to DeepLabCut docker! -""" +def _assert_jupyter_image(ref: str) -> None: + """Verify remote img supports Jupyter: exit otherwise.""" + r = subprocess.run( + _docker() + + [ + "image", + "inspect", + ref, + "--format", + "{{json .Config.Entrypoint}} {{json .Config.Cmd}}", + ], + capture_output=True, + text=True, + ) + if r.returncode != 0: + sys.exit(f"Could not inspect image {ref!r} after pull.\n{r.stderr.strip()}") + blob = (r.stdout or "").lower() + if "jupyter" not in blob or "notebook" not in blob: + sys.exit( + f"Image {ref!r} does not look like a Jupyter Notebook image " + "(entrypoint/cmd should reference jupyter and notebook). " + "Use an official *jupyter* tag or `bash` with --image." + ) + + +def _build_user_image(remote: str, local: str) -> None: + """Build a small local image on top of remote with the current UID/GID user.""" + try: + uid, gid = os.getuid(), os.getgid() + except AttributeError: + sys.exit("deeplabcut-docker requires a POSIX system (Linux or macOS).") + user = pwd.getpwuid(uid).pw_name + group = grp.getgrgid(gid).gr_name + _log(f"Configuring a local image for user {user} ({uid}) in group {group} ({gid})") + dockerfile = ( + "\n".join( + ( + f"FROM {remote}", + "RUN mkdir -p /home /app", + f"RUN groupadd -g {gid} {group} || groupmod -o -g {gid} {group}", + f"RUN useradd -d /home -s /bin/bash -u {uid} -g {gid} {user}", + f"RUN chown -R {user}:{group} /home /app", + f"USER {user}", + ) + ) + + "\n" + ) + subprocess.run( + _docker() + ["build", "-q", "-t", local, "-"], + input=dockerfile.encode(), + check=True, + ) + _log("Build succeeded") -def _parse_args(): +def _parse_args() -> tuple[argparse.Namespace, list[str]]: + """Parse CLI args and return (namespace, extra args for docker run).""" parser = argparse.ArgumentParser( - "deeplabcut-docker", + prog="deeplabcut-docker", description=( - "Utility tool for launching DeepLabCut docker containers. " - "Only a single argument is given to specify the container type. " - "By default, the current directory is mounted into the container " - "and used as the current working directory. You can additionally " - "specify any additional docker argument specified in " - "https://docs.docker.com/engine/reference/commandline/cli/." + "Launch DeepLabCut Docker containers. The current directory is mounted " + "at /app and used as the working directory. Additional arguments are " + "passed through to `docker run` (see " + "https://docs.docker.com/engine/reference/commandline/cli/)." ), ) parser.add_argument( "container", - type=str, - choices=["notebook", "bash"], + choices=("notebook", "bash"), help=( - "The container to launch. A list of all containers is available on " - "https://hub.docker.com/r/deeplabcut/deeplabcut/tags. By default, the " - "latest DLC version will be selected and automatically updated, if " - "possible. All containers are currently launched in interactive mode " - "by default, meaning you can use Ctrl+C in your terminal session to " - "terminate a command." + "notebook: Jupyter server; bash: interactive shell. " + "Image tags: https://hub.docker.com/r/deeplabcut/deeplabcut/tags — " + "use DLC_VERSION and CUDA_VERSION to select a versioned tag; unset " + "DLC_VERSION uses latest / latest-jupyter." + ), + ) + parser.add_argument( + "--image", + metavar="REF", + help=( + "Use this image (name:tag or digest) instead of the default from " + "DLC_VERSION / CUDA_VERSION. For notebook, the image is checked for " + "a Jupyter Notebook entrypoint after pull." ), ) return parser.parse_known_args() -def main(): - """Main entry point. +def main() -> None: + """Entry point: pull, user-layer build, and run the container.""" + _check_system() + args, docker_run_args = _parse_args() + mode = args.container + + remote = args.image or _remote_tag(mode) + local = f"deeplabcut-local-{mode}" + subprocess.run(_docker() + ["pull", remote], check=True) + if mode == "notebook" and args.image: + _assert_jupyter_image(remote) + _build_user_image(remote, local) - Parse arguments and launch container. - """ - launch_args, docker_arguments = _parse_args() - argv = ["deeplabcut_docker.sh", launch_args.container, *docker_arguments] - print(_MOTD, file=sys.stderr) - pty.spawn(argv) - print("Container stopped.", file=sys.stderr) + run = _docker() + ["run", "-it", "--rm", "-v", f"{os.getcwd()}:/app", "-w", "/app"] + if mode == "notebook": + port = os.environ.get("DLC_NOTEBOOK_PORT", "8888") + _log("Starting the notebook server.") + _log(f"Open your browser at http://127.0.0.1:{port}") + _log("If prompted for a password, enter 'deeplabcut'.") + run += ["-p", f"127.0.0.1:{port}:8888"] + run += docker_run_args + [local] + ([] if mode == "notebook" else ["bash"]) + sys.exit(subprocess.run(run).returncode) if __name__ == "__main__": diff --git a/docker/package/deeplabcut_docker.sh b/docker/package/deeplabcut_docker.sh deleted file mode 100755 index 05c929c450..0000000000 --- a/docker/package/deeplabcut_docker.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/bin/bash -# -# Helper script for launching deeplabcut docker UI containers -# Usage: -# $ ./deeplabcut-docker.sh [notebook|bash] - -DOCKER=${DOCKER:-docker} -CUDA_VERSION=${CUDA_VERSION:-"12.4"} -CUDNN_VERSION=${CUDNN_VERSION:-"9"} -DLC_VERSION=${DLC_VERSION:-"3.0.0"} -DLC_NOTEBOOK_PORT=${DLC_NOTEBOOK_PORT:-8888} - -# Check if the current users has privileges to start -# a docker container. -check_system() { - if [[ $(uname -s) == Linux ]]; then - if [ $(groups | grep -c docker) -eq 0 ]; then - if [[ "$DOCKER" == "sudo docker" ]]; then - return 0 - fi - err "The current user $(id -u) is not " - err "part of the \"docker\" group. " - err "Please either: " - err " 1) Launch this script with the DOCKER environment " - err " variable set to DOCKER=\"sudo docker\" (use this " - err " with care)! " - err " 2) Add your user to the docker group. You might need " - err " to log in and out again to see the effect of the " - err " change. " - exit 1 - fi - elif [[ $(uname -s) == Darwin ]]; then - err "Please note that macOSX support is currently experimental" - err "If you encounter errors, please open an issue on" - err "https://github.com/DeepLabCut/DeepLabCut/issues" - err "Thanks for testing the package!" - fi -} - -get_mount_args() { - args=( - "-v $(pwd):/app -w /app" - ) - echo "${args[@]}" -} - -get_container_name() { - echo "deeplabcut/deeplabcut:${DLC_VERSION}-$1-cuda${CUDA_VERSION}-cudnn${CUDNN_VERSION}" -} - -get_local_container_name() { - echo "deeplabcut-${DLC_VERSION}-$1-cuda${CUDA_VERSION}-cudnn${CUDNN_VERSION}" -} - -### Start of helper functions ### - -# Print error messages to stderr -# Ref. https://google.github.io/styleguide/shellguide.html#stdout-vs-stderr -err() { - echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2 -} - -# Update the docker container -update() { - $DOCKER pull $(get_container_name $1) -} - -# Build the docker container -# Usage: build [core|jupyter] -build() { - tag=$1 - _build $(get_container_name $tag) $(get_local_container_name $tag) || exit 1 -} - -_build() { - remote_name=$1 - local_name=$2 - - uname=$(id -un) - uid=$(id -u) - gname=$(id -gn) - gid=$(id -g) - - err "Configuring a local container for user $uname ($uid) in group $gname ($gid)" - $DOCKER build -q -t "${local_name}" - < Date: Tue, 21 Apr 2026 15:06:42 +0200 Subject: [PATCH 2/6] update deeplabcut_docker: modernize setup via pyproject.toml --- docker/package/MANIFEST.in | 4 ---- docker/package/pyproject.toml | 33 +++++++++++++++++++++++++++- docker/package/setup.cfg | 41 ----------------------------------- 3 files changed, 32 insertions(+), 46 deletions(-) delete mode 100644 docker/package/MANIFEST.in delete mode 100644 docker/package/setup.cfg diff --git a/docker/package/MANIFEST.in b/docker/package/MANIFEST.in deleted file mode 100644 index 644ae86674..0000000000 --- a/docker/package/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include pyproject.toml -include PYPI_README.md -include LICENSE -include deeplabcut_docker.sh diff --git a/docker/package/pyproject.toml b/docker/package/pyproject.toml index b4f08b1697..90f00b1b1d 100644 --- a/docker/package/pyproject.toml +++ b/docker/package/pyproject.toml @@ -1,3 +1,34 @@ [build-system] build-backend = "setuptools.build_meta" -requires = [ "setuptools", "wheel" ] +requires = [ "setuptools>=77", "wheel" ] + +[project] +name = "deeplabcut-docker" +description = "A helper package to launch DeepLabCut docker images" +readme = { file = "PYPI_README.md", content-type = "text/markdown" } +license = "LGPL-3.0-or-later" +license-files = [ "LICENSE" ] +authors = [ + { name = "M-Lab of Adaptive Intelligence", email = "mackenzie@deeplabcut.org" }, + { name = "Mathis Group for Computational Neuroscience and AI", email = "alexander@deeplabcut.org" }, +] +requires-python = ">=3.10" +classifiers = [ + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Utilities", +] +dynamic = [ "version" ] +urls."Bug Tracker" = "https://github.com/DeepLabCut/DeepLabCut/issues" +urls.Homepage = "https://github.com/DeepLabCut/DeepLabCut/tree/main/docker" +scripts.deeplabcut-docker = "deeplabcut_docker:main" + +[tool.setuptools] +py-modules = [ "deeplabcut_docker" ] +dynamic.version = { attr = "deeplabcut_docker.__version__" } diff --git a/docker/package/setup.cfg b/docker/package/setup.cfg deleted file mode 100644 index 00d64ed987..0000000000 --- a/docker/package/setup.cfg +++ /dev/null @@ -1,41 +0,0 @@ -[metadata] -name = deeplabcut-docker -version = attr: deeplabcut_docker.__version__ -author = A. & M. Mathis Labs -author_email = alexander@deeplabcut.org -maintainer = Steffen Schneider -maintainer_email = stes@hey.com -description = A helper package to launch DeepLabCut docker images -url = https://github.com/DeepLabCut/DeepLabCut/tree/main/docker -project_urls = - Bug Tracker = https://github.com/DeepLabCut/DeepLabCut/issues -classifiers = - Programming Language :: Python :: 3 - License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) - Operating System :: MacOS - Operating System :: POSIX :: Linux - Topic :: Utilities -license = LGPLv3 -long_description = file: PYPI_README.md -long_description_content_type = text/markdown -platform = any - -[options] -package_dir = - = . -py_modules = deeplabcut_docker -python_requires = >=3.10 -include_package_data = True - -[options.entry_points] -console_scripts = - deeplabcut-docker = deeplabcut_docker:main - -[options.packages.find] -where = . - -[options.data_files] -bin = deeplabcut_docker.sh - -[bdist_wheel] -universal = 1 From 7fc17724f4ded7fc0eeaecd29c78de0c37c0e2d3 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter Date: Tue, 21 Apr 2026 15:11:09 +0200 Subject: [PATCH 3/6] update deeplabcut_docker README --- docker/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docker/README.md b/docker/README.md index b2608c8ad7..9615f1f243 100644 --- a/docker/README.md +++ b/docker/README.md @@ -51,16 +51,15 @@ when calling `docker run`: deeplabcut-docker bash --gpus all -v /home/john:/home/john ``` -You can select which DeepLabCut version and CUDA version to use through the -`DLC_VERSION` and `CUDA_VERSION` environment variables. So to launch a container with -CUDA 12.1 and DLC 3.0.0, you can run: +Use `DLC_VERSION` and `CUDA_VERSION` to select the Hub tag (unset `DLC_VERSION` uses +`latest` / `latest-jupyter`): ```bash -DLC_VERSION=3.0.0 CUDA_VERSION=12.1 deeplabcut-docker bash --gpus all +DLC_VERSION=3.0.0rc14 CUDA_VERSION=12.4 deeplabcut-docker bash --gpus all ``` -*Note: Advanced users can also directly download and use the `deeplabcut-docker.sh` -script if this is preferred over a python helper script.* +To use a specific image instead of the default tags, pass `--image repo:tag`. +Make sure that the image supports jupyter notebooks when passing `notebook`. ### Jupyter Notebooks Running on Remote Servers From 09733c0f0145191fb88533f714537294c68d9533 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter Date: Tue, 28 Apr 2026 18:08:50 +0200 Subject: [PATCH 4/6] docker: fix default user home directory -> home/{user} --- docker/README.md | 6 +++--- docker/package/deeplabcut_docker.py | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docker/README.md b/docker/README.md index 7cafbf7e72..6e03a281ae 100644 --- a/docker/README.md +++ b/docker/README.md @@ -128,11 +128,11 @@ ARG UNAME ARG GNAME # Create same user as on the host system -RUN mkdir -p /home +RUN mkdir -p /home/${UNAME} RUN mkdir -p /app RUN groupadd -g ${GID} ${GNAME} || groupmod -o -g ${GID} ${GNAME} -RUN useradd -d /home -s /bin/bash -u ${UID} -g ${GID} ${UNAME} -RUN chown -R ${UNAME}:${GNAME} /home +RUN useradd -d /home/${UNAME} -s /bin/bash -u ${UID} -g ${GID} ${UNAME} +RUN chown -R ${UNAME}:${GNAME} /home/${UNAME} RUN chown -R ${UNAME}:${GNAME} /app WORKDIR /app diff --git a/docker/package/deeplabcut_docker.py b/docker/package/deeplabcut_docker.py index d705d41651..dba7dc20f3 100644 --- a/docker/package/deeplabcut_docker.py +++ b/docker/package/deeplabcut_docker.py @@ -61,8 +61,8 @@ def _remote_tag(mode: str) -> str: return f"{_IMAGE}:latest" -def _assert_jupyter_image(ref: str) -> None: - """Verify remote img supports Jupyter: exit otherwise.""" +def _warn_if_not_jupyter_image(ref: str) -> None: + """Warn if the image does not appear to have a Jupyter entrypoint.""" r = subprocess.run( _docker() + [ @@ -78,11 +78,11 @@ def _assert_jupyter_image(ref: str) -> None: if r.returncode != 0: sys.exit(f"Could not inspect image {ref!r} after pull.\n{r.stderr.strip()}") blob = (r.stdout or "").lower() - if "jupyter" not in blob or "notebook" not in blob: - sys.exit( - f"Image {ref!r} does not look like a Jupyter Notebook image " - "(entrypoint/cmd should reference jupyter and notebook). " - "Use an official *jupyter* tag or `bash` with --image." + if "jupyter" not in blob: + _log( + f"Warning: image {ref!r} does not appear to have a Jupyter entrypoint. " + "Proceeding anyway — if the server fails to start, ensure the image " + "exposes a Jupyter-compatible entrypoint on port 8888." ) @@ -99,10 +99,10 @@ def _build_user_image(remote: str, local: str) -> None: "\n".join( ( f"FROM {remote}", - "RUN mkdir -p /home /app", + f"RUN mkdir -p /home/{user} /app", f"RUN groupadd -g {gid} {group} || groupmod -o -g {gid} {group}", - f"RUN useradd -d /home -s /bin/bash -u {uid} -g {gid} {user}", - f"RUN chown -R {user}:{group} /home /app", + f"RUN useradd -d /home/{user} -s /bin/bash -u {uid} -g {gid} {user}", + f"RUN chown -R {user}:{group} /home/{user} /app", f"USER {user}", ) ) @@ -159,7 +159,7 @@ def main() -> None: local = f"deeplabcut-local-{mode}" subprocess.run(_docker() + ["pull", remote], check=True) if mode == "notebook" and args.image: - _assert_jupyter_image(remote) + _warn_if_not_jupyter_image(remote) _build_user_image(remote, local) run = _docker() + ["run", "-it", "--rm", "-v", f"{os.getcwd()}:/app", "-w", "/app"] From d584c8694ec270fd89be4940cef8e18ebc52f542 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter Date: Tue, 28 Apr 2026 18:20:07 +0200 Subject: [PATCH 5/6] deeplabcut-docker: update print statement passw->token --- docker/package/deeplabcut_docker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/package/deeplabcut_docker.py b/docker/package/deeplabcut_docker.py index dba7dc20f3..ffdd94dbbd 100644 --- a/docker/package/deeplabcut_docker.py +++ b/docker/package/deeplabcut_docker.py @@ -167,7 +167,8 @@ def main() -> None: port = os.environ.get("DLC_NOTEBOOK_PORT", "8888") _log("Starting the notebook server.") _log(f"Open your browser at http://127.0.0.1:{port}") - _log("If prompted for a password, enter 'deeplabcut'.") + _log("If prompted for a token, enter 'deeplabcut' (default).") + _log("To use a custom token: add -e NOTEBOOK_TOKEN= to your arguments.") run += ["-p", f"127.0.0.1:{port}:8888"] run += docker_run_args + [local] + ([] if mode == "notebook" else ["bash"]) sys.exit(subprocess.run(run).returncode) From 4161a0079f5651342e9c8678afdc73b2695d4eb8 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter Date: Tue, 28 Apr 2026 18:23:34 +0200 Subject: [PATCH 6/6] deeplabcut-docker: update pyproject.toml Python range 3.10-3.12 --- docker/package/pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/package/pyproject.toml b/docker/package/pyproject.toml index 90f00b1b1d..c46f5d3678 100644 --- a/docker/package/pyproject.toml +++ b/docker/package/pyproject.toml @@ -12,7 +12,7 @@ authors = [ { name = "M-Lab of Adaptive Intelligence", email = "mackenzie@deeplabcut.org" }, { name = "Mathis Group for Computational Neuroscience and AI", email = "alexander@deeplabcut.org" }, ] -requires-python = ">=3.10" +requires-python = ">=3.10,<3.13" classifiers = [ "Operating System :: MacOS", "Operating System :: POSIX :: Linux", @@ -20,8 +20,6 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", "Topic :: Utilities", ] dynamic = [ "version" ]