Repository: Yukasama/nextjs-template
Pull request #5: chore: add secure docker image
Author: login: Yukasama
Created: 2025-05-04T15:35:23Z | Merged: 2025-05-04T20:03:04Z

- make Docker image include private key secret


Diff:
diff --git a/.dockerignore b/.dockerignore
index 279b5b5..8caaad9 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,13 +7,6 @@
 !next.config.ts
 !public/
 !src/
-!.env.production
-
-# Optional: if you use custom server
-!server.js
 
 # Required for build
-!.next/
-!.dockerignore
-!tsconfig.json
-!Dockerfile
+!tsconfig.json
\ No newline at end of file
diff --git a/.env.example b/.env.example
index 90cff33..ff042f6 100644
--- a/.env.example
+++ b/.env.example
@@ -11,4 +11,9 @@ LOG_LEVEL=debug
 # ----------------------------------------
 # Code Analysis (SonarQube)
 # ----------------------------------------
-SONAR_TOKEN=token
\ No newline at end of file
+SONAR_TOKEN=token
+
+# ----------------------------------------
+# Private API Key (Example)
+# ----------------------------------------
+PRIVATE_EXAMPLE_API_KEY=
\ No newline at end of file
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 0d90325..eaa6277 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -35,6 +35,9 @@ jobs:
         run: pnpm install --frozen-lockfile --ignore-scripts
 
       - name: Build application
+        env:
+          NEXT_PUBLIC_HOST_URL: ${{ secrets.NEXT_PUBLIC_HOST_URL }}
+          PRIVATE_EXAMPLE_API_KEY: ${{ secrets.PRIVATE_EXAMPLE_API_KEY }}
         run: pnpm build
 
   lint:
@@ -80,6 +83,9 @@ jobs:
   tests:
     runs-on: ubuntu-latest
     timeout-minutes: 60
+    env:
+      NEXT_PUBLIC_HOST_URL: ${{ secrets.NEXT_PUBLIC_HOST_URL }}
+      PRIVATE_EXAMPLE_API_KEY: ${{ secrets.PRIVATE_EXAMPLE_API_KEY }}
     steps:
       - uses: actions/checkout@v4
       - uses: pnpm/action-setup@v4
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 17d4f33..9a74eb5 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -61,6 +61,8 @@ jobs:
           push: ${{ github.event_name != 'pull_request' }}
           platforms: ${{ matrix.platform }}
           tags: ghcr.io/${{ steps.owner.outputs.owner_lc }}/${{ env.IMAGE_NAME }}:${{ steps.tags.outputs.arch_tag }}
