diff --git a/infra/nodered/Dockerfile b/infra/nodered/Dockerfile
index 42b3122..ff23a4c 100644
--- a/infra/nodered/Dockerfile
+++ b/infra/nodered/Dockerfile
@@ -12,6 +12,9 @@ FROM nodered/node-red:5.0.0
# Install Dashboard 2.0 (and any future deps) at image-build time.
# Versions are pinned in infra/nodered/package.json for reproducibility.
+# The NR base image defaults USER to node-red (uid 1000), so switch to root
+# for the install (writes to /data/node_modules need root then chown).
+USER root
COPY package.json /data/package.json
WORKDIR /data
RUN npm install --omit=dev --no-audit --no-fund \
@@ -19,9 +22,23 @@ RUN npm install --omit=dev --no-audit --no-fund \
&& echo "[Dockerfile] installed @flowfuse/node-red-dashboard"
# start.sh handles the SPA base-href patch (idempotent: grep guard).
-# Chmod so the compose `user: 1000:1000` can run it.
-COPY start.sh /data/start.sh
-RUN chmod +x /data/start.sh
+# Inline-write + chmod so we don't hit the COPY-then-chmod permission issue
+# (the base image's /data ACLs block chmod on COPY'd files even as root).
+RUN printf "%s\n" \
+ "#!/bin/sh" \
+ "INDEX=/data/node_modules/@flowfuse/node-red-dashboard/dist/index.html" \
+ "BASE_HREF=\"\${DASHBOARD_BASE_HREF:-/dashboard/}\"" \
+ "if [ -f \"\$INDEX\" ] && ! grep -q \"" \
+ " /data/start.sh \
+ && chmod +x /data/start.sh \
+ && chown 1000:1000 /data/start.sh
-# Reset to NR's working dir; userDir is /data via the compose volume mount.
+# Drop back to NR's default user; userDir is /data via the compose volume mount.
+USER node-red
WORKDIR /usr/src/node-red