Search code examples
dockerwebpackdocker-composeopen-sourcenext.js

Docker compose build time args from file


I'm aware of the variable substitutions available, where I could use a .env at the root of the project and that would be done, but in this case I'm adapting an existing project, where existing .env file locations are expected and I would like to prevent having to have var entries on multiple files!

See documentation for more info, and all the code is available as WIP on the docker-support branch of the repo, but I'll succinctly describe the project and issue below:

Project structure

|- root
|  |- .env # mongo and mongo-express vars (not on git!)
|  |- docker-compose.yaml # build and ups a staging env
|  |- docker-compose.prod.yaml # future wip
|  |- api # the saas-api service
|     |- Dockerfile # if 'docked' directly should build production
|     |- .env # api relative vars (not on git!)
|  |- app # the saas-app service
|     |- Dockerfile # if 'docked' directly should build production
|     |- .env # api relative vars (not on git!)

Or see the whole thing here, it works great by the way for the moment, but there's one problem with saas-app when building an image for staging/production that I could identify so far.

Issue

At build time Next.js builds a static version of the pages using webpack to do it's thing about process.env substitution, so it requires the actual eventual running vars to be included at docker build stage so next.js doesnt need to rebuild again at runtime and also so that I can safely spawn multiple instances when traffic requires!

I'm aware that if at runtime the same vars are not sent it will have to rebuild again defying the point of this exercise, but that's precisely what I'm trying to prevent here, to that if the wrong values are sent it's on us an not the project!

And I also need to consider Next.js BUILD ID managemement, but that's for another time/question.

Attempts

I've been testing with including the ARG and ENV declarations for each of the variables expected by the app on it's Dockerfile, e.g.:

ARG GA_TRACKING_ID=
ENV GA_TRACKING_ID ${GA_TRACKING_ID}

This works as expected, however it forces me to manually declare them on the docker-compose.yml file, which is not ideal:

  saas-app:
    build:
      context: app
      args:
        GA_TRACKING_ID: UA-xXxXXXX-X

I cannot use variable substitution here because my root .env does not include this var, it's on ./app/.env, and I also tested leaving the value empty but it is not picking it up from the env_file or enviroment definitions, which I believe is as expected.

I've pastbinned a full output of docker-compose config with the existing version on the repository:

Ideally, I'd like:

  saas-app:
    build:
      args:
        LOG_LEVEL: notice
        NODE_ENV: development
        PORT: '3000'
      context: /home/pedro/src/opensource/saas-boilerplate/app
    command: yarn start
    container_name: saas-app
    depends_on:
    - saas-api
    environment:
      ...

To become:

  saas-app:
    build:
      args:
        LOG_LEVEL: notice
        NODE_ENV: development
        PORT: '3000'
        BUCKET_FOR_POSTS: xxxxxx
        BUCKET_FOR_TEAM_AVATARS: xxxxxx
        GA_TRACKING_ID: ''
        LAMBDA_API_ENDPOINT: xxxxxxapi
        NODE_ENV: development
        STRIPEPUBLISHABLEKEY: pk_test_xxxxxxxxxxxxxxx
        URL_API: http://api.saas.localhost:8000
        URL_APP: http://app.saas.localhost:3000
      context: /home/pedro/src/opensource/saas-boilerplate/app
    command: yarn start
    container_name: saas-app
    depends_on:
    - saas-api
    environment:
      ...

Questions

How would I be able to achieve this, if possible, but:

  1. Without merging the existing .env files into a single root, or having to duplicate vars on multiple files.
  2. Without manually declaring the values on the compose file, or having to infer them on the command e.g. docker-compose build --build-arg GA_TRACKING_ID=UA-xXxXXXX-X?
  3. Without having to COPY each .env file during the build stage, because it doesn't feel right and/or secure?
  4. Maybe a args_file on the compose build options feature request for the compose team seems to me to be a valid, would you also say so?
  5. Or perhaps have a root option on the compose file where you could set more than one .env file for variable substituion?
  6. Or perhaps another solution i'm not seeing? Any ideas?
  7. I wouldn't mind sending each .env file as a config or secret, it's a cleaner solution than splitting the compose files, is anyone running such an example for production?

Solution

  • I've managed to achieve a compromise that does not affect any of the existing development workflows, nor does it allow for app to build without env variables (a requirement that will be more crucial for production builds).

    I've basically decided to reuse the internal ability of docker to read the .env file and use those in variable substitution on the compose file, here's an example:

    # compose
    COMPOSE_TAG_NAME=stage
    
    # common to api and app (build and run)
    LOG_LEVEL=notice
    NODE_ENV=development
    URL_APP=http://app.saas.localhost:3000
    URL_API=http://api.saas.localhost:8000
    API_PORT=8000
    APP_PORT=3000
    
    # api (run)
    MONGO_URL=mongodb://saas:secret@saas-mongo:27017/saas
    SESSION_NAME=saas.localhost.sid
    SESSION_SECRET=3NvS3Cr3t!
    COOKIE_DOMAIN=.saas.localhost
    GOOGLE_CLIENTID=
    GOOGLE_CLIENTSECRET=
    AMAZON_ACCESSKEYID=
    AMAZON_SECRETACCESSKEY=
    EMAIL_SUPPORT_FROM_ADDRESS=
    MAILCHIMP_API_KEY=
    MAILCHIMP_REGION=
    MAILCHIMP_SAAS_ALL_LIST_ID=
    STRIPE_TEST_SECRETKEY=
    STRIPE_LIVE_SECRETKEY=
    STRIPE_TEST_PUBLISHABLEKEY=
    STRIPE_LIVE_PUBLISHABLEKEY=
    STRIPE_TEST_PLANID=
    STRIPE_LIVE_PLANID=
    STRIPE_LIVE_ENDPOINTSECRET=
    
    # app (build and run)
    STRIPEPUBLISHABLEKEY=
    BUCKET_FOR_POSTS=
    BUCKET_FOR_TEAM_AVATARS=
    LAMBDA_API_ENDPOINT=
    GA_TRACKING_ID=
    

    See the updated docker-compose.yml I've also made use of Extension fields to make sure only the correct and valid vars are sent across on build and run.

    It breaks rule 1. from the question, but I feel it's a good enough compromise, because it no longer relies on the other .env files, that would potentically be development keys most of the time anyway!

    Unfortunately we will need to mantain the compose file if the vars change in the future, and the same .env file has to be used for a production build, but since that will probably be done externally on some CI/CD, that does not worry much.

    I'm posting this but not fully closing the question, if anyone else could chip in with a better idea, I'd be greatly appreciated.