+          build-args: NEXT_PUBLIC_HOST_URL=${{ secrets.NEXT_PUBLIC_HOST_URL }}
+          secrets: "private_api_key=${{ secrets.PRIVATE_EXAMPLE_API_KEY }}"
           cache-from: type=registry,ref=ghcr.io/${{ steps.owner.outputs.owner_lc }}/${{ env.IMAGE_NAME }}:buildcache
           cache-to: type=registry,ref=ghcr.io/${{ steps.owner.outputs.owner_lc }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
 
@@ -95,4 +97,4 @@ jobs:
             --tag $REPO:$short_sha \
             --tag $REPO:latest \
             $REPO:${short_sha}-amd64 \
-            $REPO:${short_sha}-arm64
+            $REPO:${short_sha}-arm64
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index a340364..6d7400e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,6 +23,12 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
 FROM base AS builder
 WORKDIR /app
 
+# Has to be provided at build time with --build-arg
+ARG NEXT_PUBLIC_HOST_URL
+
+ENV NODE_ENV=production \
+    NEXT_TELEMETRY_DISABLED=1
+
 COPY --from=deps /app/node_modules ./node_modules
 COPY --from=deps /app/package.json /app/pnpm-lock.yaml ./
 
@@ -31,9 +37,11 @@ COPY tsconfig.json ./
 COPY public ./public
 COPY src ./src
 
-ENV NEXT_TELEMETRY_DISABLED=1
-RUN pnpm build
-
+# Mount private API key as a secret
+# Has to be provided at build time with --secret id=private_api_key,required
+RUN --mount=type=secret,id=private_api_key,env=PRIVATE_EXAMPLE_API_KEY \
+    pnpm build
+  
 # --------------------------------------------------------
 # Stage 3: Run the application
 # --------------------------------------------------------
@@ -54,8 +62,7 @@ COPY --from=deps /app/node_modules ./node_modules
 COPY --from=builder /app/.next/standalone ./
 COPY --from=builder /app/.next/static ./.next/static
 
-# Drop privileges and expose only needed port
 USER nextjs
 EXPOSE 3000
 
-CMD ["node", "server.js"]
+CMD ["server.js"]
diff --git a/README.md b/README.md
index b7c8761..5f97637 100644
--- a/README.md
+++ b/README.md
@@ -13,38 +13,53 @@ pnpm exec playwright install
 pnpm dev
 ```
 
-## Use Docker
+## Features
+
+- Next.js 15
+- Tailwind v4
+- Dark Theme (next-themes)
+- End-To-End Tests (Playwright)
+- Logging (Pino)
+- Typesafe Environment Variables (t3-env)
+- Formatting (Prettier)
+- Linting (ESLint 9 + SonarQube)
+- A+ Security Headers (CSP with Nonce)
+- Docker Ready
+- CI/CD with GitHub Actions for Linting, Testing, Security
+
+## Guide
+
+### Use Docker
 
 ```bash
 # Build Docker image
-docker build -t nextjs-template:prod .
+# You have to pass NEXT_PUBLIC_ vars as build arguments
+# Secrets are passed with a secret mount as you can see in the Dockerfile
+docker build -t nextjs-template:prod --build-arg NEXT_PUBLIC_HOST_URL=http://localhost:3000 --secret id=env_file,src=.env .
 
 # Start with Docker compose
 cd resources/deploy
 docker compose up
 ```
 
-## SonarQube Scan
+### SonarQube Scan
 
 ```bash
 # Start SonarQube image
 cd resources/sonarqube
 docker compose up
 
-# Do sonar scan
+# Do sonar scan (from root directory)
 pnpm sonar
 ```
 
-## Features
+### GitHub Actions
+
+Note: For the CI to work, you have to add the following secrets to your repository:
+
+```bash
+NEXT_PUBLIC_HOST_URL=
+PRIVATE_EXAMPLE_API_KEY=
+SNYK_TOKEN=from-your-snyk-account
+```
 
-- Next.js 15
-- Tailwind v4
-- Dark Theme (next-themes)
-- End-To-End Tests (Playwright)
-- Logging (Pino)
-- Typesafe Environment Variables (t3-env)
-- Formatting (Prettier)
-- Linting (ESLint 9 + SonarQube)
-- A+ Security Headers (CSP with Nonce)
-- Docker Ready
-- CI/CD with GitHub Actions for Linting, Testing, Security
diff --git a/package.json b/package.json
index 7e72e8c..9b7b5e0 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
   "dependencies": {
     "@radix-ui/react-slot": "^1.2.0",
     "@t3-oss/env-nextjs": "^0.13.4",
-    "@tanstack/react-query": "^5.75.1",
+    "@tanstack/react-query": "^5.75.2",
     "@vercel/analytics": "^1.5.0",
     "@vercel/speed-insights": "^1.2.0",
     "class-variance-authority": "^0.7.1",
@@ -63,7 +63,7 @@
     "eslint-plugin-perfectionist": "^4.12.3",
     "eslint-plugin-playwright": "^2.2.0",
     "eslint-plugin-prefer-arrow-functions": "^3.6.2",
-    "eslint-plugin-prettier": "^5.2.6",
+    "eslint-plugin-prettier": "^5.3.1",
     "eslint-plugin-promise": "^7.2.1",
     "eslint-plugin-react-hooks": "^5.2.0",
     "eslint-plugin-regexp": "^2.7.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 34505b5..d03451a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,8 +15,8 @@ importers:
         specifier: ^0.13.4
         version: 0.13.4(arktype@2.1.20)(typescript@5.8.3)(zod@3.24.3)
       '@tanstack/react-query':
-        specifier: ^5.75.1
-        version: 5.75.1(react@19.1.0)
+        specifier: ^5.75.2
+        version: 5.75.2(react@19.1.0)
       '@vercel/analytics':
         specifier: ^1.5.0
         version: 1.5.0(next@15.3.1(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
@@ -148,8 +148,8 @@ importers:
         specifier: ^3.6.2
         version: 3.6.2(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)
       eslint-plugin-prettier:
-        specifier: ^5.2.6
-        version: 5.2.6(eslint-config-prettier@10.1.2(eslint@9.26.0(jiti@2.4.2)))(eslint@9.26.0(jiti@2.4.2))(prettier@3.2.5)
+        specifier: ^5.3.1
+        version: 5.3.1(eslint-config-prettier@10.1.2(eslint@9.26.0(jiti@2.4.2)))(eslint@9.26.0(jiti@2.4.2))(prettier@3.2.5)
       eslint-plugin-promise:
         specifier: ^7.2.1
         version: 7.2.1(eslint@9.26.0(jiti@2.4.2))
@@ -840,8 +840,8 @@ packages:
   '@tanstack/query-core@5.75.0':
     resolution: {integrity: sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg==}
 
-  '@tanstack/react-query@5.75.1':
-    resolution: {integrity: sha512-tN+gG+eXCHYm+VpmdXUP1rfE9LUrRzgYozTkBZtJV1/WFM3vwWNKQC8G6b2RKcs+2cPg+hdToZHZfjL3bF4yIQ==}
+  '@tanstack/react-query@5.75.2':
+    resolution: {integrity: sha512-8F8VOsWUfSkCFoi62O9HSZT9jDgg28Ln8Z2dYKfRo/O2A0sgvr0uxTuNoon3PPXoDuHofv5V3elBI1M2Gh1MPg==}
     peerDependencies:
       react: ^18 || ^19
 
@@ -1716,8 +1716,8 @@ packages:
     peerDependencies:
       eslint: '>=9.17.0'
 
-  eslint-plugin-prettier@5.2.6:
-    resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==}
+  eslint-plugin-prettier@5.3.1:
+    resolution: {integrity: sha512-vad9VWgEm9xaVXRNmb4aeOt0PWDc61IAdzghkbYQ2wavgax148iKoX1rNJcgkBGCipzLzOnHYVgL7xudM9yccQ==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
       '@types/eslint': '>=8.0.0'
@@ -4142,7 +4142,7 @@ snapshots:
 
   '@tanstack/query-core@5.75.0': {}
 
-  '@tanstack/react-query@5.75.1(react@19.1.0)':
+  '@tanstack/react-query@5.75.2(react@19.1.0)':
     dependencies:
       '@tanstack/query-core': 5.75.0
       react: 19.1.0
@@ -5115,7 +5115,7 @@ snapshots:
       - supports-color
       - typescript
 
-  eslint-plugin-prettier@5.2.6(eslint-config-prettier@10.1.2(eslint@9.26.0(jiti@2.4.2)))(eslint@9.26.0(jiti@2.4.2))(prettier@3.2.5):
+  eslint-plugin-prettier@5.3.1(eslint-config-prettier@10.1.2(eslint@9.26.0(jiti@2.4.2)))(eslint@9.26.0(jiti@2.4.2))(prettier@3.2.5):
     dependencies:
       eslint: 9.26.0(jiti@2.4.2)
       prettier: 3.2.5
diff --git a/resources/deploy/compose.yaml b/resources/deploy/compose.yaml
index 87b3936..540af18 100644
--- a/resources/deploy/compose.yaml
+++ b/resources/deploy/compose.yaml
@@ -1,11 +1,17 @@
 services:
   nextjs:
-    image: nextjs-template:prod # or use ghcr.io/yukasama/nextjs-template:latest
+    build:
+      context: ../..
+      args:
+        NEXT_PUBLIC_HOST_URL: http://localhost:3000
+      dockerfile: Dockerfile
     restart: unless-stopped
     ports:
       - '3000:3000'
     environment:
       - NODE_ENV=production
+    env_file:
+      - ../../.env
     cap_drop:
       - ALL
     cap_add:
diff --git a/scripts/sonar-scanner.js b/scripts/sonar-scanner.js
index 06ea395..b7b059d 100644
--- a/scripts/sonar-scanner.js
+++ b/scripts/sonar-scanner.js
@@ -5,15 +5,18 @@ import { scan } from 'sonarqube-scanner';
 dotenv.config();
 const sonarToken = process.env.SONAR_TOKEN;
 
-void scan(
+await scan(
   {
     login: sonarToken,
     options: {
       'sonar.exclusions':
         'node_modules/**,.extras/**,.scannerwork/*,.vscode/*,coverage/**,dist/*,log/*',
       'sonar.javascript.environments': 'node',
+      'sonar.javascript.file.suffixes': '.js,.jsx',
       'sonar.javascript.lcov.reportPaths': './coverage/lcov.info',
-      'sonar.projectDescription': 'Analyze stocks your way',
+      'sonar.lang.patterns.js': '**/*.js,**/*.jsx',
+      'sonar.lang.patterns.ts': '**/*.ts,**/*.tsx',
+      'sonar.projectDescription': 'A template for Next.js projects',
       'sonar.projectKey': 'nextjs-template',
       'sonar.projectName': 'nextjs-template',
       'sonar.projectVersion': '2024.04.0',
@@ -21,6 +24,7 @@ void scan(
       'sonar.sources': 'src',
       'sonar.tests': 'tests',
       'sonar.token': sonarToken,
+      'sonar.typescript.file.suffixes': '.ts,.tsx',
     },
     serverUrl: 'http://localhost:9000',
     token: sonarToken,
diff --git a/src/env.mjs b/src/env.mjs
index a8f91c2..02f2c10 100644
--- a/src/env.mjs
+++ b/src/env.mjs
@@ -3,21 +3,19 @@ import { z } from 'zod';
 
 export const env = createEnv({
   client: {
-    NEXT_PUBLIC_HOST_URL: z
-      .string()
-      .url()
-      .optional()
-      .default('http://localhost:3000'),
+    NEXT_PUBLIC_HOST_URL: z.string().url(),
   },
   runtimeEnv: {
     LOG_LEVEL: process.env.LOG_LEVEL,
     NEXT_PUBLIC_HOST_URL: process.env.NEXT_PUBLIC_HOST_URL,
     NODE_ENV: process.env.NODE_ENV,
+    PRIVATE_EXAMPLE_API_KEY: process.env.PRIVATE_EXAMPLE_API_KEY,
     SONAR_TOKEN: process.env.SONAR_TOKEN,
   },
   server: {
     LOG_LEVEL: z.string().optional(),
     NODE_ENV: z.string().optional(),
+    PRIVATE_EXAMPLE_API_KEY: z.string().min(1),
     SONAR_TOKEN: z.string().optional(),
   },
 });
diff --git a/src/lib/logger.ts b/src/lib/logger.ts
index f0e5954..edabefe 100644
--- a/src/lib/logger.ts
+++ b/src/lib/logger.ts
@@ -15,17 +15,15 @@ if (!fs.existsSync(logDir)) {
 
 const fileStream = pino.destination({ dest: logFile, sync: false });
 
+const baseOptions = {
+  base: { pid: false },
+  level: defaultLogLevel,
+};
+
 export const logger: pino.Logger = isProduction
-  ? pino(
-      {
-        base: { pid: false },
-        level: defaultLogLevel,
-      },
-      fileStream,
-    )
+  ? pino(baseOptions, fileStream)
   : pino({
-      base: { pid: false },
-      level: defaultLogLevel,
+      ...baseOptions,
       transport: {
         targets: [
           {
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index bdec1d6..91bbff2 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -2,7 +2,3 @@ import { type ClassValue, clsx } from 'clsx';
 import { twMerge } from 'tailwind-merge';
 
 export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
-
-export const capitalize = (text: string) => {
-  return text.charAt(0).toUpperCase() + text.slice(1);
-};


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR updates the project's environment configuration and deployment setup to build a secure Docker image that includes a private key secret. Key changes include:
- Updating the environment configuration to add PRIVATE_EXAMPLE_API_KEY.
- Modifying the Sonar scanner script to use top-level await and updating file suffix settings for proper scan coverage.
- Changing the Docker Compose setup to build the Docker image with custom build arguments and environment file configuration.

### Reviewed Changes

Copilot reviewed 4 out of 7 changed files in this pull request and generated no comments.

| File                           | Description                                                            |
| ------------------------------ | ---------------------------------------------------------------------- |
| src/env.mjs                    | Adjusted environment variable definitions; PRIVATE_EXAMPLE_API_KEY added. |
| scripts/sonar-scanner.js       | Changed scan invocation to use await and updated Sonar options.        |
| resources/deploy/compose.yaml  | Updated service build configuration and added env_file declarations.   |
| README.md                      | Minor update to sonar scan instructions.                               |

<details>
<summary>Files not reviewed (3)</summary>

* **.dockerignore**: Language not supported
* **.env.example**: Language not supported
* **Dockerfile**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/env.mjs:12**
* PRIVATE_EXAMPLE_API_KEY is added in the runtime configuration, which may expose sensitive data if the runtime environment is accessible to the client. Verify that this key is intended to be exposed only in secure contexts.
```
    PRIVATE_EXAMPLE_API_KEY: process.env.PRIVATE_EXAMPLE_API_KEY,
```
**scripts/sonar-scanner.js:8**
* Confirm that using top-level await in this context is supported by your Node.js version and module configuration; if not, consider wrapping the scan call within an async function.
```
await scan(
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-04T15:35:44Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR aims to update the project configuration for deployment and security by creating a secure Docker image that includes a private key secret. Key changes include:
- Removal of the unused "capitalize" utility function.
- Refactoring of logger configuration to use base options.
- Updates to environment variables, Docker Compose settings, and CI/CD configurations for improved security and scanning.

### Reviewed Changes

Copilot reviewed 7 out of 11 changed files in this pull request and generated 4 comments.

<details>
<summary>Show a summary per file</summary>

| File                                | Description                                                         |
| ----------------------------------- | ------------------------------------------------------------------- |
| src/lib/utils.ts                    | Removed the unused "capitalize" function                            |
| src/lib/logger.ts                   | Refactored logger initialization using a shared baseOptions variable  |
| src/env.mjs                         | Updated environment variable definitions for secure key management   |
| scripts/sonar-scanner.js            | Switched to await scan and added configuration enhancements          |
| resources/deploy/compose.yaml       | Changed nextjs service from using an image to building locally         |
| README.md                           | Clarified sonar scan instructions                                    |
| .github/workflows/snyk.yaml          | Commented out the GitHub Code Scanning upload step                     |
</details>


<details>
<summary>Files not reviewed (4)</summary>

* **.dockerignore**: Language not supported
* **.env.example**: Language not supported
* **.github/workflows/publish.yaml**: Language not supported
* **Dockerfile**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (3)</summary>

**src/lib/utils.ts:4**
* Removing the 'capitalize' function could break dependencies if any modules still use it. Confirm that its removal is intentional and update any dependent code accordingly.
```
export const capitalize = (text: string) => {
```
**src/env.mjs:6**
* Removing the default value for NEXT_PUBLIC_HOST_URL may require all environments to explicitly set this variable. Ensure that this change is coordinated with developers to avoid local environment issues.
```
NEXT_PUBLIC_HOST_URL: z.string().url(),
```
**scripts/sonar-scanner.js:8**
* Ensure that top-level await is supported in the target Node.js version to prevent runtime issues.
```
await scan(
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-04T15:47:37Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR updates the project's Docker setup to include secure handling of sensitive keys and secret management, along with associated configuration and documentation updates.
- Removed an unused capitalization utility from utils.ts.
- Refactored logger initialization and updated environment variable configuration to support a private key secret.
- Adjusted Docker build configuration, CI/CD workflows, and documentation for secure secret management.

### Reviewed Changes

Copilot reviewed 8 out of 12 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                              | Description                                                                                         |
| --------------------------------- | --------------------------------------------------------------------------------------------------- |
| src/lib/utils.ts                  | Removed the unused capitalize function to clean up legacy code.                                     |
| src/lib/logger.ts                 | Refactored logger initialization by introducing a baseOptions object for readability and reuse.     |
| src/env.mjs                       | Added PRIVATE_EXAMPLE_API_KEY to both runtime and server environments with stricter validation.     |
| scripts/sonar-scanner.js          | Replaced void with await for scanning and added additional file suffix configurations.              |
| resources/deploy/compose.yaml     | Updated to build the Docker image with build arguments and an environment file for improved security.|
| README.md                         | Revised documentation to reflect the new Docker build process and secret management practices.      |
| .github/workflows/snyk.yaml        | Commented out the SARIF upload step; ensure this is an intentional change.                          |
| .github/workflows/main.yaml        | Updated the build job to pass secrets (NEXT_PUBLIC_HOST_URL and PRIVATE_EXAMPLE_API_KEY) as env vars.   |
</details>


<details>
<summary>Files not reviewed (4)</summary>

* **.dockerignore**: Language not supported
* **.env.example**: Language not supported
* **.github/workflows/publish.yaml**: Language not supported
* **Dockerfile**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (4)</summary>

**src/lib/utils.ts:4**
* [nitpick] Confirm that the removal of the capitalize function is intentional and no components depend on it.
```
export const capitalize = (text: string) => {
```
**scripts/sonar-scanner.js:8**
* [nitpick] Verify that your Node.js environment supports top-level await in this file; ensure the module configuration allows for it.
```
await scan(
```
**README.md:38**
* [nitpick] Ensure that the documentation about secret handling (i.e., the use of secret mounts) accurately reflects the Dockerfile configuration and secure secret management practices.
```
docker build -t nextjs-template:prod --build-arg NEXT_PUBLIC_HOST_URL=http://localhost:3000 .
```
**.github/workflows/snyk.yaml:38**
* [nitpick] Confirm that disabling the SARIF upload step is intentional and that code scanning results are managed elsewhere.
```
# - name: Upload result to GitHub Code Scanning
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-04T19:17:18Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR aims to enhance the security of the Docker image by including a private key secret and updating related configurations.  
- Removed an unused utility function ("capitalize") to streamline the code.  
- Refactored logger instantiation and updated environment variable validations to support the new secure configuration.  
- Updated Docker, SonarQube, and GitHub workflow configurations to properly pass the secure variables.

### Reviewed Changes

Copilot reviewed 8 out of 11 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                | Description                                                         |
| ----------------------------------- | ------------------------------------------------------------------- |
| src/lib/utils.ts                    | Removed the unused "capitalize" utility function.                   |
| src/lib/logger.ts                   | Refactored logger creation using a shared "baseOptions" constant.     |
| src/env.mjs                         | Updated environment variable definitions, notably removing a default for NEXT_PUBLIC_HOST_URL and adding PRIVATE_EXAMPLE_API_KEY. |
| scripts/sonar-scanner.js            | Changed invocation to top-level await and updated SonarQube options.  |
| resources/deploy/compose.yaml       | Modified the compose file to build images using arguments and an env_file. |
| README.md                           | Revised documentation to reflect updated Docker build and secret handling. |
| .github/workflows/publish.yaml      | Updated build arguments and secret mapping for the Docker image build. |
| .github/workflows/main.yaml         | Added environment variable injection for build and test steps.        |
</details>


<details>
<summary>Files not reviewed (3)</summary>

* **.dockerignore**: Language not supported
* **.env.example**: Language not supported
* **Dockerfile**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (4)</summary>

**src/lib/utils.ts:4**
* Ensure that the removal of the 'capitalize' function is intentional and that no dependent code is relying on this utility.
```
export const capitalize = (text: string) => {
```
**.github/workflows/publish.yaml:65**
* Confirm that the secret name 'private_api_key' is consistent with what is expected in the Dockerfile and other deployment scripts to prevent misconfiguration.
```
secrets: "private_api_key=${{ secrets.PRIVATE_EXAMPLE_API_KEY }}"
```
**src/env.mjs:6**
* Confirm that removing the default value for NEXT_PUBLIC_HOST_URL is expected, as this change requires the environment variable to always be provided.
```
NEXT_PUBLIC_HOST_URL: z.string().url(),
```
**scripts/sonar-scanner.js:8**
* Verify that top-level await is supported in the execution environment to ensure that the asynchronous scan call functions as intended.
```
await scan(
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-04T19:56:55Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR primarily updates the Docker configuration to securely include a private key secret while also making some refactoring and documentation improvements. Key changes include removal of an unused utility function, refactoring of the logger configuration, and updates to environment variable handling and Docker build instructions across several files.

### Reviewed Changes

Copilot reviewed 8 out of 13 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                              | Description                                                                               |
| --------------------------------- | ----------------------------------------------------------------------------------------- |
| src/lib/utils.ts                  | Removed the unused capitalize helper function                                             |
| src/lib/logger.ts                 | Refactored logger setup using a baseOptions object                                          |
| src/env.mjs                       | Added PRIVATE_EXAMPLE_API_KEY to both runtime and server environments with new validation    |
| scripts/sonar-scanner.js          | Modified sonar scan invocation and updated options                                         |
| resources/deploy/compose.yaml     | Updated Docker build configuration to include build args and an env_file                     |
| README.md                         | Revised documentation and Docker usage instructions                                          |
| .github/workflows/publish.yaml    | Added build-args and secret configuration for the secure Docker image                        |
| .github/workflows/main.yaml       | Updated environment configurations for build and test jobs                                   |
</details>


<details>
<summary>Files not reviewed (5)</summary>

* **.dockerignore**: Language not supported
* **.env.example**: Language not supported
* **Dockerfile**: Language not supported
* **package.json**: Language not supported
* **pnpm-lock.yaml**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**.github/workflows/publish.yaml:65**
* The secret key is named 'private_api_key' in the workflow while the corresponding environment variable is PRIVATE_EXAMPLE_API_KEY. Renaming it for consistency would help avoid confusion.
```
+          secrets: "private_api_key=${{ secrets.PRIVATE_EXAMPLE_API_KEY }}"
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-04T19:58:09Z

Review comments:
diff_hunk: @@ -3,21 +3,19 @@ import { z } from 'zod';
 
 export const env = createEnv({
   client: {
-    NEXT_PUBLIC_HOST_URL: z
-      .string()
-      .url()
-      .optional()
-      .default('http://localhost:3000'),
+    NEXT_PUBLIC_HOST_URL: z.string().url(),
   },
   runtimeEnv: {
     LOG_LEVEL: process.env.LOG_LEVEL,
     NEXT_PUBLIC_HOST_URL: process.env.NEXT_PUBLIC_HOST_URL,
     NODE_ENV: process.env.NODE_ENV,
+    PRIVATE_EXAMPLE_API_KEY: process.env.PRIVATE_EXAMPLE_API_KEY,
     SONAR_TOKEN: process.env.SONAR_TOKEN,
   },
   server: {
     LOG_LEVEL: z.string().optional(),
     NODE_ENV: z.string().optional(),
path: src/env.mjs
user: login: Copilot
body: [nitpick] Add documentation to clarify that PRIVATE_EXAMPLE_API_KEY must be provided in the runtime environment for secure operations.
```suggestion
    NODE_ENV: process.env.NODE_ENV,
    // PRIVATE_EXAMPLE_API_KEY: A private API key required for secure operations.
    // Ensure this is set in the runtime environment.
    PRIVATE_EXAMPLE_API_KEY: process.env.PRIVATE_EXAMPLE_API_KEY,
    SONAR_TOKEN: process.env.SONAR_TOKEN,
  },
  server: {
    LOG_LEVEL: z.string().optional(),
    NODE_ENV: z.string().optional(),
    // PRIVATE_EXAMPLE_API_KEY: A private API key required for secure operations.
    // Ensure this is set in the runtime environment.
```
created_at: 2025-05-04T15:47:37Z
author_association: NONE
diff_hunk: @@ -3,21 +3,19 @@ import { z } from 'zod';
 
 export const env = createEnv({
   client: {
-    NEXT_PUBLIC_HOST_URL: z
-      .string()
-      .url()
-      .optional()
-      .default('http://localhost:3000'),
+    NEXT_PUBLIC_HOST_URL: z.string().url(),
   },
   runtimeEnv: {
     LOG_LEVEL: process.env.LOG_LEVEL,
     NEXT_PUBLIC_HOST_URL: process.env.NEXT_PUBLIC_HOST_URL,
     NODE_ENV: process.env.NODE_ENV,
+    PRIVATE_EXAMPLE_API_KEY: process.env.PRIVATE_EXAMPLE_API_KEY,
     SONAR_TOKEN: process.env.SONAR_TOKEN,
   },
   server: {
     LOG_LEVEL: z.string().optional(),
     NODE_ENV: z.string().optional(),
path: src/env.mjs
user: login: Copilot
body: Document that PRIVATE_EXAMPLE_API_KEY is a required secret for the server environment to ensure consistency with secure deployment practices.
```suggestion
    NODE_ENV: z.string().optional(),
    // PRIVATE_EXAMPLE_API_KEY is a required secret for the server environment.
    // It is used for authenticating with the example API and must be set in the runtime environment.
```
created_at: 2025-05-04T15:47:37Z
author_association: NONE
diff_hunk: @@ -1,11 +1,17 @@
 services:
   nextjs:
-    image: nextjs-template:prod # or use ghcr.io/yukasama/nextjs-template:latest
+    build:
path: resources/deploy/compose.yaml
user: login: Copilot
body: [nitpick] Provide documentation for the transition from using a pre-built image to building locally, especially regarding the injection of environment variables via env_file.
created_at: 2025-05-04T15:47:37Z
author_association: NONE
diff_hunk: @@ -35,7 +35,7 @@ jobs:
       - name: Snyk Code test
         run: snyk code test --sarif-file-output=snyk.sarif || true
 
-      - name: Upload result to GitHub Code Scanning
-        uses: github/codeql-action/upload-sarif@v3
-        with:
-          sarif_file: snyk.sarif
+      # - name: Upload result to GitHub Code Scanning
path: .github/workflows/snyk.yaml
user: login: Copilot
body: [nitpick] If the removal of the Code Scanning upload step is intentional, consider cleaning up the related configuration to avoid confusion in the workflow.
created_at: 2025-05-04T15:47:37Z
author_association: NONE
diff_hunk: @@ -3,21 +3,19 @@ import { z } from 'zod';
 
 export const env = createEnv({
   client: {
-    NEXT_PUBLIC_HOST_URL: z
-      .string()
-      .url()
-      .optional()
-      .default('http://localhost:3000'),
+    NEXT_PUBLIC_HOST_URL: z.string().url(),
   },
   runtimeEnv: {
     LOG_LEVEL: process.env.LOG_LEVEL,
     NEXT_PUBLIC_HOST_URL: process.env.NEXT_PUBLIC_HOST_URL,
     NODE_ENV: process.env.NODE_ENV,
+    PRIVATE_EXAMPLE_API_KEY: process.env.PRIVATE_EXAMPLE_API_KEY,
path: src/env.mjs
user: login: Copilot
body: Consider applying a zod schema (e.g., z.string()) for PRIVATE_EXAMPLE_API_KEY in runtimeEnv to mirror the stricter validation used in the server configuration.
```suggestion
    PRIVATE_EXAMPLE_API_KEY: z.string().parse(process.env.PRIVATE_EXAMPLE_API_KEY),
```
created_at: 2025-05-04T19:17:18Z
author_association: NONE
diff_hunk: @@ -3,21 +3,19 @@ import { z } from 'zod';
 
 export const env = createEnv({
   client: {
-    NEXT_PUBLIC_HOST_URL: z
-      .string()
-      .url()
-      .optional()
-      .default('http://localhost:3000'),
+    NEXT_PUBLIC_HOST_URL: z.string().url(),
   },
   runtimeEnv: {
     LOG_LEVEL: process.env.LOG_LEVEL,
     NEXT_PUBLIC_HOST_URL: process.env.NEXT_PUBLIC_HOST_URL,
     NODE_ENV: process.env.NODE_ENV,
+    PRIVATE_EXAMPLE_API_KEY: process.env.PRIVATE_EXAMPLE_API_KEY,
     SONAR_TOKEN: process.env.SONAR_TOKEN,
   },
   server: {
     LOG_LEVEL: z.string().optional(),
     NODE_ENV: z.string().optional(),
+    PRIVATE_EXAMPLE_API_KEY: z.string().min(1),
path: src/env.mjs
user: login: Copilot
body: There is an inconsistency between the runtime and server schema for PRIVATE_EXAMPLE_API_KEY. In runtimeEnv the key is optional, whereas in the server configuration it is required. Consider aligning these validations for consistent behavior.
created_at: 2025-05-04T19:58:09Z
author_association: NONE
diff_hunk: @@ -13,38 +13,53 @@ pnpm exec playwright install
 pnpm dev
 ```
 
-## Use Docker
+## Features
+
+- Next.js 15
+- Tailwind v4
+- Dark Theme (next-themes)
+- End-To-End Tests (Playwright)
+- Logging (Pino)
+- Typesafe Environment Variables (t3-env)
+- Formatting (Prettier)
+- Linting (ESLint 9 + SonarQube)
+- A+ Security Headers (CSP with Nonce)
+- Docker Ready
+- CI/CD with GitHub Actions for Linting, Testing, Security
+
+## Guide
+
+### Use Docker
 
 ```bash
 # Build Docker image
-docker build -t nextjs-template:prod .
+# You have to pass NEXT_PUBLIC_ vars as build arguments
+# Secrets are passed with a secret mount as you can see in the Dockerfile
path: README.md
user: login: Copilot
body: [nitpick] The Docker build instructions mention passing a secret mount but do not reference PRIVATE_EXAMPLE_API_KEY. It would be beneficial to update the guide to clarify how and when this secret is used.
```suggestion
# Secrets, such as PRIVATE_EXAMPLE_API_KEY, are passed with a secret mount as you can see in the Dockerfile.
# Ensure that PRIVATE_EXAMPLE_API_KEY is included in your .env file before building the Docker image.
```
created_at: 2025-05-04T19:58:09Z
author_association: NONE

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-04T19:57:49Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Yukasama_nextjs-template&pullRequest=5) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=Yukasama_nextjs-template&pullRequest=5&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Yukasama_nextjs-template&pullRequest=5&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Yukasama_nextjs-template&pullRequest=5&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Yukasama_nextjs-template&pullRequest=5&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Yukasama_nextjs-template&pullRequest=5&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Yukasama_nextjs-template&pullRequest=5)


author_association: NONE