diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index a3569c0..3cb7519 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -22,6 +22,22 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Read Node.js version + id: node_version + run: echo "NODE_VER=$(cat .nvmrc)" >> $GITHUB_OUTPUT + + - name: Install pnpm + uses: pnpm/action-setup@v2 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ steps.node_version.outputs.NODE_VER }} + cache: 'pnpm' + + - name: Security Audit + run: pnpm audit --audit-level high + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -50,6 +66,8 @@ jobs: labels: ${{ steps.meta_main.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + build-args: | + NODE_VERSION=${{ steps.node_version.outputs.NODE_VER }} secrets: | "env_variables=${{ secrets.ENV_VARIABLES}}" diff --git a/Dockerfile b/Dockerfile index c2d07e2..018fffb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,23 @@ -FROM node:20 AS builder +ARG NODE_VERSION=20 +FROM node:${NODE_VERSION}-alpine AS dependencies WORKDIR /app RUN corepack enable -COPY ./pnpm-lock.yaml ./package.json ./ +COPY package.json pnpm-lock.yaml ./ -RUN pnpm install +RUN pnpm install --frozen-lockfile --ignore-scripts + +FROM node:${NODE_VERSION}-alpine AS builder + +WORKDIR /app + +RUN corepack enable + +COPY package.json pnpm-lock.yaml ./ + +COPY --from=dependencies /app/node_modules ./node_modules COPY . . @@ -15,15 +26,30 @@ RUN --mount=type=secret,id=env_variables \ RUN pnpm ioc-generate -# Un comment if using graphql instead of REST -# RUN pnpm graphql +RUN pnpm graphql RUN pnpm build -FROM nginx +FROM nginx:alpine -# delete default nginx static files -RUN rm -rf /usr/share/nginx/html/* +RUN apk add --no-cache dumb-init -# copy build files from builder stage +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy build files from builder stage COPY --from=builder /app/dist /usr/share/nginx/html + +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d && \ + touch /var/run/nginx.pid && \ + chown -R nginx:nginx /var/run/nginx.pid + +USER nginx + +EXPOSE 80 + +ENTRYPOINT ["/usr/bin/dumb-init", "--"] + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..32af290 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,35 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA fallback + location / { + try_files $uri $uri/ /index.html; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +}