From 5d46e2830aff78ae4090028393fbe86c8ce29ef3 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Tue, 20 Apr 2021 15:59:31 +0200 Subject: [PATCH] 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 --- .dockerignore | 9 ++++++++ api-server/src/server/index.js | 2 +- client.Dockerfile | 40 ++++++++++++++++++++++++++++++++++ config/env.js | 16 ++++++++------ docker-compose.api.yml | 8 +++++++ docker-compose.client.yml | 8 +++++++ sample.env | 26 ++++++++++------------ server.Dockerfile | 28 ++++++++++++++++++++++++ 8 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 .dockerignore mode change 100755 => 100644 api-server/src/server/index.js create mode 100644 client.Dockerfile create mode 100644 docker-compose.api.yml create mode 100644 docker-compose.client.yml create mode 100644 server.Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..b332862629 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +client/.cache +client/public +.env +.git +.gitignore +.dockerignore +*Dockerfile* +*docker-compose* +**/node_modules diff --git a/api-server/src/server/index.js b/api-server/src/server/index.js old mode 100755 new mode 100644 index a9ad7fb205..d411ecf961 --- a/api-server/src/server/index.js +++ b/api-server/src/server/index.js @@ -32,7 +32,7 @@ Rx.config.longStackSupport = process.env.NODE_DEBUG !== 'production'; const app = loopback(); 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.use(loopback.token()); app.use( diff --git a/client.Dockerfile b/client.Dockerfile new file mode 100644 index 0000000000..2289fcf806 --- /dev/null +++ b/client.Dockerfile @@ -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"] diff --git a/config/env.js b/config/env.js index 8c0918ecaf..a7262f665c 100644 --- a/config/env.js +++ b/config/env.js @@ -1,13 +1,15 @@ const path = require('path'); -const fs = require('fs'); -// PIPELINE_ENV is 'true' in the build pipeline -if (process.env.PIPELINE_ENV !== 'true') { - const envPath = path.resolve(__dirname, '../.env'); - if (!fs.existsSync(envPath)) { - throw Error('.env not found, please copy sample.env to .env.'); +const envPath = path.resolve(__dirname, '../.env'); +const { error } = require('dotenv').config({ path: envPath }); + +if (error) { + 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 { diff --git a/docker-compose.api.yml b/docker-compose.api.yml new file mode 100644 index 0000000000..7fe731e24c --- /dev/null +++ b/docker-compose.api.yml @@ -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}' diff --git a/docker-compose.client.yml b/docker-compose.client.yml new file mode 100644 index 0000000000..6f22932022 --- /dev/null +++ b/docker-compose.client.yml @@ -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}"] diff --git a/sample.env b/sample.env index c8e000a9e7..9605c00fdb 100644 --- a/sample.env +++ b/sample.env @@ -3,11 +3,11 @@ # --------------------- # Database -MONGOHQ_URL='mongodb://localhost:27017/freecodecamp' +MONGOHQ_URL=mongodb://localhost:27017/freecodecamp # Logging SENTRY_DSN=dsn_from_sentry_dashboard -SENTRY_ENVIRONMENT='staging' +SENTRY_ENVIRONMENT=staging # Auth0 - OAuth 2.0 Credentials AUTH0_CLIENT_ID=client_id_from_auth0_dashboard @@ -39,17 +39,15 @@ STRIPE_SECRET_KEY=sk_from_stripe_dashboard # PayPal PAYPAL_CLIENT_ID=id_from_paypal_dashboard PAYPAL_SECRET=secret_from_paypal_dashboard -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_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_WEBHOOK_ID=webhook_id_from_paypal_dashboard # --------------------- # Build variants # --------------------- - -PIPELINE_ENV=false -DEPLOYMENT_ENV='staging' -FREECODECAMP_NODE_ENV='development' +DEPLOYMENT_ENV=staging +FREECODECAMP_NODE_ENV=development # Languages to build CLIENT_LOCALE=english @@ -59,11 +57,11 @@ CURRICULUM_LOCALE=english SHOW_UPCOMING_CHANGES=false # Application paths -HOME_LOCATION='http://localhost:8000' -API_LOCATION='http://localhost:3000' -FORUM_LOCATION='https://forum.freecodecamp.org' -NEWS_LOCATION='https://www.freecodecamp.org/news' -RADIO_LOCATION='https://coderadio.freecodecamp.org' +HOME_LOCATION=http://localhost:8000 +API_LOCATION=http://localhost:3000 +FORUM_LOCATION=https://forum.freecodecamp.org +NEWS_LOCATION=https://www.freecodecamp.org/news +RADIO_LOCATION=https://coderadio.freecodecamp.org # --------------------- # Debugging Mode Keys @@ -74,4 +72,4 @@ DEBUG=true LOCAL_MOCK_AUTH=true # Webhook proxy url from smee.io for PayPal -WEBHOOK_PROXY_URL='' +WEBHOOK_PROXY_URL= diff --git a/server.Dockerfile b/server.Dockerfile new file mode 100644 index 0000000000..fe0a6266bb --- /dev/null +++ b/server.Dockerfile @@ -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