Skip to content

Dockerized React application works locally but fails when built by ci pipeline #9280

@zjpiazza

Description

@zjpiazza

Describe the bug

I have two pages, one called JobList and one called NewJob. In both pages I'm utilizing useState to initialize an array which I populate at mount time with the useEffect hook. Here is the error message I'm seeing when the app is deployed. It is clearly pointing to the places in the code where I'm using the map function on my arrays.

When I run using the development server with "npm start", via static build, or with a docker build command on my LOCAL machine everything works fine. However if I create an Ubuntu 18.04 VM and pull the Dockerfile, I'm getting the same result that my build pipeline is producing, which is the following error on the pages mentioned. I have a feeling there is some dependency which is not present on the remote build server causing this failure. However, issuing an "npm list -g --depth 0" returns the following which I wouldn't think would cause any interference.

/usr/lib
├── npm@6.14.5
├── package-diff@0.0.6
├── serve@11.3.2
├── webpack@4.43.0
└── webpack-cli@3.3.12

Did you try recovering your dependencies?

Yes, I have deleted the package-lock.json and node-modules folder and recreated the environment locally.

Environment

npx create-react-app --info output

  npx: installed 98 in 3.783s

  Environment Info:

  current version of create-react-app: 3.4.1
  running from /home/zach/.npm/_npx/12540/lib/node_modules/create-react-app

  System:
    OS: Linux 5.3 Ubuntu 18.04.4 LTS (Bionic Beaver)
    CPU: (12) x64 AMD Ryzen 5 2600 Six-Core Processor
  Binaries:
    Node: 12.18.0 - /usr/bin/node
    Yarn: 1.22.4 - /usr/bin/yarn
    npm: 6.14.5 - /usr/bin/npm
  Browsers:
    Chrome: 83.0.4103.97
    Firefox: 78.0.1
  npmPackages:
    react: ^16.13.1 => 16.13.1 
    react-dom: ^16.13.1 => 16.13.1 
    react-scripts: ^3.4.1 => 3.4.1 
  npmGlobalPackages:
    create-react-app: Not Found

Error Messages

TypeError: i.map is not a function
    at me (JobList.js:51)
    at Wi (react-dom.production.min.js:153)
    at ko (react-dom.production.min.js:175)
    at gs (react-dom.production.min.js:263)
    at cl (react-dom.production.min.js:246)
    at sl (react-dom.production.min.js:246)
    at Js (react-dom.production.min.js:239)
    at react-dom.production.min.js:123
    at t.unstable_runWithPriority (scheduler.production.min.js:19)
    at Va (react-dom.production.min.js:122)
TypeError: f.map is not a function
    at oe (NewJob.js:108)
    at Wi (react-dom.production.min.js:153)
    at ko (react-dom.production.min.js:175)
    at gs (react-dom.production.min.js:263)
    at cl (react-dom.production.min.js:246)
    at sl (react-dom.production.min.js:246)
    at Js (react-dom.production.min.js:239)
    at react-dom.production.min.js:123
    at t.unstable_runWithPriority (scheduler.production.min.js:19)
    at Va (react-dom.production.min.js:122)

Javascript Code

NewJob.js:

import React, {useState, useEffect} from 'react';
import { useForm } from 'react-hook-form';
import Loading from "../components/Loading";
import { useAuth0 } from "../react-auth0-spa";
import { Form, Input, Alert } from 'reactstrap';
import API from "../utils/Api";
let S = require('string');

