From d39ab010a0b888ae81addf4ff24328a6971d7d21 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sat, 27 Dec 2025 08:54:54 +0100 Subject: [PATCH] feat(docker): update Dockerfile for improved security and add entrypoint script for permission handling --- backend/Dockerfile | 32 +++++++++++++++-------- backend/docker-entrypoint.sh | 10 ++++++++ backend/src/db/client.ts | 50 +++++++++++++++++++++++++++++++----- 3 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 backend/docker-entrypoint.sh diff --git a/backend/Dockerfile b/backend/Dockerfile index a799091..8ef7e63 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,7 +4,6 @@ # Security measures applied: # - Non-root user execution # - Multi-stage build (minimal runtime image) -# - No shell in final image (distroless) # - Read-only filesystem compatible # - No unnecessary packages # - Specific image versions pinned @@ -31,16 +30,27 @@ RUN npm ci --omit=dev --ignore-scripts && \ npm cache clean --force # ----------------------------------------------------------------------------- -# Stage 2: Production Runtime (Distroless - no shell, minimal attack surface) +# Stage 2: Production Runtime # ----------------------------------------------------------------------------- -FROM gcr.io/distroless/nodejs22-debian12 AS runner +FROM node:22-slim AS runner WORKDIR /app -# Copy built application with correct ownership (nonroot = uid 65532) -COPY --from=builder --chown=65532:65532 /app/node_modules ./node_modules -COPY --from=builder --chown=65532:65532 /app/dist ./dist -COPY --from=builder --chown=65532:65532 /app/package.json ./ +# Create non-root user with specific UID for consistent bind mount permissions +RUN groupadd --gid 1000 appgroup && \ + useradd --uid 1000 --gid appgroup --shell /bin/sh --create-home appuser + +# Copy built application +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./ + +# Create data directory and set ownership +RUN mkdir -p /app/data && chown -R appuser:appgroup /app + +# Copy entrypoint script +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh # Environment configuration ENV NODE_ENV=production @@ -49,8 +59,8 @@ ENV PORT=3000 # Expose application port EXPOSE 3000 -# Run as non-root user (distroless default user) -USER nonroot +# Entrypoint runs as root to fix permissions, then drops to appuser +ENTRYPOINT ["/docker-entrypoint.sh"] -# Start application - migrations handled in index.ts -CMD ["dist/index.js"] +# Start application +CMD ["node", "dist/index.js"] diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh new file mode 100644 index 0000000..fbdea49 --- /dev/null +++ b/backend/docker-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +# Ensure data directory exists and has correct ownership +# This script runs as root, fixes permissions, then node runs as appuser via USER directive +mkdir -p /app/data +chown -R 1000:1000 /app/data + +# Execute the main command as appuser (UID 1000) +exec runuser -u appuser -- "$@" diff --git a/backend/src/db/client.ts b/backend/src/db/client.ts index 3fca9a6..26a3a59 100644 --- a/backend/src/db/client.ts +++ b/backend/src/db/client.ts @@ -1,7 +1,7 @@ import { createClient } from "@libsql/client"; import { drizzle } from "drizzle-orm/libsql"; -import { existsSync, mkdirSync } from "fs"; -import { dirname, resolve } from "path"; +import { existsSync, mkdirSync, accessSync, constants, statSync, writeFileSync } from "fs"; +import { resolve } from "path"; import dotenv from "dotenv"; dotenv.config({ path: process.env.DOTENV_PATH || ".env" }); @@ -11,13 +11,49 @@ const dataDir = resolve(process.cwd(), "data"); const dbPath = resolve(dataDir, "medassist-ng.db"); const url = `file:${dbPath}`; -// Ensure data directory exists before creating database -if (!existsSync(dataDir)) { - mkdirSync(dataDir, { recursive: true }); - console.log(`[DB] Created data directory: ${dataDir}`); +console.log(`[DB] Data directory: ${dataDir}`); +console.log(`[DB] Database path: ${dbPath}`); +console.log(`[DB] Database URL: ${url}`); + +// Ensure data directory exists and is writable +try { + if (!existsSync(dataDir)) { + mkdirSync(dataDir, { recursive: true }); + console.log(`[DB] Created data directory: ${dataDir}`); + } else { + console.log(`[DB] Data directory exists: ${dataDir}`); + } + + // Check if directory is writable + accessSync(dataDir, constants.W_OK); + console.log(`[DB] Data directory is writable`); + + // Log directory stats + const stats = statSync(dataDir); + console.log(`[DB] Directory permissions: ${stats.mode.toString(8)}`); + console.log(`[DB] Directory UID: ${stats.uid}, GID: ${stats.gid}`); + + // Try to create a test file to verify write access + const testFile = resolve(dataDir, ".write-test"); + writeFileSync(testFile, "test"); + console.log(`[DB] Write test successful`); + +} catch (err: any) { + console.error(`[DB] ERROR: Cannot access data directory: ${err.message}`); + console.error(`[DB] Please ensure the volume mount has correct permissions.`); + console.error(`[DB] Try running on host: sudo chown -R 1000:1000 ${dataDir}`); + process.exit(1); } -const client = createClient({ url }); +let client; +try { + client = createClient({ url }); + console.log(`[DB] Database client created successfully`); +} catch (err: any) { + console.error(`[DB] ERROR: Failed to create database client: ${err.message}`); + console.error(`[DB] Database path: ${dbPath}`); + process.exit(1); +} export const db = drizzle(client);