From 70fd9f1370a011320c21b5bef5b456d8fbcda8b5 Mon Sep 17 00:00:00 2001 From: Mohamed Hedi CHLAGOU Date: Tue, 10 Mar 2026 23:56:59 +0100 Subject: [PATCH] SCRUM-6: Optimize existing Dockerfile --- .dojops/.gitignore | 7 ++ .dojops/history/audit.jsonl | 2 + .dojops/plans/plan-853caa77.json | 144 +++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 8 ++ DOJOPS.md | 104 ++++++++++++++++++++++ Dockerfile | 43 +++++++-- Makefile | 23 +++++ 7 files changed, 324 insertions(+), 7 deletions(-) create mode 100644 .dojops/.gitignore create mode 100644 .dojops/history/audit.jsonl create mode 100644 .dojops/plans/plan-853caa77.json create mode 100644 DOJOPS.md create mode 100644 Makefile diff --git a/.dojops/.gitignore b/.dojops/.gitignore new file mode 100644 index 0000000..f1dc84c --- /dev/null +++ b/.dojops/.gitignore @@ -0,0 +1,7 @@ +# DojOps project state +session.json +execution-logs/ +approvals/ +sessions/ +memory/ +audit-key diff --git a/.dojops/history/audit.jsonl b/.dojops/history/audit.jsonl new file mode 100644 index 0000000..8335536 --- /dev/null +++ b/.dojops/history/audit.jsonl @@ -0,0 +1,2 @@ +{"timestamp":"2026-03-10T22:55:11.075Z","user":"hedi","command":"apply plan-853caa77","action":"apply","planId":"plan-853caa77","status":"success","durationMs":65190,"seq":1,"previousHash":"genesis","hash":"613a6256d089019e5c00d31cb05be38fc2ef4b2a4da889c2b893a7ad792c8fbc"} +{"timestamp":"2026-03-10T22:56:15.587Z","user":"hedi","command":"apply plan-853caa77","action":"apply","planId":"plan-853caa77","status":"success","durationMs":64174,"seq":2,"previousHash":"613a6256d089019e5c00d31cb05be38fc2ef4b2a4da889c2b893a7ad792c8fbc","hash":"54ffa394f8d2bd588d26df2bf4a7dcb48fdc96c0404ea160145296792edb92a0"} diff --git a/.dojops/plans/plan-853caa77.json b/.dojops/plans/plan-853caa77.json new file mode 100644 index 0000000..367b2ed --- /dev/null +++ b/.dojops/plans/plan-853caa77.json @@ -0,0 +1,144 @@ +{ + "id": "plan-853caa77", + "goal": "Optimize existing Dockerfile with best practices and linting", + "createdAt": "2026-03-10T22:54:05.874Z", + "risk": "MEDIUM", + "tasks": [ + { + "id": "analyze-existing-dockerfile", + "tool": "dockerfile", + "description": "Analyze current Dockerfile structure and identify optimization opportunities", + "dependsOn": [], + "input": { + "prompt": "Analyze the existing Dockerfile at the project root. Do NOT generate any files. Return your findings as plain text including: current structure, stages, base images, user configuration, caching strategy, and any issues found regarding best practices (multi-stage builds, non-root user, layer caching).", + "outputPath": "." + }, + "toolType": "built-in" + }, + { + "id": "create-optimized-dockerfile", + "tool": "dockerfile", + "description": "Create optimized Dockerfile with multi-stage builds, non-root user, and layer caching", + "dependsOn": [ + "analyze-existing-dockerfile" + ], + "input": { + "prompt": "Create ONLY the file Dockerfile at the project root — an optimized multi-stage Dockerfile for a Next.js application with all best practices. Requirements: 1) Use multi-stage builds with at least two stages (builder and production). 2) Create and use a non-root user in the production stage. 3) Implement layer caching by ordering dependencies properly and using COPY --from for stage artifacts. 4) Use appropriate base images (e.g., node:alpine for builder, node:alpine or a minimal image for production). 5) Include health checks and proper labels. 6) Ensure the build is fast by minimizing layers and using cache-friendly commands. Do NOT output any other files.", + "outputPath": "." + }, + "toolType": "built-in" + }, + { + "id": "create-dockerignore", + "tool": "dockerfile", + "description": "Create .dockerignore file to exclude unnecessary files and improve build performance", + "dependsOn": [ + "analyze-existing-dockerfile" + ], + "input": { + "prompt": "Create ONLY the file .dockerignore at the project root — a .dockerignore file that excludes node_modules, .git, logs, and other unnecessary files to improve Docker build performance and reduce image size. Include common patterns for Next.js projects. Do NOT output any other files.", + "outputPath": "." + }, + "toolType": "built-in" + }, + { + "id": "create-hadolint-config", + "tool": "makefile", + "description": "Create Makefile target to lint Dockerfile with Hadolint", + "dependsOn": [ + "create-optimized-dockerfile" + ], + "input": { + "prompt": "Create ONLY the file Makefile at the project root — a Makefile with a target 'lint-docker' that runs Hadolint on the Dockerfile. If a Makefile already exists, add this target while keeping all existing targets unchanged. The target should use hadolint/hadolint Docker image or local installation. Do NOT output any other files.", + "outputPath": "." + }, + "toolType": "built-in" + }, + { + "id": "update-github-actions-for-lint", + "tool": "github-actions", + "description": "Update existing GitHub Actions workflow to include Dockerfile linting", + "dependsOn": [ + "create-hadolint-config" + ], + "input": { + "prompt": "Update ONLY the file .github/workflows/ci.yml. Keep ALL existing jobs and steps unchanged. Add a new job or step that runs the 'make lint-docker' command to lint the Dockerfile with Hadolint. Ensure it runs as part of the CI pipeline. Do NOT output any other files — only .github/workflows/ci.yml.", + "outputPath": "." + }, + "toolType": "built-in" + } + ], + "files": [], + "approvalStatus": "PARTIAL", + "executionContext": { + "provider": "deepseek", + "dojopsVersion": "1.0.8", + "policySnapshot": "9a84cdbc57ac477e", + "toolVersions": { + "dockerfile": "2.1.0", + "makefile": "2.1.0", + "github-actions": "2.2.0" + } + }, + "results": [ + { + "taskId": "analyze-existing-dockerfile", + "status": "completed", + "output": { + "generated": "## Analysis of Existing Dockerfile\n\n### Current Structure\n- **Multi-stage build**: Yes (3 stages)\n- **Stages**: \n 1. `deps` - Production dependencies only\n 2. `builder` - Build stage\n 3. `runner` - Final runtime stage\n\n### Base Images\n- All stages use: `node:20-alpine`\n- **Issue**: Using same base for all stages misses optimization opportunity\n\n### User Configuration\n- **Non-root user**: Yes (`nextjs:nodejs` with UID/GID 1001)\n- **User switching**: Properly implemented with `USER nextjs`\n- **Permission handling**: Correctly sets ownership for `.next` directory\n\n### Caching Strategy\n- **Dependency layer caching**: Good - copies `package.json` and `package-lock.json` first\n- **Build layer separation**: Good - separates dependency installation from source code\n- **Issue**: No `.dockerignore` file mentioned, which could affect build context size\n\n### Best Practices Assessment\n\n**✅ Good Practices:**\n1. Multi-stage build implemented\n2. Non-root user with proper permissions\n3. Pinned Node.js version (20-alpine)\n4. Layer caching for dependencies\n5. HEALTHCHECK included\n6. WORKDIR set before operations\n7. COPY used instead of ADD\n8. EXPOSE for documented port (3000)\n9. ENV variables for configuration\n10. Labels for metadata\n\n**⚠️ Issues Found:**\n\n1. **Base image consistency**: All stages use `node:20-alpine`, but builder stage could use full Node image for build tools\n2. **Missing `.dockerignore`**: No mention of `.dockerignore` file to exclude unnecessary files\n3. **Build stage optimization**: Could use `node:20` (full) for builder stage and `node:20-alpine` for runtime\n4. **Next.js specific**: Uses `.next/standalone` which suggests Next.js 12+ output mode\n5. **Healthcheck dependency**: Healthcheck assumes `/api/health` endpoint exists\n6. **Port configuration**: Uses both `EXPOSE 3000` and `ENV PORT=3000` (redundant but not harmful)\n7. **Layer combination**: Some RUN commands could be combined for fewer layers\n\n**🔍 Specific Observations:**\n- Uses Next.js standalone output mode (good for production)\n- Properly handles file ownership for Next.js cache\n- Includes both `package.json` and `package-lock.json*` (wildcard for lockfile variations)\n- Sets `NODE_ENV=production` in runner stage only\n- Uses `npm ci --only=production` in deps stage (correct for production)\n\n**Recommendations:**\n1. Add `.dockerignore` file to exclude `node_modules`, `.git`, etc.\n2. Consider using `node:20` for builder stage and `node:20-alpine` for runtime\n3. Verify healthcheck endpoint exists in the application\n4. Consider combining related RUN commands where possible\n\nThe configuration is generally production-ready with minor optimization opportunities.", + "isUpdate": true + }, + "filesCreated": [], + "executionStatus": "failed", + "executionApproval": "approved", + "error": "Multi-file output must be valid JSON. The LLM returned non-JSON content. First 200 chars: ## Analysis of Existing Dockerfile\n\n### Current Structure\n- **Multi-stage build**: Yes (3 stages)\n- **Stages**: \n 1. `deps` - Production dependencies only\n 2. `builder` - Build stage\n 3. `runner` -" + }, + { + "taskId": "create-optimized-dockerfile", + "status": "completed", + "output": { + "generated": "{\n \"files\": {\n \"Dockerfile\": \"FROM node:20-alpine AS deps\\nWORKDIR /app\\nCOPY package.json package-lock.json* ./\\nRUN npm ci --only=production\\n\\nFROM node:20-alpine AS builder\\nWORKDIR /app\\nCOPY --from=deps /app/node_modules ./node_modules\\nCOPY . .\\nRUN npm run build\\n\\nFROM node:20-alpine AS runner\\nWORKDIR /app\\n\\nENV NODE_ENV=production\\n\\nRUN addgroup --system --gid 1001 nodejs && \\\\\\n adduser --system --uid 1001 nextjs\\n\\nCOPY --from=builder /app/public ./public\\n\\n# Set the correct permission for prerender cache\\nRUN mkdir .next && \\\\\\n chown nextjs:nodejs .next\\n\\n# Automatically leverage output traces to reduce image size\\n# https://nextjs.org/docs/advanced-features/output-file-tracing\\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./\\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static\\n\\nUSER nextjs\\n\\nEXPOSE 3000\\n\\nENV PORT=3000\\nENV HOSTNAME=\\\"0.0.0.0\\\"\\n\\nHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\\\\\n CMD node -e \\\"require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)}).on('error', () => process.exit(1))\\\"\\n\\nLABEL maintainer=\\\"your-team@example.com\\\" \\\\\\n version=\\\"1.0.0\\\" \\\\\\n description=\\\"Next.js application\\\" \\\\\\n org.opencontainers.image.source=\\\"https://github.com/your-org/your-repo\\\"\\n\\nCMD [\\\"node\\\", \\\"server.js\\\"]\"\n }\n}", + "isUpdate": true + }, + "filesCreated": [], + "executionStatus": "completed", + "executionApproval": "approved" + }, + { + "taskId": "create-dockerignore", + "status": "completed", + "output": { + "generated": "{\n \"files\": {\n \".dockerignore\": \"# Dependencies\\nnode_modules\\nnpm-debug.log*\\nyarn-debug.log*\\nyarn-error.log*\\n\\n# Build outputs\\n.next\\nout\\ndist\\nbuild\\ncoverage\\n\\n# Environment variables\\n.env*.local\\n.env\\n.env.production\\n.env.development\\n\\n# Version control\\n.git\\n.gitignore\\n.gitattributes\\n\\n# Documentation\\n*.md\\nLICENSE\\nCHANGELOG.md\\nREADME.md\\n\\n# Logs\\nlogs\\n*.log\\n\\n# IDE and editor files\\n.vscode\\n.idea\\n*.swp\\n*.swo\\n*~\\n\\n# OS files\\n.DS_Store\\nThumbs.db\\n\\n# Docker files\\n.dockerignore\\nDockerfile*\\ndocker-compose*.yml\\n\\n# Test files\\n*.test.js\\n*.spec.js\\n__tests__\\n\\n# Next.js specific\\n.next/cache\\n.next/standalone\\n.next/static\\n.next/server\\n\\n# Temporary files\\n*.tmp\\n*.temp\\n\\n# Configuration files (except those needed for build)\\n*.config.js\\n*.config.ts\\ntsconfig.json\\njsconfig.json\\nnext.config.js\\n\\n# Source maps\\n*.map\\n\\n# TypeScript declaration files\\n*.d.ts\\n\\n# Optional: exclude local development files\\n.dockerignore.local\\nDockerfile.local\\n\"\n }\n}", + "isUpdate": true + }, + "filesCreated": [], + "executionStatus": "failed", + "executionApproval": "skipped", + "error": "Verification failed: unexpected 'n'\nexpecting '#', '\\', ADD, ARG, CMD, COPY, ENTRYPOINT, ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, MAINTAINER, ONBUILD, RUN, SHELL, STOPSIGNAL, USER, VOLUME, WORKDIR, a pragma, at least one space, or end of input" + }, + { + "taskId": "create-hadolint-config", + "status": "completed", + "output": { + "generated": ".PHONY: help lint-docker\n\n.DEFAULT_GOAL := help\n\n# Variables\nDOCKERFILE ?= Dockerfile\nHADOLINT_IMAGE ?= hadolint/hadolint:latest\nHADOLINT_CMD ?= docker run --rm -i $(HADOLINT_IMAGE)\n\nhelp: ## Show this help message\n\t@echo \"Usage: make [target]\"\n\t@echo \"\"\n\t@echo \"Targets:\"\n\t@awk 'BEGIN {FS = \":.*?## \"} /^[a-zA-Z_-]+:.*?## / {printf \" %-15s %s\\n\", $$1, $$2}' $(MAKEFILE_LIST)\n\nlint-docker: ## Lint Dockerfile using Hadolint\nifeq (, $(shell which hadolint 2>/dev/null))\n\t@echo \"Using Docker image for Hadolint...\"\n\t@cat $(DOCKERFILE) | $(HADOLINT_CMD)\nelse\n\t@echo \"Using local Hadolint installation...\"\n\t@hadolint $(DOCKERFILE)\nendif", + "isUpdate": false + }, + "filesCreated": [ + "Makefile" + ], + "executionStatus": "completed", + "executionApproval": "approved" + }, + { + "taskId": "update-github-actions-for-lint", + "status": "completed", + "output": { + "generated": "{\n \"files\": {\n \".github/workflows/ci.yml\": \"name: CI\\n\\non:\\n push:\\n branches: [main]\\n pull_request:\\n branches: [main]\\n\\njobs:\\n build:\\n runs-on: ubuntu-latest\\n steps:\\n - uses: actions/checkout@v4\\n\\n - uses: actions/setup-node@v4\\n with:\\n node-version: 20\\n\\n - run: npm ci\\n - run: npm run lint\\n - run: npm run build\\n\\n lint-docker:\\n runs-on: ubuntu-latest\\n steps:\\n - uses: actions/checkout@v4\\n - name: Run Hadolint to lint Dockerfile\\n run: make lint-docker\\n shell: bash\"\n }\n}", + "isUpdate": true + }, + "filesCreated": [], + "executionStatus": "completed", + "executionApproval": "approved" + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dbd466..a7441de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,3 +19,11 @@ jobs: - run: npm ci - run: npm run lint - run: npm run build + + lint-docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run Hadolint to lint Dockerfile + run: make lint-docker + shell: bash \ No newline at end of file diff --git a/DOJOPS.md b/DOJOPS.md new file mode 100644 index 0000000..ad00d23 --- /dev/null +++ b/DOJOPS.md @@ -0,0 +1,104 @@ +--- +dojops: 1 +scannedAt: '2026-03-10T22:53:41.306Z' +primaryLanguage: node +languages: + - name: node + confidence: 0.9 + indicator: package.json + - name: typescript + confidence: 0.85 + indicator: tsconfig.json +packageManager: null +ci: + - platform: github-actions + configPath: .github/workflows/ci.yml +container: + hasDockerfile: true + hasCompose: false + hasSwarm: false +infra: + hasTerraform: false + tfProviders: [] + hasState: false + hasKubernetes: false + hasHelm: false + hasAnsible: false + hasKustomize: false + hasVagrant: false + hasPulumi: false + hasCloudFormation: false + hasPacker: false + hasCdk: false + hasSkaffold: false + hasArgoCD: false + hasTiltfile: false + hasHelmfile: false +monitoring: + hasPrometheus: false + hasNginx: false + hasSystemd: false + hasHaproxy: false + hasTomcat: false + hasApache: false + hasCaddy: false + hasEnvoy: false +scripts: + shellScripts: [] + pythonScripts: [] + hasJustfile: false +security: + hasEnvExample: false + hasGitignore: true + hasCodeowners: false + hasSecurityPolicy: false + hasDependabot: false + hasRenovate: false + hasSecretScanning: false + hasEditorConfig: false +meta: + isGitRepo: true + isMonorepo: false + hasMakefile: false + hasReadme: true + hasEnvFile: false +relevantDomains: + - ci-cd + - ci-debugging + - containerization +devopsFiles: + - .github/workflows/ci.yml + - .gitignore + - Dockerfile +--- + +# DojOps Project Context + +> Managed by DojOps CLI. Run `dojops init` to refresh. The Notes section is yours to edit. + +## Overview + +**Tech Stack:** node, Docker, github-actions + +## Detected Stack + +- **Primary Language:** node (also: typescript) +- **CI/CD:** github-actions (`.github/workflows/ci.yml`) +- **Container:** Dockerfile + +**DevOps Files:** +- `.github/workflows/ci.yml` +- `.gitignore` +- `Dockerfile` + +## Notes + + + + +## Recent Activity + + +- 2026-03-10T22:56:15Z — Plan applied: PARTIAL +- 2026-03-10T22:55:11Z — Plan applied: PARTIAL (`Makefile`, `Dockerfile`, `./.github/workflows/ci.yml`) + diff --git a/Dockerfile b/Dockerfile index d380312..3f23798 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,46 @@ -FROM node:20 AS deps +FROM node:20-alpine AS deps WORKDIR /app COPY package.json package-lock.json* ./ -RUN npm ci +RUN npm ci --only=production -FROM node:20 AS builder +FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build -FROM node:20 +FROM node:20-alpine AS runner WORKDIR /app -COPY --from=builder /app/out ./out -RUN npm i -g serve + +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next && \ + chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + EXPOSE 3000 -CMD ["serve", "out", "-l", "3000"] + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)}).on('error', () => process.exit(1))" + +LABEL maintainer="your-team@example.com" \ + version="1.0.0" \ + description="Next.js application" \ + org.opencontainers.image.source="https://github.com/your-org/your-repo" + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9653b71 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: help lint-docker + +.DEFAULT_GOAL := help + +# Variables +DOCKERFILE ?= Dockerfile +HADOLINT_IMAGE ?= hadolint/hadolint:latest +HADOLINT_CMD ?= docker run --rm -i $(HADOLINT_IMAGE) + +help: ## Show this help message + @echo "Usage: make [target]" + @echo "" + @echo "Targets:" + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +lint-docker: ## Lint Dockerfile using Hadolint +ifeq (, $(shell which hadolint 2>/dev/null)) + @echo "Using Docker image for Hadolint..." + @cat $(DOCKERFILE) | $(HADOLINT_CMD) +else + @echo "Using local Hadolint installation..." + @hadolint $(DOCKERFILE) +endif \ No newline at end of file