From 6e14838392d48c1a7f2d4c5bd710bc439b62bfdb Mon Sep 17 00:00:00 2001 From: Weapons Forge <56998001+weaponsforge@users.noreply.github.com> Date: Fri, 1 Mar 2024 23:58:22 +0800 Subject: [PATCH 1/6] Set the example ALLOW_CORS variable to 1 --- server/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/.env.example b/server/.env.example index cd01f64..b9c7490 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,5 +1,5 @@ ALLOWED_ORIGINS=http://localhost:3000 -ALLOW_CORS=0 +ALLOW_CORS=1 API_RATE_LIMIT=100 API_WINDOW_MS_MINUTES=15 MONGO_URI=mongodb://localhost/todo-next From 83f2bf963c5b8435e6da95cd2ed5628d64dcc648 Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Sun, 14 Jul 2024 21:31:19 +0800 Subject: [PATCH 2/6] chore: update client/server eslint rules --- client/.eslintrc.json | 2 +- server/.eslintrc.js | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/client/.eslintrc.json b/client/.eslintrc.json index 136efc9..118c67e 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -10,6 +10,6 @@ "semi": ["error", "never"], "no-unused-vars": "error", "no-undef": "error", - "no-console": 2 + "no-console": ["error", { "allow": ["error"] }] } } diff --git a/server/.eslintrc.js b/server/.eslintrc.js index ef34b2d..2bc0979 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -16,11 +16,14 @@ module.exports = { ecmaVersion: 2022 }, rules: { - 'indent': ['error', 2], + indent: ['error', 2], 'linebreak-style': ['error', 'unix'], - 'quotes': ['error', 'single'], - 'semi': ['error', 'never'], - // 'no-unused-vars': 'off', - // 'no-undef': 'off' + camelcase: 'off', + quotes: ['error', 'single'], + semi: ['error', 'never'], + 'no-unused-vars': 'error', + 'no-undef': 'error', + 'no-trailing-spaces': 'error', + // 'no-console': ['error', { allow: ['error'] }] } } From 25d7d9d763640f67be58add09f690e7ddf37988b Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Sun, 14 Jul 2024 21:43:59 +0800 Subject: [PATCH 3/6] feat: dockerize client + server, #61 --- .dockerignore | 7 +++ README.md | 67 ++++++++++++++++++++++- client/.dockerignore | 6 ++ client/.env.example | 6 +- client/Dockerfile | 30 ++++++++++ client/README.md | 3 +- client/nginx/nginx.conf | 29 ++++++++++ client/nginx/nginx.full.conf | 103 +++++++++++++++++++++++++++++++++++ docker-compose.dev.yml | 42 ++++++++++++++ docker-compose.prod.yml | 36 ++++++++++++ server/.dockerignore | 8 +++ server/.env.example | 3 + server/Dockerfile | 26 +++++++++ server/README.md | 4 +- 14 files changed, 365 insertions(+), 5 deletions(-) create mode 100644 .dockerignore create mode 100644 client/.dockerignore create mode 100644 client/Dockerfile create mode 100644 client/nginx/nginx.conf create mode 100644 client/nginx/nginx.full.conf create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.prod.yml create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f821084 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +node_modules +npm-debug.log +Dockerfile +.dockerignore +.vercel \ No newline at end of file diff --git a/README.md b/README.md index 22e6a17..6884313 100644 --- a/README.md +++ b/README.md @@ -9,5 +9,70 @@ todo-next is a regular TO-DO notes listing app aimed for testing using NextJS an 2. Read the instructions on the **README** files inside the `/server` and `/client` directories for more information on configuring and using the client and backend. +## Installation Using Docker + +We can use Docker to run dockerized client and server apps for local development and production mode. The following methods require Docker and Docker compose correctly installed and set up on your development machine. + +### Docker Dependencies + +The following dependencies are used to build and run the image. Please feel feel free to use other OS and versions as needed. + +1. Ubuntu 22.04.1 + - Docker version 23.0.1, build a5eeb1 + - Docker Compose v2.16.0 +2. Microsoft Windows 10 Pro + - version 10.0.19045 Build 19045 + - Docker Desktop + - Docker Compose version v2.27.1-desktop.1 + - Docker Engine version 26.1.4, build 5650f9b + +### Docker for Localhost Development + +1. Set up the environment variables for the `/client` and `/server` directories like mentioned in the [Manual Installation](#manual-installation) section, step no. 2. +2. Verify that ports 3000 and 3001 are free because the client and server containers will use these ports. +3. Stop current-running my-phonebook containers, if any. + ``` + docker compose -f docker-compose.dev.yml down + docker compose -f docker-compose.prod.yml up + ``` +4. Stop and delete all docker instances for a fresh start. + - > **NOTE:** Running this script will delete all docker images, containers, volumes, and networks. Run this script if you feel like everything is piling but do not proceed if you have important work on other running Docker containers. + - ``` + sudo chmod u+x scripts/docker-cleanup.sh + ./scripts/docker-cleanup.sh + # Answer all proceeding prompts + ``` +5. Edit any of the files under the `/client` or `/server` directory after running step no. 2.2 and wait for their live reload on `http://localhost:3000` (client) and `http://localhost:3001` (server). + ``` + # 2.1. Build the client and server containers for localhost development. + docker compose -f docker-compose.dev.yml build + + # 2.2. Create and start the development client and server containers + docker compose -f docker-compose.dev.yml up + + # 2.3. Stop and remove the development containers, networks, images and volumes + docker compose -f docker-compose.dev.yml down + ``` + +### Docker for Production Deployment + +The following docker-compose commands build a small client image targeted for creating optimized dockerized apps running on self-managed production servers. An Nginx service serves the frontend client on port 3000. Hot reload is NOT available when editing source codes from the `/client` and `/server` directories. + +1. Follow step numbers 1 - 4 in the [Docker for Localhost Development](#docker-for-localhost-development) section. + +2. Build the client and server containers for production deployment.
+ - > **NOTE:** Run this step only once or as needed when housekeeping docker images or if there are new source code updates in the **/client** or **/server** directories. + - `docker compose -f docker-compose.prod.yml build` + +3. Load the production mode apps on `http://localhost:3000` (client) and `http://localhost:3001` (server) after running step no. 3.1. + ``` + # 3.1. Create and start the production client and server containers. + docker compose -f docker-compose.prod.yml up + + # 3.2. Stop and remove the production containers, networks, images and volumes + docker compose -f docker-compose.prod.yml down + ``` + @weaponsforge
-20220820 +20220820
+20240714 \ No newline at end of file diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 0000000..12eb063 --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,6 @@ +.git +.gitignore +node_modules +npm-debug.log +Dockerfile +.dockerignore diff --git a/client/.env.example b/client/.env.example index 4a9d7c5..982cab3 100644 --- a/client/.env.example +++ b/client/.env.example @@ -1,2 +1,4 @@ -NEXT_PUBLIC_BASE_PATH='' -BASE_API_URL=http://localhost:3001/api +NEXT_PUBLIC_BASE_PATH='' +BASE_API_URL=http://localhost:3001/api +# Uncomment this line if using WSL2 on Windows OS +# WATCHPACK_POLLING=true \ No newline at end of file diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..7ca3acd --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,30 @@ +FROM node:18.14.2-alpine as base +RUN mkdir -p /opt/client +WORKDIR /opt/client +RUN adduser -S client +RUN chown -R client /opt/client +COPY package*.json ./ + +# BUILD TARGET +FROM base as build +RUN npm install && npm cache clean --force +COPY . ./ +# RUN mkdir /opt/out && chown -R client /opt/out +RUN npm run export +USER client + +# DEVELOPMENT CLIENT PROFILE +FROM base as development +ENV NODE_ENV=development +RUN npm install && npm cache clean --force +COPY . ./ +EXPOSE 3000 +CMD ["npm", "run", "dev"] + +# PRODUCTION CLIENT PROFILE +FROM nginx:1.22.0-alpine as production +COPY --from=build /opt/client/out /usr/share/nginx/html +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx/nginx.conf /etc/nginx/conf.d +EXPOSE 3000 +CMD ["nginx", "-g", "daemon off;"] diff --git a/client/README.md b/client/README.md index 801082f..2787eaf 100644 --- a/client/README.md +++ b/client/README.md @@ -25,7 +25,8 @@ This directory will contain the web user interfaces for interacting with the Tod | Variable Name | Description | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | NEXT_PUBLIC_BASE_PATH | Directory name of assets and media that NextJS uses for the app.

