diff --git a/Dockerfile b/Dockerfile index 94778e4..69d4d64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,27 +13,30 @@ FROM node:22-alpine WORKDIR /app ENV NODE_ENV=production -# production dependencies only (ws) +# production dependencies only (ws) + su-exec for privilege dropping COPY package.json package-lock.json ./ -RUN npm ci --omit=dev && npm cache clean --force +RUN npm ci --omit=dev && npm cache clean --force && \ + apk add --no-cache su-exec COPY server/server.mjs server/server.mjs COPY server/db.mjs server/db.mjs COPY shared/ shared/ COPY --from=build /app/dist dist/ +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh # persistent account/progress database (mount a volume here on the host) -RUN mkdir -p /app/data && chown -R node:node /app/data +RUN mkdir -p /app/data VOLUME ["/app/data"] -# run as unprivileged user -USER node - EXPOSE 3001 ENV PORT=3001 ENV DATA_DIR=/app/data +# The entrypoint fixes permissions on bind-mounted /app/data and drops +# privileges to the unprivileged "node" user before running the app. +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +CMD ["node", "server/server.mjs"] + HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD node -e "fetch('http://localhost:3001/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" - -CMD ["node", "server/server.mjs"] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..dc93ab7 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +# Ensure the data directory exists and is owned by the node user. +# This fixes permission issues with bind-mounted volumes (e.g. ./data:/app/data) +# where the host directory is owned by root but the container runs as node. +mkdir -p /app/data +chown -R node:node /app/data 2>/dev/null || true + +# Drop privileges and run the actual application as node +exec su-exec node "$@" \ No newline at end of file