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..b81d96c 100644
--- a/README.md
+++ b/README.md
@@ -9,5 +9,72 @@ 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.
+ - Visit the `client/README.md` and `server/README.md` files for more information.
+ - 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 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/.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/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/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
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 cd01f64..9ab8fd1 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -1,6 +1,9 @@
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
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/.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'] }]
}
}
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