skip to Main Content

My dockerized react app is ready for deployment but shuts down after 10 or so seconds in the production environment. I have restart: unless-stopped in the docker-compose.prod.yml file which makes it restart every 10 or so seconds.

I have tried adding std_in: true and tty: true to the docker-compose.prod.yml file, as suggested, to no avail.

Docker Compose:

version: "3.8"

services:
  db:
    image: postgres:13-alpine
    restart: unless-stopped
    env_file:
      - .env
    volumes:
      - ./data:/var/lib/postgresql/data

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.prod
    image: "${BACKEND_IMAGE}"
    ports:
      - 8000:8000
    env_file: .env
    volumes:
      - static_volume:/backend/static
    restart: unless-stopped
    depends_on:
      - db

  # redis:
  #   image: redis:alpine
  #   restart: unless-stopped
  #   depends_on:
  #     - backend

  # celery:
  #   build:
  #     context: ./backend
  #   image: "${BACKEND_IMAGE}"
  #   command: celery -A backend worker -l info
  #   env_file: .env
  #   volumes:
  #     - ./backend/:/usr/src/app/
  #   restart: unless-stopped
  #   depends_on:
  #     - redis

  # celery-beat:
  #   build:
  #     context: ./backend
  #   image: "${BACKEND_IMAGE}"
  #   command: celery -A backend beat -l info
  #   env_file: .env
  #   volumes:
  #     - ./backend/:/usr/src/app/
  #   restart: unless-stopped
  #   depends_on:
  #     - redis

  frontend:
    image: "${FRONTEND_IMAGE}"
    stdin_open: true
    volumes:
      - frontend_build:/frontend/build
    restart: unless-stopped
    depends_on:
      - backend
    env_file: .env

  nginx:
    image: "${NGINX_IMAGE}"
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./nginx/certbot/conf:/etc/letsencrypt
      - ./nginx/certbot/www:/var/www/certbot
      - static_volume:/backend/static
      - frontend_build:/var/www/frontend
    restart: unless-stopped
    depends_on:
      - backend
      - frontend
      - db
    command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"'''

  certbot:
    image: certbot/certbot
    restart: unless-stopped
    volumes:
      - ./nginx/certbot/conf:/etc/letsencrypt
      - ./nginx/certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

volumes:
  frontend_build:
  static_volume:
  media_volume:

Dockerfile.prod:

FROM node:16.13.0

# Create and set the working directory on the container
# then copy over the package.json and package-lock.json
WORKDIR /frontend
COPY package*.json ./

# Install the node packages before copying the files
RUN npm install
COPY . .

# Run the production build
CMD ["npm", "run", "build"]

docker logs [frontend_container]:

The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  https://cra.link/deployment


> [email protected] build
> react-scripts build

Creating an optimized production build...

Thoughts on how to fix the issue? Could this be a permissions issue?

EDIT: I am serving with Nginx in a docker container:

upstream backend_server {
    server backend:8000;
}