export default function NewJob() {

  const { loading, user, getTokenSilently } = useAuth0();
  const { register, handleSubmit, errors } = useForm();
  const [filters, setFilters] = useState([]);
  const [alert, setAlert] = useState({});
  const [alertVisible, setAlertVisible] = useState(false);
  const [page, setPage] = useState([1]);
  
  const onDismiss = () => setAlertVisible(false);
  
  useEffect(() => {
    getTokenSilently()
    .then(token => {
      API.get(`/filters/?page=${page}`, {
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        }})
        .then(response => {
          setFilters(response.data);
        })
        .catch(error => {
          setAlert({
            body: 'Unable to retrieve filters',
            color: 'warning'
          })
          setAlertVisible(true);
        });
    })
  }, [page]);

  const onSubmit = async(data, e) => {
    getTokenSilently()
    .then(token => {
      API.post('/jobs/', {
        source_video_url: data.sourceVideoUrl,
        selected_filter: data.filter,
        email: data.email
        },
        {
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        },
      })
      .then(jsonResp => {
        setAlert({
          body: 'Job successfully submitted!',
          color: 'success'
        })
        setAlertVisible(true);
      })
      .catch(error => {
        setAlert({
          body: 'Error, job was not submitted',
          color: 'danger'
        })
        setAlertVisible(true);
      })
    })
    };

  const fieldOptions = {
    'sourceVideoUrl': {
        required: true,
        pattern: {
            value: /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})?$/i,
            message: 'Invalid YouTube URL'
        },
    },
    'filter': { 
        required: true
    },
    'email': {
        required: true,
        pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
            message: 'Enter a valid email address'
        }
    }
  }
  
  if (loading || !user) {
    return <Loading />;
  }

  return (
    <>
    <Form onSubmit={handleSubmit(onSubmit)}>
      <Input 
        type="text"
        defaultValue="https://www.youtube.com/watch?v=qoLh9K5K5vc"
        name="sourceVideoUrl"
        innerRef={register(fieldOptions.sourceVideoUrl)}
        invalid={errors.sourceVideoUrl}
        />
      <br />
      <Input type="select" name="filter" innerRef={register(fieldOptions.filter)}>
        {filters.map(filter => (
          <option value={filter.name}>{S(filter.name).capitalize().s}</option>
        ))}
      </Input>
      <br />
      <Input type="text" defaultValue="zjpiazza@gmail.com" name="email" innerRef={register(fieldOptions.email)} invalid={errors.email} />
      <br />
      <Input type="submit" />
    </Form>
    <br />
    <Alert color={alert.color} isOpen={alertVisible} toggle={onDismiss} >
        {alert.body}
    </Alert>
    </>
  );
}

JobList.js:

import React, {useState, useEffect} from 'react';
import { Table, Alert } from 'reactstrap';
import { useAuth0 } from "../react-auth0-spa";
import Loading from "../components/Loading";
import API from "../utils/Api";
let S = require('string');

export default function JobList (props) {
  const { loading, user, getTokenSilently } = useAuth0();
  const [jobs, setJobs] = useState([]);
  const [page, setPage] = useState([1]);
  const [alert, setAlert] = useState({});
  const [alertVisible, setAlertVisible] = useState(false);
  const onDismiss = () => setAlertVisible(false);

  useEffect(() => {
    getTokenSilently()
    .then(token => {
      API.get(`/jobs/?page=${page}`, {
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        }})
        .then(response => {
          setJobs(response.data);
        })
        .catch(error => {
          setAlert({
            body: 'Error, unable to retrieve jobs',
            color: 'danger'
          })
        });
    })
  }, [page, getTokenSilently]);
  
  if (loading || !user) {
    return <Loading />;
  }
  
  return (
    <>
    <Table striped>
      <thead>
        <tr>
          <th>Job ID</th>
          <th>Status</th>
          <th>Download Link</th>
        </tr>
      </thead>
      <tbody>
      {jobs.map(job => (
            <tr key={job.id}>
                <th scope="row">{job.id}</th>
                <td>{S(job.status).capitalize().s}</td>
                <td>{job.download_link}</td>
            </tr>
        ))}
      </tbody>
    </Table>
    <br />
    <Alert color={alert.color} isOpen={alertVisible} toggle={onDismiss} >
        {alert.body}
    </Alert>
    </>
  );
}

Dockerfile and Build Definition

Dockerfile:

# build environment
FROM node:14.5.0-alpine3.10 as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json ./
COPY package-lock.json ./
RUN npm ci --silent --force
RUN npm install react-scripts@3.4.1 -g --silent
COPY . ./
RUN npm run build

# production environment
FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
# new
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Azure DevOps build definition:

# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker

trigger:
- master

resources:
- repo: self

variables:
  # Container registry service connection established during pipeline creation
  dockerRegistryServiceConnection: '3919c5b1-8ce6-4f6c-aa87-73632208d6a2'
  imageRepository: 'videoeffectsreact'
  containerRegistry: 'videoeffects.azurecr.io'
  dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
  tag: '$(Build.BuildId)'
  
  # Agent VM image name
  vmImageName: 'ubuntu-18.04'

stages:
- stage: Build
  displayName: Build and push stage
  jobs:  
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)
    steps:
    - task: Docker@2
      displayName: Build and push an image to container registry
      inputs:
        command: buildAndPush
        repository: $(imageRepository)
        dockerfile: $(dockerfilePath)
        containerRegistry: $(dockerRegistryServiceConnection)
        tags: |
          $(tag)
          latest
  

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions