diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..97e1eee --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +node_modules +dist +data +docker-data +backups +.env +.env.docker +.git +.gitignore +*.db +*.db-wal +*.db-shm +*.tsbuildinfo +test +coverage +.cursor-sqlite-audit.cjs diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..6c81248 --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,5 @@ +# Copy to .env.docker for docker compose (gitignored). +# Do not commit real secrets. + +JWT_SECRET=replace-with-a-strong-secret +CORS_ORIGIN=http://localhost:5174 diff --git a/.env.example b/.env.example index cab73ba..bebf437 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,9 @@ NODE_ENV=development # CORS (Vue dev server) CORS_ORIGIN=http://localhost:5173 -# SQLite — production database -# DB_PATH=E:/absolute/path/to/backend-nest/data/app.db +# SQLite — path inside container or on host (DB_PATH or DATABASE_PATH) +# DB_PATH=/app/data/app.db +# DATABASE_PATH=/app/data/app.db # JWT — must match legacy Fastify when migrating existing tokens JWT_SECRET=gold-land-dev-secret-change-in-production diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bd5580e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 + +FROM node:22.12.0-bookworm-slim AS builder + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ \ + && rm -rf /var/lib/apt/lists/* + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY nest-cli.json tsconfig.json tsconfig.build.json ./ +COPY src ./src + +RUN npm run build + +FROM node:22.12.0-bookworm-slim AS production + +WORKDIR /app + +ENV NODE_ENV=production \ + PORT=3000 \ + HOST=0.0.0.0 \ + DB_PATH=/app/data/app.db + +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ \ + && rm -rf /var/lib/apt/lists/* + +COPY package.json package-lock.json ./ + +RUN npm ci --omit=dev \ + && npm rebuild better-sqlite3 \ + && apt-get purge -y python3 make g++ \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/dist ./dist + +RUN mkdir -p /app/data \ + && chown -R node:node /app + +USER node + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:3000/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +CMD ["node", "dist/main.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cfded23 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + backend: + build: + context: . + dockerfile: Dockerfile + container_name: child-gold-land-backend + ports: + - "3000:3000" + env_file: + - .env.docker + environment: + NODE_ENV: production + PORT: 3000 + HOST: 0.0.0.0 + DB_PATH: /app/data/app.db + volumes: + - ./docker-data:/app/data + restart: unless-stopped diff --git a/src/config/env.validation.ts b/src/config/env.validation.ts index fbc047c..860eb7d 100644 --- a/src/config/env.validation.ts +++ b/src/config/env.validation.ts @@ -19,6 +19,11 @@ export class EnvironmentVariables { @IsString() DB_PATH?: string + /** Alias for DB_PATH (e.g. Docker Compose). DB_PATH takes precedence when both are set. */ + @IsOptional() + @IsString() + DATABASE_PATH?: string + @IsOptional() @IsString() JWT_SECRET?: string @@ -43,7 +48,7 @@ export function validateEnv(config: Record): EnvironmentVariabl PORT: Number(config.PORT ?? 3001), HOST: (config.HOST as string | undefined) ?? '0.0.0.0', CORS_ORIGIN: (config.CORS_ORIGIN as string | undefined) ?? 'http://localhost:5173', - DB_PATH: config.DB_PATH as string | undefined, + DB_PATH: (config.DB_PATH ?? config.DATABASE_PATH) as string | undefined, JWT_SECRET: jwtSecret ?? 'gold-land-dev-secret-change-in-production', } diff --git a/src/database/database.service.ts b/src/database/database.service.ts index b798fe4..e4be839 100644 --- a/src/database/database.service.ts +++ b/src/database/database.service.ts @@ -12,7 +12,10 @@ export class DatabaseService implements OnModuleInit, OnModuleDestroy { private readonly dbPath: string constructor(@Optional() @Inject(ConfigService) private readonly config?: ConfigService) { - const configured = this.config?.get('DB_PATH') ?? process.env.DB_PATH + const configured = + this.config?.get('DB_PATH') ?? + process.env.DB_PATH ?? + process.env.DATABASE_PATH this.dbPath = configured ?? join(process.cwd(), 'data', 'app.db') }