diff --git a/docker/Dockerfile.app b/docker/Dockerfile.app index b5466fe3..1d9ce32b 100644 --- a/docker/Dockerfile.app +++ b/docker/Dockerfile.app @@ -65,7 +65,8 @@ RUN if [ -n "$APK_MIRROR_ARG" ]; then \ build-essential postgresql-client default-mysql-client ca-certificates tzdata sed curl bash vim wget \ libsqlite3-0 \ python3 python3-pip python3-dev libffi-dev libssl-dev \ - nodejs npm && \ + nodejs npm \ + gosu && \ python3 -m pip install --break-system-packages --upgrade pip setuptools wheel && \ mkdir -p /home/appuser/.local/bin && \ curl -LsSf https://astral.sh/uv/install.sh | CARGO_HOME=/home/appuser/.cargo UV_INSTALL_DIR=/home/appuser/.local/bin sh && \ @@ -89,16 +90,20 @@ COPY --from=builder /app/scripts ./scripts COPY --from=builder /app/migrations ./migrations COPY --from=builder /app/dataset/samples ./dataset/samples COPY --from=builder /app/skills/preloaded ./skills/preloaded +# Keep a read-only backup so bind-mount cannot erase built-in skills +COPY --from=builder /app/skills/preloaded ./skills/_builtin COPY --from=builder /root/.duckdb /home/appuser/.duckdb COPY --from=builder /app/WeKnora . +# Copy and make entrypoint script executable +COPY --from=builder /app/scripts/docker-entrypoint.sh ./scripts/docker-entrypoint.sh + # Make scripts executable RUN chmod +x ./scripts/*.sh # Expose ports EXPOSE 8080 -# Switch to non-root user and run the application directly -USER appuser +ENTRYPOINT ["./scripts/docker-entrypoint.sh"] CMD ["./WeKnora"] diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh new file mode 100755 index 00000000..a32b6fb3 --- /dev/null +++ b/scripts/docker-entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +# ─── Fix ownership of bind-mounted directories ─── +# When users bind-mount host directories (e.g. ./skills/preloaded), +# the mount inherits the host UID/GID which may differ from the +# container's appuser. This entrypoint runs as root, fixes ownership, +# then drops privileges to appuser via gosu — the same pattern used +# by official postgres/redis images. + +# Directories that may be bind-mounted and need appuser access +MOUNT_DIRS=( + /app/skills/preloaded + /data/files +) + +for dir in "${MOUNT_DIRS[@]}"; do + if [ -d "$dir" ]; then + chown -R appuser:appuser "$dir" 2>/dev/null || true + fi +done + +# ─── Merge built-in skills into preloaded ─── +# Built-in skills are backed up at /app/skills/_builtin during image build. +# After a bind-mount replaces /app/skills/preloaded, copy back any +# missing built-in skills (without overwriting user-provided ones). +BUILTIN_DIR="/app/skills/_builtin" +PRELOADED_DIR="/app/skills/preloaded" + +if [ -d "$BUILTIN_DIR" ]; then + mkdir -p "$PRELOADED_DIR" + for skill_dir in "$BUILTIN_DIR"/*/; do + [ -d "$skill_dir" ] || continue + skill_name="$(basename "$skill_dir")" + if [ ! -d "$PRELOADED_DIR/$skill_name" ]; then + cp -r "$skill_dir" "$PRELOADED_DIR/$skill_name" + fi + done + chown -R appuser:appuser "$PRELOADED_DIR" +fi + +# ─── Drop privileges and exec the main process ─── +exec gosu appuser "$@"