feat: add Docker build (#41187)

* feat(docker): build and use client and api images

* feat: always use .env

dotenv fails without throwing if the .env file is missing and never
overwrites variables if they already exist. As such, we can use it in
build pipelines.

* fix: remove quotes from env vars

dotenv normalises quoted and unquoted strings (X=x, X='x' and
X="x") all become the same .env object {X: 'x'}. However, Docker's
env_file does not (the three cases are distinct).  As a result, we
should use unquoted strings for consistency.

* fix: provide custom warning when .env is missing

* feat(docker): include client-config

* fix(docker): remove build packages from api image

* fix(docker): run script from correct dir

* fix(docker): correct permissions and dests

* fix(docker): consolidate run steps

This is standard practice, but did not have a noticable affect on the
image size

* fix(docker): clean the npm cache

Prior to this step the image was 1.11GB uncompressed and we got a modest
saving, 1.09GB after.

* refactor(docker): regexless COPY directives

* feat(docker): use alpine

This shrinks the image down to 259MB

* fix(docker): update build scripts

* fix: correct the server Dockerfile RUNs

* DEBUG: expose mysql port for seeding

* chore: update client Dockerfile's node versions

* fix: remove executable permissions from index.js

It's not a cli, so I don't think it needs to be executable.

* chore: update node and remove stale comments

* feat: use ENTRYPOINT + CMD to allow runtime config

* fix: add CURRICULUM_LOCALE arg

* feat: allow client port configuration

* feat: allow api port to be configured

* refactor: use unique variable names for ports

* fix: add default CLIENT_PORT

* refactor: clean up
This commit is contained in:
Oliver Eyton-Williams
2021-04-20 15:59:31 +02:00
committed by GitHub
parent 0858f078e2
commit 5d46e2830a
8 changed files with 115 additions and 22 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
client/.cache
client/public
.env
.git
.gitignore
.dockerignore
*Dockerfile*
*docker-compose*
**/node_modules

2
api-server/src/server/index.js Executable file → Normal file
View File

@ -32,7 +32,7 @@ Rx.config.longStackSupport = process.env.NODE_DEBUG !== 'production';
const app = loopback(); const app = loopback();
app.set('state namespace', '__fcc__'); app.set('state namespace', '__fcc__');
app.set('port', process.env.PORT || 3000); app.set('port', process.env.API_PORT || 3000);
app.set('views', path.join(__dirname, 'views')); app.set('views', path.join(__dirname, 'views'));
app.use(loopback.token()); app.use(loopback.token());
app.use( app.use(

40
client.Dockerfile Normal file
View File

@ -0,0 +1,40 @@
FROM node:14.16.1-buster AS builder
# this is a bit clunky, perhaps there's a more concise way of passing in build
# arguments
ARG FREECODECAMP_NODE_ENV
ARG HOME_LOCATION
ARG API_LOCATION
ARG FORUM_LOCATION
ARG NEWS_LOCATION
ARG CLIENT_LOCALE
ARG CURRICULUM_LOCALE
ARG STRIPE_PUBLIC_KEY
ARG ALGOLIA_APP_ID
ARG ALGOLIA_API_KEY
ARG PAYPAL_CLIENT_ID
ARG DEPLOYMENT_ENV
ARG SHOW_UPCOMING_CHANGES
# node images create a non-root user that we can use
USER node
WORKDIR /home/node/build
COPY --chown=node:node . .
RUN npm ci
# we don't need to separately run ensure-env, since it gets called as part of
# build:client
RUN npm run build:client
WORKDIR /home/node/config
RUN git clone https://github.com/freeCodeCamp/client-config.git client
FROM node:14.16.1-alpine
RUN npm i -g serve
USER node
WORKDIR /home/node
COPY --from=builder /home/node/build/client/public/ client/public
COPY --from=builder /home/node/config/client/serve.json client
COPY --from=builder /home/node/config/client/www/ client
ENTRYPOINT ["serve", "-c", "../serve.json", "client/public"]
CMD ["-l", "8000"]

View File

@ -1,13 +1,15 @@
const path = require('path'); const path = require('path');
const fs = require('fs');
// PIPELINE_ENV is 'true' in the build pipeline const envPath = path.resolve(__dirname, '../.env');
if (process.env.PIPELINE_ENV !== 'true') { const { error } = require('dotenv').config({ path: envPath });
const envPath = path.resolve(__dirname, '../.env');
if (!fs.existsSync(envPath)) { if (error) {
throw Error('.env not found, please copy sample.env to .env.'); if (process.env.FREECODECAMP_NODE_ENV === 'development') {
console.warn('.env not found, please copy sample.env to .env');
} else {
console.warn(`.env not found. If env vars are not being set another way,
this could be a problem.`);
} }
require('dotenv').config({ path: envPath });
} }
const { const {

8
docker-compose.api.yml Normal file
View File

@ -0,0 +1,8 @@
version: '3.7'
services:
api:
env_file: .env
image: fcc_api
restart: unless-stopped
ports:
- '${API_PORT:-3000}:${API_PORT:-3000}'

View File

@ -0,0 +1,8 @@
version: '3.7'
services:
client:
image: fcc_client
restart: unless-stopped
ports:
- '${CLIENT_PORT:-8000}:${CLIENT_PORT:-8000}'
command: ["-l", "${CLIENT_PORT:-8000}"]

View File

@ -3,11 +3,11 @@
# --------------------- # ---------------------
# Database # Database
MONGOHQ_URL='mongodb://localhost:27017/freecodecamp' MONGOHQ_URL=mongodb://localhost:27017/freecodecamp
# Logging # Logging
SENTRY_DSN=dsn_from_sentry_dashboard SENTRY_DSN=dsn_from_sentry_dashboard
SENTRY_ENVIRONMENT='staging' SENTRY_ENVIRONMENT=staging
# Auth0 - OAuth 2.0 Credentials # Auth0 - OAuth 2.0 Credentials
AUTH0_CLIENT_ID=client_id_from_auth0_dashboard AUTH0_CLIENT_ID=client_id_from_auth0_dashboard
@ -39,17 +39,15 @@ STRIPE_SECRET_KEY=sk_from_stripe_dashboard
# PayPal # PayPal
PAYPAL_CLIENT_ID=id_from_paypal_dashboard PAYPAL_CLIENT_ID=id_from_paypal_dashboard
PAYPAL_SECRET=secret_from_paypal_dashboard PAYPAL_SECRET=secret_from_paypal_dashboard
PAYPAL_VERIFY_WEBHOOK_URL='https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature' PAYPAL_VERIFY_WEBHOOK_URL=https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature
PAYPAL_API_TOKEN_URL='https://api.sandbox.paypal.com/v1/oauth2/token' PAYPAL_API_TOKEN_URL=https://api.sandbox.paypal.com/v1/oauth2/token
PAYPAL_WEBHOOK_ID=webhook_id_from_paypal_dashboard PAYPAL_WEBHOOK_ID=webhook_id_from_paypal_dashboard
# --------------------- # ---------------------
# Build variants # Build variants
# --------------------- # ---------------------
DEPLOYMENT_ENV=staging
PIPELINE_ENV=false FREECODECAMP_NODE_ENV=development
DEPLOYMENT_ENV='staging'
FREECODECAMP_NODE_ENV='development'
# Languages to build # Languages to build
CLIENT_LOCALE=english CLIENT_LOCALE=english
@ -59,11 +57,11 @@ CURRICULUM_LOCALE=english
SHOW_UPCOMING_CHANGES=false SHOW_UPCOMING_CHANGES=false
# Application paths # Application paths
HOME_LOCATION='http://localhost:8000' HOME_LOCATION=http://localhost:8000
API_LOCATION='http://localhost:3000' API_LOCATION=http://localhost:3000
FORUM_LOCATION='https://forum.freecodecamp.org' FORUM_LOCATION=https://forum.freecodecamp.org
NEWS_LOCATION='https://www.freecodecamp.org/news' NEWS_LOCATION=https://www.freecodecamp.org/news
RADIO_LOCATION='https://coderadio.freecodecamp.org' RADIO_LOCATION=https://coderadio.freecodecamp.org
# --------------------- # ---------------------
# Debugging Mode Keys # Debugging Mode Keys
@ -74,4 +72,4 @@ DEBUG=true
LOCAL_MOCK_AUTH=true LOCAL_MOCK_AUTH=true
# Webhook proxy url from smee.io for PayPal # Webhook proxy url from smee.io for PayPal
WEBHOOK_PROXY_URL='' WEBHOOK_PROXY_URL=

28
server.Dockerfile Normal file
View File

@ -0,0 +1,28 @@
FROM node:14.16.1-alpine as builder
USER node
WORKDIR /home/node/build
COPY --chown=node:node . .
RUN npm ci
RUN npm run build:curriculum
RUN npm run build:server
FROM node:14.16.1-alpine
USER node
WORKDIR /home/node/api
# get and install deps
COPY --from=builder --chown=node:node /home/node/build/package.json /home/node/build/package-lock.json ./
COPY --from=builder --chown=node:node /home/node/build/api-server/package.json /home/node/build/api-server/package-lock.json api-server/
RUN npm ci --production --ignore-scripts \
&& cd api-server \
&& npm ci --production \
&& npm cache clean --force
COPY --from=builder --chown=node:node /home/node/build/api-server/lib/ api-server/lib/
COPY --from=builder --chown=node:node /home/node/build/utils/ utils/
COPY --from=builder --chown=node:node /home/node/build/config/ config/
WORKDIR /home/node/api/api-server
CMD ["npm", "start"]
# TODO: don't copy mocks/fixtures