On Mac, the --network=host
option doesn't work right (see https://docs.docker.com/network/drivers/host/) , requiring specific ports to be mapped. Building off of @Błażej Michalik
's answer, I wrote the connect_docker.py
script shown below.
Gist: modify the kernel.json
to call my script instead of the kernel process. My script reads the connection file and starts up the kernel in the specified docker image with the necessary ports mapped and a modified connection file passed to the kernel in the docker image.
connect_docker.py
from pathlib import Path
import argparse
import json
import subprocess
import signal
import os
def main(connect_file: Path, docker_path, docker_tag, docker_run, docker_args):
print('connection_file', connect_file)
with open(connect_file) as file:
connection_info = json.load(file)
# Change host IP to "0.0.0.0" so port-forwarding works right
connection_info['ip'] = '0.0.0.0'
with open(connect_file, 'w') as file:
json.dump(connection_info, file)
port_maps = [
tok
for key in ['shell_port', 'iopub_port', 'stdin_port', 'hb_port']
for tok in f'-p {connection_info[key]}:{connection_info[key]}'.split()
]
container = f'kernel-{connection_info["key"]}'
command = [docker_path, 'run',
'--rm', # remove the container after it closes. Comment this out to debug a failed container.
*port_maps,
*docker_args.split(),
'--name', container,
'-v', f'{connect_file}:/connection_file.json',
'-v', f'{os.getcwd()}:/notebook', # mount current dir to /notebook
'-w', '/notebook', # and run in /notebook in the container
docker_tag,
*docker_run.split()
]
print(' '.join(command))
proc = subprocess.Popen(command)
try:
proc.wait()
except KeyboardInterrupt:
subprocess.run([docker_path, 'stop', container])
raise
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("connection_file", help="Path to the ipykernel connection file", type=Path)
parser.add_argument("docker_tag", help="Tag of the docker image to run in")
parser.add_argument("docker_run", help="Command to run in docker (i.e. to start the kernel)",
default="python -m ipykernel_launcher -f /connection_file.json")
parser.add_argument("--docker-args", help="Additional arguments to docker run", default="")
parser.add_argument("--docker-exec", help="Path to the docker executable", default="/usr/bin/docker")
args = parser.parse_args()
main(args.connection_file, args.docker_exec, args.docker_tag, args.docker_run, args.docker_args)
""" Example Connection File (used for debugging this script during development)
{
"shell_port": 63101,
"iopub_port": 63102,
"stdin_port": 63103,
"control_port": 63106,
"hb_port": 63105,
"ip": "127.0.0.1",
"key": "44ca1254-f8fb96abfcf5fc752e404e23",
"transport": "tcp",
"signature_scheme": "hmac-sha256",
"kernel_name": "py311-docker"
}
"""
"""
Here's an example kernel.json
(fill in the ...
with your relevant paths):
{
"argv": [
".../python",
".../connect_docker.py",
"--docker-exec",
"/usr/local/bin/docker",
"--docker-args",
"--platform linux/amd64",
"{connection_file}",
"byubean/xeus-cling-kernel:winter2024",
"micromamba run -n cling xcpp -f /connection_file.json -std=c++17"
],
"display_name": "C++17 (docker)",
"language": "C++17"
}