server {
    listen 80;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location /api/ {
        return 301 https://$host$request_uri;
    }

    location /admin/ {
        return 301 https://$host$request_uri;
    }
    
    location /static/ {      
        return 301 https://$host$request_uri;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    root /var/www/frontend;
    server_name example.co www.example.co;

    ssl_certificate /etc/letsencrypt/live/exmplae.co/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.co/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location /api/ {
        proxy_pass http://backend_server$request_uri;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $https;
        proxy_connect_timeout 360s;
        proxy_read_timeout 360s;
    }

    location /admin/ {
        proxy_pass http://backend_server$request_uri;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $https;
        proxy_connect_timeout 360s;
        proxy_read_timeout 360s;
    }

    location /static/ {      
        alias /backend/static/;
    }

    location / {
        try_files $uri /index.html;
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    So, the other answers are correct. The way I had it setup, the npm run build command is run, then the container exits. Also, as the other answers are correct, a multi-stage build seems to work best here for the production image - though I made some adjustments, for a better CI/CD pipeline, that I would like to share.

    First, I do not like merging my frontend build and my nginx build as these are separate services. Nginx interacts with not only the frontend, but the backend as well and I would like to run frontend tests on my React code before building the nginx image. Therefore I separated the frontend image and the nginx image to help in my CI/CD pipeline.

    Here is my project tree structure:

    .
    ├── Dockerfile
    ├── Dockerfile.prod
    ├── backend
    ├── data
    ├── docker-compose.ci.yml
    ├── docker-compose.prod.yml
    ├── docker-compose.yml
    ├── frontend
    ├── init-letsencrypt.sh
    ├── nginx
    └── .github
    

    First, I build a frontend image using a Dockerfile.ci file which runs tests using the CMD option - the CMD option is important here as it will be overridden if you pass another CMD in a Dockerfile or docker-compose.yml file (which we will do later on):

    FROM node:16.13.0
    
    # Create and set the working directory on the container
    # then copy over the package.json and package-lock.json
    WORKDIR /frontend
    COPY package*.json ./
    
    # Install the node packages before copying the files
    RUN npm install
    COPY . .
    
    # Run the development server
    CMD ["npm", "run", "test"]
    

    My GitHub Actions workflow runs the frontend build on push to the repo. If tests fail, it won't get to the next step - building the nginx image. If tests pass, it will push the image to GitHub container registry and build the nginx image.

    The nginx image uses the frontend image as a build image:

    # Here we use the image which was pushed to the registry
    FROM ghcr.io/digibrainllc/digibrain-advertising-api/frontend:latest as build
    
    # Then we create production build by **overriding** the previous `CMD` statment
    CMD ["npm", "run", "build"]
    
    # Production environment
    FROM nginx:1.21-alpine
    
    COPY --from=build /frontend/build /usr/share/nginx/html
    
    RUN rm /etc/nginx/conf.d/default.conf
    COPY /nginx/conf/nginx.prod.conf /etc/nginx/conf.d
    

    This way the docker-compose.ci, which is used for building images and running tests, looks like this:

    version: "3.8"
    
    services:
      db:
        image: postgres:13-alpine
        restart: unless-stopped
        volumes:
          - ./data:/var/lib/postgresql/data
        env_file:
          - .env
    
      backend:
        build:
          context: ./backend
          dockerfile: Dockerfile.prod
        image: "${BACKEND_IMAGE}"
        volumes:
          - static_volume:/backend/static
          - media_volume:/backend/media
        expose:
          - 8000
        env_file: .env
        depends_on:
          - db
    
      frontend:
        build:
          context: ./frontend
          dockerfile: Dockerfile.ci
        image: "${FRONTEND_IMAGE}"
        restart: unless-stopped
        environment:
          CHOKIDAR_USEPOLLING: "true"
        stdin_open: true
        env_file: .env
    
      nginx:
        build:
          context: .
          dockerfile: Dockerfile.prod
        image: "${NGINX_IMAGE}"
        restart: unless-stopped
        ports:
          - 80:80
          - 443:443
        volumes:
          - ./nginx/certbot/conf:/etc/letsencrypt
          - ./nginx/certbot/www:/var/www/certbot
          - static_volume:/backend/static
        depends_on:
          - backend
          - db
        # Could also put a `CMD` here which would run last in the `nginx` image build
    
    volumes:
      static_volume:
      media_volume:
    

    This keeps a separation between the services! Hope someone finds this useful!


  2. Once you have build your app, you should serve your app with a webserver like nginx. So, in your dockerfile add these lines to use an nginx docker image, copy your build into into and run it to the port 80.

    FROM node:16.13.0
    
    # Create and set the working directory on the container
    # then copy over the package.json and package-lock.json
    WORKDIR /frontend
    COPY package*.json ./
    
    # Install the node packages before copying the files
    RUN npm install
    
    
    # install react-scripts if needed
    RUN npm install [email protected] -g --silent
    
    COPY . .
    
    # build your app
    RUN npm run build
    
    # production environment
    FROM nginx:1.17.4-alpine
    COPY --from=build /frontend/build /usr/share/nginx/html
    RUN rm /etc/nginx/conf.d/default.conf
    # change the left path with yours, below the file content
    COPY src/nginx/nginx.conf /etc/nginx/conf.d
    EXPOSE 80
    CMD ["nginx", "-g", "daemon off;"]
    

    This is the nginx.conf:

    server {
    
      listen 80;
    
      location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
      }
    
      error_page   500 502 503 504  /50x.html;
    
      location = /50x.html {
        root   /usr/share/nginx/html;
      }
    
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search