Skip to content

[Docker] Add the ability to run with the same user id/gid as the hermes installing user - #237

Closed
mmartial wants to merge 13 commits into
nesquena:masterfrom
mmartial:master
Closed

[Docker] Add the ability to run with the same user id/gid as the hermes installing user#237
mmartial wants to merge 13 commits into
nesquena:masterfrom
mmartial:master

Conversation

@mmartial

@mmartial mmartial commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Using as a non-root user (with sudo privileges if needed) installation of hermes-agent, this PR will allow the files in .hermes to not be created/modified by the root user (the default user of the container).

This PR proposes using a hermeswebui user within the container; that user's UID/GID can be configured via the WANTED_UID and WANTED_GID environment variables to match the user who owns the .hermes folder.
I updated the README.md to reflect the change and explain its use case.

The docker_init.bash file contains most of the user-switching logic.

This is the same technique I have used successfully in the ComfyUI project I am maintaining.

I tested functionality using:

docker compose up --build
[...]
[+] up 2/2
 ✔ Image hermes-webui-hermes-webui       Built                                                                                                                                           0.9s
 ✔ Container hermes-webui-hermes-webui-1 Recreated                                                                                                                                       0.6s
Attaching to hermes-webui-1
hermes-webui-1  |
hermes-webui-1  |
hermes-webui-1  | ======================================
hermes-webui-1  | =================== Starting script (ID: 1)
hermes-webui-1  | == Running hermeswebui_init.bash in / as hermeswebuitoo
hermes-webui-1  |   - script_fullname: /hermeswebui_init.bash
hermes-webui-1  | -- WANTED_UID: "1000"
hermes-webui-1  | -- WANTED_GID: "1000"
hermes-webui-1  | == Most Environment variables set
hermes-webui-1  | == user (hermeswebuitoo)
hermes-webui-1  |   uid: 1025 / WANTED_UID: 1000
hermes-webui-1  |   gid: 1025 / WANTED_GID: 1000
hermes-webui-1  | -- Running as hermeswebuitoo, will switch hermeswebui to the desired UID/GID
hermes-webui-1  | -- Saving environment variables to /tmp/hermeswebuitoo_env.txt
hermes-webui-1  | -- Restarting as hermeswebui user with UID 1000 GID 1000
hermes-webui-1  |
hermes-webui-1  |
hermes-webui-1  | ======================================
hermes-webui-1  | =================== Starting script (ID: 38)
hermes-webui-1  | == Running hermeswebui_init.bash in / as hermeswebui
hermes-webui-1  |   - script_fullname: /hermeswebui_init.bash
hermes-webui-1  | -- WANTED_UID: "1000"
hermes-webui-1  | -- WANTED_GID: "1000"
hermes-webui-1  | == Most Environment variables set
hermes-webui-1  | == user (hermeswebui)
hermes-webui-1  |   uid: 1000 / WANTED_UID: 1000
hermes-webui-1  |   gid: 1000 / WANTED_GID: 1000
hermes-webui-1  |
hermes-webui-1  | == Running as hermeswebui
hermes-webui-1  | -- Loading not already set environment variables from /tmp/hermeswebuitoo_env.txt
hermes-webui-1  | -- Loading environment variables from /tmp/hermeswebuitoo_env.txt (overwrite existing: true) (ignorelist: HOME PWD USER SHLVL TERM OLDPWD SHELL _ SUDO_COMMAND HOSTNAME LOGNAME MAIL SUDO_GID SUDO_UID SUDO_USER CHECK_NV_CUDNN_VERSION VIRTUAL_ENV VIRTUAL_ENV_PROMPT ENV_IGNORELIST ENV_OBFUSCATE_PART) (obfuscate: TOKEN API KEY)
hermes-webui-1  |   ++ Setting environment variable DEBIAN_FRONTEND [noninteractive]
hermes-webui-1  |   ++ Setting environment variable GPG_KEY [**OBFUSCATED**]
hermes-webui-1  |   ++ Setting environment variable HERMES_WEBUI_DEFAULT_WORKSPACE [/workspace]
hermes-webui-1  |   ++ Setting environment variable HERMES_WEBUI_HOST [0.0.0.0]
hermes-webui-1  |   ++ Setting environment variable HERMES_WEBUI_PORT [8787]
hermes-webui-1  |   ++ Setting environment variable HERMES_WEBUI_STATE_DIR [/home/hermeswebui/.hermes/webui-mvp]
hermes-webui-1  |   == Environment variable LANG [en_US.utf8] already set and value is unchanged
hermes-webui-1  |   == Environment variable LC_ALL [C] already set and value is unchanged
hermes-webui-1  |   @@ Overwriting environment variable PATH [/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin] -> [/home/hermeswebuitoo/.local/bin/:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin]
hermes-webui-1  |   ++ Setting environment variable PYTHONDONTWRITEBYTECODE [1]
hermes-webui-1  |   ++ Setting environment variable PYTHONIOENCODING [utf-8]
hermes-webui-1  |   ++ Setting environment variable PYTHONPATH [/app]
hermes-webui-1  |   ++ Setting environment variable PYTHONUNBUFFERED [1]
hermes-webui-1  |   ++ Setting environment variable PYTHON_SHA256 [c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684]
hermes-webui-1  |   ++ Setting environment variable PYTHON_VERSION [3.12.13]
hermes-webui-1  |   ++ Setting environment variable UV_PROJECT_ENVIRONMENT [venv]
hermes-webui-1  |   == Environment variable WANTED_GID [1000] already set and value is unchanged
hermes-webui-1  |   == Environment variable WANTED_UID [1000] already set and value is unchanged
hermes-webui-1  |
hermes-webui-1  | -- Making sure /app is owned by the hermeswebui user to avoid permission issues when running the server and installing packages
hermes-webui-1  |
hermes-webui-1  | == Checking required environment variables for hermes-webui
hermes-webui-1  |
hermes-webui-1  | -- HERMES_WEBUI_VERSION: Where to store sessions, workspaces, and other state (default: ~/.hermes/webui-mvp)
hermes-webui-1  | -- HERMES_WEBUI_STATE_DIR: /home/hermeswebui/.hermes/webui-mvp
hermes-webui-1  |
hermes-webui-1  | -- HERMES_WEBUI_DEFAULT_WORKSPACE: Default workspace directory shown on first launch
hermes-webui-1  | -- HERMES_WEBUI_DEFAULT_WORKSPACE: /workspace
hermes-webui-1  |
hermes-webui-1  | ===================
hermes-webui-1  | == Running hermes-webui
hermes-webui-1  |
hermes-webui-1  |   Hermes Web UI -- startup config
hermes-webui-1  |   --------------------------------
hermes-webui-1  |   repo root   : /app
hermes-webui-1  |   agent dir   : /home/hermeswebui/.hermes/hermes-agent  [ok]
hermes-webui-1  |   python      : /app/venv/bin/python3
hermes-webui-1  |   state dir   : /home/hermeswebui/.hermes/webui-mvp
hermes-webui-1  |   workspace   : /workspace
hermes-webui-1  |   host:port   : 0.0.0.0:8787
hermes-webui-1  |   config file : /home/hermeswebui/.hermes/config.yaml  (found)
hermes-webui-1  |
hermes-webui-1  | [!!] WARNING: Binding to 0.0.0.0 with NO PASSWORD SET.
hermes-webui-1  |      Anyone on the network can access your filesystem and agent.
hermes-webui-1  |      Set a password via Settings or HERMES_WEBUI_PASSWORD env var.
hermes-webui-1  |      To suppress: bind to 127.0.0.1 or set a password.
hermes-webui-1  | [!!] Warning: Hermes agent found but missing modules: ['run_agent']
hermes-webui-1  |      run_agent: ModuleNotFoundError: No module named 'fire'
hermes-webui-1  |      Attempting to install missing dependencies from agent requirements.txt...
hermes-webui-1  |      Installing from /home/hermeswebui/.hermes/hermes-agent/requirements.txt ...
hermes-webui-1  | [ok] pip install completed.
hermes-webui-1  | [ok] Agent dependencies installed successfully.
hermes-webui-1  |   Hermes Web UI listening on http://0.0.0.0:8787
hermes-webui-1  |   Then open:     http://localhost:8787

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Thanks for this PR @mmartial! The use-case is real — file ownership problems when mounting .hermes from a non-root host user into a root-running container are a genuine pain point, and the UID/GID matching approach you've used in ComfyUI is a proven pattern.

