Docker on a Desktop - slgrobotics/articubot_one GitHub Wiki

Docker setup on a Desktop with GPU

Contents:


Here are my notes on setting up a Docker container on an Intel (AMD64) machine with an NVIDIA RTX 3060 GPU.

The goal is to create a reasonably isolated container with full GPU access for running AI workloads. Initially, I planned to run OpenClaw, but NanoBot is also an option.

Note:

  • for pre-made OpenClaw Docker images see "Useful Links" section below.
  • I’ll use the name “claw” for the container and related examples below — adjust as needed for your project. You can see private LAN addresses here - yours can be different.

The machine

I used my old-but-good machine for the experiment:

sudo lshw -short
bus            Asus P8Z68-V PRO (~2014)
processor      Intel(R) Core(TM) i7-2600K CPU @ 3.40GHz (4 core 8 threads, Sandy Bridge)
memory         16GiB System Memory  (1600 MHz)
display        GA104 [GeForce RTX 3060 Ti Lite Hash Rate]

nvidia-smi
| NVIDIA-SMI 535.288.01             Driver Version: 535.288.01   CUDA Version: 12.2     |
|   0  NVIDIA GeForce RTX 3060 Ti     Off | 00000000:01:00.0  On |                  N/A |
|  0%   43C    P5              24W / 200W |   1458MiB /  8192MiB |      0%      Default |

It is running Ubuntu 24.04 Desktop with the usual development tools installed.

The installation process

1. Install Docker

Follow this guide.

Install NVIDIA Container Toolkit to give Docker access to the GPU:

sudo apt-get install -y nvidia-container-toolkit

Note: docker commands should run under sudo, unless you do the following:

sudo groupadd docker
sudo usermod -aG docker $USER

At this point it is better to log out or reboot, or you will have to use sudo for all docker commands.

2. Create Dockerfile

This file defines how the container image is built — including the base OS image, installed software, configuration, and startup processes. It is a repeatable process: you can edit the Dockerfile, run the build command, and the image will be rebuilt.

The build process takes a while and will fail if there are errors in the Dockerfile.

When you run the image, the container starts quickly and from a clean state each time. Any changes made during the container’s lifetime (installing software, saving files locally, etc.) will be lost unless you use volumes or bind mounts for persistence.

First, create a directory for the Dockerfile:

mkdir ~/claw
cd ~/claw

Here is the Dockerfile I am using:

# Use Nvidia's Ubuntu 24.04 base with CUDA 12.4 support
FROM nvidia/cuda:13.1.1-base-ubuntu24.04

# Prevent interactive prompts during build
ENV DEBIAN_FRONTEND=noninteractive

# Install system dependencies and useful components
RUN apt-get update && apt-get install -y \
    net-tools avahi-daemon inetutils-ping \
    ca-certificates curl git sudo python3 make g++

# 1. Create the user 'claw'
# 2. Assign /bin/bash as the shell
# 3. Add to 'sudo' group
# 4. Remove password requirement for sudo (crucial for container automation)
RUN useradd -m -s /bin/bash claw && \
    usermod -aG sudo claw && \
    echo "claw ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# ------------ Application Software --------------------------------
# Install NodeJS
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
RUN apt-get install -y nodejs

# Verify NodeJS installation
RUN node --version && npm --version
# ------------------------------------------------------------------

# Clean installation files - no need to keep them in the image:
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

# Switch to the new user
USER claw
WORKDIR /home/claw

# Bring up shell prompt, but also can start anything in a non-interactive container
# Standard command (can be overridden by OpenClaw's specific start command)
CMD ["/bin/bash"]

# ------------ Application Startup --------------------------------
# Expose the port the application runs on
ENV PORT=8080
EXPOSE 8080

# Command to run the application
#CMD ["npm", "start"]
#ENTRYPOINT ["/app/scripts/entrypoint.sh"]
# -----------------------------------------------------------------

Note: (thanks, Marco Walther!)

  • In Docker, every RUN, COPY, and ADD instruction creates a new layer. Because Docker uses a Union File System, these layers are additive and read-only. My Dockerfile currently deviates from best practices, as it does not combine installation commands into a single RUN instruction.
  • Additionally, the claw user should be restricted; it should only have the permissions necessary for its specific task and should not be able to escalate to root.

3. Build the image:

docker build -t claw .

Networks and isolation

By default, Docker creates a network interface which is transparent between the host and container.

There are several ways to isolate the container - i.e. to restrict network access for the processes running inside it.

The "Network Bubble"

If you want to allow OpenClaw to talk to your robot but not the open internet, you can create a custom internal network (on the host):

docker network create --internal robot-net

Add --network robot-net to your docker run command.

This prevents the container from reaching any external IP addresses while still allowing it to communicate with other containers on that specific virtual network.

Macvlan: The "Ghost Device" Method

Instead of being "behind" your PC's IP, the container sits "beside" it on the wire. It gets IP traffic directly from your router and has a static address (e.g., 172.20.1.50).

Create the Macvlan network in Docker

# determine the name of host's network interface:
ip route get 172.20.1.1
172.20.1.1 dev eno1 src 172.20.1.120 uid 1000    # "eno1" is our parent on host
# create docker network:
docker network create -d macvlan --subnet=172.20.1.0/24 --gateway=172.20.1.1 -o parent=eno1 robot_net

When running the container on this network:

docker run -it --rm --gpus all --network robot_net --ip 172.20.1.50 --name claw -h claw claw

Now the docker instance can ping anything, except the host, and the host cannot ping docker IP addr - this is by design!

For security reasons, the Linux kernel isolates macvlan interfaces from their parent host interface. Even though they are on the same "physical wire", the host (172.20.1.120) and the container (172.20.1.50) are forbidden from talking to each other directly.

Port mapping [TODO]

Example:

# Maps host port 8080 to container port 3000
docker run -it --rm -p 127.0.0.1:8080:3000 --name claw -h claw claw

Access from host: open http://localhost:8080 From other computers - blocked

Total isolation

Use docker run --network none -it ...

Running a container

There are many ways to start a container once it has been built. You can set the container name, its OS hostname and the level of isolation on your network.

Here are some commands:

# has only 172.21.0.* interface, (docker network), but sees host's LAN 172.20.1.* (can ping both ways):
docker run -it --rm --gpus all --name claw -h claw --user claw claw

# isolated, on 172.22.0.* network (see "Network Bubble" above):
docker run -it --rm --gpus all --name claw -h claw --user claw --network robot-net claw

# sees host LAN, 172.22.0.* and 172.21.0.* - pretty open:
docker run -it --rm --gpus all --name claw -h claw --user claw --network host claw

# The "Ghost Device" with static IP addr (see "Macvlan" above) - pings everything:
docker run -it --rm --gpus all --network robot_net --ip 172.20.1.50 --name claw -h claw claw

Note: When the process started by the "start command" exits - the container terminates. For example, if it is bash - a Ctrl/D will kill the container.

Validating access to GPU

Test access to the GPU for the base image (no need to build a container):

docker run --rm --gpus all  nvidia/cuda:13.1.1-base-ubuntu24.04  nvidia-smi

Or, inside the running "claw" container:

claw@claw:~$ nvidia-smi
Fri Feb 20 20:20:44 2026       
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.288.01             Driver Version: 535.288.01   CUDA Version: 13.1     |
|-----------------------------------------+----------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |
|                                         |                      |               MIG M. |
|=========================================+======================+======================|
|   0  NVIDIA GeForce RTX 3060 Ti     Off | 00000000:01:00.0  On |                  N/A |
|  0%   42C    P8              11W / 200W |   1328MiB /  8192MiB |      3%      Default |
|                                         |                      |                  N/A |
+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|

Useful commands

Note: docker commands should run under sudo, unless you do the following:

sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker  # opens a new shell. Or - you should log out and log in, or reboot
  • To list all locally stored container images and their sizes:
docker image ls
  • To see running containers:
docker ps -a
  • To peek into a running container (brings up a shell prompt):
docker exec -it claw bash
  • Debugging and Maintenance:
docker info
docker logs -f <name>    # Follows live output from a container's console.
docker inspect           # Provides detailed JSON metadata about a container or image.
docker stats             # Displays a live stream of CPU, memory, and network usage.
  • Cleanup:
docker stop $(docker ps -q) # Gracefully halts a running container
docker rm $(docker ps -aq)  # Deletes a stopped container
docker system prune -a --volumes # Removes all data (Docker engine "factory reset")

Useful Links


Back to Home

⚠️ **GitHub.com Fallback** ⚠️