Skip to content

Commit

Permalink
(flavor) docker v3.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
devinivy committed Apr 12, 2021
1 parent 839e7af commit fa47ac9
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 17 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
server/.env*
128 changes: 128 additions & 0 deletions DOCKER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
## `docker` Flavor
This flavor of the pal boilerplate is designed to get someone up and running
using Docker with scalable best practices. Certain features require the `deployment`
flavor of the pal boilerplate to be installed as well in order to function.

## Basics
This [Dockerfile](./server/Dockerfile) uses
[multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/)
to create different Docker images depending on the environment the images will
be running in, while ensuring best practices are followed to keep image size low
and create a security sandbox if you are running a server.

We are using Docker Compose to define `web` and `test` services. The `web` wervice uses
the `release` Dockerfile stage, and the `test` service uses the `test` Dockerfile stage.

### `docker-compose.yml` Services
The `docker-compose.yml` file defines four services. The reason for the `*_base` services
is that certain CI providers like CircleCI do not support voluming in files, and you will
get strange file system errors if you try to run with a `volumes` block.

#### `web_base`
All configuration for running the webserver locally, minus the `volumes` block.

#### `web`
`web_base`, plus the `volumes` block required for hotreloading to function locally.

#### `test_base`
All configuration for running the tests, minus the `volumes` block.

#### `test`
`test_base`, plus the `volumes` block required to allow re-running tests in
`watch` mode or by hand, but without requiring a full image rebuild.

### Commands

#### `npm run build:test`
Rebuilds the `test` service used for running tests, and will re-install npm
dependencies when they change.

#### `npm run build:web`
Rebuilds the `web` service used locally, and will re-install npm dependencies when
they change.

#### `npm run docker:lint`
Runs the `npm run lint` command in the `test` service.

#### `npm run docker:start`
Starts the `web` service in daemon mode, making it available at `localhost:3000`.

#### `npm run docker:test`
Runs the `npm test` command in the `test` service.

#### `npm run docker:test:ci`
Starts the `test_base` service, which triggers both a `lint` and `test` npm script
run. The process will exit with the same code the internal process exits with,
making this the ideal command to use when integrating with CI.

### `.env`
This flavor no longer uses `.env` files, as it is safer to start the container declaring
the environment variables in either the `docker-compose.yml` file or via the `docker` cli.

If you need to add any environment variables, you can do so in the `docker-compose.yml`
files as shown, or add them to `.env` and load them in as shown
[here](https://docs.docker.com/compose/compose-file/#env_file).

### Stages
There are currently four stages in the Dockerfile that are used.

#### `base`
Contains all package installs, files system modifications, and entrypoint declaration.
Any commands that will not change across the other stages or are required for multiple
other stages to execute properly should be put in this stage.

#### `dependencies`
The `dependencies` stage is used to install npm dependencies, copy all of your source
files, and then optionally run any build commands that need to be executed. The desired
outcome of this stage is an image that contains all dependencies, all source files, and
all built assets (if necessary).

In order to run build scripts, you will need to locate this stage in the Dockerfile
and uncomment the `RUN npm run build` command that is in there, possibly changing the
command if you are using multiple build scripts in your `package.json`.

#### `test`
The `test` stage is used for running your test suite. It changes the user to `node`
to mimic the production environment, and then runs `npm lint && npm test` which should
run your full test suite. The container will exit with the same code as your test
runner, which means that in CI you can run `npm run docker:test:ci` as your test step to
automatically build your test image and then run your tests inside of it.

#### `development`
The `development` stage is meant to run your server locally with dev deps installed. The
`docker-compose.yml` file can be used to tweak the environment variables used for
this stage when running locally, and the logic for building this stage should not
make any assumptions about what environment the output will be used in.

#### `release`
The `release` stage is meant to run your server in a production-like setup without dev deps.
The `docker-compose.yml` file can be used to tweak the environment variables used for
this stage when running locally, and the logic for building this stage should not
make any assumptions about what environment the output will be used in.

## Local Development
If you are using the `development` flavor alongside this `docker` flavor, you can simply
run `npm run docker:start` to build the release stage of the Dockerfile, and then start
it as a `daemon` in Docker. You can then visit `localhost:3000` to hit your server.

By default, the server is started using `nodemon` when you start it using
`docker-compose`, which means every time you save a file the server will be restarted
automatically with your fresh changes.

### Running Tests
Running tests locally is extremely easy. Simply run `npm run docker:test`, and linting
and testing will be run. If you modify the `package*.json` files, you will need to run
`npm run build:test` to trigger a reinstall of your dependencies.

### Secret Management (`.env`)
This removes the option to have a `.env` files for secrets locally, because those environment
variables can now just be set in the `docker-compose.yml` file `environment` section. If
you have true secrets that you need locally and cannot check into Git, you can re-create
the `server/.env` file, and then add a key in the `*_base` services in `docker-compose.yml`
that is simply `env_file: server/.env` at the same level in the yaml as the `environment` key.

## Changing the Port
Currently the build process assumes that the port default of `3000` in
`server/manifest.js` is being used. `server/Dockerfile` has an `EXPOSE` command that
references this port, and `docker-compose.yml` also contains a mapping of port `3000`
in the container to port `3000` on the host machine.
42 changes: 42 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
version: '3.4'
services:
web_base: &web_base
build:
context: .
dockerfile: server/Dockerfile
target: development
command: [
"./node_modules/.bin/nodemon",
"--watch",
"lib",
"--watch",
"server",
"server/index.js"
]
environment:
- NODE_ENV=development
ports:
- 3000:3000
web:
# hack for docker-compose v3 dropping `extends` support
<< : *web_base
# allows for hot-reloading of server
volumes:
- ./lib:/app/lib
- ./server:/app/server
- ./test:/app/test
- ./package.json:/app/package.json
test_base: &test_base
build:
context: .
dockerfile: server/Dockerfile
target: test
test:
# hack for docker-compose v3 dropping `extends` support
<< : *test_base
# allows for hot-reloading of server
volumes:
- ./lib:/app/lib
- ./server:/app/server
- ./test:/app/test
- ./package.json:/app/package.json
18 changes: 12 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,35 @@
"version": "3.0.0",
"main": "lib/index.js",
"scripts": {
"build:test": "docker-compose build test",
"build:web": "docker-compose build web",
"docker:lint": "docker-compose run test npm run lint",
"docker:start": "docker-compose up -d web",
"docker:test": "docker-compose run test npm test",
"docker:test:ci": "docker-compose up --build test_base",
"start": "node server",
"test": "lab -a @hapi/code -L",
"lint": "eslint ."
},
"dependencies": {
"@hapi/boom": "9.x.x",
"@hapi/glue": "8.x.x",
"@hapi/hapi": "20.x.x",
"@hapipal/confidence": "6.x.x",
"@hapipal/haute-couture": "4.x.x",
"@hapipal/toys": "3.x.x",
"exiting": "6.x.x",
"joi": "17.x.x"
},
"devDependencies": {
"@hapi/code": "8.x.x",
"@hapi/eslint-config-hapi": "13.x.x",
"@hapi/eslint-plugin-hapi": "4.x.x",
"@hapi/glue": "8.x.x",
"@hapi/hapi": "20.x.x",
"@hapi/lab": "24.x.x",
"@hapipal/confidence": "6.x.x",
"@hapipal/hpal": "3.x.x",
"@hapipal/hpal-debug": "2.x.x",
"@hapipal/toys": "3.x.x",
"babel-eslint": "10.x.x",
"dotenv": "8.x.x",
"eslint": "7.x.x",
"exiting": "6.x.x"
"nodemon": "2.x.x"
}
}
6 changes: 0 additions & 6 deletions server/.env-keep

