From f690739a493ecca976137a24baad913a0b404237 Mon Sep 17 00:00:00 2001 From: IT-WIBRC Date: Tue, 7 Oct 2025 18:04:10 +0100 Subject: [PATCH] =?UTF-8?q?docs:=20guide=20to=20Containerizing=20a=20Moder?= =?UTF-8?q?n=20JavaScript=20SPA=20(Vite)=20with=20a=20Multi-Stage=20Nginx?= =?UTF-8?q?=20Build=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 ++- TODO.md | 4 + .../Dockerfile | 62 ++++++ .../docker-compose.yaml | 25 +++ .../nginx-main.conf | 22 +++ .../nginx.conf | 24 +++ .../readme.md | 177 ++++++++++++++++++ 7 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 remarks/containerization/multi-stage-nginx-spa-with-online-api/Dockerfile create mode 100644 remarks/containerization/multi-stage-nginx-spa-with-online-api/docker-compose.yaml create mode 100644 remarks/containerization/multi-stage-nginx-spa-with-online-api/nginx-main.conf create mode 100644 remarks/containerization/multi-stage-nginx-spa-with-online-api/nginx.conf create mode 100644 remarks/containerization/multi-stage-nginx-spa-with-online-api/readme.md diff --git a/README.md b/README.md index 519305f..3cc422e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The goal is to foster consistent, maintainable, and testable code, serving as a - [Core Principles & General Advice](#core-principles--general-advice) - [Architectural Patterns & Design Choices](#architectural-patterns--design-choices) - [Language & Framework Specific Remarks](#language--framework-specific-remarks) +- [Containerization & Deployment](#containerization--deployment) - [Utilities](#utilities) - [Composables](#composables) - [Useful Tools & Resources](#useful-tools--resources) @@ -50,8 +51,7 @@ These are foundational guidelines applicable to almost any programming context. - Key skills you miss out on when AI does the heavy lifting. - How to use AI as a tool, not a crutch: struggle first, then ask for help. - The importance of critical thinking, problem-solving, and building intuition. -- [** Why Foundational Skills Still Matter in the Age of Frameworks and AI - **](./remarks/general/foundational-skills.md) +- [**Why Foundational Skills Still Matter in the Age of Frameworks and AI**](./remarks/general/foundational-skills.md) - The importance of understanding core programming concepts. - How frameworks and AI can abstract away complexity, but foundational skills are still crucial. - Examples of how foundational skills help in debugging, optimization, and architecture decisions. @@ -128,6 +128,19 @@ Practical advice and common patterns specific to particular technologies. --- +## Containerization & Deployment + +Best practices and architectural patterns for packaging and running applications using container technologies like Docker and Podman. + +- [**Containerizing a Modern SPA with Multi-Stage Nginx Build**](./remarks/containerization/multi-stage-nginx-spa-with-online-api/readme.md) + - **Goal:** Creating a small, secure, and production-ready container for Vue/React/SPA applications. + - **Key Strategy:** Using a **Multi-Stage Build** to discard the large Node.js build environment. + - **Security Best Practice:** Running the final Nginx process as a **Non-Root User (`USER nginx`)** to minimize attack surface. + - **Nginx Configuration Fixes:** Overcoming common non-root errors by moving the **PID file location** and configuring Nginx to **listen on a high port (e.g., 8080)**. + - **Local Workflow:** Automating the build and run process using **`docker-compose.yaml`** (or `podman compose`). + +--- + ## Utilities This section contains practical utility classes that demonstrate various coding patterns and provide reusable solutions for common tasks. @@ -249,6 +262,11 @@ This section outlines the current directory and file structure for the `coding-r │ | └── README.md # Documentation for the composable | └── README.md ├── npm/ # NPM package related remarks and troubleshooting +├── containerization/ +│ ├── multi-stage-nginx-spa-with-online-api +│ │ └── readme.md +├──npm-publish-troubleshooting.md +│ └── stylelint-configuration.md │ ├── eslint-plugin-local-testing.md │ ├── npm-publish-troubleshooting.md │ └── stylelint-configuration.md diff --git a/TODO.md b/TODO.md index db16d2f..48f0f35 100644 --- a/TODO.md +++ b/TODO.md @@ -42,3 +42,7 @@ ## Soft Skills - [ ] Skills needed (communication, awkward communication, critical thinking, good decision making, goal and organization) + +## CI/CD + +- [x] Guide to Containerizing a Modern JavaScript SPA (Vite) with a Multi-Stage Nginx Build diff --git a/remarks/containerization/multi-stage-nginx-spa-with-online-api/Dockerfile b/remarks/containerization/multi-stage-nginx-spa-with-online-api/Dockerfile new file mode 100644 index 0000000..49ec2df --- /dev/null +++ b/remarks/containerization/multi-stage-nginx-spa-with-online-api/Dockerfile @@ -0,0 +1,62 @@ +# Stage 1: Build the Application (Named 'build') +FROM node:20-alpine AS build + +# 1. Define the ARG (Argument) to hold the API URL +# This value will be passed during the `docker build` command. +# Use a placeholder default value for safety/development. +ARG VITE_API_URL=http://localhost:3000/api + +# 2. Set the ENV (Environment Variable) +# This makes the ARG available to your build process (npm run build). +ENV VITE_API_URL=$VITE_API_URL + +# Set the working directory inside the container +WORKDIR /app + +# Optimization: Copy package*.json first to leverage Docker's build cache +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy the rest of the application source code +COPY . . + +# Build the application +RUN npm run build:deploy + +# --- +# Stage 2: Serve the Application with Nginx (Named 'final') +FROM docker.io/library/nginx:stable-alpine AS final + +# 🛠️ TWEAK: Ensure the default server config is REMOVED +# This is usually the source of the persistent port 80 bind. +RUN rm -f /etc/nginx/conf.d/default.conf + +# Remove the base image's main nginx.conf before copying ours +RUN rm /etc/nginx/nginx.conf + +# 🛠️ NEW STEP: Copy our custom main Nginx configuration +COPY nginx-main.conf /etc/nginx/nginx.conf + +# 🛠️ FIX: Ensure cache and run directories are owned by the non-root user +RUN mkdir -p /var/run/nginx \ + && chown -R nginx:nginx /var/cache/nginx \ + && chown -R nginx:nginx /var/run/nginx + +# Copy our custom server block (nginx.conf) +COPY nginx.conf /etc/nginx/conf.d/ + +# Copy the static built files from the 'build' stage to the Nginx public folder +COPY --from=build /app/dist /usr/share/nginx/html + +# Expose the port Nginx is running on (default HTTP port) +EXPOSE 8080 + +# 🛠️ Security Improvement: Run as a non-root user +# Nginx stable-alpine often provides a user named 'nginx' +# We switch to this user for the final running stage. +USER nginx + +# Command to run Nginx when the container starts +CMD ["nginx", "-g", "daemon off;"] diff --git a/remarks/containerization/multi-stage-nginx-spa-with-online-api/docker-compose.yaml b/remarks/containerization/multi-stage-nginx-spa-with-online-api/docker-compose.yaml new file mode 100644 index 0000000..d63d4d2 --- /dev/null +++ b/remarks/containerization/multi-stage-nginx-spa-with-online-api/docker-compose.yaml @@ -0,0 +1,25 @@ +version: "3.8" # Use a recent version of the Docker Compose file format + +services: + frontend: + # 1. Build Instructions: Tells Compose to use the Dockerfile in the current directory (.). + # It also passes the required build argument (VITE_API_URL). + build: + context: . + dockerfile: Dockerfile + args: + # Use your specific remote backend URL here + VITE_API_URL: http://localhost:3000/api/v1 + + # 2. Container Configuration: + container_name: web-radio-frontend + + # 3. Port Mapping: Automatically maps host port 8080 to container port 8080. + ports: + - "8080:8080" + + # 4. Image Tagging: Tags the final image with a specific name. + image: wrf:v1.0.0 + + # 5. Restart Policy: Ensures the container restarts if it crashes (good for production). + restart: always diff --git a/remarks/containerization/multi-stage-nginx-spa-with-online-api/nginx-main.conf b/remarks/containerization/multi-stage-nginx-spa-with-online-api/nginx-main.conf new file mode 100644 index 0000000..54b660e --- /dev/null +++ b/remarks/containerization/multi-stage-nginx-spa-with-online-api/nginx-main.conf @@ -0,0 +1,22 @@ +# nginx-main.conf + +# Set the process ID file location (Required for non-root user) +pid /var/run/nginx/nginx.pid; + +# Nginx main context setup +user nginx; # This is ignored due to the USER directive in Dockerfile, but is good practice +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + # Include our custom configuration file for the frontend app + include /etc/nginx/conf.d/*.conf; +} diff --git a/remarks/containerization/multi-stage-nginx-spa-with-online-api/nginx.conf b/remarks/containerization/multi-stage-nginx-spa-with-online-api/nginx.conf new file mode 100644 index 0000000..1496181 --- /dev/null +++ b/remarks/containerization/multi-stage-nginx-spa-with-online-api/nginx.conf @@ -0,0 +1,24 @@ +server { + # 1. Server Setup + listen 8080; + root /usr/share/nginx/html; + index index.html; + + # Security: Disable server signature to prevent Nginx version display + server_tokens off; + + # 2. SPA Routing Configuration + # This block handles all requests that don't match a physical file. + location / { + # Try to serve the requested file ($uri) or directory ($uri/). + # If neither is found, fall back to serving the index.html file. + # This allows the frontend router (e.g., React Router) to take over and handle the path. + try_files $uri $uri/ /index.html; + } + + # 4. Security (Optional but Recommended) + # Block access to potentially sensitive files like .git or node_modules artifacts + location ~ /\. { + deny all; + } +} diff --git a/remarks/containerization/multi-stage-nginx-spa-with-online-api/readme.md b/remarks/containerization/multi-stage-nginx-spa-with-online-api/readme.md new file mode 100644 index 0000000..6d59c0f --- /dev/null +++ b/remarks/containerization/multi-stage-nginx-spa-with-online-api/readme.md @@ -0,0 +1,177 @@ +# Guide to Containerizing a Modern JavaScript SPA (Vue/Vite/React) with a Multi-Stage Nginx Build 🚀 + +## Introduction + +This guide outlines the professional, multi-stage Docker strategy required to package a built Single Page Application (SPA), ensuring the final image is secure, minimal, and correctly configured to serve the application and handle remote API calls. This architecture is valid for **Vue, Vite, React, Angular, or Svelte** applications. + +--- + +## 1\. Multi-Stage Dockerfile: Build vs. Serve + +This file uses a two-stage build to drastically reduce the final image size (eliminating Node.js and build tools) and enhance security. + +```dockerfile +# Dockerfile + +# Stage 1: Build the Application (AS build) +FROM docker.io/node:20-alpine AS build + +# 1. Inject API URL at build time +ARG VITE_API_URL=http://localhost:3000/api +ENV VITE_API_URL=$VITE_API_URL + +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build:deploy + +# --- + +# Stage 2: Serve the Application (AS final) +FROM docker.io/library/nginx:stable-alpine AS final + +# 1. Custom Nginx Configuration Setup +RUN rm /etc/nginx/nginx.conf +COPY nginx-main.conf /etc/nginx/nginx.conf +COPY nginx.conf /etc/nginx/conf.d/ + +# 2. Grant Non-Root User Permissions (CRITICAL for security & stability) +# Fixes 'mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)' +RUN mkdir -p /var/run/nginx \ + && chown -R nginx:nginx /var/cache/nginx \ + && chown -R nginx:nginx /var/run/nginx + +# 3. Copy only the required static files from the build stage +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 8080 + +# 4. Run as Non-Root User (Security Best Practice) +USER nginx + +CMD ["nginx", "-g", "daemon off;"] +``` + +--- + +## 2\. Nginx Configuration Files (The Missing Pieces) + +These custom files are necessary to resolve critical permission and port errors when running Nginx as a non-root user. + +### A. Main Configuration (`nginx-main.conf`) + +This file replaces the default `/etc/nginx/nginx.conf`. It controls the Master Process directives. + +```nginx +# nginx-main.conf (Copied to /etc/nginx/nginx.conf) + +# Fixes 'open() "/run/nginx.pid" failed (13: Permission denied)' +# This MUST be in the main context. +pid /var/run/nginx/nginx.pid; + +# Sets the user for worker processes (ignored by Dockerfile's USER, but good practice) +user nginx; +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + # Include our custom server block + include /etc/nginx/conf.d/*.conf; +} +``` + +### B. Server Block (`nginx.conf`) + +This file defines how your application is served and is placed in the `conf.d` directory. + +```nginx +# nginx.conf (Copied to /etc/nginx/conf.d/nginx.conf) + +server { + # Fixes 'bind() to 0.0.0.0:80 failed (13: Permission denied)' + # Nginx listens on a high port (>1024) which the non-root user can bind to. + listen 8080; + root /usr/share/nginx/html; + index index.html; + + # Security: Hides the Nginx version + server_tokens off; + + # SPA Routing: Directs all unmatched requests to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Security: Block access to hidden files + location ~ /\. { + deny all; + } +} +``` + +--- + +## 3\. Local Development with Podman Compose + +Using a `docker-compose.yaml` file automates the entire process. + +```yaml +version: "3.8" + +services: + frontend: + build: + context: . + dockerfile: Dockerfile + args: + VITE_API_URL: https://admin.radiowebapp.com + + container_name: frontend-container + + # Maps host's 8080 port to the container's 8080 listening port + ports: + - "8080:8080" + + image: my-frontend-app:v1.0.0 + restart: always +``` + +### Essential Podman Compose Commands + +| Command | Action | +| :--------------------- | :------------------------------------------------------------- | +| `podman compose build` | Executes the `Dockerfile` with the specified `args`. | +| `podman compose up -d` | Builds (if needed) and starts the container in the background. | +| `podman compose ps` | Checks the running status of the service(s). | +| `podman compose down` | Stops and removes the container(s) and network. | + +--- + +## 4\. Guide Reusability and Troubleshooting + +This architecture is reusable for nearly any SPA. Just update the **`VITE_API_URL`** in `docker-compose.yaml` and adjust the build script (`npm run build:deploy`) and output folder (`/app/dist`) in the `Dockerfile` if needed. + +The most critical errors encountered and resolved are: + +| Error Logged | Cause | Solution in this Guide | +| :-------------------------------- | :----------------------------------------------------------------------- | :--------------------------------------------------------------- | +| `bind() to 0.0.0.0:80 failed` | Non-root user attempting to use privileged port 80. | Change `listen` to **`8080`** in `nginx.conf`. | +| `open() "/run/nginx.pid" failed` | Non-root user lacking write permission to the default PID file location. | Add **`pid /var/run/nginx/nginx.pid;`** to `nginx-main.conf`. | +| `mkdir() "/var/cache/..." failed` | Non-root user lacking permission to Nginx cache directory. | Add **`chown -R nginx:nginx /var/cache/nginx`** to `Dockerfile`. | + +You can see all these solutions implemented in the provided configuration files at the same level as this guide. + +--- + +## 5\. Conclusion + +By implementing this Multi-Stage Build and carefully configuring Nginx for a non-root user, you've achieved the trifecta of modern container deployment: security, stability, and speed. Every step—from moving the PID file to changing the listen port—is a direct defense against common production pitfalls and security vulnerabilities. This standardized approach is the most reliable way to ship any modern JavaScript SPA, ensuring your application moves seamlessly from local development to production. You are now equipped with the knowledge to troubleshoot the most common containerization challenges and build a robust, production-ready frontend environment. 🥳