I've reviewed the full diff. Overall the approach is solid, but I have a few items I'd want addressed before merging:

Issues to address

1. requirements.txt adds openai — likely unintentional

+openai

The openai package is a full SDK (~50 MB) with many transitive deps. The WebUI itself doesn't use it directly; agent communication goes through hermes-agent. This looks like it was added during testing and shouldn't be in the server's requirements.

2. Hard-coded WANTED_UID=1000 / WANTED_GID=1000 in docker-compose.yml

environment:
  - WANTED_UID=1000
  - WANTED_GID=1000

UID 1000 is a reasonable default, but the ideal default is the calling user's UID (as you do in the docker run example with `id -u`). In compose, this can be done via variable substitution:

environment:
  - WANTED_UID=${UID:-1000}
  - WANTED_GID=${GID:-1000}

Users would then export UID and GID before running docker compose up, or set them in a .env file. Otherwise the compose path always creates files owned by UID 1000, which fails for any user with a different UID.

3. docker-compose.yml changes the port binding from 127.0.0.1 to open (0.0.0.0)

-      - "127.0.0.1:8787:8787"
+#      - "127.0.0.1:8787:8787"
+      - "8787:8787"

The original 127.0.0.1 binding was intentional — it means "localhost only" by default, which is safer for a tool that has access to the host filesystem. The commented-out line and the note in the README about exposing to the network both exist precisely to make this an opt-in step. Changing the default to open exposure is a security regression for new users. Please revert this to 127.0.0.1:8787:8787.

4. The _get_ai_agent() retry helper in api/streaming.py is a good fix

The deferred import for AIAgent when dependencies are installed at runtime is clean and addresses the Docker volume-mount case where the agent dir appears after server startup. No issues here.

5. HERMES_WEBUI_DEFAULT_WORKSPACE is now required

The init script exits with an error if HERMES_WEBUI_DEFAULT_WORKSPACE is not set:

if [ -z "${HERMES_WEBUI_DEFAULT_WORKSPACE+x}\" ]; then error_exit "HERMES_WEBUI_DEFAULT_WORKSPACE not set"; fi

Previously this variable was optional and had a reasonable internal default. Making it required is a breaking change for anyone who upgrades without updating their compose file or docker run command. Consider defaulting to something like /workspace if not set, or accepting a missing value gracefully.


Summary

Item Status
UID/GID matching via docker_init.bash ✅ Good approach
_get_ai_agent() retry helper ✅ Good fix
requirements.txt adds openai ❌ Remove
docker-compose.yml hardcoded UID 1000 ⚠️ Use ${UID:-1000}
Port binding changed to open ❌ Revert to 127.0.0.1
HERMES_WEBUI_DEFAULT_WORKSPACE required ⚠️ Make optional with a default

Happy to merge after these are addressed. Let me know if you have questions about any of the points.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Seeing the new commit "recover original default" pushed just after the review — it looks like that addresses the port binding issue (item 3 in the review).

To confirm where things stand, could you let me know which of the review items the new commits address? Specifically:

  1. openai in requirements.txt — removed?
  2. WANTED_UID/WANTED_GID in docker-compose.yml — changed to ${UID:-1000} / ${GID:-1000}?
  3. Port binding reverted to 127.0.0.1:8787:8787 — looks like this one is done based on "recover original default"
  4. HERMES_WEBUI_DEFAULT_WORKSPACE required — made optional with a fallback default?

Once those are confirmed, I'll do a final pass and we can get this merged.

…at we are running within the container to show additional information from within the server .py start
…ional details when used "within the container"
… script; include uv setup and addition of hermes-agent's pyproject.toml for clean startup
@mmartial mmartial changed the title Add the ability to run with the same user id/gid as the hermes installing user [Docker] Add the ability to run with the same user id/gid as the hermes installing user Apr 11, 2026
@mmartial

mmartial commented Apr 11, 2026

Copy link
Copy Markdown
Contributor Author

Addressed remaining items.

Manual review of docker-compose.yml recommended: docker compose will start the container as root (even when started by a local user). Therefore, UID:GID will be 0:0, which defeats the purpose of the WANTED_UID:WANTED_GID

@mmartial

mmartial commented Apr 11, 2026

Copy link
Copy Markdown
Contributor Author

Updated startup log:

docker compose up --build
[+] Building 2.2s (21/21) FINISHED
 => [internal] load local bake definitions                                                                                                                                              0.0s
 => => reading from stdin 538B                                                                                                                                                          0.0s
 => [internal] load build definition from Dockerfile                                                                                                                                    0.0s
 => => transferring dockerfile: 2.45kB                                                                                                                                                  0.0s
 => [internal] load metadata for docker.io/library/python:3.12-slim                                                                                                                     0.2s
 => [internal] load .dockerignore                                                                                                                                                       0.0s
 => => transferring context: 96B                                                                                                                                                        0.0s
 => [internal] load build context                                                                                                                                                       0.0s
 => => transferring context: 10.12kB                                                                                                                                                    0.0s
 => [ 1/14] FROM docker.io/library/python:3.12-slim@sha256:804ddf3251a60bbf9c92e73b7566c40428d54d0e79d3428194edf40da6521286                                                             0.0s
 => => resolve docker.io/library/python:3.12-slim@sha256:804ddf3251a60bbf9c92e73b7566c40428d54d0e79d3428194edf40da6521286                                                               0.0s
 => CACHED [ 2/14] RUN if [ "A${BUILD_APT_PROXY:-}" != "A" ]; then         echo "Using APT proxy: ${BUILD_APT_PROXY}";         printf 'Acquire::http::Proxy "%s";\n' "$BUILD_APT_PROXY  0.0s
 => CACHED [ 3/14] RUN apt-get update -y --fix-missing --no-install-recommends     && apt-get install -y --no-install-recommends     apt-utils     locales     ca-certificates     sud  0.0s
 => CACHED [ 4/14] RUN localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8                                                                                     0.0s
 => CACHED [ 5/14] WORKDIR /apptoo                                                                                                                                                      0.0s
 => CACHED [ 6/14] RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers                                                                                                              0.0s
 => CACHED [ 7/14] RUN groupadd -g 1024 hermeswebui     && groupadd -g 1025 hermeswebuitoo                                                                                              0.0s
 => CACHED [ 8/14] RUN useradd -u 1024 -d /home/hermeswebui -g hermeswebui -s /bin/bash -m hermeswebui     && usermod -G users hermeswebui     && adduser hermeswebui sudo              0.0s
 => CACHED [ 9/14] RUN useradd -u 1025 -d /home/hermeswebuitoo -g hermeswebuitoo -s /bin/bash -m hermeswebuitoo     && usermod -G users hermeswebuitoo     && adduser hermeswebuitoo s  0.0s
 => CACHED [10/14] RUN chown -R hermeswebuitoo:hermeswebuitoo /apptoo                                                                                                                   0.0s
 => CACHED [11/14] COPY --chmod=555 docker_init.bash /hermeswebui_init.bash                                                                                                             0.0s
 => [12/14] RUN touch /.within_container                                                                                                                                                0.3s
 => [13/14] RUN rm -rf /var/lib/apt/lists/* /etc/apt/apt.conf.d/01proxy     && apt-get clean                                                                                            0.4s
 => [14/14] COPY . /apptoo                                                                                                                                                              0.1s
 => exporting to image                                                                                                                                                                  0.8s
 => => exporting layers                                                                                                                                                                 0.4s
 => => exporting manifest sha256:d02f80d50b01e05d06d87921f821b143e023cd6ef7635997a3c10f2cd0449662                                                                                       0.0s
 => => exporting config sha256:5b883c89068eeadbd3d67180a1f47c3270dcec709397edbe626ab8dae44b3e16                                                                                         0.0s
 => => exporting attestation manifest sha256:52f3e6be12bb1dfdf5d86d007b5ebe0e39506838df83c447fffae30073d0b35e                                                                           0.1s
 => => exporting manifest list sha256:6caf87654a21146bbe047eb650a158e7f1a13f2795bb96539bbc6c0c59652350                                                                                  0.0s
 => => naming to docker.io/library/hermes-webui-hermes-webui:latest                                                                                                                     0.0s
 => => unpacking to docker.io/library/hermes-webui-hermes-webui:latest                                                                                                                  0.1s
 => resolving provenance for metadata file                                                                                                                                              0.0s
[+] up 2/2
 ✔ Image hermes-webui-hermes-webui       Built                                                                                                                                           2.3s
 ✔ Container hermes-webui-hermes-webui-1 Recreated                                                                                                                                       0.6s
Attaching to hermes-webui-1
hermes-webui-1  |
hermes-webui-1  |
hermes-webui-1  | ======================================
hermes-webui-1  | =================== Starting script (ID: 1)
hermes-webui-1  | == Running hermeswebui_init.bash in / as hermeswebuitoo
hermes-webui-1  |   - script_fullname: /hermeswebui_init.bash
hermes-webui-1  | -- WANTED_UID: "1000"
hermes-webui-1  | -- WANTED_GID: "1000"
hermes-webui-1  | == Most Environment variables set
hermes-webui-1  | == user (hermeswebuitoo)
hermes-webui-1  |   uid: 1025 / WANTED_UID: 1000
hermes-webui-1  |   gid: 1025 / WANTED_GID: 1000
hermes-webui-1  | -- Running as hermeswebuitoo, will switch hermeswebui to the desired UID/GID
hermes-webui-1  | -- Saving environment variables to /tmp/hermeswebuitoo_env.txt
hermes-webui-1  | -- Restarting as hermeswebui user with UID 1000 GID 1000
hermes-webui-1  |
hermes-webui-1  |
hermes-webui-1  | ======================================
hermes-webui-1  | =================== Starting script (ID: 38)
hermes-webui-1  | == Running hermeswebui_init.bash in / as hermeswebui
hermes-webui-1  |   - script_fullname: /hermeswebui_init.bash
hermes-webui-1  | -- WANTED_UID: "1000"
hermes-webui-1  | -- WANTED_GID: "1000"
hermes-webui-1  | == Most Environment variables set
hermes-webui-1  | == user (hermeswebui)
hermes-webui-1  |   uid: 1000 / WANTED_UID: 1000
hermes-webui-1  |   gid: 1000 / WANTED_GID: 1000
hermes-webui-1  |
hermes-webui-1  | == Running as hermeswebui
hermes-webui-1  | -- Loading not already set environment variables from /tmp/hermeswebuitoo_env.txt
hermes-webui-1  | -- Loading environment variables from /tmp/hermeswebuitoo_env.txt (overwrite existing: true) (ignorelist: HOME PWD USER SHLVL TERM OLDPWD SHELL _ SUDO_COMMAND HOSTNAME LOGNAME MAIL SUDO_GID SUDO_UID SUDO_USER CHECK_NV_CUDNN_VERSION VIRTUAL_ENV VIRTUAL_ENV_PROMPT ENV_IGNORELIST ENV_OBFUSCATE_PART) (obfuscate: TOKEN API KEY)
hermes-webui-1  |   ++ Setting environment variable DEBIAN_FRONTEND [noninteractive]
hermes-webui-1  |   ++ Setting environment variable GPG_KEY [**OBFUSCATED**]
hermes-webui-1  |   ++ Setting environment variable HERMES_WEBUI_HOST [0.0.0.0]
hermes-webui-1  |   ++ Setting environment variable HERMES_WEBUI_PORT [8787]
hermes-webui-1  |   ++ Setting environment variable HERMES_WEBUI_STATE_DIR [/home/hermeswebui/.hermes/webui-mvp]
hermes-webui-1  |   == Environment variable LANG [en_US.utf8] already set and value is unchanged
hermes-webui-1  |   == Environment variable LC_ALL [C] already set and value is unchanged
hermes-webui-1  |   @@ Overwriting environment variable PATH [/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin] -> [/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin]
hermes-webui-1  |   ++ Setting environment variable PYTHONDONTWRITEBYTECODE [1]
hermes-webui-1  |   ++ Setting environment variable PYTHONIOENCODING [utf-8]
hermes-webui-1  |   ++ Setting environment variable PYTHONUNBUFFERED [1]
hermes-webui-1  |   ++ Setting environment variable PYTHON_SHA256 [c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684]
hermes-webui-1  |   ++ Setting environment variable PYTHON_VERSION [3.12.13]
hermes-webui-1  |   == Environment variable WANTED_GID [1000] already set and value is unchanged
hermes-webui-1  |   == Environment variable WANTED_UID [1000] already set and value is unchanged
hermes-webui-1  |
hermes-webui-1  | -- Making sure /app is owned by the hermeswebui user to avoid permission issues when running the server
hermes-webui-1  | sending incremental file list
hermes-webui-1  | ./
hermes-webui-1  | .dockerignore
hermes-webui-1  | .gitignore
hermes-webui-1  | AGENTS.md
hermes-webui-1  | ARCHITECTURE.md
hermes-webui-1  | BUGS.md
hermes-webui-1  | CHANGELOG.md
hermes-webui-1  | Dockerfile
hermes-webui-1  | HERMES.md
hermes-webui-1  | LICENSE
hermes-webui-1  | README.md
hermes-webui-1  | ROADMAP.md
hermes-webui-1  | SPRINTS.md
hermes-webui-1  | TESTING.md
hermes-webui-1  | THEMES.md
hermes-webui-1  | docker-compose.yml
hermes-webui-1  | docker_init.bash
hermes-webui-1  | requirements.txt
hermes-webui-1  | server.py
hermes-webui-1  | start.sh
hermes-webui-1  | .github/
hermes-webui-1  | .github/workflows/
hermes-webui-1  | .github/workflows/release.yml
hermes-webui-1  | api/
hermes-webui-1  | api/__init__.py
hermes-webui-1  | api/auth.py
hermes-webui-1  | api/config.py
hermes-webui-1  | api/helpers.py
hermes-webui-1  | api/models.py
hermes-webui-1  | api/profiles.py
hermes-webui-1  | api/routes.py
hermes-webui-1  | api/startup.py
hermes-webui-1  | api/state_sync.py
hermes-webui-1  | api/streaming.py
hermes-webui-1  | api/updates.py
hermes-webui-1  | api/upload.py
hermes-webui-1  | api/workspace.py
hermes-webui-1  | docs/
hermes-webui-1  | docs/images/
hermes-webui-1  | docs/images/ui-sessions.png
hermes-webui-1  | docs/images/ui-workspace.png
hermes-webui-1  | static/
hermes-webui-1  | static/boot.js
hermes-webui-1  | static/commands.js
hermes-webui-1  | static/i18n.js
hermes-webui-1  | static/icons.js
hermes-webui-1  | static/index.html
hermes-webui-1  | static/login.js
hermes-webui-1  | static/messages.js
hermes-webui-1  | static/panels.js
hermes-webui-1  | static/sessions.js
hermes-webui-1  | static/style.css
hermes-webui-1  | static/ui.js
hermes-webui-1  | static/workspace.js
hermes-webui-1  |
hermes-webui-1  | sent 2,278,138 bytes  received 952 bytes  4,558,180.00 bytes/sec
hermes-webui-1  | total size is 2,274,549  speedup is 1.00
hermes-webui-1  |
hermes-webui-1  | == Checking required environment variables for hermes-webui
hermes-webui-1  |
hermes-webui-1  | -- HERMES_WEBUI_VERSION: Where to store sessions, workspaces, and other state (default: ~/.hermes/webui-mvp)
hermes-webui-1  | -- HERMES_WEBUI_STATE_DIR: /home/hermeswebui/.hermes/webui-mvp
hermes-webui-1  |
hermes-webui-1  | -- HERMES_WEBUI_DEFAULT_WORKSPACE: Default workspace directory shown on first launch
hermes-webui-1  | HERMES_WEBUI_DEFAULT_WORKSPACE not set, setting to /workspace
hermes-webui-1  | -- HERMES_WEBUI_DEFAULT_WORKSPACE: /workspace
hermes-webui-1  |
hermes-webui-1  | ===================
hermes-webui-1  |
hermes-webui-1  | == Installing uv and creating a new virtual environment for hermes-webui
hermes-webui-1  | downloading uv 0.11.6 x86_64-unknown-linux-gnu
hermes-webui-1  | installing to /home/hermeswebui/.local/bin
hermes-webui-1  |   uv
hermes-webui-1  |   uvx
hermes-webui-1  | everything's installed!
hermes-webui-1  |
hermes-webui-1  | To add $HOME/.local/bin to your PATH, either restart your shell or run:
hermes-webui-1  |
hermes-webui-1  |     source $HOME/.local/bin/env (sh, bash, zsh)
hermes-webui-1  |     source $HOME/.local/bin/env.fish (fish)
hermes-webui-1  | Using CPython 3.12.13 interpreter at: /usr/local/bin/python
hermes-webui-1  | Creating virtual environment at: venv
hermes-webui-1  | Activate with: source venv/bin/activate
hermes-webui-1  |
hermes-webui-1  | == Activating hermes webui's virtual environment
hermes-webui-1  |
hermes-webui-1  | == Installing hermes-webui dependencies
hermes-webui-1  | Using Python 3.12.13 environment at: venv
hermes-webui-1  | Resolved 1 package in 171ms
hermes-webui-1  | Prepared 1 package in 67ms
hermes-webui-1  | Installed 1 package in 6ms
hermes-webui-1  |  + pyyaml==6.0.3
hermes-webui-1  | Using Python 3.12.13 environment at: venv
hermes-webui-1  | Resolved 2 packages in 160ms
hermes-webui-1  | Downloading pip (1.7MiB)
hermes-webui-1  |  Downloaded pip
hermes-webui-1  | Prepared 2 packages in 161ms
hermes-webui-1  | Installed 2 packages in 26ms
hermes-webui-1  |  + pip==26.0.1
hermes-webui-1  |  + setuptools==82.0.1
hermes-webui-1  |
hermes-webui-1  | == Adding hermes-agent's pyproject.toml base dependencies to the virtual environment
hermes-webui-1  | Using Python 3.12.13 environment at: venv
hermes-webui-1  | Resolved 57 packages in 769ms
hermes-webui-1  |    Building hermes-agent @ file:///home/hermeswebui/.hermes/hermes-agent
hermes-webui-1  | Downloading pygments (1.2MiB)
hermes-webui-1  | Downloading pydantic-core (2.0MiB)
hermes-webui-1  | Downloading cryptography (4.3MiB)
hermes-webui-1  | Downloading aiohttp (1.7MiB)
hermes-webui-1  | Downloading openai (1.1MiB)
hermes-webui-1  |  Downloaded pydantic-core
hermes-webui-1  |  Downloaded aiohttp
hermes-webui-1  |  Downloaded cryptography
hermes-webui-1  |  Downloaded pygments
hermes-webui-1  |  Downloaded openai
hermes-webui-1  |       Built hermes-agent @ file:///home/hermeswebui/.hermes/hermes-agent
hermes-webui-1  | Prepared 56 packages in 3.08s
hermes-webui-1  | Installed 56 packages in 53ms
hermes-webui-1  |  + aiohappyeyeballs==2.6.1
hermes-webui-1  |  + aiohttp==3.13.5
hermes-webui-1  |  + aiosignal==1.4.0
hermes-webui-1  |  + annotated-types==0.7.0
hermes-webui-1  |  + anthropic==0.94.0
hermes-webui-1  |  + anyio==4.13.0
hermes-webui-1  |  + attrs==26.1.0
hermes-webui-1  |  + certifi==2026.2.25
hermes-webui-1  |  + cffi==2.0.0
hermes-webui-1  |  + charset-normalizer==3.4.7
hermes-webui-1  |  + cryptography==46.0.7
hermes-webui-1  |  + distro==1.9.0
hermes-webui-1  |  + docstring-parser==0.17.0
hermes-webui-1  |  + edge-tts==7.2.8
hermes-webui-1  |  + exa-py==2.11.0
hermes-webui-1  |  + fal-client==0.13.2
hermes-webui-1  |  + fire==0.7.1
hermes-webui-1  |  + firecrawl-py==4.22.1
hermes-webui-1  |  + frozenlist==1.8.0
hermes-webui-1  |  + h11==0.16.0
hermes-webui-1  |  + hermes-agent==0.8.0 (from file:///home/hermeswebui/.hermes/hermes-agent)
hermes-webui-1  |  + httpcore==1.0.9
hermes-webui-1  |  + httpx==0.28.1
hermes-webui-1  |  + httpx-sse==0.4.3
hermes-webui-1  |  + idna==3.11
hermes-webui-1  |  + jinja2==3.1.6
hermes-webui-1  |  + jiter==0.14.0
hermes-webui-1  |  + markdown-it-py==4.0.0
hermes-webui-1  |  + markupsafe==3.0.3
hermes-webui-1  |  + mdurl==0.1.2
hermes-webui-1  |  + msgpack==1.1.2
hermes-webui-1  |  + multidict==6.7.1
hermes-webui-1  |  + nest-asyncio==1.6.0
hermes-webui-1  |  + openai==2.31.0
hermes-webui-1  |  + parallel-web==0.4.2
hermes-webui-1  |  + prompt-toolkit==3.0.52
hermes-webui-1  |  + propcache==0.4.1
hermes-webui-1  |  + pycparser==3.0
hermes-webui-1  |  + pydantic==2.12.5
hermes-webui-1  |  + pydantic-core==2.41.5
hermes-webui-1  |  + pygments==2.20.0
hermes-webui-1  |  + pyjwt==2.12.1
hermes-webui-1  |  + python-dotenv==1.2.2
hermes-webui-1  |  + requests==2.33.1
hermes-webui-1  |  + rich==14.3.4
hermes-webui-1  |  + sniffio==1.3.1
hermes-webui-1  |  + tabulate==0.10.0
hermes-webui-1  |  + tenacity==9.1.4
hermes-webui-1  |  + termcolor==3.3.0
hermes-webui-1  |  + tqdm==4.67.3
hermes-webui-1  |  + typing-extensions==4.15.0
hermes-webui-1  |  + typing-inspection==0.4.2
hermes-webui-1  |  + urllib3==2.6.3
hermes-webui-1  |  + wcwidth==0.6.0
hermes-webui-1  |  + websockets==16.0
hermes-webui-1  |  + yarl==1.23.0
hermes-webui-1  |
hermes-webui-1  | == Running hermes-webui
hermes-webui-1  |
hermes-webui-1  |   Hermes Web UI -- startup config
hermes-webui-1  |   --------------------------------
hermes-webui-1  |   repo root   : /app
hermes-webui-1  |   agent dir   : /home/hermeswebui/.hermes/hermes-agent  [ok]
hermes-webui-1  |   python      : /app/venv/bin/python3
hermes-webui-1  |   state dir   : /home/hermeswebui/.hermes/webui-mvp
hermes-webui-1  |   workspace   : /workspace
hermes-webui-1  |   host:port   : 0.0.0.0:8787
hermes-webui-1  |   config file : /home/hermeswebui/.hermes/profiles/gemma4/config.yaml  (found)
hermes-webui-1  |
hermes-webui-1  | [ok] Running within container.
hermes-webui-1  | [!!] WARNING: Binding to 0.0.0.0 with NO PASSWORD SET.
hermes-webui-1  |      Anyone on the network can access your filesystem and agent.
hermes-webui-1  |      Set a password via Settings or HERMES_WEBUI_PASSWORD env var.
hermes-webui-1  |      To suppress: bind to 127.0.0.1 or set a password.
hermes-webui-1  |      Note: You are running within a container, must bind to 0.0.0.0 to published the port.
hermes-webui-1  |   Hermes Web UI listening on http://0.0.0.0:8787
hermes-webui-1  |   Remote access: ssh -N -L 8787:127.0.0.1:8787 <user>@<your-server>
hermes-webui-1  |   Then open:     http://localhost:8787

@mmartial

mmartial commented Apr 11, 2026

Copy link
Copy Markdown
Contributor Author

@nesquena-hermes ready for review

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Thanks for the detailed update and the new commits, @mmartial.

Looking at what you've addressed since the last review:

  • Port binding (item 3) ✅ — the "recover original default" commit and the updated startup log showing [ok] Running within container. with the in-container detection looks right. The server now correctly explains why it binds to 0.0.0.0 when inside a container, rather than just doing it silently.
  • HERMES_WEBUI_DEFAULT_WORKSPACE required (item 5) ✅ — the startup log shows HERMES_WEBUI_DEFAULT_WORKSPACE not set, setting to /workspace, which is the graceful fallback I was asking for.
  • Removing openai from requirements.txt (item 1) — the new startup log shows openai still being installed, but it's coming from hermes-agent's pyproject.toml dependencies, not from the webui's requirements.txt. That's fine — hermes-agent legitimately needs it. If the webui's own requirements.txt no longer lists it explicitly, that resolves the concern.
  • WANTED_UID/WANTED_GID in docker-compose.yml (item 2) — you noted that docker compose runs as root so ${UID:-1000} wouldn't capture the calling user's UID. That's correct — the shell variable UID is set by the user's shell but is not automatically exported to Docker Compose's environment. The practical options are either keeping the hardcoded 1000 default (with a README note to override it via a .env file or export UID GID before docker compose up), or documenting the docker run -e WANTED_UID=$(id -u) path as the more reliable approach. The current state seems reasonable as long as the README explains how to set WANTED_UID for non-1000 users.

If items 1 and 2 are addressed as described above, this looks ready for a final merge pass. Could you confirm:

  1. Is openai absent from the webui's own requirements.txt (even if it still gets installed via hermes-agent's deps)?
  2. Does the README explain how users with a non-1000 UID should set WANTED_UID before running docker compose up?

@nesquena

Copy link
Copy Markdown
Owner

Full Review: PR #237 — Docker user id/gid support

Thanks @mmartial! Real problem, working solution. The UID/GID mismatch on mounted volumes is a genuine pain point.

Security Audit

  • docker_init.bash uses sudo for usermod/groupmod/chown/rsync — necessary for the UID/GID switch pattern. Passwordless sudo is limited to the container.
  • Environment variable passthrough (save_env/load_env) obfuscates secrets (TOKEN, API, KEY patterns) — good.
  • rsync -av from /apptoo to /app with --chown is clean — the two-user approach (hermeswebuitoo for initial boot, hermeswebui with desired UID/GID for runtime) is a proven Docker pattern.

Code Review

Docker changes are solid. The Dockerfile, init script, and compose file work together correctly. The _get_ai_agent() lazy import retry in streaming.py is a good addition for the Docker case where deps install after first import.

Issues found:

  1. docker-compose.yml has duplicate WANTED_GID — lines show both WANTED_GID=${GID:-1000} and WANTED_GID=1000. The second overrides the first, making the env var ineffective. Remove the hardcoded duplicate.

  2. /apptoo workdir name is confusingWORKDIR /apptoo then rsync to /app works but the name doesn't convey intent. A comment explaining "staging dir for ownership transfer" would help.

  3. curl | sh for uv installcurl -LsSf https://astral.sh/uv/install.sh | sh runs at every container start. Should be in the Dockerfile build stage instead, or cached. Each restart re-downloads and reinstalls uv.

  4. Indentation fix in streaming.py — the _ENV_LOCK block indentation change (2-space → 4-space) is a good cleanup but it's bundled into this Docker PR. It matches the surrounding code style so it's fine, just noting it.

Tests

554 passed, 0 failed, 42 skipped. No regressions.

Verdict

Good to merge after fixing the duplicate WANTED_GID in docker-compose.yml. The rest is solid.

@mmartial

Copy link
Copy Markdown
Contributor Author

@nesquena-hermes:

  • Items 1 and 2 are addressed, including duplicate WANTED_GID in docker-compose.yml.
  • Listed issues are explained in comments in Dockerfile or echo in init script
  • Issue 3: uv is installed as the final user to avoid a system installation and proper ownership

…t will be ignored, please remove it to avoid potential confusion"
@mmartial

Copy link
Copy Markdown
Contributor Author

Note latest commit removes version in docker-compose.yml to address:

the attribute version is obsolete, it will be ignored, please remove it to avoid potential confusion

nesquena-hermes pushed a commit that referenced this pull request Apr 11, 2026
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.

Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.

Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
nesquena-hermes pushed a commit that referenced this pull request Apr 11, 2026
…le permissions (#243)

Adds response-layer credential redaction to three endpoints:
  - GET /api/session — messages[], tool_calls[], and title
  - GET /api/session/export — download also redacted
  - SSE done event — session payload in stream
  - GET /api/memory — MEMORY.md and USER.md content

Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.

Merged with #237 container detection changes in server.py.
nesquena-hermes added a commit that referenced this pull request Apr 11, 2026
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)

Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. &lt;code&gt; becoming &amp;lt;code&amp;gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.

Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.

Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.

Fixes #240

* fix: restore custom model list discovery with config api key (#238)

get_available_models() now reads api_key from config.yaml before env vars:
  1. model.api_key
  2. providers.<active>.api_key / providers.custom.api_key
  3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)

Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.

Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.

* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)

Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.

Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.

Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.

* security: redact credentials from API responses and fix credential file permissions (#243)

Adds response-layer credential redaction to three endpoints:
  - GET /api/session — messages[], tool_calls[], and title
  - GET /api/session/export — download also redacted
  - SSE done event — session payload in stream
  - GET /api/memory — MEMORY.md and USER.md content

Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.

Merged with #237 container detection changes in server.py.

* fix: cancel button now interrupts agent and cleans up UI state (#244)

Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.

Changes:
  - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
  - api/streaming.py: stores agent in AGENT_INSTANCES after creation,
    checks CANCEL_FLAGS immediately after store (race condition fix),
    calls agent.interrupt() in cancel_stream(), cleans up in finally block
  - static/boot.js: removes stale setStatus(cancelling) call
  - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel

Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.

New test file: tests/test_cancel_interrupt.py with 6 unit tests.

* docs: v0.46.0 release notes, bump version, update test counts

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Agent review — APPROVED ✅ (merged to v0.46.0)

Reviewed full diff, PR comments, all 7 comment threads.

What this adds: docker_init.bash entrypoint with hermeswebui/hermeswebuitoo user pattern. Solves the root-owned .hermes volume mount problem for non-root host users.

Fix applied during review: Removed a duplicate WANTED_GID=1000 hardcoded line in docker-compose.yml that appeared directly after WANTED_GID=${GID:-1000}. The hardcoded 1000 was winning the Docker env var precedence and defeating the purpose of the variable expansion.

Security review: Passwordless sudo is scoped to the container only. Environment variable save/load obfuscates values matching TOKEN, API, KEY patterns. No secrets leaked to the env file in plaintext. Port binding correctly returns to 127.0.0.1:8787:8787.

openai in requirements.txt: Confirmed removed in later commits — not present on the merged branch.

Tests: Docker changes don't affect Python tests. 624 passed on stage.

Merged to master via stage branch in PR #249 (v0.46.0).

JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request Apr 25, 2026
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (nesquena#239)

Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. &lt;code&gt; becoming &amp;lt;code&amp;gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.

Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.

Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.

Fixes nesquena#240

* fix: restore custom model list discovery with config api key (nesquena#238)

get_available_models() now reads api_key from config.yaml before env vars:
  1. model.api_key
  2. providers.<active>.api_key / providers.custom.api_key
  3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)

Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.

Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.

* feat: Docker UID/GID matching to avoid root-owned .hermes files (nesquena#237)

Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.

Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.

Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.

* security: redact credentials from API responses and fix credential file permissions (nesquena#243)

Adds response-layer credential redaction to three endpoints:
  - GET /api/session — messages[], tool_calls[], and title
  - GET /api/session/export — download also redacted
  - SSE done event — session payload in stream
  - GET /api/memory — MEMORY.md and USER.md content

Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.

Merged with nesquena#237 container detection changes in server.py.

* fix: cancel button now interrupts agent and cleans up UI state (nesquena#244)

Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.

Changes:
  - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
  - api/streaming.py: stores agent in AGENT_INSTANCES after creation,
    checks CANCEL_FLAGS immediately after store (race condition fix),
    calls agent.interrupt() in cancel_stream(), cleans up in finally block
  - static/boot.js: removes stale setStatus(cancelling) call
  - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel

Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.

New test file: tests/test_cancel_interrupt.py with 6 unit tests.

* docs: v0.46.0 release notes, bump version, update test counts

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (nesquena#239)

Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. &lt;code&gt; becoming &amp;lt;code&amp;gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.

Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.

Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.

Fixes nesquena#240

* fix: restore custom model list discovery with config api key (nesquena#238)

get_available_models() now reads api_key from config.yaml before env vars:
  1. model.api_key
  2. providers.<active>.api_key / providers.custom.api_key
  3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)

Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.

Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.

* feat: Docker UID/GID matching to avoid root-owned .hermes files (nesquena#237)

Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.

Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.

Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.

* security: redact credentials from API responses and fix credential file permissions (nesquena#243)

Adds response-layer credential redaction to three endpoints:
  - GET /api/session — messages[], tool_calls[], and title
  - GET /api/session/export — download also redacted
  - SSE done event — session payload in stream
  - GET /api/memory — MEMORY.md and USER.md content

Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.

Merged with nesquena#237 container detection changes in server.py.

* fix: cancel button now interrupts agent and cleans up UI state (nesquena#244)

Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.

Changes:
  - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
  - api/streaming.py: stores agent in AGENT_INSTANCES after creation,
    checks CANCEL_FLAGS immediately after store (race condition fix),
    calls agent.interrupt() in cancel_stream(), cleans up in finally block
  - static/boot.js: removes stale setStatus(cancelling) call
  - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel

Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.

New test file: tests/test_cancel_interrupt.py with 6 unit tests.

* docs: v0.46.0 release notes, bump version, update test counts

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants