How do I access env variables in a React app served by Flask, containerized by Docker and hosted on Heroku?

I’m having trouble accessing environment variables in a React app which has its build served by a Flask app. The client was initialized with CRA and later modified to use CRACO for customization. The whole project is containerized with Docker and hosted on Heroku. I’m trying to figure out the best way to handle and pass environment variables in the builds. Any advice on how to properly configure this setup for accessing env vars on the client side? It works only locally. Things I have tried:

  • Set env var in Docker and Heroku configs
  • Updated CRACO config to include webpack.DefinePlugin

Here is the Dockerfile:

FROM node:18 as builder
WORKDIR /frontend
COPY /frontend/package.json .
COPY /frontend .
ARG REACT_APP_SOCKET_URL
ENV REACT_APP_SOCKET_URL=$REACT_APP_SOCKET_URL
RUN npm install && npm run build

FROM python:3.10-alpine
WORKDIR /app

COPY /backend/requirements.txt ./backend/
RUN pip install -r ./backend/requirements.txt

# Install dockerize to allow containers to wait for dependency containers to be done
RUN apk add openssl \
    && wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-alpine-linux-amd64-v0.6.1.tar.gz \
    && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-v0.6.1.tar.gz \
    && rm dockerize-alpine-linux-amd64-v0.6.1.tar.gz

COPY /backend ./backend
ENV PYTHONPATH "${PYTHONPATH}:/app/backend"

COPY --from=builder /frontend/build ./build

CMD ["python", "backend/app.py"]

And the docker-compose file:

version: "3.8"
services:
  web:
    build:
      context: ./
      dockerfile: Dockerfile.web
      args:
        - REACT_APP_SOCKET_URL=${REACT_APP_SOCKET_URL}
    env_file:
      - ./.env
    ports:
      - "5001:5001"
    volumes:
      - ./backend:/app/backend
    depends_on:
      - db
      - migration
      - redis
      - elasticsearch
    command: >
      sh -c "
      dockerize -wait tcp://db:5432 -wait tcp://redis:6379 -wait tcp://elasticsearch:9200 -timeout 120s
      && python backend/app.py
      "
  db:
    image: postgres:alpine
    environment:
      POSTGRES_DB: dev
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

  migration:
    build:
      context: ./
      dockerfile: Dockerfile.migration
    env_file:
      - ./.env
    volumes:
      - ./backend/migrations:/app/migrations
    depends_on:
      - db
    command: >
      sh -c "
      dockerize -wait tcp://db:5432 -timeout 10s
      && flask --app run_migrations:app db upgrade
      "

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.13.4
    environment:
      - discovery.type=single-node
    volumes:
      - esdata:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    depends_on:
      - db
      - migration

volumes:
  postgres-data:
  esdata:
    driver: local

Heroku config:

build:
  docker:
    web: Dockerfile.web
    migration: Dockerfile.migration
release:
  image: migration
  command:
    - flask --app run_migrations:app db upgrade
run:
  web:
    image: web
    command:
      - gunicorn backend.app:app

And Webpack plugins setting in craco-config:

    plugins: [
      new webpack.DefinePlugin({
        "process.env.REACT_APP_SOCKET_URL": JSON.stringify(
          process.env.REACT_APP_SOCKET_URL
        )
      })
    ]

To resolve the issue of environment variables not being accessible in your React app when it’s deployed on Heroku and containerized with Docker, there are a few critical points to address:

1. Handling Environment Variables in React with Docker and Heroku

React apps, including those using Create React App (CRA) and CRACO, need to have environment variables injected at build time. Since the environment variables are static after the build, they must be correctly passed to the React build process.

In your setup, you are already using webpack.DefinePlugin in your CRACO configuration, which is good. However, there are some additional things you should ensure:

2. Correctly Passing Environment Variables During Build

Ensure that the environment variables are passed during the React build step, especially when containerizing with Docker.

In your Dockerfile:

FROM node:18 as builder
WORKDIR /frontend
COPY /frontend/package.json . 
COPY /frontend . 
ARG REACT_APP_SOCKET_URL  # Argument to pass environment variable during build
ENV REACT_APP_SOCKET_URL=$REACT_APP_SOCKET_URL  # Make the ARG accessible as an ENV var
RUN npm install && npm run build  # Build the React app with the environment variable

In this part of the docker-compose.yml:

services:
  web:
    build:
      context: ./
      dockerfile: Dockerfile.web
      args:
        - REACT_APP_SOCKET_URL=${REACT_APP_SOCKET_URL}  # Pass this argument during build

Ensure that REACT_APP_SOCKET_URL is available in your .env file and passed properly through the Docker build process.

3. Environment Variables in Heroku for Docker

For Heroku, environment variables can be passed in the Docker build process as build-time arguments. However, Heroku doesn’t support dynamic build-time variables for multi-stage builds out of the box. You’ll need to explicitly set them as runtime variables and ensure they’re available to your Flask server and passed to the frontend.

In Heroku, make sure that you have your environment variables (REACT_APP_SOCKET_URL, etc.) correctly configured using the Heroku CLI or dashboard:

heroku config:set REACT_APP_SOCKET_URL=<your_socket_url>

4. Accessing Environment Variables in Flask

Since your Flask app is serving the React build, ensure that the environment variables are passed to the frontend correctly. You can expose the REACT_APP_SOCKET_URL through Flask by embedding the value into your HTML template dynamically.

You could use Flask to serve a dynamic HTML file with the environment variable:

In your Flask backend:

import os
from flask import render_template

@app.route('/')
def index():
    return render_template('index.html', socket_url=os.getenv('REACT_APP_SOCKET_URL'))

In your HTML template (index.html):

<script>
  window.REACT_APP_SOCKET_URL = "{{ socket_url }}";
</script>

This allows you to inject environment variables dynamically into the React app at runtime instead of build time.

5. CRACO Webpack Configuration

In your craco.config.js, you’re already using webpack.DefinePlugin. Ensure that it’s properly configured:

const webpack = require('webpack');

module.exports = {
  webpack: {
    plugins: [
      new webpack.DefinePlugin({
        'process.env.REACT_APP_SOCKET_URL': JSON.stringify(process.env.REACT_APP_SOCKET_URL)
      })
    ]
  }
};

Ensure that the process.env.REACT_APP_SOCKET_URL is passed correctly when building in Docker or Heroku.

6. Deployment Considerations

  • Docker Build: Ensure the ARG and ENV for REACT_APP_SOCKET_URL are correctly passed during the build process.
  • Heroku: Ensure the environment variables are set in Heroku config using heroku config:set.
  • Serving Static Files: Flask needs to serve the React build correctly while ensuring dynamic environment variables (like REACT_APP_SOCKET_URL) are passed.

By configuring the environment variables during the build and ensuring Flask serves them correctly in your HTML, the variables should be accessible in your deployed React app.

Final Steps

  1. Ensure .env in Docker/Heroku is set correctly.
  2. Configure Flask to dynamically inject environment variables into the HTML template if needed.
  3. Ensure Docker builds with ARG and ENV correctly passing variables.
  4. Test locally and then push to Heroku.

This approach should resolve the issue of environment variables not being accessible in your production setup while working fine locally.