├── .devcontainer ├── devcontainer.json └── docker-compose.yml ├── .dockerignore ├── .github ├── FUNDING.yml ├── dependabot.yml ├── linters │ ├── .eslintrc.yml │ ├── .hadolint.yaml │ ├── .markdown-lint.yml │ ├── .textlintrc │ └── .yaml-lint.yml └── workflows │ └── call-super-linter.yaml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── app.js ├── bin └── www ├── build-multi-arch.sh ├── compose.yaml ├── dockerfiles ├── 1.Dockerfile ├── 2.Dockerfile ├── 3.Dockerfile ├── distroless.Dockerfile ├── snyk.sh ├── tags.txt ├── ubuntu-copy.Dockerfile ├── ubuntu-deb.Dockerfile └── wrong.Dockerfile ├── healthchecks ├── README.md └── postgres-healthcheck ├── manifest └── pod.yaml ├── package-lock.json ├── package.json ├── public ├── images │ ├── picard0.gif │ ├── picard1.gif │ ├── picard2.gif │ ├── picard3.gif │ ├── picard4.gif │ ├── picard5.gif │ ├── picard6.gif │ ├── picard7.gif │ ├── picard8.gif │ └── picard9.gif └── stylesheets │ └── style.css ├── routes ├── index.js └── users.js ├── test └── smoke.js └── views ├── error.hbs ├── index.hbs └── layout.hbs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/docker-existing-docker-compose 3 | // If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. 4 | { 5 | "name": "Existing Docker Compose (Extend)", 6 | 7 | // Update the 'dockerComposeFile' list if you have more compose files or use different names. 8 | // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. 9 | "dockerComposeFile": [ 10 | "../docker-compose.yml", 11 | "docker-compose.yml" 12 | ], 13 | 14 | // The 'service' property is the name of the service for the container that VS Code should 15 | // use. Update this value and .devcontainer/docker-compose.yml to the real service name. 16 | "service": "node", 17 | 18 | // The optional 'workspaceFolder' property is the path VS Code should open by default when 19 | // connected. This is typically a file mount in .devcontainer/docker-compose.yml 20 | "workspaceFolder": "/workspace", 21 | 22 | // Set *default* container specific settings.json values on container create. 23 | "settings": {}, 24 | 25 | // Add the IDs of extensions you want installed when the container is created. 26 | "extensions": [], 27 | 28 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 29 | // "forwardPorts": [], 30 | 31 | // Uncomment the next line if you want start specific services in your Docker Compose config. 32 | // "runServices": [], 33 | 34 | // Uncomment the next line if you want to keep your containers running after VS Code shuts down. 35 | // "shutdownAction": "none", 36 | 37 | // Uncomment the next line to run commands after the container is created - for example installing curl. 38 | // "postCreateCommand": "apt-get update && apt-get install -y curl", 39 | 40 | // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. 41 | "remoteUser": "node" 42 | } 43 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | # Update this to the name of the service you want to work with in your docker-compose.yml file 4 | node: 5 | # If you want add a non-root user to your Dockerfile, you can use the "remoteUser" 6 | # property in devcontainer.json to cause VS Code its sub-processes (terminals, tasks, 7 | # debugging) to execute as the user. Uncomment the next line if you want the entire 8 | # container to run as this user instead. Note that, on Linux, you may need to 9 | # ensure the UID and GID of the container user you create matches your local user. 10 | # See https://aka.ms/vscode-remote/containers/non-root for details. 11 | # 12 | # user: vscode 13 | 14 | # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer 15 | # folder. Note that the path of the Dockerfile and context is relative to the *primary* 16 | # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" 17 | # array). The sample below assumes your primary file is in the root of your project. 18 | # 19 | # build: 20 | # context: . 21 | # dockerfile: .devcontainer/Dockerfile 22 | 23 | volumes: 24 | # Update this to wherever you want VS Code to mount the folder of your project 25 | - .:/workspace:cached 26 | 27 | # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details. 28 | # - /var/run/docker.sock:/var/run/docker.sock 29 | 30 | # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. 31 | # cap_add: 32 | # - SYS_PTRACE 33 | # security_opt: 34 | # - seccomp:unconfined 35 | 36 | # Overrides default command so things don't shut down after the process ends. 37 | command: /bin/sh -c "while sleep 1000; do :; done" 38 | 39 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # add git-ignore syntax here of things you don't want copied into docker image 2 | 3 | # for all code you usually don't want .git history in image, just the current commit you have checked out 4 | .git 5 | 6 | # you usually don't want dockerfile and compose files in the image either 7 | *Dockerfile* 8 | *docker-compose* 9 | 10 | # for Node.js apps, you want to build the node_modules in the image, and not copy from host 11 | node_modules 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: bretfisher 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | -------------------------------------------------------------------------------- /.github/linters/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | commonjs: true 4 | es6: true 5 | node: true 6 | extends: 'eslint:recommended' 7 | globals: 8 | Atomics: readonly 9 | SharedArrayBuffer: readonly 10 | parserOptions: 11 | ecmaVersion: 2018 12 | rules: 13 | no-unused-vars: off 14 | no-undef: off 15 | ignorePatterns: ["node_modules"] -------------------------------------------------------------------------------- /.github/linters/.hadolint.yaml: -------------------------------------------------------------------------------- 1 | # README: https://github.com/hadolint/hadolint 2 | 3 | # Often it's a good idea to do inline disables rather that repo-wide in this file. 4 | # Example of inline Dockerfile rules: 5 | # hadolint ignore=DL3018 6 | #RUN apk add --no-cache git 7 | 8 | # or just ignore rules repo-wide 9 | ignored: 10 | - DL3003 #ignore that we use cd sometimes 11 | - DL3006 #image pin versions 12 | - DL3008 #apt pin versions 13 | - DL3018 #apk add pin versions 14 | - DL3022 #bad rule for COPY --from 15 | - DL3028 #gem install pin versions 16 | - DL3059 #multiple consecutive runs 17 | - DL4006 #we don't need pipefail in this 18 | - SC2016 #we want single quotes sometimes 19 | 20 | 21 | # FULL TEMPLATE 22 | # failure-threshold: string # name of threshold level (error | warning | info | style | ignore | none) 23 | # format: string # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | gnu | codacy) 24 | # ignored: [string] # list of rules 25 | # label-schema: # See Linting Labels below for specific label-schema details 26 | # author: string # Your name 27 | # contact: string # email address 28 | # created: timestamp # rfc3339 datetime 29 | # version: string # semver 30 | # documentation: string # url 31 | # git-revision: string # hash 32 | # license: string # spdx 33 | # no-color: boolean # true | false 34 | # no-fail: boolean # true | false 35 | # override: 36 | # error: [string] # list of rules 37 | # warning: [string] # list of rules 38 | # info: [string] # list of rules 39 | # style: [string] # list of rules 40 | # strict-labels: boolean # true | false 41 | # disable-ignore-pragma: boolean # true | false 42 | # trustedRegistries: string | [string] # registry or list of registries -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | # MD013/line-length - Line length 2 | MD013: 3 | # Number of characters, default is 80 4 | # I'm OK with long lines. All editors now have wordwrap 5 | line_length: 9999 6 | # Number of characters for headings 7 | heading_line_length: 100 8 | # check code blocks? 9 | code_blocks: false 10 | -------------------------------------------------------------------------------- /.github/linters/.textlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "filters": { 3 | "comments": true 4 | }, 5 | "rules": { 6 | "terminology": { 7 | "severity" : "warning" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /.github/linters/.yaml-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################################### 3 | # These are the rules used for # 4 | # linting all the yaml files in the stack # 5 | # NOTE: # 6 | # You can disable line with: # 7 | # # yamllint disable-line # 8 | ########################################### 9 | rules: 10 | braces: 11 | level: warning 12 | min-spaces-inside: 0 13 | max-spaces-inside: 0 14 | min-spaces-inside-empty: 1 15 | max-spaces-inside-empty: 5 16 | brackets: 17 | level: warning 18 | min-spaces-inside: 0 19 | max-spaces-inside: 0 20 | min-spaces-inside-empty: 1 21 | max-spaces-inside-empty: 5 22 | colons: 23 | level: warning 24 | max-spaces-before: 0 25 | max-spaces-after: 1 26 | commas: 27 | level: warning 28 | max-spaces-before: 0 29 | min-spaces-after: 1 30 | max-spaces-after: 1 31 | comments: disable 32 | comments-indentation: disable 33 | document-end: disable 34 | document-start: disable 35 | empty-lines: 36 | level: warning 37 | max: 2 38 | max-start: 0 39 | max-end: 0 40 | hyphens: 41 | level: warning 42 | max-spaces-after: 1 43 | indentation: 44 | level: warning 45 | spaces: consistent 46 | indent-sequences: true 47 | check-multi-line-strings: false 48 | key-duplicates: enable 49 | line-length: disable 50 | new-line-at-end-of-file: disable 51 | new-lines: 52 | type: unix 53 | trailing-spaces: disable -------------------------------------------------------------------------------- /.github/workflows/call-super-linter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # template source: https://github.com/bretfisher/super-linter-workflow/blob/main/templates/call-super-linter.yaml 3 | name: Lint Code Base 4 | 5 | on: 6 | 7 | push: 8 | branches: [main] 9 | 10 | pull_request: 11 | 12 | jobs: 13 | call-super-linter: 14 | 15 | name: Call Super-Linter 16 | 17 | permissions: 18 | contents: read # clone the repo to lint 19 | statuses: write #read/write to repo custom statuses 20 | 21 | ### use Reusable Workflows to call my workflow remotely 22 | ### https://docs.github.com/en/actions/learn-github-actions/reusing-workflows 23 | ### you can also call workflows from inside the same repo via file path 24 | 25 | uses: bretfisher/super-linter-workflow/.github/workflows/reusable-super-linter.yaml@main 26 | 27 | ### Optional settings examples 28 | 29 | with: 30 | ### For a DevOps-focused repository. Prevents some code-language linters from running 31 | ### defaults to false 32 | devops-only: true 33 | 34 | ### A regex to exclude files from linting 35 | ### defaults to empty 36 | # filter-regex-exclude: html/.* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://docs.npmjs.com/cli/shrinkwrap#caveats 27 | node_modules 28 | 29 | # Debug log from npm 30 | npm-debug.log 31 | 32 | .dccache -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch on Host", 11 | "program": "${workspaceRoot}/bin/www", 12 | "cwd": "${workspaceRoot}" 13 | }, 14 | { 15 | "type": "node2", 16 | "request": "attach", 17 | "name": "Docker 9229", 18 | "port": 9229 19 | }, 20 | { 21 | "name": "Docker 5858", 22 | "type": "node", 23 | "request": "attach", 24 | "port": 5858, 25 | "address": "localhost", 26 | "restart": false, 27 | "sourceMaps": false, 28 | "localRoot": "${workspaceRoot}", 29 | "remoteRoot": "/usr/src/app" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enabled": false 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2119 Bret Fisher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Rocks in Docker 2 | 3 | > My DockerCon 2023 talk, which is an update of my DockerCon 2022 talk, which is an update of my DockerCon 2019 talk "Node.js Rocks in Docker and DevOps" 4 | 5 | - [DockerCon 2023 talk on YouTube](https://www.youtube.com/watch?v=GEPW008G250) 6 | - [DockerCon 2022 talk on YouTube](https://www.youtube.com/watch?v=Z0lpNSC1KbM) 7 | - [DockerCon 2019 talk on YouTube](https://www.youtube.com/watch?v=Zgx0o8QjJk4) 8 | 9 | **Want more? [Get my Docker Mastery for Node.js course with a coupon](https://www.bretfisher.com/docker-mastery-for-nodejs/): 9 hours of video to help a Node.js developer use all the best Docker features.** 10 | 11 | Also, here's [my other example repositories](https://github.com/bretfisher/bretfisher) including DevOps automation, Docker, and Kubernetes stuff. 12 | 13 | ## Who is this for? 14 | 15 | - **You know some Node.js** 16 | - **You know some Docker** 17 | - **You want more Node+Docker awesomesauce** 18 | 19 | ## Table of Contents 20 | 21 | - [Searching for the best Node.js base image](#searching-for-the-best-nodejs-base-image) 22 | - [TL;DR](#tldr) 23 | - [General goals of a Node.js image](#general-goals-of-a-nodejs-image) 24 | - [Node.js base image comparison stats, September 25th, 2023](#nodejs-base-image-comparison-stats-september-25th-2023) 25 | - [My recommended (v18)](#my-recommended-v18) 26 | - [Comparison highlights](#comparison-highlights) 27 | - [Ruling out Alpine](#ruling-out-alpine) 28 | - [Ruling out `node:latest` or `node:lts`](#ruling-out-nodelatest-or-nodelts) 29 | - [Ruling out `debian:*-slim` as a custom base](#ruling-out-debian-slim-as-a-custom-base) 30 | - [Building a custom Node.js image based on Ubuntu](#building-a-custom-nodejs-image-based-on-ubuntu) 31 | - [Ruling out NodeSource deb packages](#ruling-out-nodesource-deb-packages) 32 | - [👉 My favorite custom Node.js base image](#-my-favorite-custom-nodejs-base-image) 33 | - [Using distroless](#using-distroless) 34 | - [The better distroless setup?](#the-better-distroless-setup) 35 | - [Dockerfile best practices for Node.js](#dockerfile-best-practices-for-nodejs) 36 | - [You've got a `.dockerignore` right?](#youve-got-a-dockerignore-right) 37 | - [Use `npm ci --only=production` first, then layer dev/test on top](#use-npm-ci---onlyproduction-first-then-layer-devtest-on-top) 38 | - [Change user to `USER node`](#change-user-to-user-node) 39 | - [Proper Node.js startup: `tini`](#proper-nodejs-startup-tini) 40 | - [Avoid `node` process managers (npm, yarn, nodemon, forever, pm2)](#avoid-node-process-managers-npm-yarn-nodemon-forever-pm2) 41 | - [Add Multi-Stage For a Single Dev-Test-Prod Dockerfile](#add-multi-stage-for-a-single-dev-test-prod-dockerfile) 42 | - [Adding test, lint, and auditing stages](#adding-test-lint-and-auditing-stages) 43 | - [Add multi-architecture builds](#add-multi-architecture-builds) 44 | - [Proper Node.js shutdown](#proper-nodejs-shutdown) 45 | - [Compose v2 and easy local workflows](#compose-v2-and-easy-local-workflows) 46 | - [`target: dev`](#target-dev) 47 | - [Dependency startup utopia: Use `depends_on:`, with `condition: service_healthy`](#dependency-startup-utopia-use-depends_on-with-condition-service_healthy) 48 | - [Node.js development in a container or not?](#nodejs-development-in-a-container-or-not) 49 | - [Production Checklist](#production-checklist) 50 | 51 | ## Searching for the best Node.js base image 52 | 53 | Honestly, this is one of the hardest choices you'll make at first. After supporting Node.js on VMs (and now images) for over a decade, I can say there is no perfect solution. Everything is a compromise. Often you'll be trading simplicy for increased flexibility, security, or smaller images. The farther down the rabit hole I go of "the smallest, most secure, most reliable Node.js image", the stranger the setup seems to get. I do have a recommended setup though, but to convince you, I need to explain how we get there. 54 | 55 | ### TL;DR 56 | 57 | Below I list all the data and justification for my recommendations, but if you just want the result, then: 58 | 59 | - General dev/test/prod image that's easy to use: `node:16-bullseye-slim` 60 | - Better image that has less CVE's, build your own base with `ubuntu:20.04` and Node install (official build, image COPY, or deb package) 61 | - Tiny prod image that's using a supported Node.js build: `gcr.io/distroless/nodejs:16` 62 | 63 | ### General goals of a Node.js image 64 | 65 | My goals/requirements, in order of priority, for the final production stage image: 66 | 67 | - [Tier 1](https://github.com/nodejs/node/blob/master/BUILDING.md#platform-list) support by the Node.js team. 68 | - Minimal CVEs. No HIGH or CRITICAL vulnerabilities. 69 | - Version (even to patch level) is controlled, to ensure reproducable builds/tests. 70 | - Doesn't contain unneeded packages, like Python or build tools. 71 | - Under 200MB image size (without code or node_modules). 72 | 73 | ### Node.js base image comparison stats, September 25th, 2023 74 | 75 | Here's a compairison of the resonable options I've come up with. Most I've tried in real workloads at some point. Some are shown as base images without Node.js just so you can see their CVE count and realize they're a non-starter. Others are a combo of a base image with Node.js installed (in various ways). Lastly, remember that in multi-stage setups, we can always have one base for dev/build/test environments and another for production. 76 | 77 | - CVE counts are crit/high/med/low. 78 | - Docker Hub Official `node:18`, `20`, and `slim` tags are based on Debian 12 (bookworm). 79 | - Node versions are generally `20.7.0` or `18.18.0`. 80 | 81 | | Image Name | Snyk CVEs | Docker Scout CVEs | Trivy CVEs | Grype CVEs | Image Size | 82 | | ------------------------------------------ | --------- | ----------------- | ------------ | ----------- | ---------- | 83 | | `node:18` (lts) | 0/0/2/159 | 0/0/3/82 | 3/58/215/468 | 3/57/198/30 | 1,042MB | 84 | | `node:18-slim` | 0/0/1/31 | 0/0/0/17 | 0/3/7/50 | 0/3/5/3 | 264MB | 85 | | `node:18-alpine` [^1] | 0/0/0/0 | 0/0/0/0 | 0/0/0/0 | 0/0/0/0 | 175MB | 86 | | `debian:12-slim` (NO node) | 0/0/1/31 | 0/0/0/17 | 0/3/7/50 | 0/3/5/3 | 97MB | 87 | | `ubuntu:22.04` (NO node) | 0/1/3/11 | 0/0/2/9 | 0/0/6/15 | 0/2/6/12 | 69MB | 88 | | `ubuntu:23.04` (NO node) | 0/1/2/6 | 0/0/0/0 [^3] | 0/2/3/12 | 0/2/3/6 | 93MB | 89 | | `ubuntu:22.04+nodesource18` (apt pkg) | 0/2/25/23 | 0/3/25/22 | 0/3/32/39 | 0/3/32/35 | 263MB | 90 | | `ubuntu:22.04+node:18` (img copy) | 0/0/3/11 | 0/0/2/9 | 0/0/6/15 | 0/0/6/12 | 225MB | 91 | | `ubuntu:23.04+node:18` (img copy) | 0/0/2/6 | 0/0/0/0 | 0/0/3/12 | 0/0/3/6 | 248MB | 92 | | `gcr.io/distroless/nodejs18-debian12` [^2] | 0/0/2/15 | 0/0/0/0 [^4] | 0/1/8/12 | 0/1/8/0 | 178MB | 93 | | `cgr.dev/chainguard/node:latest` [^5] | 0/0/0/0 | 0/0/0/0 | 0/0/0/0 | 0/0/0/0 | 108MB | 94 | 95 | [^1]: 1. Alpine's [musl libc](https://musl.libc.org/) is only Experimant support by the Node.js project, and I only recommend Tier 1 support for production servers. 2. While Alpine-based images have image tags for versioning, apk packages you need inside them can't be relelablly versioned over time (eventually packages are pulled from Alpine's apk and builds will fail much eariler than Ubuntu.) 96 | 97 | [^2]: 1. Distroless Node.js versions weren't always up to date. 2. Distroless can only be pinned (in image tag) to the major Node.js version. That is disapointing. You can technically use the sha256 hash of any image to pin for determinstic builds, but the process for doing so (and determining what hashes are which verions later) is far from ideal. 3. It also doesn't have a package manager and can only be the last stage in build. 98 | 99 | [^3]: Docker is aware that Scout is not scanning this image correctly. 100 | 101 | [^4]: Docker is aware that Scout is not scanning distroless correctly. 102 | 103 | [^5]: Chainguard `latest` tag is the lts version. Chainguard public images don't let you pin to version tags, so pin to the sha hash to stay determinstic. Chainguard has [paid plans that give access to version tags](https://www.chainguard.dev/unchained/important-updates-for-chainguard-images-public-catalog-users). 104 | 105 | ### My recommended (v18) 106 | | Image Name | Snyk CVEs | Docker Scout CVEs | Trivy CVEs | Grype CVEs | Image Size | 107 | | ------------------------------------------ | --------- | ----------------- | ------------ | ----------- | ---------- | 108 | | `node:18-slim` | 0/0/1/31 | 0/0/0/17 | 0/3/7/50 | 0/3/5/3 | 264MB | 109 | | `ubuntu:23.04+node:18` (img copy) | 0/0/2/6 | 0/0/0/0 | 0/0/3/12 | 0/0/3/6 | 248MB | 110 | | `cgr.dev/chainguard/node:latest` | 0/0/0/0 | 0/0/0/0 | 0/0/0/0 | 0/0/0/0 | 108MB | 111 | 112 | ### Comparison highlights 113 | 114 | - While Alpine isn't showing CVEs, it's not the smallest image, nor is it a supported [Tier 1](https://github.com/nodejs/node/blob/master/BUILDING.md#platform-list) build by the Node.js team. **Those are just a few of the reasons I don't recommend Alpine-based Node.js images** (see next heading below). 115 | - Note my use of `node:16-bullseye-slim`. Then notice the better CVE count of it vs. `node:16-slim`. **Node.js Debian images don't change the Debian version after a major Node version is released.** If you want to combine the latest Node.js LTS with the current Debian stable, you'll need to use a different tag. In this example, Debian 11 (bullseye) is newer than the default `node:16` Debian 10 (buster) release. Why isn't Debian updated? For stability of that Node.js major version. Once you start using a specific Node.js major release (say 16.x), you can expect the underlying Debian major version to not change for any future Node.js 16.x release of official images. Once Debian 11 (bullseye) came out, you would have to change your image tag to specify that Debian version if you wanted to change the Debian base during a Node.js major release cycle. Changing the underlying Debian version to a new major release may cause major apt package changes. 116 | - **Ubuntu is historially better at reducing CVEs in images than Debian.** You'll notice lower CVE counts in Ubuntu-based images. It's my go to default base for [JIT-based](https://en.wikipedia.org/wiki/Just-in-time_compilation) programming languages (Node.js, Python, Ruby, etc.) 117 | - Google's [Distroless image](https://github.com/GoogleContainerTools/distroless) only has 1% of the file count compared to the others, yet it still similar CVE numbers to Ubuntu, and only saves 60MB in size. **Is distroless really worth the added complexity?** 118 | - CVE counts are a moving target, so I don't make long-term image decisions base on a small CVE count difference (under 10), but we see a trend here. The High+Critical count are the most important, and these images options tend to have under twenty, or the other side of the spectrium, **hundreds**. You can't reason with hundreds. It's a non-starter. It's very rare that anyone's going to analize that many and determine your true risk. With under twenty, someone can evaluate each for the "true risk" in that use case (e.g. is the vunerable file even executied). Anything that's "zero CVEs" today won't always be zero. 119 | 120 | ### Ruling out Alpine 121 | 122 | I'm a fan of the *idea* of Alpine images, but for Node.js, they have several fatal flaws. Alpine image variants are based on [busybox](https://hub.docker.com/_/busybox) and [musl libc](https://musl.libc.org/), which are security focused, but have side affects. The official Alpine-based Node.js image `node:alpine` has multiple non-starters for me: 123 | 124 | - Musl libc is only considered [Expiremental by Node.js](https://github.com/nodejs/node/blob/master/BUILDING.md#platform-list). 125 | - Alpine package versions can't be **relilablity** pinned at the minor or patch level. You can pin the image tag, but if you pin apk packages inside it, eventually it'll fail to build once the apk system updates the package version. 126 | - The justification of using Alpine for the sake of image size is over-hyped, and app dependencies are usually far bigger then the base image size itself. I often see Node.js images with 500-800MB of `node_modules` and rendered frontend content. Many other base image options (`node:slim`, `ubuntu`, and distroless) have nearly the same size as Alpine without any of the potental negatives. 127 | - I've personally had multiple prod issues with Alpine that didn't exist in debian-based containers, including file I/O and performance issues. Many others have told me the same over the years and for Node.js (and Python) apps. Prod teams get burned too many times to consider Alpine a safe alternative. 128 | 129 | Sorry Alpine fans. It's still a great OS and I still use the `alpine` official image regurarly for utilites and troubleshooting. 130 | 131 | ### Ruling out `node:latest` or `node:lts` 132 | 133 | It's convient to use the standard official images. However, these non-slim variants were foused on ease of use for new Docker users, and are not as good for production. They include a ton of packages that you'll likely never need in production, like imagemagick, compilers, mysql client, even svn/mercurial. That's why they have dozens of high and critical CVE's. That's a non-starter for production. 134 | 135 | Here's another argument against them that I see with existing (brownfield) apps that are migrated to Docker-based builds: 136 | 137 | > ⚠️ If you start on these non-slim official node images, you may not realize the *true* dependencies of your app, because it turns out you needed more then just the nodejs package, and if you ever switch to a different base image or package manager, you'll find that your app doesn't work, because it needed some apt/yum package that was in the bloated default base images, but aren't included in in slim/alpine/distroless images. 138 | 139 | You might think "who doesn't know their exact system depdenencies?". With 10-year old apps, I see it often that teams don't have a true list of everything they need. They might know that on CentOS 7.3 they need x/y/z, but if they swap to a different base, it turns out there was a library included in CentOS for convicene that isn't in that new base. 140 | 141 | Docker slim images really help ensure you have an accurate list of apt/yum dependencies. 142 | 143 | ### Ruling out `debian:*-slim` as a custom base 144 | 145 | `debian:12-slim` saves 44MB and 2k files, but **Debian slim has the same CVE count as the default `debian:latest` image**. Too bad. 146 | 147 | ### Building a custom Node.js image based on Ubuntu 148 | 149 | The `ubuntu:22.04` image is a great, low-CVE, small image. I know multiple teams that use it as their base for *everything*, and make their own custom base images on top of it. 150 | 151 | How you get Node.js into that image is the subject of this debate. You can't just `apt update && apt install nodejs`, because you'll get a wickedly old version (v12 at last check). Here's two other ways to install Node.js in Ubuntu's base image. 152 | 153 | #### Ruling out NodeSource deb packages 154 | 155 | NodeSource provides the official Debian (apt) packages, but they have issues and limitations, which is ultmiatly why I don't use them often for custom built node base images. 156 | 157 | 1. The package repositories drop off old versions, so you can't pin a Node.js version. A workaround is to manually download the .deb file and "pin" to its URL. This isn't a big deal, but it is a downside to adoption. People either have to discover this through trial and error, or are already apt-pros. 158 | 2. It requires Python3 to isntall Node.js. Um, what? Yes. Every time you use a NodeSource apt package, you are adding Python 3.x minimal and any potential CVEs that come with them. I've figured out it's 20MB of additional stuff. 159 | 160 | ### 👉 My favorite custom Node.js base image 161 | 162 | I didn't want to do this. I prefer easy. I prefer someone *else* maintain my Node.js base image, but here we are. The other options aren't great and given that this has worked so well for me, I'm now recommending and using this with others. Tell me what you think in this GitHub repositories Discussions tab, on [Twitter](https://twitter.com/bretfisher), or in my [DevOps Discord Server](https://devops.fan). 163 | 164 | > to get one of the smallest images, with the least CVEs, and a shell + package manager included: Use a stock ubuntu LTS image, and `COPY` in the Node.js binaries and libraries from the official Node.js slim image. 165 | 166 | It basically looks like this, with a full example in [./dockerfiles/ubuntu-copy.Dockerfile](./dockerfiles/ubuntu-copy.Dockerfile): 167 | 168 | ```Dockerfile 169 | FROM node:16.14.2-bullseye-slim as node 170 | FROM ubuntu:focal-20220404 as base 171 | COPY --from=node /usr/local/ /usr/local/ 172 | # this ensures we fix simlinks for npx, Yarn, and PnPm 173 | RUN corepack disable && corepack enable 174 | ENTRYPOINT ["/usr/local/bin/node"] 175 | # rest of your stuff goes here 176 | ``` 177 | 178 | Note, if you don't like this COPY method, and feel it's a bit hacky, you could also just download the Node.js distros from nodejs.org and copy the binaries and libraries into your image. This is what the [official Node.js slim image does](https://github.com/nodejs/docker-node/blob/6e8f32de3f620833e563e9f2b427d50055783801/16/bullseye-slim/Dockerfile), but it's a bit more complex then my example above that just copies from one official image to another. 179 | 180 | One negative here. Most CVE scanners use package lists to determine if a image or system is vunerable. When we COPY in binaries and libraries, those aren't tracked by package systems, so they won't show up on CVE scans. The workaround is to also scan the FROM image that you COPY Node.js from. 181 | 182 | ### Using distroless 183 | 184 | I consider this a more advanced solution, because it doesn't include a shell or any utilities like package managers. A distroless image is something you `COPY` your app directory tree into as the last Dockerfile stage. It's meant to keep the image at an absolute minimum, and has the low CVE count to match. 185 | 186 | > **It cuts the base image file count to 1% of the others, which is amazing**, but it doesn't lesson the CVEs compared to Ubuntu and only saves us 50MB over ubuntu+node. It also isn't usable in dev or test stages because they often need a shell and package manager. 187 | 188 | Also, and I can't believe this is an issue, but the distroless images can't easily be pinned to a specific version. It can only be pinned to the Major version, like `gcr.io/distroless/nodejs20-debian12`. So those of us who want determinatic builds, can't use the version tag. A determinstic build would mean that every component is pinned to the exact version and if we built the image two times, a month apart, that nothing should be different. Now, distroless can be determinastic if you pin the sha256 hash of the image, not the version. But each time they ship a image update, the tag is reused and there's no way to go back and see what hashes match old versions (without your own manual tracking), so this isn't good. 189 | 190 | So, while I think the ubuntu+node solution is less secure than distroless in theory, the CVE improvement in distroless just isn't there (today) to justify this extra effort of using it. I could be convinced otherwise though, so here's how I would use it: 191 | 192 | ### The better distroless setup? 193 | 194 | My recommended usage usage is to set `node:*-slim` everywhere execpt the final production stage, where you `COPY --chown=1000:1000 /app` to distroless... AND you also pin to the sha256 hash of your specific distroless image, then I think that's a reasonable solution. If you're tracking both images well, you can be sure that your distroless is using the same base Debian that your official `node:*-slim` image is. That's ideal, then any dev/test OS (apt) libraries will be very close or identical. 195 | 196 | Get the full image name:id with the sha256 hash from the registry by downloading the image and inspecting it. You're looking for the `RepoDigests`, or just grep like this: 197 | 198 | ```shell 199 | docker pull gcr.io/distroless/nodejs:16 200 | docker inspect gcr.io/distroless/nodejs:16 | grep "gcr.io/distroless/nodejs@sha256" 201 | # or an easier way to see all image digests 202 | docker images --digests 203 | ``` 204 | 205 | Then add it to your prod stage like this: 206 | 207 | ```Dockerfile 208 | FROM gcr.io/distroless/nodejs20-debian12:latest@sha256:6499c05db574451eeddda4d3ddb374ac1aba412d6b2f5d215cc5e23c40c0e4d3 as distroless 209 | COPY --from=source --chown=1000:1000 /app /app 210 | COPY --from=base /usr/bin/tini /usr/bin/tini 211 | ``` 212 | 213 | Remember that since it's a new image vs prior stages, you'll need to repeat any metadata that you need, including ENVs, ARGs, LABEL, EXPOSE, ENTRYPOINT, CMD, WORKDIR, or USER that you set in previous stages. 214 | 215 | A full example of using Distroless is here: [./dockerfiles/distroless.Dockerfile](./dockerfiles/distroless.Dockerfile) 216 | 217 | ## Dockerfile best practices for Node.js 218 | 219 | These are "better" practices really, I'll let you decide if they are "best" for you. 220 | 221 | ### You've got a `.dockerignore` right? 222 | 223 | If so it should have at least `.git` and `node_modules` in it, to avoid unnecessary files in your image. 224 | 225 | ### Use `npm ci --only=production` first, then layer dev/test on top 226 | 227 | In the `base` stage above, you'll want to copy in your package files and then only install production dependencies. Use npm's `ci` command that will only reference the lock file for which exact versions to install. Apparently it's faster than `npm install`. 228 | 229 | Then you'll install `devDependencies` in a future stage, but `ci` doesn't support dev-only dependency install, so you'll need to use `npm install` in the `dev` stage. 230 | 231 | ### Change user to `USER node` 232 | 233 | There's (almost) no reason to run as root in a Node.js container. The offical node images already have this user created in the base image, so to switch your user in the Dockerfile, use the `USER node` directive. 234 | 235 | You'll likely need more then that though. You'll want all files you copy in, and the directory you use `WORKDIR` in, to be owned by `node`. 236 | 237 | This smallest Dockerfile would have lines in it like this, for setting directory permissions, setting file permissions during any `COPY` commands, etc: 238 | 239 | ```Dockerfile 240 | RUN mkdir /app && chown -R node:node /app 241 | WORKDIR /app 242 | USER node 243 | COPY --chown=node:node package*.json yarn*.lock ./ 244 | RUN npm ci --only=production && npm cache clean --force 245 | COPY --chown=node:node . . 246 | ``` 247 | 248 | ProTip: If you need to run commands/shells in the container as root, add `--user=root` to your Docker commands. 249 | 250 | ### Proper Node.js startup: `tini` 251 | 252 | When I'm writing production-quality Dockerfiles for programming languages, I usually add `tini` to the ENTRYPOINT. The [tini project](https://github.com/krallin/tini) is a simple, lightweight, and portable init process that can be used to start a Node.js process, and more importantly, it properly handles Linux Kernel signals, and reaps any [zombie processes](https://en.wikipedia.org/wiki/Zombie_process) that get lost in the suffle. 253 | 254 | See *Proper Node.js shutdown* below for the other half of this process up/down problem. 255 | 256 | ### Avoid `node` process managers (npm, yarn, nodemon, forever, pm2) 257 | 258 | `yarn`, `npm`, `nodemon`, `forever`, or `pm2` are not needed for launching the `node` binary. 259 | 260 | - They add unnecessary complexity. 261 | - They often don't listen for Linux signals (`tini` can help, but still. 262 | - We don't want an external process launching multiple `node` processes, that's what docker/containerd/cri-o are for. If you need more replicas, use your orchestrator to launch more containers. 263 | 264 | ### Add Multi-Stage For a Single Dev-Test-Prod Dockerfile 265 | 266 | The way I approach JIT complied languages like Node.js is to have a single Dockerfile that is used for dev, test, and prod. This is a good way to keep your production images small and still have access to that "fat" dev and test image. However, it means that the single Dockerfile will get more complex. 267 | 268 | General Dockerfile flow of stages: 269 | 270 | 1. base: all prod dependencies, no code yet 271 | 2. dev, from base: all dev dependencies, no code yet (in dev, source code is bind-mounted anyway) 272 | 3. source, from base: add code 273 | 4. test/audit, from source: then `COPY --from=dev` for dev dependencies, then run tests. Optionally, audit and lint code (if you don't do it on git push already). 274 | 5. prod, from source: no change from source stage, but listed last so in case a stage isn't targeted, the builder will default to this stage 275 | 276 | `--target dev` for local development where you bind-mount into the container 277 | `--target test` for automated CI testing, like unit tests and npm audit 278 | `--target prod` for running on servers, with no devDependencies included, and no "uninstalls" or removing things to slim down 279 | 280 | Note, if you're using a special prod image like distroless, the `prod` stage is where you COPY in your app from the `source` stage. 281 | 282 | ### Adding test, lint, and auditing stages 283 | 284 | In my [DockerCon 2019 version](https://www.youtube.com/watch?v=Zgx0o8QjJk4) of this talk, I showed off even more stages for running `npm test`, `npm lint`, `npm audit` and more. I no longer recommend this "inside the build" method, but it's still possible. It just depends on if you already have an automation platform. 285 | 286 | > I don't recommend running tests/lint/audit *inside* the docker build because we have better automation platforms that are easier to troubleshoot, have better logging, and likely already have tools to test/lint/audit built-in. 287 | 288 | I'm a big GitHub Actions fan (checkout [my GHA templates here](https://github.com/BretFisher/github-actions-templates)) and now use Super-Linter, Trivy/Snyk CVE scanners, and more in their own jobs, *after* the image is built. If you've got your own automation platform (CI/CD) then I think that's a better place to perform these tasks. 289 | 290 | I also find that I can parallelize those things much easier in CI/CD automation rather than in a really long Dockerfile. 291 | 292 | ## Add multi-architecture builds 293 | 294 | Now that Apple M1's are mainstream, and Windows arm64 laptops are catching up, it's the perfect time for you to build not just x86_64 (amd64) images, but also build arm64/v8 as well, at the same time. 295 | 296 | With Docker Desktop, you can build and push multiple architectures at once. 297 | 298 | ```shell 299 | # if you haven't created a new custom builder instance, run this once: 300 | docker buildx create --use 301 | 302 | # now build and push an image for two architectures: 303 | docker buildx build -f dockerfile/5.Dockerfile --target prod --name :latest --platform=linux/amd64,linux/arm64 . 304 | ``` 305 | 306 | A better way is to build in automation on every pull request push, and every push to a release branch. Docker has a [GitHub Action that's great for this](https://github.com/marketplace/actions/build-and-push-docker-images). **[You can also watch my talk on GitHub Actions for Docker CI/CD Workflows in that repository](https://github.com/BretFisher/allhands22)**. 307 | 308 | ## Proper Node.js shutdown 309 | 310 | This topic deserves more importance, as many tend to assume it'll all work out when you're doing production rolling updates. 311 | 312 | But, can you be sure that, once your container runtime has ask the container to stop, that: 313 | 314 | - DB transactions are complete. 315 | - Any long-running functions are complete, like file upload/download, loops, PDF generation, etc. 316 | - Long-polling connections are properly closed. 317 | - Incoming connections have completed and *gracefully* closed (TCP FIN Packet) so they can re-connect to a new container. 318 | 319 | > ⚠️ The more you dig into this problem, the more you may realize you're providing a poor user experience during container reboots and replacements. 320 | 321 | The end goal is if you have two replicas of a container running with a service/LB in front of it, and gracefully shutdown one of the containers, that clients/users never notice. The container will wait for processing to complete (including long-polling, file upload/download, etc.) and only then will it shut down. 322 | 323 | Also note that Docker & Kubernetes can get in the way if not configured in the runtime config. Within 15-30s, both will kill the container unless you override that default. Some may even need *60 minutes* as a grace peroid. 324 | 325 | `docker run --stop-timeout` in seconds 326 | 327 | In Kubernetes, look for `terminationGracePeriodSeconds` 328 | 329 | Projects like [http-terminator](https://github.com/gajus/http-terminator) can help you solve this. 330 | 331 | ## Compose v2 and easy local workflows 332 | 333 | I don't always develop *in* a container, but I always start my dependencies in them. My prefered way to do that is in the `docker compose` CLI. 334 | 335 | > Notice I didn't say `docker-compose` (with the dash). That's now "old school" v1 CLI. Docker rewrote the Compose CLI in [golang](https://go.dev/) and made it a proper Docker plug-in. [See more](https://github.com/docker/compose#readme). 336 | 337 | Remember `version: 3.9` or `version: 2.7`? Delete it. You no longer need a version line in your compose files (since 2020 at least) and the Compose CLI now uses the Compose Spec, which is version-less, and our Compose CLI supports all the 2.x and 3.x features in the same file! 338 | 339 | Checkout this repoisitories [`docker-compose.yml`](./docker-compose.yml) file for these details: 340 | 341 | ### `target: dev` 342 | 343 | Now that we have a dev stage in our Dockerfile, we need to target it for local `docker compose build`. 344 | 345 | ```yaml 346 | services: 347 | node: 348 | build: 349 | dockerfile: dockerfiles/5.Dockerfile 350 | context: . 351 | target: dev 352 | ``` 353 | 354 | ### Dependency startup utopia: Use `depends_on:`, with `condition: service_healthy` 355 | 356 | This is a multi-step approach. First add healthchecks to any Compose service that is a dependency. You might need them for databases, backend services, or even proxies. Then, add a dependency section to your app service like so: 357 | 358 | ```yaml 359 | depends_on: 360 | db: 361 | condition: service_healthy 362 | ``` 363 | 364 | A normal `depends_on: db` only waits for the db to *start*, not for it to be ready for connections. The internet is filled with workarounds for this problem, like "waitforit" scripts. Those aren't needed anymore. 365 | 366 | If you set `condition: service_healthy`, docker will monitor that service until the healthcheck passes, and only then, start the primary service. 367 | 368 | ### Node.js development in a container or not? 369 | 370 | I do both, it just depends on the project, the complexity, and if I have a similar node version installed on my host. VS Code's [native ability to devleop inside a container](https://code.visualstudio.com/docs/remote/containers) is dope and I recommend you give it a shot! It can use your existing Dockerfile and docker-compose.yml to more seamlessly develop in a container, and may be easier/faster than do-it-yourself setups. 371 | 372 | ## Production Checklist 373 | 374 | Based on all the tips above. This list, in order of priority (highest pri first), is my personal checklist for Node.js apps in production (or any JIT langauge like Ruby, Python, PHP, etc.) 375 | 376 | 1. Slim base image with 0 high/crit CVEs via Trivy/Snyk/Grype scan. 377 | 2. Running as non-root user (`USER node`). 378 | 3. `npm audit` inside image during CI has 0 high/crit CVEs. 379 | 4. Only production dependencies (`npm ci --only=production`). 380 | 5. Tini init is *considered* for ENTRYPOINT *and* always used with healthchecks. 381 | 6. `npm`, `nodemon`, `forever`, or `pm2` are not used. App launches `node` directly (or via `tini`). 382 | 7. At least a basic healthcheck/liveness probe is used. `HEALTCHECK` is good for documentation as well as Docker/Compose/Swarm healthchecks. 383 | 8. The app code listens for Linux signals (`SIGTERM`, `SIGINT`) and gracefully shuts down. 384 | 9. If an HTTP-based app, use a better shutdown strategy in code to ensure connections are tracked, and closed gracefully during container/pod updates (TCP FIN, etc.) 385 | 10. Even-numbered LTS Node.js release is used [(current,active, or maintenance status)](https://nodejs.dev/en/about/releases/). 386 | 11. `.dockerignore` prevents `.git`, the host `node_modules`, and unwanted files. 387 | 12. `EXPOSE` has the listening ports shown. 388 | 13. Multi-platform builds are enabled for running on amd64 or arm64 when necessary. 389 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var logger = require('morgan'); 4 | var cookieParser = require('cookie-parser'); 5 | var bodyParser = require('body-parser'); 6 | 7 | var index = require('./routes/index'); 8 | var users = require('./routes/users'); 9 | 10 | var app = express(); 11 | 12 | // view engine setup 13 | app.set('views', path.join(__dirname, 'views')); 14 | app.set('view engine', 'hbs'); 15 | 16 | // uncomment after placing your favicon in /public 17 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 18 | app.use(logger('dev')); 19 | app.use(bodyParser.json()); 20 | app.use(bodyParser.urlencoded({ extended: false })); 21 | app.use(cookieParser()); 22 | app.use(express.static(path.join(__dirname, 'public'))); 23 | 24 | app.use('/', index); 25 | app.use('/users', users); 26 | 27 | // catch 404 and forward to error handler 28 | app.use(function(req, res, next) { 29 | var err = new Error('Not Found'); 30 | err.status = 404; 31 | next(err); 32 | }); 33 | 34 | // error handler 35 | app.use(function(err, req, res, next) { 36 | // set locals, only providing error in development 37 | res.locals.message = err.message; 38 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 39 | 40 | // render the error page 41 | res.status(err.status || 500); 42 | res.render('error'); 43 | }); 44 | 45 | module.exports = app; 46 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('sample-02:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | 92 | -------------------------------------------------------------------------------- /build-multi-arch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # if you haven't created a new custom builder instance, run this once: 4 | docker buildx create --use 5 | 6 | # now build and push an image for two architectures: 7 | docker buildx build -f dockerfile/5.Dockerfile --target prod --name :latest --platform=linux/amd64,linux/arm64 . -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | ### 2 | ## for local dev, wait for db to pass healthcheck before we start node 3 | ## also, build custom dockerfile to the dev stage 4 | ### 5 | 6 | # version key is DEPRECATED 7 | # v2 and v3 features now combined in compose CLI 8 | 9 | services: 10 | node: 11 | build: 12 | dockerfile: dockerfiles/3.Dockerfile 13 | context: . 14 | # build to the stage named dev 15 | target: dev 16 | # Not needed when `develop: watch` is used 17 | # volumes: 18 | # - .:/app 19 | ports: 20 | # use docker compose ps to see which host port is used 21 | - "3000" 22 | depends_on: 23 | db: 24 | condition: service_healthy 25 | develop: 26 | watch: 27 | - action: sync 28 | path: ./ 29 | target: /app 30 | - action: rebuild 31 | path: package.json 32 | - action: rebuild 33 | path: package-lock.json 34 | 35 | db: 36 | image: postgres:alpine 37 | environment: 38 | POSTGRES_USER: postgres 39 | POSTGRES_PASSWORD: postgres 40 | volumes: 41 | - ./healthchecks:/healthchecks 42 | healthcheck: 43 | test: /healthchecks/postgres-healthcheck 44 | interval: "5s" 45 | -------------------------------------------------------------------------------- /dockerfiles/1.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ### 4 | ## Example: The most basic, CORRECT, Dockerfile for Node.js 5 | ### 6 | 7 | # alwyas use slim and the lastest debian distro offered 8 | FROM node:20-bookworm-slim@sha256:8d26608b65edb3b0a0e1958a0a5a45209524c4df54bbe21a4ca53548bc97a3a5 9 | 10 | EXPOSE 3000 11 | 12 | # add user first, then set WORKDIR to set permissions 13 | USER node 14 | 15 | WORKDIR /app 16 | 17 | # copy in with correct permissions. Using * prevents errors if file is missing 18 | COPY --chown=node:node package*.json ./ 19 | 20 | # use ci to only install packages from lock files 21 | RUN npm ci --omit=dev && npm cache clean --force 22 | 23 | # copy files with correct permissions 24 | COPY --chown=node:node . . 25 | 26 | # change command to run node directly 27 | CMD ["node", "./bin/www"] 28 | -------------------------------------------------------------------------------- /dockerfiles/2.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ### 4 | ## Example: run tini first, as PID 1 5 | ### 6 | 7 | FROM node:20-bookworm-slim@sha256:8d26608b65edb3b0a0e1958a0a5a45209524c4df54bbe21a4ca53548bc97a3a5 8 | 9 | # replace npm in CMD with tini for better kernel signal handling 10 | ENV NODE_ENV=production 11 | ENV TINI_VERSION=v0.19.0 12 | ADD --chmod=755 https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/local/bin/tini 13 | 14 | # set entrypoint to always run commands with tini 15 | ENTRYPOINT ["/usr/local/bin/tini", "--"] 16 | 17 | EXPOSE 3000 18 | 19 | USER node 20 | 21 | WORKDIR /app 22 | 23 | COPY --chown=node:node package*.json ./ 24 | 25 | RUN npm ci --omit=dev && npm cache clean --force 26 | 27 | COPY --chown=node:node . . 28 | 29 | CMD ["node", "./bin/www"] 30 | -------------------------------------------------------------------------------- /dockerfiles/3.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ### 4 | ## Adding stages for dev and prod 5 | ### 6 | 7 | FROM node:20-bookworm-slim@sha256:8d26608b65edb3b0a0e1958a0a5a45209524c4df54bbe21a4ca53548bc97a3a5 as base 8 | ENV NODE_ENV=production 9 | ENV TINI_VERSION=v0.19.0 10 | ADD --chmod=755 https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/local/bin/tini 11 | EXPOSE 3000 12 | USER node 13 | WORKDIR /app 14 | COPY --chown=node:node package*.json ./ 15 | RUN npm ci --omit=dev && npm cache clean --force 16 | ENV PATH=/app/node_modules/.bin:$PATH 17 | 18 | # dev stage 19 | FROM base as dev 20 | ENV NODE_ENV=development 21 | RUN npm install 22 | COPY --chown=node:node . . 23 | CMD ["nodemon", "./bin/www", "--inspect=0.0.0.0:9229"] 24 | 25 | # prod stage 26 | FROM base as prod 27 | COPY --chown=node:node . . 28 | ENTRYPOINT ["/usr/local/bin/tini", "--"] 29 | CMD ["node", "./bin/www"] 30 | -------------------------------------------------------------------------------- /dockerfiles/distroless.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ### 4 | ## Distroless in Prod. Multi-stage dev/test/prod with distroless 5 | ### 6 | 7 | FROM gcr.io/distroless/nodejs20-debian12:latest@sha256:6499c05db574451eeddda4d3ddb374ac1aba412d6b2f5d215cc5e23c40c0e4d3 as distroless 8 | FROM node:20-slim as base 9 | ENV NODE_ENV=production 10 | RUN apt-get update \ 11 | && apt-get install -y --no-install-recommends \ 12 | tini \ 13 | && rm -rf /var/lib/apt/lists/* 14 | EXPOSE 3000 15 | RUN mkdir /app && chown -R node:node /app 16 | WORKDIR /app 17 | USER node 18 | COPY --chown=node:node package*.json yarn*.lock ./ 19 | RUN npm ci --only=production && npm cache clean --force 20 | 21 | FROM base as dev 22 | ENV NODE_ENV=development 23 | ENV PATH=/app/node_modules/.bin:$PATH 24 | RUN npm install && npm cache clean --force 25 | CMD ["nodemon", "./bin/www", "--inspect=0.0.0.0:9229"] 26 | 27 | FROM base as source 28 | COPY --chown=node:node . . 29 | 30 | # switch to distroless for prod 31 | # use version tags for always building with latest 32 | # (more risky for stability, but likely more secure) 33 | # gcr.io/distroless/nodejs:16 34 | # OR pin to the sha256 hash for stable, deterministic builds, 35 | # but less secure if you don't update it regularly 36 | # NOTE: I like to set versions at the top of files, 37 | # so I set the image used in line 1 above, so I can just use the alias here 38 | FROM distroless as prod 39 | COPY --from=source --chown=1000:1000 /app /app 40 | COPY --from=base /usr/bin/tini /usr/bin/tini 41 | USER 1000 42 | EXPOSE 3000 43 | ENV NODE_ENV=production 44 | ENV PATH=/app/node_modules/.bin:$PATH 45 | WORKDIR /app 46 | ENTRYPOINT ["/usr/bin/tini", "--"] 47 | CMD ["/nodejs/bin/node", "./bin/www"] 48 | -------------------------------------------------------------------------------- /dockerfiles/snyk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "[" > summary.json 4 | for image in $(cat tags.txt); do 5 | image_file=$(echo ${image} | tr '/' '-' | tr ':' '-') 6 | tag=$(echo ${image} | cut -f 2 -d '/' | cut -f 2 -d ':') 7 | echo "Testing ${image}..." 8 | 9 | if [[ "$1" == "--no-cache" || ! -f snyk.${image_file}.json ]]; then 10 | DOCKER_CLI_HINTS=false docker pull ${image} 11 | snyk container test ${image} --exclude-app-vulns --json-file-output=snyk.${image_file}.json --group-issues > snyk.${image_file}.log 12 | fi 13 | summary=$(jq -c '[ .vulnerabilities[].severity] | reduce .[] as $sev ({}; .[$sev] +=1) | { image: "'${image}'", low: (.low // 0), medium: (.medium // 0), high: (.high // 0), critical: (.critical // 0)} | .total = .low + .medium + .high + .critical ' snyk.${image_file}.json) 14 | echo " ${summary}," >> summary.json 15 | done 16 | echo "]" >> summary.json 17 | 18 | cat summary.json -------------------------------------------------------------------------------- /dockerfiles/tags.txt: -------------------------------------------------------------------------------- 1 | node:20 2 | node:20-slim 3 | node:20-alpine 4 | node:18 5 | node:18-slim 6 | node:18-alpine 7 | debian:12 8 | debian:12-slim 9 | ubuntu:22.04 10 | bretfisher/node:ubuntu-22.04-nodesource18 11 | bretfisher/node:ubuntu-22.04-nodesource20 12 | bretfisher/node:ubuntu-22.04-node20-copy 13 | gcr.io/distroless/nodejs20-debian12 14 | cgr.dev/chainguard/node:latest -------------------------------------------------------------------------------- /dockerfiles/ubuntu-copy.Dockerfile: -------------------------------------------------------------------------------- 1 | ### 2 | ## ubuntu base with nodejs coppied in from official image, for a more secure base 3 | ### 4 | #cache our node version for installing later 5 | #FROM node:20.7-slim as node 6 | FROM node:18.18-slim as node 7 | FROM ubuntu:lunar-20230816 as base 8 | 9 | # replace npm in CMD with tini for better kernel signal handling 10 | # You may also need development tools to build native npm addons: 11 | # apt-get install gcc g++ make 12 | RUN apt-get update \ 13 | && apt-get -qq install -y --no-install-recommends \ 14 | tini \ 15 | && rm -rf /var/lib/apt/lists/* 16 | ENTRYPOINT ["/usr/bin/tini", "--"] 17 | 18 | # new way to get node, let's copy in the specific version we want from a docker image 19 | # this avoids depdency package installs (python3) that the deb package requires 20 | COPY --from=node /usr/local/include/ /usr/local/include/ 21 | COPY --from=node /usr/local/lib/ /usr/local/lib/ 22 | COPY --from=node /usr/local/bin/ /usr/local/bin/ 23 | RUN corepack disable && corepack enable 24 | 25 | # create node user and group 26 | RUN groupadd --gid 1001 node \ 27 | && useradd --uid 1001 --gid node --shell /bin/bash --create-home node 28 | 29 | # you'll likely need more stages for dev/test, but here's our basic prod layer with source code 30 | FROM base as prod 31 | EXPOSE 3000 32 | USER node 33 | WORKDIR /app 34 | COPY --chown=node:node package*.json ./ 35 | RUN npm ci && npm cache clean --force 36 | COPY --chown=node:node . . 37 | CMD ["node", "./bin/www"] 38 | -------------------------------------------------------------------------------- /dockerfiles/ubuntu-deb.Dockerfile: -------------------------------------------------------------------------------- 1 | ### 2 | ## ubuntu base with nodejs deb package, for a more secure base 3 | ### 4 | FROM ubuntu:jammy-20230816 as base 5 | 6 | # version of Node.js we will install later 7 | ENV NODE_MAJOR=20 8 | 9 | # replace npm in CMD with tini for better kernel signal handling 10 | RUN apt-get update \ 11 | && apt-get -qq install -y --no-install-recommends \ 12 | tini \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # You may also need development tools to build native npm addons: 16 | # apt-get install gcc g++ make 17 | 18 | ENTRYPOINT ["/usr/bin/tini", "--"] 19 | 20 | # create node user and group, then create app dir 21 | RUN groupadd --gid 1000 node \ 22 | && useradd --uid 1000 --gid node --shell /bin/bash --create-home node \ 23 | && mkdir /app \ 24 | && chown -R node:node /app 25 | 26 | # get full list of packages at https://deb.nodesource.com/node_18.x/pool/main/n/nodejs/ 27 | # this basic TARGETARCH design only works on amd64 and arm64 builds. 28 | # for more on multi-platform builds, see https://github.com/BretFisher/multi-platform-docker-build 29 | ARG TARGETARCH 30 | RUN apt-get -qq update \ 31 | && apt-get -qq install -y ca-certificates curl gnupg \ 32 | && mkdir -p /etc/apt/keyrings \ 33 | && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ 34 | && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ 35 | && apt-get update \ 36 | && apt-get -qq install -y nodejs --no-install-recommends \ 37 | && apt-get -qq remove curl \ 38 | && apt-get -qq autoremove -y \ 39 | && rm -rf /var/lib/apt/lists/* \ 40 | && which npm 41 | 42 | # you'll likely need more stages for dev/test, but here's our basic prod layer with source code 43 | FROM base as prod 44 | EXPOSE 3000 45 | WORKDIR /app 46 | USER node 47 | COPY --chown=node:node package*.json yarn*.lock ./ 48 | RUN npm ci --only=production && npm cache clean --force 49 | COPY --chown=node:node . . 50 | CMD ["node", "./bin/www"] 51 | -------------------------------------------------------------------------------- /dockerfiles/wrong.Dockerfile: -------------------------------------------------------------------------------- 1 | # this file is wrong, but common in examples or 2 | # basic dockerfile 101 blogs 3 | # look at other Dockerfiles and the README.md for improvements 4 | 5 | # FIXME: don't use latest. Pin to a specific version, debian distro, and use slim 6 | # FIXME: ProTip: pin to sha with @sha256:hash 7 | FROM node:latest 8 | 9 | EXPOSE 3000 10 | 11 | # FIXME: Don't use root. Add USER node first, then (as of 2019) WORKDIR sets perms to match USER 12 | # then set USER node or USER 1000 13 | WORKDIR /app 14 | 15 | # FIXME: Don't use COPY, use COPY --chown=node:node 16 | # FIXME: Also include package-lock.json or yarn.lock: COPY package*.json yarn*.lock ./ 17 | COPY package.json . 18 | 19 | # FIXME: Don't install dev dependencies in a image used in production 20 | # use npm ci for images that run on servers 21 | RUN npm install && npm cache clean --force 22 | 23 | # FIXME: Use COPY --chown=node:node . . 24 | COPY . . 25 | 26 | # FIXME: Don't use npm, nodemon, pm2, forever, or any process manager on servers 27 | # call node and your starting .js directly. 28 | # Scale with docker/kubernetes, not process managers, which only add complexity 29 | CMD ["npm", "start"] 30 | -------------------------------------------------------------------------------- /healthchecks/README.md: -------------------------------------------------------------------------------- 1 | # Docker healthchecks for databases in Docker Compose 2 | 3 | See the [example repository](https://github.com/docker-library/healthcheck) for more examples (mysql, mongo, redis, etc.) 4 | -------------------------------------------------------------------------------- /healthchecks/postgres-healthcheck: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | host="$(hostname -i || echo '127.0.0.1')" 5 | user="${POSTGRES_USER:-postgres}" 6 | db="${POSTGRES_DB:-$POSTGRES_USER}" 7 | export PGPASSWORD="${POSTGRES_PASSWORD:-}" 8 | 9 | args=( 10 | # force postgres to not use the local unix socket (test "external" connectibility) 11 | --host "$host" 12 | --username "$user" 13 | --dbname "$db" 14 | --quiet --no-align --tuples-only 15 | ) 16 | 17 | if select="$(echo 'SELECT 1' | psql "${args[@]}")" && [ "$select" = '1' ]; then 18 | exit 0 19 | fi 20 | 21 | exit 1 -------------------------------------------------------------------------------- /manifest/pod.yaml: -------------------------------------------------------------------------------- 1 | # generic pod spec that's usable inside a deployment or other higher level k8s spec 2 | # via https://github.com/BretFisher/podspec 3 | 4 | apiVersion: v1 5 | kind: Pod 6 | metadata: 7 | name: node 8 | 9 | spec: 10 | 11 | containers: 12 | 13 | - name: node 14 | image: nodejs-rocks-in-docker-node:latest 15 | imagePullPolicy: Never 16 | ports: 17 | - containerPort: 3000 18 | protocol: TCP 19 | 20 | readinessProbe: # I always recommend using these, even if your app has no listening ports (this affects any rolling update) 21 | httpGet: # Lots of timeout values with defaults, be sure they are ideal for your workload 22 | path: /ready 23 | port: 8080 24 | livenessProbe: # only needed if your app tends to go unresponsive or you don't have a readinessProbe, but this is up for debate 25 | httpGet: # Lots of timeout values with defaults, be sure they are ideal for your workload 26 | path: /alive 27 | port: 8080 28 | 29 | resources: # Because if limits = requests then QoS is set to "Guaranteed" 30 | limits: 31 | memory: "500Mi" # If container uses over 500MB it is killed (OOM) 32 | #cpu: "2" # Not normally needed, unless you need to protect other workloads or QoS must be "Guaranteed" 33 | requests: 34 | memory: "500Mi" # Scheduler finds a node where 500MB is available 35 | cpu: "1" # Scheduler finds a node where 1 vCPU is available 36 | 37 | # per-container security context 38 | # lock down privileges inside the container 39 | securityContext: 40 | allowPrivilegeEscalation: false # prevent sudo, etc. 41 | privileged: false # prevent acting like host root 42 | 43 | terminationGracePeriodSeconds: 600 # default is 30, but you may need more time to gracefully shutdown (HTTP long polling, user uploads, etc) 44 | 45 | # per-pod security context 46 | # enable seccomp and force non-root user 47 | securityContext: 48 | 49 | seccompProfile: 50 | type: RuntimeDefault # enable seccomp and the runtimes default profile 51 | 52 | runAsUser: 1001 # hardcode user to non-root if not set in Dockerfile 53 | runAsGroup: 1001 # hardcode group to non-root if not set in Dockerfile 54 | runAsNonRoot: true # hardcode to non-root. Redundant to above if Dockerfile is set USER 1000 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-stage-test", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "dev": "../node_modules/nodemon/bin/nodemon.js ./bin/www --inspect=0.0.0.0:9229", 8 | "test": "mocha --timeout 10000 --exit" 9 | }, 10 | "dependencies": { 11 | "body-parser": "^1.20.3", 12 | "cookie-parser": "^1.4.4", 13 | "debug": "^4.1.1", 14 | "express": "^4.21.2", 15 | "hbs": "^4.0.6", 16 | "morgan": "^1.9.1", 17 | "serve-favicon": "^2.5.0" 18 | }, 19 | "devDependencies": { 20 | "chai": "^4.2.0", 21 | "chai-http": "^4.3.0", 22 | "eslint": "^6.8.0", 23 | "mocha": "^6.2.2", 24 | "nodemon": "^2.0.20" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/images/picard0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard0.gif -------------------------------------------------------------------------------- /public/images/picard1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard1.gif -------------------------------------------------------------------------------- /public/images/picard2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard2.gif -------------------------------------------------------------------------------- /public/images/picard3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard3.gif -------------------------------------------------------------------------------- /public/images/picard4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard4.gif -------------------------------------------------------------------------------- /public/images/picard5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard5.gif -------------------------------------------------------------------------------- /public/images/picard6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard6.gif -------------------------------------------------------------------------------- /public/images/picard7.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard7.gif -------------------------------------------------------------------------------- /public/images/picard8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard8.gif -------------------------------------------------------------------------------- /public/images/picard9.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BretFisher/nodejs-rocks-in-docker/54dafe3cd1b9b5802ef4207cdbeba45468b81157/public/images/picard9.gif -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page */ 5 | router.get('/', function(req, res, next) { 6 | var imageNumber = Math.floor(Math.random()*(9)); 7 | res.render('index', { title: 'Node.js Express App', image: imageNumber }); 8 | }); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET users listing. */ 5 | router.get('/', function(req, res, next) { 6 | res.send('respond with a resource'); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /test/smoke.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"); 2 | const expect = chai.expect; 3 | 4 | describe("smoke test", function() { 5 | it("checks equality", function() { 6 | expect(true).to.be.true; 7 | }); 8 | }); -------------------------------------------------------------------------------- /views/error.hbs: -------------------------------------------------------------------------------- 1 |

{{message}}

2 |

{{error.status}}

3 |
{{error.stack}}
4 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 |

{{title}}

2 |

It Worked! Change! Enjoy a random Captain's gif

3 | 4 | 5 | -------------------------------------------------------------------------------- /views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 7 | 8 | {{{body}}} 9 | 10 | 11 | --------------------------------------------------------------------------------