Docker: Build Optimization and Enhanced Non-Root/Volumeless Support (#5024) * docker: optimize build layers and enhance permission handling - Pre-created hardcoded dirs in Dockerfile to support volumeless non-root runs. - Enhanced slightly docker-entrypoint.sh with robust volume detection and safer chown logic. - Included legacy 'backups' directory... again. - Added dos2unix to install list. - Updated .dockerignore - Updated comments - Smaller fixes * fix(docker): removed unnecessary comment, and the... *sighs* backups dir, again * Exclude DS_Store everywhere * Exclude tests and all jsconfigs from docker images * Exclude local plugins from docker builds * fix(docker): backups are back... yay xD * feat(docker): add robust healthcheck script - Added `docker/healthcheck.cjs`: A standalone, dependency-free Node.js script for verifying server status. - Updated `Dockerfile`: Added HEALTHCHECK instruction and script copy step. - Features: Auto-detects port from env/config, handles IPv4/IPv6 fallback, auto-retries HTTPS on socket hangup, and sets custom User-Agent. * Fix .dockerignore permission * Revert "feat(docker): add robust healthcheck script" This reverts commit fa634fb08884cdef9245a12271cb9a13b487365f. --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -1,21 +1,61 @@ | |||
| 1 | # --- Git & CI --- | ||
| 1 | .git | 2 | .git |
| 2 | .github | 3 | .github |
| 3 | .vscode | 4 | .gitignore |
| 4 | node_modules | 5 | |
| 5 | npm-debug.log | 6 | # --- Docker --- |
| 6 | readme* | 7 | /Dockerfile |
| 7 | Start.bat | 8 | /.dockerignore |
| 8 | /dist | 9 | /docker/docker-compose.yml |
| 9 | /backups | ||
| 10 | cloudflared.exe | ||
| 11 | access.log | ||
| 12 | /data | ||
| 13 | /cache | ||
| 14 | .DS_Store | ||
| 15 | /public/scripts/extensions/third-party | ||
| 16 | /colab | ||
| 17 | .gemini | ||
| 18 | /docker/config | 10 | /docker/config |
| 19 | /docker/extensions | 11 | /docker/extensions |
| 20 | /docker/data | 12 | /docker/data |
| 21 | /docker/plugins | 13 | /docker/plugins |
| 14 | /public/scripts/extensions/third-party | ||
| 15 | |||
| 16 | # --- Plugins (keep only package files) --- | ||
| 17 | /plugins/* | ||
| 18 | !/plugins/package.json | ||
| 19 | !/plugins/package-lock.json | ||
| 20 | |||
| 21 | # --- The Folders --- | ||
| 22 | /backups | ||
| 23 | /cache | ||
| 24 | /colab | ||
| 25 | /data | ||
| 26 | /dist | ||
| 27 | /node_modules | ||
| 28 | /tests | ||
| 29 | |||
| 30 | # --- Sensitive Info --- | ||
| 31 | **/.env* | ||
| 32 | **/*.pem | ||
| 33 | **/certs | ||
| 34 | |||
| 35 | # --- Documentation --- | ||
| 36 | readme* | ||
| 37 | *.md | ||
| 38 | Update-Instructions.txt | ||
| 39 | |||
| 40 | # --- OS & System Junk --- | ||
| 41 | **/.DS_Store | ||
| 42 | *.bat | ||
| 43 | *.cmd | ||
| 44 | *.exe | ||
| 45 | start.sh | ||
| 46 | |||
| 47 | # --- Dev Config --- | ||
| 48 | .editorconfig | ||
| 49 | .eslintrc.cjs | ||
| 50 | .eslintrc* | ||
| 51 | .vscode | ||
| 52 | **/jsconfig.json | ||
| 53 | .npmignore | ||
| 54 | .gemini | ||
| 55 | replit.nix | ||
| 56 | .replit | ||
| 57 | .nomedia | ||
| 58 | |||
| 59 | # -- Logs & Temp --- | ||
| 60 | *.log | ||
| 61 | **/tmp | ||
| @@ -4,8 +4,8 @@ FROM node:lts-alpine3.23 | |||
| 4 | ARG APP_HOME=/home/node/app | 4 | ARG APP_HOME=/home/node/app |
| 5 | 5 | ||
| 6 | # Install system dependencies | 6 | # Install system dependencies |
| 7 | # Added su-exec and shadow to support optional PUID/PGID user mapping | 7 | # "Don't rely on the base image for tools; if you call it, you install it." ;) |
| 8 | RUN apk add --no-cache gcompat tini git git-lfs su-exec shadow | 8 | RUN apk add --no-cache gcompat tini git git-lfs su-exec shadow dos2unix |
| 9 | 9 | ||
| 10 | # Create app directory and set ownership | 10 | # Create app directory and set ownership |
| 11 | WORKDIR ${APP_HOME} | 11 | WORKDIR ${APP_HOME} |
| @@ -21,28 +21,28 @@ RUN \ | |||
| 21 | echo "*** Install npm packages ***" && \ | 21 | echo "*** Install npm packages ***" && \ |
| 22 | npm ci --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force | 22 | npm ci --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force |
| 23 | 23 | ||
| 24 | # Create config directory and link config.yaml | 24 | # Create config directory and link config.yaml. Added hardcoded dirs(constants.js?) |
| 25 | # that must be present for Non-Root Mode and volumeless docker runs. | ||
| 25 | RUN \ | 26 | RUN \ |
| 26 | rm -f "config.yaml" || true && \ | 27 | rm -f "config.yaml" || true && \ |
| 27 | ln -s "./config/config.yaml" "config.yaml" || true && \ | 28 | mkdir -p config data plugins public/scripts/extensions/third-party backups && \ |
| 28 | mkdir "config" || true | 29 | chown -R node:node config data plugins public/scripts/extensions/third-party backups && \ |
| 29 | # Set ownership | 30 | ln -s "./config/config.yaml" "config.yaml" |
| 30 | RUN chown -R node:node config | ||
| 31 | 31 | ||
| 32 | # Pre-compile public libraries | 32 | # Pre-compile public libraries |
| 33 | RUN \ | 33 | RUN \ |
| 34 | echo "*** Run Webpack ***" && \ | 34 | echo "*** Run Webpack ***" && \ |
| 35 | node "./docker/build-lib.js" | 35 | node "./docker/build-lib.js" |
| 36 | 36 | ||
| 37 | # Set the entrypoint script | 37 | # Set the entrypoint script and cleanup |
| 38 | RUN \ | 38 | RUN \ |
| 39 | echo "*** Cleanup ***" && \ | 39 | echo "*** Cleanup ***" && \ |
| 40 | mv "./docker/docker-entrypoint.sh" "./" && \ | 40 | mv "./docker/docker-entrypoint.sh" "./" && \ |
| 41 | rm -rf "./docker" && \ | ||
| 42 | echo "*** Make docker-entrypoint.sh executable ***" && \ | 41 | echo "*** Make docker-entrypoint.sh executable ***" && \ |
| 43 | chmod +x "./docker-entrypoint.sh" && \ | 42 | chmod +x "./docker-entrypoint.sh" && \ |
| 44 | echo "*** Convert line endings to Unix format ***" && \ | 43 | echo "*** Convert line endings to Unix format ***" && \ |
| 45 | dos2unix "./docker-entrypoint.sh" | 44 | dos2unix "./docker-entrypoint.sh" && \ |
| 45 | rm -rf "./docker" | ||
| 46 | 46 | ||
| 47 | # Fix extension repos permissions | 47 | # Fix extension repos permissions |
| 48 | RUN git config --global --add safe.directory "*" | 48 | RUN git config --global --add safe.directory "*" |
| @@ -8,7 +8,7 @@ start_sillytavern() { | |||
| 8 | # Config Check | 8 | # Config Check |
| 9 | if [ ! -e "config/config.yaml" ]; then | 9 | if [ ! -e "config/config.yaml" ]; then |
| 10 | echo "Resource not found, copying from defaults: config.yaml" | 10 | echo "Resource not found, copying from defaults: config.yaml" |
| 11 | $PREFIX cp -r "default/config.yaml" "config/config.yaml" | 11 | $PREFIX cp "default/config.yaml" "config/config.yaml" |
| 12 | fi | 12 | fi |
| 13 | 13 | ||
| 14 | # Execute postinstall to auto-populate config.yaml with missing values | 14 | # Execute postinstall to auto-populate config.yaml with missing values |
| @@ -20,7 +20,7 @@ start_sillytavern() { | |||
| 20 | 20 | ||
| 21 | # Dirs that MUST be present at this point (e.g for volumeless docker runs). | 21 | # Dirs that MUST be present at this point (e.g for volumeless docker runs). |
| 22 | # Please update list, if in the future a related perm issue appear. | 22 | # Please update list, if in the future a related perm issue appear. |
| 23 | CORE_DIRS="config data plugins public/scripts/extensions/third-party" | 23 | CORE_DIRS="config data plugins public/scripts/extensions/third-party backups" |
| 24 | 24 | ||
| 25 | # Mounted Volumes (External) | 25 | # Mounted Volumes (External) |
| 26 | # Parse mounts, handling files vs directories | 26 | # Parse mounts, handling files vs directories |
| @@ -35,11 +35,7 @@ for mount in $RAW_MOUNTS; do | |||
| 35 | 35 | ||
| 36 | # Performance Safety: If the file is in the root of the app, | 36 | # Performance Safety: If the file is in the root of the app, |
| 37 | # we do NOT add the parent (App Root), or we will recursively scan the whole app. | 37 | # we do NOT add the parent (App Root), or we will recursively scan the whole app. |
| 38 | if [ "$PARENT_DIR" = "/home/node/app" ]; then | 38 | [ "$PARENT_DIR" != "/home/node/app" ] && MOUNTED_DIRS="$MOUNTED_DIRS $PARENT_DIR" || MOUNTED_DIRS="$MOUNTED_DIRS $mount" |
| 39 | MOUNTED_DIRS="$MOUNTED_DIRS $mount" | ||
| 40 | else | ||
| 41 | MOUNTED_DIRS="$MOUNTED_DIRS $PARENT_DIR" | ||
| 42 | fi | ||
| 43 | else | 39 | else |
| 44 | # It is a directory, add it directly | 40 | # It is a directory, add it directly |
| 45 | MOUNTED_DIRS="$MOUNTED_DIRS $mount" | 41 | MOUNTED_DIRS="$MOUNTED_DIRS $mount" |
| @@ -53,21 +49,19 @@ CHECK_DIRS=$(echo "$CORE_DIRS $MOUNTED_DIRS" | tr ' ' '\n' | sort -u) | |||
| 53 | for dir in $CHECK_DIRS; do | 49 | for dir in $CHECK_DIRS; do |
| 54 | if [ ! -e "$dir" ]; then | 50 | if [ ! -e "$dir" ]; then |
| 55 | echo "Creating missing directory: $dir" | 51 | echo "Creating missing directory: $dir" |
| 56 | mkdir -p "$dir" | 52 | mkdir -p "$dir" 2>/dev/null || echo "Warning: Could not create $dir" >&2 |
| 57 | fi | 53 | fi |
| 58 | done | 54 | done |
| 59 | 55 | ||
| 60 | # Change permissions only if started as Root(UID 0) and needed. | 56 | # Mode Selection |
| 61 | if [ "$(id -u)" = "0" ]; then | 57 | if [ "$(id -u)" = "0" ]; then |
| 62 | # Check if PUID/PGID variables are provided | 58 | # Check if PUID/PGID variables are provided |
| 63 | if [ -n "$PUID" ] && [ -n "$PGID" ]; then | 59 | if [ -n "$PUID" ] && [ -n "$PGID" ]; then |
| 64 | TARGET_UID=$PUID | 60 | echo "Mode: PUID/PGID (UID:$PUID GID:$PGID)" |
| 65 | TARGET_GID=$PGID | ||
| 66 | echo "Non-root mode requested (UID:$TARGET_UID GID:$TARGET_GID)." | ||
| 67 | 61 | ||
| 68 | # Update the internal 'node' user to match requested IDs | 62 | # Update the internal 'node' user to match requested IDs |
| 69 | groupmod -o -g "$TARGET_GID" node | 63 | groupmod -o -g "$PGID" node |
| 70 | usermod -o -u "$TARGET_UID" -g "$TARGET_GID" node | 64 | usermod -o -u "$PUID" -g "$PGID" node |
| 71 | 65 | ||
| 72 | for dir in $CHECK_DIRS; do | 66 | for dir in $CHECK_DIRS; do |
| 73 | if [ -d "$dir" ]; then | 67 | if [ -d "$dir" ]; then |
| @@ -75,11 +69,9 @@ if [ "$(id -u)" = "0" ]; then | |||
| 75 | DIR_UID=$(stat -c '%u' "$dir") | 69 | DIR_UID=$(stat -c '%u' "$dir") |
| 76 | DIR_GID=$(stat -c '%g' "$dir") | 70 | DIR_GID=$(stat -c '%g' "$dir") |
| 77 | 71 | ||
| 78 | if [ "$DIR_UID" != "$TARGET_UID" ] || [ "$DIR_GID" != "$TARGET_GID" ]; then | 72 | if [ "$DIR_UID" != "$PUID" ] || [ "$DIR_GID" != "$PGID" ]; then |
| 79 | echo "(Detected mismatch) Adjusting permissions for: $dir." | 73 | echo "(Detected mismatch) Adjusting permissions for: $dir." |
| 80 | if ! chown -R node:node "$dir"; then | 74 | chown -R node:node "$dir" || echo "Warning: Failed to update permissions for '$dir'." >&2 |
| 81 | echo "Error: Failed to update permissions for '$dir'." | ||
| 82 | fi | ||
| 83 | fi | 75 | fi |
| 84 | fi | 76 | fi |
| 85 | done | 77 | done |
| @@ -87,16 +79,17 @@ if [ "$(id -u)" = "0" ]; then | |||
| 87 | # Fix config file specifically | 79 | # Fix config file specifically |
| 88 | chown node:node "config/config.yaml" 2>/dev/null | 80 | chown node:node "config/config.yaml" 2>/dev/null |
| 89 | 81 | ||
| 82 | # Set execution prefix to run as 'node' user | ||
| 90 | EXEC_PREFIX="su-exec node:node" | 83 | EXEC_PREFIX="su-exec node:node" |
| 91 | else | 84 | else |
| 92 | # Default: Run as Root (original behavior) | 85 | # Default: Run as Root (original behavior) |
| 93 | echo "Running in default (root) mode." | 86 | echo "Mode: Default (Root)" |
| 94 | EXEC_PREFIX="" | 87 | EXEC_PREFIX="" |
| 95 | fi | 88 | fi |
| 96 | 89 | ||
| 97 | else | 90 | else |
| 98 | # Non-Root Mode (Docker CLI --user flag) | 91 | # Non-Root Mode (Docker CLI --user flag) |
| 99 | echo "Running as detected user (UID: $(id -u))." | 92 | echo "Mode: Strict Non-Root (UID: $(id -u))" |
| 100 | # We CANNOT auto-fix permissions in this mode because we lack privileges. | 93 | # We CANNOT auto-fix permissions in this mode because we lack privileges. |
| 101 | # Relying solely on the user configuring their host permissions correctly. | 94 | # Relying solely on the user configuring their host permissions correctly. |
| 102 | EXEC_PREFIX="" | 95 | EXEC_PREFIX="" |