Dev Dock Manager is a small self-hosted dashboard for handing out Linux desktops on one machine. Each person logs in, creates a container from the dev-dock image, and gets a full Xfce desktop in the browser (NoVNC), a web terminal and an SSH port, without touching Docker themselves. This version has a Rust API (axum + bollard) and a Next.js frontend, all behind Traefik.
| Repo | What it is |
|---|---|
| dev-dock | The gui-vnc image: Xfce desktop, VNC, noVNC and SSH in one container. Included here as gui/. |
| dev-dock-manager | The original manager, with a Django (Python) API. |
| dev-dock-manager-rust (this repo) | The same dashboard, routes and container setup, with the API rewritten in Rust. |
The two managers look the same from the browser and start the same containers, so pick one per machine. What differs is the backend:
| dev-dock-manager-rust | dev-dock-manager | |
|---|---|---|
| API | one Rust binary (axum, bollard) | Django + Daphne, run by supervisord |
| Background jobs | a worker task inside the API process, Redis list as the queue | a separate rqworker process (RQ) |
| Users | users table in SQLite, Argon2 password hashes |
Django users in SQLite |
| Login tokens | access 1 hour, refresh 1 day | access 60 hours, refresh 1 day |
| Extras | /health |
Django admin and Swagger pages |
Everything you open goes through Traefik on port 8000, which picks the target by path:
| Path | Goes to |
|---|---|
/api/*, /dashboard/api/*, /ws/* |
the Rust API (d-gui-manager-backend, port 8000) |
/novnc/<name>/ |
the noVNC page of the desktop called <name> (port 6901), with the prefix stripped |
| everything else | the Next.js dashboard (d-gui-manager-frontend, port 3000) |
Traefik learns these routes from Docker labels. The services get theirs from docker-compose.yml, and each
desktop gets its own when the API creates it. All of them share the external network d-gui-network. SSH is
the one thing that skips Traefik: each desktop publishes port 22 on a host port you choose.
What each click does:
- Log in. The dashboard posts your username and password to
/api/auth/tokenand keeps the access and refresh tokens (JWT) in the browser's localStorage. Every API call sends the access token. - Create. The API checks the name (two characters or more, starting with a letter) and that the SSH port
is free, puts a job on the Redis queue and answers with a
task_idright away. - The worker starts the container from
gui-vnc: your passwords as environment variables, port 22 on the SSH port you picked,/etc/localtimemounted read-only, the Traefik labels for/novnc/<name>/, and optionally privileged mode or all NVIDIA GPUs. - The list refreshes when
CREATEDarrives on the/ws/notifications/WebSocket. - Console opens a WebSocket to
/ws/console/?container=<id>, sends the token in the first message and gets abashstarted withdocker exec(a root shell). Attach connects to the container's main process instead. Keys and window size go up aspty_inputandpty_resize. - NoVNC opens
/novnc/<name>/through Traefik and asks for the VNC password you set when creating it. - Start, stop, restart, remove go through the same queue. You see
WAITING, thenSTARTED,STOPPEDand so on.
You need Linux with Docker running (the API talks to /var/run/docker.sock). On Windows, use Docker inside WSL.
-
Get the code with the image submodule:
git clone --recurse-submodules https://github.com/NatLee/dev-dock-manager-rust.git cd dev-dock-manager-rust # already cloned? git submodule update --init --recursive
-
Build the desktop image (it ends up as
gui-vnc; a first build takes 10 to 30 minutes and about 7.8 GB):cd gui && docker compose build && cd ..
Or pull a pre-built one from natlee/gui-vnc and name it
gui-vnc, because that's the name the API starts containers from:docker pull natlee/gui-vnc:20260208-arm64 docker tag natlee/gui-vnc:20260208-arm64 gui-vnc
-
Create the network and start the stack:
docker network create d-gui-network docker compose build && docker compose up -dThe
nvidia-cudaservice only prints "CUDA image ready" and exits. -
Create the first user:
./dev-tool.sh create-superuser
It asks for a username and password (defaults
admin/1234). To skip the prompts or pass options:docker exec -it d-gui-manager-backend /app/dev-dock-manager-api create-user admin mypassword --staff -
Open http://localhost:8000/ and log in. It sends you to
/dashboard/containers.
- Opening it from another machine. The dashboard calls the API at
NEXT_PUBLIC_API_ORIGINfromfrontend/.env.local, which ishttp://localhost:8000, and Next.js bakes that in at build time. For any other address, set it to that address, or leave it empty (NEXT_PUBLIC_API_ORIGIN=) so the dashboard uses whatever address it was opened from. Then rundocker compose build frontend && docker compose up -d. - Ports. 8000 is the app, 8080 is the Traefik dashboard. Each desktop also takes the SSH port you pick.
- Data. Users live in
./backend-data/db.sqlite3. The desktops are ordinary containers named after what you typed, sodocker psshows them. - Keep the image name
gui-vnc. The API lists, creates and manages containers whose image starts withgui-vnc.DOCKER_IMAGE_NAMEonly changes which images the dashboard lists. - Stopping. The API handles SIGTERM and SIGINT, so
docker compose stopends it cleanly in about a second.
It's built for a trusted group on one machine. Every account can see and control every desktop (there's no per-user ownership), and can create privileged containers. With the Docker socket mounted, that amounts to root on the host. Also:
- change
JWT_SECRETindocker-compose.yml(the default ischange-me-in-production); - the Traefik dashboard on 8080 runs with
--api.insecure, so don't expose that port.
Optional. With NVIDIA drivers and the NVIDIA Container Toolkit on the host, the create dialog lets you give a
container all GPUs (/dashboard/api/nvdocker/check tells the dashboard whether that works).
From the project root:
./dev-tool.sh --helpbash: shell in the backend container (d-gui-manager-backend).create-superuser: create a login user (Rustcreate-user).logs: follow backend logs.backend-debug: run the backend in a one-off container withRUST_LOG=debug.
- Prefix:
/api, and the same routes again under/dashboard/api(e.g./api/auth/token,/dashboard/api/containers,/dashboard/api/ports). - Health:
GET /health→ok, on the API itself (port 8000 inside the Docker network). Traefik doesn't route/health, solocalhost:8000/healthfrom the host reaches the frontend and returns 404. - Web terminal: the frontend calls
GET /dashboard/api/console/:action/:idfor metadata, then opens the WebSocket/ws/console/?container=<container_id>. The first message must carry the JWT:{"token": "<access token>", "action": "shell" | "attach", "payload": {"Id": "<container_id>"}}. After that,pty_inputandpty_resizemessages carry keys and window size. - Notifications:
/ws/notifications/pushes{"message": {"action": "CREATED" | "STARTED" | …, "details": "…"}}.
Backend layout: backend/src/api/ (auth, containers, images, ports), backend/src/ws/ (console and
notifications), backend/src/docker/ (bollard), backend/src/queue/ (Redis queue and worker),
backend/src/db/ (users, SQLite + Argon2). See backend/README.md for running it locally.




