Humanoid sim stack for the Correll Lab H1 robot: MuJoCo, ROS 2, and Isaac Sim running in separate containers and sharing a CycloneDDS ROS domain.
docker/— Dockerfiles,docker-compose.yml, and the build/run scripts.core_ws/— ROS 2 workspace (bringup, IK, perception, safety). Submodules.h1_mujoco/— MuJoCo simulator entry point and bridges.CL_Assets/— URDF, MuJoCo XML, and Isaac USD assets.
- Docker (with Compose v2) and the NVIDIA Container Toolkit.
- Git LFS (
git lfs install) — required to fetch the large binary assets (URDF meshes, MuJoCo XML, Isaac USD) tracked via LFS. git submodule update --init --recursiveto populatecore_ws/src.- Copy
docker/.env.exampletodocker/.envand fill in yourGEMINI_API_KEYandROS_DOMAIN_ID(see Configuration).
A few things worth knowing before you run anything:
- The build/run scripts can be invoked from any directory — they resolve their
own location, so
docker/scripts/docker_run.sh mujocoworks just as well from/tmpas from the repo root. ROS_DOMAIN_IDis read fromdocker/.envand forwarded into the MuJoCo and ROS containers. If it is unset or0, it is normalized to1(domain 0 is reserved for the real robot).- Isaac currently overrides this and pins its DDS bridge to channel 1 — see the Isaac section below.
Runtime settings live in docker/.env (git-ignored — it holds your API key).
Copy the template once and edit it; you never need to export these variables
in your shell:
cp docker/.env.example docker/.env
# then edit docker/.env:
# GEMINI_API_KEY=... # https://aistudio.google.com/apikey
# ROS_DOMAIN_ID=1 # any non-zero value; 0 is reserved for the real robotBoth docker compose and docker/scripts/docker_run.sh load docker/.env
automatically, so every container and every terminal sees the same values.
ROS_DOMAIN_ID is passed to all containers; GEMINI_API_KEY is passed only to
the ros container (the vision pipeline's Gemini backbone and h12_skills need
it). Because the run scripts source the file, docker/.env takes precedence
over any value left exported in your shell.
docker/scripts/docker_build.sh # all three
docker/scripts/docker_build.sh mujoco ros # subset
docker/scripts/docker_build.sh isaac # isaac onlyThe MuJoCo and ROS images both inherit from humanoid_sim_base, which is
built first automatically when either profile is selected. Isaac is
self-contained and does not use the base.
docker/scripts/docker_run.sh mujoco # windowed viewer
docker/scripts/docker_run.sh mujoco --headless # no DISPLAY / SSH / CI
docker/scripts/docker_run.sh mujoco bash # drop to a shell instead
# to use a custom ROS domain (e.g. several devs on one network),
# set ROS_DOMAIN_ID in docker/.env (see Configuration above)Once it's up, MuJoCo publishes rt/lowstate over CycloneDDS plus
/head/color/image_raw, /head/depth/image_raw, /head/color/camera_info,
/lidar/points, and /tf on the chosen ROS domain (default 1).
The ROS launcher only builds the workspace and drops to a shell, so bringup
is a manual step. ROS_DOMAIN_ID (all containers) and GEMINI_API_KEY (the
ros container, for the vision pipeline's Gemini backbone and h12_skills)
both come from docker/.env, so there is nothing to export — just open two
terminals:
# terminal A — start MuJoCo first so /clock is publishing
docker/scripts/docker_run.sh mujoco
# terminal B — ROS workspace shell (auto-builds core_ws on first run)
docker/scripts/docker_run.sh ros
# inside the ROS container
ros2 launch h1_bringup h1_sim_bringup.launch.pyBringup starts joint_state_publisher, robot_state_publisher, the
frame_task_server IK solver, the safety_node, and rviz2.
The Isaac profile builds and runs the same way as the others:
docker/scripts/docker_build.sh isaac
docker/scripts/docker_run.sh isaacThe launcher is docker/scripts/launch_isaac.sh. Task selection, asset
paths, and the OmniGraph DDS bridge are not yet documented here. Note that
the bridge currently unsets ROS_DOMAIN_ID and pins itself to channel 1
regardless of the host setting — bridging to a non-default domain is a
known gap.
- X11 / GUI: run
xhost +local:dockeronce per session if rviz, the MuJoCo viewer, or the slider GUI fail to open. - Talking to the sim from the host (
ros2 topic list, standalonerviz2) bypasses the run scripts, so it won't pick updocker/.envon its own — load it into your shell first:set -a; source docker/.env; set +a. - For a clean rebuild of the message workspace, wipe
container_cache/msgs_ws/on the host before relaunching.
End-to-end run of the fridge-opening demo across three terminals. All three
share ROS_DOMAIN_ID from docker/.env (terminals A and B via the run scripts,
terminal C via the already-running container), so there's nothing to export.
# terminal A — MuJoCo (start first so /clock is publishing)
docker/scripts/docker_run.sh mujoco
# terminal B — ROS workspace shell, then launch bringup
docker/scripts/docker_run.sh ros
ros2 launch h1_bringup h1_sim_bringup.launch.py
# terminal C — exec into the running ROS container and run the demo
docker exec -it humanoid_sim_ros bash
source /opt/ros/humble/setup.bash
source /home/code/core_ws/install/setup.bash
ros2 run h1_bringup open_fridge.py