Set its value to blank `''` when working on development mode in localhost.
Set its value to the sub-directory name where the exported NextJS app is to be deployed, i.e. `/` when deploying on a repository (sub-directory) of a root GitHub Pages site. | - | BASE_API_URL | Base URL of the Todo CRUD API from the `/server` directory. | + | BASE_API_URL | Base URL of the Todo CRUD API from the `/server` directory. | + | WATCHPACK_POLLING | Enables hot reload on NextJS apps (tested on NextJS v13.2.1) running inside Docker containers on a Windows host. Set it to `true` if running Docker Desktop with WSL2 on a Windows OS.| ## Usage diff --git a/client/nginx/nginx.conf b/client/nginx/nginx.conf new file mode 100644 index 0000000..49d73ce --- /dev/null +++ b/client/nginx/nginx.conf @@ -0,0 +1,29 @@ +# Minimal nginx configuration for running locally in containers +server { + listen 3000; + + root /usr/share/nginx/html; + include /etc/nginx/mime.types; + index index.html index.html; + + server_name localhost; + server_tokens off; + + # Rewrite all React URLs/routes to index.html + # location / { + # try_files $uri $uri/ /index.html =404; + # } + + # Reverse proxy to the backend API server + # Requires the backend service running on a container named 'todo-server-prod' + location /api { + proxy_pass http://todo-server-prod:3001; + proxy_set_header Host $host; + } + + # Other error pages + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/client/nginx/nginx.full.conf b/client/nginx/nginx.full.conf new file mode 100644 index 0000000..132efa6 --- /dev/null +++ b/client/nginx/nginx.full.conf @@ -0,0 +1,103 @@ +# Full nginx configuration with SSL certificate for nginx running on host machine +# Requires a registered domain name, letsencrypt SSL certificates +# and local client/server apps (running in containers or manually installed on host) + +server { + listen 80; + listen [::]:80; + server_name www.; + return 301 https://$request_uri; +} + +server { + listen 80; + listen [::]:80; + server_name ; + return 301 https://$request_uri; +} + +server { + listen 443 ssl; + server_name www.; + ssl_certificate /etc/letsencrypt/live//fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live//privkey.pem; + return 301 https://$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name ; + server_tokens off; + + # Available methods + add_header Allow 'GET, POST, PATCH, DELETE, HEAD' always; + add_header X-XSS-Protection '1; mode=block'; + + if ( $request_method !~ ^(GET|POST|PATCH|DELETE|HEAD)$ ) { + return 405; + } + + ssl_certificate /etc/letsencrypt/live//fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live//privkey.pem; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; + ssl_dhparam '/etc/pki/nginx/dhparams.pem'; + + add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains' always; + + # gzip comppression settings + gzip on; + gzip_disable 'msie6'; + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_min_length 0; + gzip_types text/plain application/javascript text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype; + + # Reverse proxy to the client website + # Requires the client service running on http://:3000 (from a container or manually installed on host) + location / { + proxy_pass http://:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_cache_bypass $http_upgrade; + + # For websockets + proxy_http_version 1.1; + proxy_set_header Connection 'upgrade'; + proxy_set_header Upgrade $http_upgrade; + proxy_read_timeout 600s; + } + + # Reverse proxy to the backend API server + # Requires the backend service running on http://:3001 (from a container or manually installed on host) + location /api { + proxy_pass http://:3001; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_cache_bypass $http_upgrade; + + # For websockets + proxy_http_version 1.1; + proxy_set_header Connection 'upgrade'; + proxy_set_header Upgrade $http_upgrade; + proxy_read_timeout 600s; + } + + # Other error pages + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..7cfcb7b --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,42 @@ +version: "3" +services: + # NextJS v13 app running on development mode + todo-client-dev: + container_name: todo-client-dev + image: weaponsforge/todo-client:dev + env_file: + - ./client/.env + build: + context: ./client + dockerfile: Dockerfile + target: development + networks: + - todo-next-dev + volumes: + - ./client:/opt/client + - /opt/client/node_modules + - /opt/client/.next + ports: + - "3000:3000" + + # Express server running in development mode + todo-server-dev: + container_name: todo-server-dev + image: weaponsforge/todo-server:dev + env_file: + - ./server/.env + build: + context: ./server + dockerfile: ./Dockerfile + target: development + networks: + - todo-next-dev + volumes: + - ./server/src:/opt/server/src + ports: + - "3001:3001" + +networks: + todo-next-dev: + name: todo-next-dev + external: false diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..b142c3d --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,36 @@ +version: "3" +services: + # NextJS exported app running on an nginx webserver + todo-client-prod: + container_name: todo-client-prod + image: weaponsforge/todo-client:latest + restart: always + build: + context: ./client + dockerfile: Dockerfile + target: production + networks: + - todo-next-prod + ports: + - "3000:3000" + + # Express web server app running in production mode + todo-server-prod: + container_name: todo-server-prod + image: weaponsforge/todo-server:latest + restart: always + env_file: + - ./server/.env + build: + context: ./server + dockerfile: Dockerfile + target: production + networks: + - todo-next-prod + ports: + - "3001:3001" + +networks: + todo-next-prod: + name: todo-next-prod + external: false diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..b37f04a --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,8 @@ +.git +.gitignore +node_modules +npm-debug.log +Dockerfile +.dockerignore +.env +.vercel diff --git a/server/.env.example b/server/.env.example index b9c7490..9ab8fd1 100644 --- a/server/.env.example +++ b/server/.env.example @@ -4,3 +4,6 @@ API_RATE_LIMIT=100 API_WINDOW_MS_MINUTES=15 MONGO_URI=mongodb://localhost/todo-next DEPLOYMENT_PLATFORM=regular +# Uncomment these 2 CHOKIDAR lines if using Docker Desktop and WSL2 on Windows OS +# CHOKIDAR_USEPOLLING=true +# CHOKIDAR_INTERVAL=1000 \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..559398e --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,26 @@ +# BASE PROFILE +FROM node:18.14.2-alpine as base +RUN mkdir -p /opt/server +WORKDIR /opt/server +RUN adduser -S server +RUN chown -R server /opt/server +COPY package*.json ./ + +# PRODUCTION PROFILE TARGET +FROM base as production +ENV NODE_ENV production +RUN npm ci --only=production && npm cache clean --force +COPY . . +USER server +EXPOSE 3001 +CMD ["node", "src/index.js"] + +# DEVELOPMENT PROFILE TARGET +FROM base as development +ENV NODE_ENV development +RUN npm install -g vercel@28.16.7 +RUN npm install && npm cache clean --force +COPY . . +USER server +EXPOSE 3001 +CMD ["npm", "run", "dev"] diff --git a/server/README.md b/server/README.md index 607df92..6eaf320 100644 --- a/server/README.md +++ b/server/README.md @@ -45,7 +45,9 @@ The following dependencies are used for this project's localhost development env | ALLOW_CORS | Allow Cross-Origin Resource Sharing (CORS) on the API endpoints.

Default value is `1`, allowing access to domains listed in `ALLOWED_ORIGINS`.
Setting to `0` will make all endpoints accept requests from all domains, including Postman. | | ALLOWED_ORIGINS | IP/domain origins in comma-separated values that are allowed to access the API if `ALLOW_CORS=1`.
Include `http://localhost:3000` by default to allow CORS access to the **/client** app. | | DEPLOYMENT_PLATFORM | This variable refers to the backend `server`'s hosting platform, defaulting to `DEPLOYMENT_PLATFORM=regular`
for full-server NodeJS express apps.

Valid values are:
`regular` - for traditional full-server NodeJS express apps
`vercel` - for Vercel (serverless) | - | MONGO_URI | MongoDB connection string.
Default value uses the localhost MongoDB connection string. | + | MONGO_URI | MongoDB connection string.
Default value uses the localhost MongoDB connection string. | + | CHOKIDAR_USEPOLLING | Enables hot reload on `nodemon` running inside Docker containers on a Windows host. Set it to `true` if running Docker Desktop with WSL2 on a Windows OS. | + | CHOKIDAR_INTERVAL | Chokidar polling interval. Set it along with `CHOKIDAR_USEPOLLING=true` if running Docker Desktop with WSL2 on a Windows OS. The default value is `1000`. | ## Usage From d2c7da9ef789e14f6e78328e9b218844a2a434da Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Sun, 14 Jul 2024 21:45:56 +0800 Subject: [PATCH 4/6] chore: add cleanup script --- scripts/docker-cleanup.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 scripts/docker-cleanup.sh diff --git a/scripts/docker-cleanup.sh b/scripts/docker-cleanup.sh new file mode 100644 index 0000000..f66b53b --- /dev/null +++ b/scripts/docker-cleanup.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Stops and deletes ALL Docker resources +docker image prune +docker rmi $(docker images -a -q) +docker stop $(docker ps -a -q) +docker rm $(docker ps -a -q) +docker system prune -f +docker system prune -a +docker volume prune -f From 5ad89e227efd06a0925225d24373bff1a36c156a Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Sun, 14 Jul 2024 21:53:04 +0800 Subject: [PATCH 5/6] chore: update README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6884313..0818c67 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,9 @@ The following dependencies are used to build and run the image. Please feel feel ### Docker for Localhost Development -1. Set up the environment variables for the `/client` and `/server` directories like mentioned in the [Manual Installation](#manual-installation) section, step no. 2. +1. Set up the environment variables for the `/client` and `/server` directories. + - Visit the `client/README.md` and `server/README.md` files for more information. + - Take note of the `.env` variables setup for Windows and Linux. 2. Verify that ports 3000 and 3001 are free because the client and server containers will use these ports. 3. Stop current-running my-phonebook containers, if any. ``` From b3112a3670e18c86c4f1342db0cc1eca50c6c4de Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Sun, 14 Jul 2024 21:55:01 +0800 Subject: [PATCH 6/6] chore: update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0818c67..b81d96c 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ The following dependencies are used to build and run the image. Please feel feel 1. Set up the environment variables for the `/client` and `/server` directories. - Visit the `client/README.md` and `server/README.md` files for more information. - - Take note of the `.env` variables setup for Windows and Linux. + - Take note of the `.env` variables setup for Windows and Linux to enable hot reload. 2. Verify that ports 3000 and 3001 are free because the client and server containers will use these ports. -3. Stop current-running my-phonebook containers, if any. +3. Stop current-running containers, if any. ``` docker compose -f docker-compose.dev.yml down docker compose -f docker-compose.prod.yml up