Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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;"]
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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. 🥳