Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -129,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

Expand Down
4 changes: 0 additions & 4 deletions docker/package/MANIFEST.in

This file was deleted.

202 changes: 152 additions & 50 deletions docker/package/deeplabcut_docker.py
Original file line number Diff line number Diff line change
@@ -1,75 +1,177 @@
#!/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 _warn_if_not_jupyter_image(ref: str) -> None:
"""Warn if the image does not appear to have a Jupyter entrypoint."""
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:
_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."
)


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}",
Comment thread
deruyter92 marked this conversation as resolved.
f"RUN mkdir -p /home/{user} /app",
f"RUN groupadd -g {gid} {group} || groupmod -o -g {gid} {group}",
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}",
)
)
+ "\n"
)
subprocess.run(
_docker() + ["build", "-q", "-t", local, "-"],
Comment thread
deruyter92 marked this conversation as resolved.
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:
_warn_if_not_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 token, enter 'deeplabcut' (default).")
_log("To use a custom token: add -e NOTEBOOK_TOKEN=<your-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)


if __name__ == "__main__":
Expand Down
Loading