Repository: DanWahlin/NodeExpressMongoDBDockerApp Branch: main Commit: 170d42625853 Files: 27 Total size: 31.8 KB Directory structure: gitextract_08x7bqud/ ├── .dockerignore ├── .gitignore ├── .vscode/ │ └── settings.json ├── README.md ├── config/ │ ├── config.development.json │ └── config.production.json ├── dbSeeder.js ├── docker-compose.yml ├── lib/ │ ├── configLoader.js │ ├── dataSeeder.js │ ├── database.js │ ├── dockerCommandsRepository.js │ └── logger.js ├── logs/ │ └── .gitkeep ├── models/ │ └── dockerCommand.js ├── nginx/ │ └── index.html ├── node.dockerfile ├── package.json ├── public/ │ └── styles/ │ ├── styles.css │ └── tailwind.css ├── routes/ │ └── index.js ├── server.js ├── tailwind.config.js └── views/ ├── error.hbs ├── index.hbs ├── layouts/ │ └── masterLayout.hbs └── newcommand.hbs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ node_modules ================================================ FILE: .gitignore ================================================ node_modules/ .DS_Store .certs/ .idea/ .settings/ .build/ logs/access.log *.iml ================================================ FILE: .vscode/settings.json ================================================ { "gitdoc.enabled": false } ================================================ FILE: README.md ================================================ # Node.js with MongoDB and Docker Demo Application demo designed to show how Node.js and MongoDB can be run in Docker containers. The app uses Mongoose to create a simple database that stores Docker commands and examples. Interested in learning more about Docker? Visit https://www.pluralsight.com/courses/docker-web-development to view my Docker for Web Developers course. ### Starting the Application with Docker Containers: 1. Install [Docker Desktop](https://docker.com/get-started) 2. Open a command prompt. 3. Run the commands listed in `node.dockerfile` (see the comments at the top of the file). 4. Navigate to http://localhost:3000. ### Starting the Application with Docker Compose 1. Install [Docker Desktop](https://docker.com/get-started) 2. Open a command prompt at the root of the application's folder. 3. Run `docker compose build` 4. Run `docker compose up` 5. Open another command prompt and run `docker compose ps -a` and note the ID of the Node container 6. Run `docker exec -it sh` (replace with the proper ID) to sh into the container 7. Run `node dbSeeder.js` to seed the MongoDB database 8. Type `exit` to leave the sh session 9. Navigate to http://localhost:3000 in your browser to view the site. This assumes that's the IP assigned to VirtualBox - change if needed. 10. Run `docker-compose down` to stop the containers and remove them. ## To run the app with Node.js and MongoDB (without Docker): 1. Install and start MongoDB (https://docs.mongodb.com/manual/administration/install-community/). 2. Install the LTS version of Node.js (http://nodejs.org). 3. Open `config/config.development.json` and adjust the host name to your MongoDB server name (`localhost` normally works if you're running locally). 4. Run `npm install`. 5. Run `node dbSeeder.js` to get the sample data loaded into MongoDB. Exit the command prompt. 6. Run `npm start` to start the server. 7. Navigate to http://localhost:3000 in your browser. ================================================ FILE: config/config.development.json ================================================ { "databaseConfig": { "host": "mongodb", "database": "funWithDocker" } } ================================================ FILE: config/config.production.json ================================================ { "databaseConfig": { "host": "mongodb", "database": "funWithDocker" } } ================================================ FILE: dbSeeder.js ================================================ import dataInitializer from './lib/dataSeeder.js'; import config from './config/config.development.json' with { type: 'json' }; import db from './lib/database.js'; db.init(config.databaseConfig); console.log('Initializing Data'); dataInitializer.initializeData(function(err) { if (err) { console.log(err); } else { console.log('Data Initialized!') } }); ================================================ FILE: docker-compose.yml ================================================ services: node: container_name: nodeapp image: nodeapp build: context: . dockerfile: node.dockerfile args: PACKAGES: "nano wget curl" ports: - "3000:3000" networks: - nodeapp-network volumes: - ./logs:/var/www/logs environment: - NODE_ENV=production - APP_VERSION=1.0 depends_on: - mongodb # restart: on-failure # deploy: # replicas: 5 mongodb: container_name: mongodb image: mongo networks: - nodeapp-network networks: nodeapp-network: driver: bridge ================================================ FILE: lib/configLoader.js ================================================ import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import path from 'path'; import logger from './logger.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); let env = process.env.NODE_ENV; if (!env) { env = 'development'; } logger.log('Node environment: ' + env); logger.log('loading config.' + env + '.json'); const configPath = path.join(__dirname, '..', 'config', `config.${env}.json`); const config = JSON.parse(readFileSync(configPath, 'utf-8')); export default config; ================================================ FILE: lib/dataSeeder.js ================================================ import DockerCommand from '../models/dockerCommand.js'; const dataInitializer = { initializeData: async function(callback) { const runDockerCommand = new DockerCommand({ command: 'run', description: 'Runs a Docker container', examples: [ { example: 'docker run imageName', description: 'Creates a running container from the image. Pulls it from Docker Hub if the image is not local' }, { example: 'docker run -d -p 8080:3000 imageName', description: 'Runs a container in "detached" mode with an external port of 8080 and a container port of 3000.' } ] }); const psDockerCommand = new DockerCommand({ command: 'ps', description: 'Lists containers', examples: [ { example: 'docker ps', description: 'Lists all running containers' }, { example: 'docker ps -a', description: 'Lists all containers (even if they are not running)' } ] }); try { await runDockerCommand.save(); await psDockerCommand.save(); callback(); } catch (err) { callback(err); } } }; export default dataInitializer; ================================================ FILE: lib/database.js ================================================ import mongoose from 'mongoose'; const database = { conn: null, init: function(config) { console.log('Trying to connect to ' + config.host + '/' + config.database + ' MongoDB database'); const connString = `mongodb://${config.host}/${config.database}`; mongoose.connect(connString); this.conn = mongoose.connection; this.conn.on('error', console.error.bind(console, 'connection error:')); this.conn.once('open', () => console.log('db connection open')); return this.conn; }, close: function() { if (this.conn) { this.conn.close(() => { console.log('Mongoose default connection disconnected through app termination'); process.exit(0); }); } } }; export default database; ================================================ FILE: lib/dockerCommandsRepository.js ================================================ import DockerCommand from '../models/dockerCommand.js'; const getDockerCommands = async () => { try { const commands = await DockerCommand.find(); return commands; } catch (err) { return []; } }; export default getDockerCommands; ================================================ FILE: lib/logger.js ================================================ const logger = { log: function(msg) { if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { console.log(msg); } } }; export default logger; ================================================ FILE: logs/.gitkeep ================================================ ================================================ FILE: models/dockerCommand.js ================================================ import mongoose from 'mongoose'; const Schema = mongoose.Schema; const ObjectId = Schema.Types.ObjectId; const exampleSchema = new Schema({ example: { type: String, required: true }, description: { type: String, required: true }, }); const dockerCommandSchema = new Schema({ command: { type: String, required: true }, description: { type: String, required: true }, examples: [exampleSchema] }); const DockerCommandModel = mongoose.model('dockerCommand', dockerCommandSchema); export default DockerCommandModel; ================================================ FILE: nginx/index.html ================================================ Hello from a file stored in a volume! ================================================ FILE: node.dockerfile ================================================ FROM node:alpine LABEL author="Dan Wahlin" ARG PACKAGES=nano ENV NODE_ENV=production ENV PORT=3000 ENV TERM=xterm RUN apk update && apk add --no-cache $PACKAGES WORKDIR /var/www COPY package*.json ./ RUN npm ci --only=production && npm cache clean --force COPY . ./ EXPOSE $PORT ENTRYPOINT ["npm", "start"] # Build: docker build -f node.dockerfile -t nodeapp . # Option 1: Create a custom bridge network and add containers into it ## Create a user-defined bridge network and add containers # docker network create --driver bridge isolated_network # docker run -d --network=isolated_network --name mongodb mongo ## NOTE: Use $(pwd) on macOS/Linux, ${PWD} in PowerShell, and %cd% in Windows CMD. # macOS/Linux # docker run -d --network=isolated_network --name nodeapp -p 3000:3000 -v "$(pwd)"/logs:/var/www/logs nodeapp # PowerShell # docker run -d --network=isolated_network --name nodeapp -p 3000:3000 -v ${PWD}/logs:/var/www/logs nodeapp # Option 2 (Legacy Linking - this is the OLD way) # Start MongoDB and Node (link Node to MongoDB container with legacy linking) # docker run -d --name my-mongodb mongo # docker run -d -p 3000:3000 --link my-mongodb:mongodb --name nodeapp danwahlin/nodeapp ================================================ FILE: package.json ================================================ { "name": "expressite", "version": "1.0.0", "private": true, "type": "module", "scripts": { "start": "node server.js", "tailwind:css": "tailwindcss -i public/styles/tailwind.css -o public/styles/styles.css" }, "dependencies": { "@handlebars/allow-prototype-access": "^1.0.5", "cookie-parser": "^1.4.7", "debug": "^4.4.3", "express": "5.1.0", "express-handlebars": "8.0.3", "handlebars": "^4.7.8", "mongoose": "^8.19.2", "morgan": "^1.10.1", "readline": "1.3.0", "serve-favicon": "^2.5.1" }, "devDependencies": { "@tailwindcss/cli": "^4.1.16", "tailwindcss": "4.1.16" } } ================================================ FILE: public/styles/styles.css ================================================ /*! tailwindcss v4.1.14 | MIT License | https://tailwindcss.com */ @layer properties; @layer theme, base, components, utilities; @layer theme { :root, :host { --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --color-blue-500: oklch(62.3% 0.214 259.815); --color-gray-300: oklch(87.2% 0.01 258.338); --color-gray-400: oklch(70.7% 0.022 261.325); --color-gray-700: oklch(37.3% 0.034 259.733); --color-gray-800: oklch(27.8% 0.033 256.848); --color-gray-900: oklch(21% 0.034 264.665); --color-white: #fff; --spacing: 0.25rem; --container-7xl: 80rem; --text-sm: 0.875rem; --text-sm--line-height: calc(1.25 / 0.875); --text-base: 1rem; --text-base--line-height: calc(1.5 / 1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75 / 1.125); --font-weight-medium: 500; --font-weight-bold: 700; --radius-md: 0.375rem; --default-font-family: var(--font-sans); --default-mono-font-family: var(--font-mono); } } @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { box-sizing: border-box; margin: 0; padding: 0; border: 0 solid; } html, :host { line-height: 1.5; -webkit-text-size-adjust: 100%; tab-size: 4; font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); font-feature-settings: var(--default-font-feature-settings, normal); font-variation-settings: var(--default-font-variation-settings, normal); -webkit-tap-highlight-color: transparent; } hr { height: 0; color: inherit; border-top-width: 1px; } abbr:where([title]) { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; } h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; } a { color: inherit; -webkit-text-decoration: inherit; text-decoration: inherit; } b, strong { font-weight: bolder; } code, kbd, samp, pre { font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); font-feature-settings: var(--default-mono-font-feature-settings, normal); font-variation-settings: var(--default-mono-font-variation-settings, normal); font-size: 1em; } small { font-size: 80%; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } table { text-indent: 0; border-color: inherit; border-collapse: collapse; } :-moz-focusring { outline: auto; } progress { vertical-align: baseline; } summary { display: list-item; } ol, ul, menu { list-style: none; } img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; } img, video { max-width: 100%; height: auto; } button, input, select, optgroup, textarea, ::file-selector-button { font: inherit; font-feature-settings: inherit; font-variation-settings: inherit; letter-spacing: inherit; color: inherit; border-radius: 0; background-color: transparent; opacity: 1; } :where(select:is([multiple], [size])) optgroup { font-weight: bolder; } :where(select:is([multiple], [size])) optgroup option { padding-inline-start: 20px; } ::file-selector-button { margin-inline-end: 4px; } ::placeholder { opacity: 1; } @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { ::placeholder { color: currentcolor; @supports (color: color-mix(in lab, red, red)) { color: color-mix(in oklab, currentcolor 50%, transparent); } } } textarea { resize: vertical; } ::-webkit-search-decoration { -webkit-appearance: none; } ::-webkit-date-and-time-value { min-height: 1lh; text-align: inherit; } ::-webkit-datetime-edit { display: inline-flex; } ::-webkit-datetime-edit-fields-wrapper { padding: 0; } ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { padding-block: 0; } ::-webkit-calendar-picker-indicator { line-height: 1; } :-moz-ui-invalid { box-shadow: none; } button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { appearance: button; } ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; } [hidden]:where(:not([hidden="until-found"])) { display: none !important; } } @layer utilities { .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border-width: 0; } .container { width: 100%; @media (width >= 40rem) { max-width: 40rem; } @media (width >= 48rem) { max-width: 48rem; } @media (width >= 64rem) { max-width: 64rem; } @media (width >= 80rem) { max-width: 80rem; } @media (width >= 96rem) { max-width: 96rem; } } .mx-auto { margin-inline: auto; } .-mr-2 { margin-right: calc(var(--spacing) * -2); } .mb-2 { margin-bottom: calc(var(--spacing) * 2); } .mb-6 { margin-bottom: calc(var(--spacing) * 6); } .ml-2 { margin-left: calc(var(--spacing) * 2); } .ml-10 { margin-left: calc(var(--spacing) * 10); } .block { display: block; } .flex { display: flex; } .hidden { display: none; } .inline-flex { display: inline-flex; } .h-6 { height: calc(var(--spacing) * 6); } .h-9 { height: calc(var(--spacing) * 9); } .h-10 { height: calc(var(--spacing) * 10); } .h-16 { height: calc(var(--spacing) * 16); } .w-6 { width: calc(var(--spacing) * 6); } .w-10 { width: calc(var(--spacing) * 10); } .w-14 { width: calc(var(--spacing) * 14); } .w-full { width: 100%; } .max-w-7xl { max-width: var(--container-7xl); } .flex-shrink-0 { flex-shrink: 0; } .flex-col { flex-direction: column; } .items-baseline { align-items: baseline; } .items-center { align-items: center; } .justify-between { justify-content: space-between; } .justify-center { justify-content: center; } .space-y-1 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); } } .space-y-4 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); } } .space-x-4 { :where(& > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse)); margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); } } .rounded { border-radius: 0.25rem; } .rounded-md { border-radius: var(--radius-md); } .border { border-style: var(--tw-border-style); border-width: 1px; } .border-gray-300 { border-color: var(--color-gray-300); } .bg-gray-800 { background-color: var(--color-gray-800); } .bg-gray-900 { background-color: var(--color-gray-900); } .bg-white { background-color: var(--color-white); } .p-2 { padding: calc(var(--spacing) * 2); } .px-2 { padding-inline: calc(var(--spacing) * 2); } .px-3 { padding-inline: calc(var(--spacing) * 3); } .px-4 { padding-inline: calc(var(--spacing) * 4); } .py-2 { padding-block: calc(var(--spacing) * 2); } .py-4 { padding-block: calc(var(--spacing) * 4); } .py-6 { padding-block: calc(var(--spacing) * 6); } .pt-2 { padding-top: calc(var(--spacing) * 2); } .pb-3 { padding-bottom: calc(var(--spacing) * 3); } .font-sans { font-family: var(--font-sans); } .text-base { font-size: var(--text-base); line-height: var(--tw-leading, var(--text-base--line-height)); } .text-lg { font-size: var(--text-lg); line-height: var(--tw-leading, var(--text-lg--line-height)); } .text-sm { font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); } .font-bold { --tw-font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold); } .font-medium { --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); } .text-gray-300 { color: var(--color-gray-300); } .text-gray-400 { color: var(--color-gray-400); } .text-gray-900 { color: var(--color-gray-900); } .text-white { color: var(--color-white); } .no-underline { text-decoration-line: none; } .shadow { --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } .shadow-sm { --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } .hover\:bg-gray-700 { &:hover { @media (hover: hover) { background-color: var(--color-gray-700); } } } .hover\:text-white { &:hover { @media (hover: hover) { color: var(--color-white); } } } .focus\:border-blue-500 { &:focus { border-color: var(--color-blue-500); } } .focus\:ring-2 { &:focus { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } } .focus\:ring-blue-500 { &:focus { --tw-ring-color: var(--color-blue-500); } } .focus\:ring-white { &:focus { --tw-ring-color: var(--color-white); } } .focus\:ring-offset-2 { &:focus { --tw-ring-offset-width: 2px; --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); } } .focus\:ring-offset-gray-800 { &:focus { --tw-ring-offset-color: var(--color-gray-800); } } .focus\:outline-none { &:focus { --tw-outline-style: none; outline-style: none; } } .sm\:px-3 { @media (width >= 40rem) { padding-inline: calc(var(--spacing) * 3); } } .sm\:px-6 { @media (width >= 40rem) { padding-inline: calc(var(--spacing) * 6); } } .md\:block { @media (width >= 48rem) { display: block; } } .md\:hidden { @media (width >= 48rem) { display: none; } } .lg\:px-8 { @media (width >= 64rem) { padding-inline: calc(var(--spacing) * 8); } } } @property --tw-space-y-reverse { syntax: "*"; inherits: false; initial-value: 0; } @property --tw-space-x-reverse { syntax: "*"; inherits: false; initial-value: 0; } @property --tw-border-style { syntax: "*"; inherits: false; initial-value: solid; } @property --tw-font-weight { syntax: "*"; inherits: false; } @property --tw-shadow { syntax: "*"; inherits: false; initial-value: 0 0 #0000; } @property --tw-shadow-color { syntax: "*"; inherits: false; } @property --tw-shadow-alpha { syntax: ""; inherits: false; initial-value: 100%; } @property --tw-inset-shadow { syntax: "*"; inherits: false; initial-value: 0 0 #0000; } @property --tw-inset-shadow-color { syntax: "*"; inherits: false; } @property --tw-inset-shadow-alpha { syntax: ""; inherits: false; initial-value: 100%; } @property --tw-ring-color { syntax: "*"; inherits: false; } @property --tw-ring-shadow { syntax: "*"; inherits: false; initial-value: 0 0 #0000; } @property --tw-inset-ring-color { syntax: "*"; inherits: false; } @property --tw-inset-ring-shadow { syntax: "*"; inherits: false; initial-value: 0 0 #0000; } @property --tw-ring-inset { syntax: "*"; inherits: false; } @property --tw-ring-offset-width { syntax: ""; inherits: false; initial-value: 0px; } @property --tw-ring-offset-color { syntax: "*"; inherits: false; initial-value: #fff; } @property --tw-ring-offset-shadow { syntax: "*"; inherits: false; initial-value: 0 0 #0000; } @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { --tw-space-y-reverse: 0; --tw-space-x-reverse: 0; --tw-border-style: solid; --tw-font-weight: initial; --tw-shadow: 0 0 #0000; --tw-shadow-color: initial; --tw-shadow-alpha: 100%; --tw-inset-shadow: 0 0 #0000; --tw-inset-shadow-color: initial; --tw-inset-shadow-alpha: 100%; --tw-ring-color: initial; --tw-ring-shadow: 0 0 #0000; --tw-inset-ring-color: initial; --tw-inset-ring-shadow: 0 0 #0000; --tw-ring-inset: initial; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; } } } ================================================ FILE: public/styles/tailwind.css ================================================ @import "tailwindcss"; ================================================ FILE: routes/index.js ================================================ import express from 'express'; import getDockerCommands from '../lib/dockerCommandsRepository.js'; import DockerCommandModel from '../models/dockerCommand.js'; const router = express.Router(); /* GET home page. */ router.get('/', async (req, res, next) => { const commands = await getDockerCommands(); res.render('index', { dockerCommands: commands }); }); /* GET new command page */ router.get('/newcommand', (req, res, next) => { res.render('newcommand'); }); router.post('/newcommand', async (req, res, next) => { // Extremely simple implementation to get a command in the database const commandData = { command: req.body.command, description: req.body.description, examples: [{ example: req.body.example, description: req.body.ex_description }] } const command = new DockerCommandModel(commandData); try { const cmd = await command.save(); console.log(cmd.command + " saved to commands collection."); } catch (err) { console.log(err); } res.redirect('/'); }); export default router; ================================================ FILE: server.js ================================================ import express from 'express'; import exphbs from 'express-handlebars'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import favicon from 'serve-favicon'; import morgan from 'morgan'; import cookieParser from 'cookie-parser'; import Handlebars from 'handlebars'; import { allowInsecurePrototypeAccess } from '@handlebars/allow-prototype-access'; import config from './lib/configLoader.js'; import db from './lib/database.js'; import routes from './routes/index.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const port = process.env.PORT || 3000; const app = express(); // view engine setup const hbs = exphbs.create({ extname: '.hbs', defaultLayout: 'masterLayout', // https://www.npmjs.com/package/@handlebars/allow-prototype-access // Need to add due to security change in Handlebars 4.6+ handlebars: allowInsecurePrototypeAccess(Handlebars) }); app.engine('hbs', hbs.engine); app.set('view engine', 'hbs'); app.set('views', path.join(__dirname, 'views')); // create a write stream (in append mode) const accessLogStream = fs.createWriteStream(path.join(__dirname, '/logs/access.log'), { flags: 'a' }); app.use(morgan('combined', { stream: accessLogStream })); app.use(favicon(__dirname + '/public/images/favicon.ico')); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); //Pass database config settings db.init(config.databaseConfig); app.use('/', routes); // catch 404 and forward to error handler app.use((req, res, next) => { var err = new Error('Not Found'); err.status = 404; next(err); }); // error handlers // development error handler // will print stacktrace if (app.get('env') === 'development') { app.use((err, req, res, next) => { res.status(err.status || 500); res.render('error', { message: err.message, error: err }); }); } // production error handler // no stacktraces leaked to user app.use((err, req, res, next) => { res.status(err.status || 500); res.render('error', { message: err.message, error: {} }); }); app.listen(port, (err) => { console.log('[%s] Listening on http://localhost:%d', app.settings.env, port); }); //********************************************************* // Quick and dirty way to detect event loop blocking //********************************************************* let lastLoop = Date.now(); function monitorEventLoop() { const time = Date.now(); if (time - lastLoop > 1000) console.error('Event loop blocked ' + (time - lastLoop)); lastLoop = time; setTimeout(monitorEventLoop, 200); } if (process.env.NODE_ENV === 'development') { monitorEventLoop(); } export default app; ================================================ FILE: tailwind.config.js ================================================ export default { content: [ "./views/**/*.{hbs,handlebars,html}", "./routes/**/*.js", "./lib/**/*.js", "./public/scripts/**/*.js" ], theme: { extend: {} }, plugins: [] }; ================================================ FILE: views/error.hbs ================================================

{{message}}

{{error.status}}

{{error.stack}}
================================================ FILE: views/index.hbs ================================================ {{#each dockerCommands}}

{{ command }} Command


{{ description }}

{{#each examples}}

{{ example }}

{{ description }}
{{/each}}

{{/each}} ================================================ FILE: views/layouts/masterLayout.hbs ================================================ {{title}}

Docker Commands

{{{body}}}
================================================ FILE: views/newcommand.hbs ================================================