This file was deleted.

62 changes: 62 additions & 0 deletions server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# create base image with all packages, proper entrypoint, and directories created
FROM node:14-alpine AS base

# install any packages we need from apt here
RUN apk add --update dumb-init

# set entrypoint to `dumb-init` as it handles being pid 1 and forwarding signals
# so that you dont need to bake that logic into your node app
ENTRYPOINT ["dumb-init", "--"]

# all of our code will live in `/app`
WORKDIR /app

# using the base image, create an image containing all of our files
# and dependencies installed, devDeps and test directory included
FROM base AS dependencies

COPY package*.json ./
RUN npm set progress=false \
&& npm config set depth 0 \
&& npm i

COPY .eslintrc.json .
COPY ./server ./server
COPY ./test ./test
COPY ./lib ./lib

# if you have any build scripts to run, like for the `templated-site` flavor
# uncomment and possibly modify the following RUN command:
# RUN npm run build
# keeping all of the bash commands you can within a single RUN is generally important,
# but for this case it's likely that we want to use the cache from the prune which will
# change infrequently.

# test running image using all of the files and devDeps
FROM dependencies AS test
ENV NODE_ENV=test
# use `sh -c` so we can chain test commands using `&&`
CMD ["npm", "test"]

# dev ready image, contains devDeps for test running and debugging
FROM dependencies AS development

# expose port 3000 from in the container to the outside world
# this is the default port defined in server/manifest.js, and
# will need to be updated if you change the default port
EXPOSE 3000
CMD ["npm", "start"]

# release ready image, devDeps are pruned and tests removed for size control
FROM development AS release

ENV NODE_ENV=production

# prune non-prod dependencies, remove test files
RUN npm prune --production \
&& rm -rf ./test

# `node` user is created in base node image
# we want to use this non-root user for running the server in case of attack
# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
USER node
6 changes: 1 addition & 5 deletions server/manifest.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
'use strict';

const Dotenv = require('dotenv');
const Confidence = require('@hapipal/confidence');
const Toys = require('@hapipal/toys');

// Pull .env into process.env
Dotenv.config({ path: `${__dirname}/.env` });

// Glue manifest as a confidence store
module.exports = new Confidence.Store({
server: {
host: 'localhost',
host: '0.0.0.0',
port: {
$param: 'PORT',
$coerce: 'number',
Expand Down

0 comments on commit fa47ac9

Please sign in to comment.