How to Deploy Strapi with Next.js Frontend with Nginx Proxy and URL Redirect with Docker

November 29, 2021

Agenda

I will cover following in this post:

  • Prepare Docker image for Next.js app
  • Prepare Docker image for Strapi app
  • Prepare Docker image for Nginx proxy, which will have all redirects and acts like a load balancer
  • Docker compose yaml file, with mysql

Prepare Docker Image for Next.js app

# Install dependencies only when needed
FROM node:alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Rebuild the source code only when needed
FROM node:alpine AS builder

ARG NEXT_PUBLIC_API_URL
ARG NEXTAUTH_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXTAUTH_URL=$NEXTAUTH_URL

WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build && npm install --production --ignore-scripts --prefer-offline

# Production image, copy all the files and run next
FROM node:alpine AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

USER nextjs

EXPOSE 3000

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry.
ENV NEXT_TELEMETRY_DISABLED 1

CMD ["npm", "start"]

Build Image

docker build --build-arg NEXT_PUBLIC_API_URL=https://your-website-name/gapi --build-arg NEXTAUTH_URL=https://your-website-name -t my-ui .

Replace it with your website name. my-ui will be the name of the docker image.

Prepare Docker image for Strapi app

FROM node:12-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm ci --only=production
RUN NODE_ENV=production npm run build

# Bundle app source
COPY . .

EXPOSE 1337
CMD [ "npm", "start" ]

Build Docker Image

docker build -t my-api .

Prepare Docker image for Nginx proxy

FROM nginx
# FROM nginx:alpine
COPY ./conf.d /etc/nginx/conf.d
COPY ./script/run.sh /run.sh
EXPOSE 80 443 8090

# CMD ["nginx", "-g", "daemon off;"]
CMD ["/bin/bash", "/run.sh"]

conf.d/lb.conf

This is the only file present in conf.d folder

server_tokens off;
port_in_redirect off;

proxy_connect_timeout       600s;
proxy_send_timeout          600s;
proxy_read_timeout          600s;
send_timeout                600s;

gzip on;
gzip_proxied any;
gzip_types text/plain text/xml text/css application/x-javascript;
gzip_vary on;
gzip_disable "MSIE [1-6]\.(?!.*SV1)";

# Expires map
map $sent_http_content_type $expires {
    default                    off;
    text/html                  epoch;
    text/css                   max;
    application/javascript     max;
}

upstream ui {
    least_conn;

    server my_ui:3000 max_fails=3 fail_timeout=60 weight=1;
}

 upstream api {
    least_conn;

    server my_api:1337 max_fails=3 fail_timeout=60 weight=1;
}

ssl_session_cache shared:SSL:50m;
ssl_session_timeout 10m;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";

server {
    listen 80 default_server;
    server_name mywebsite.com;
    
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    # listen 443;
    listen 443 ssl;
    server_name mywebsite.com;

    keepalive_timeout   70;
    client_max_body_size 8M;

    ssl_certificate /etc/letsencrypt/live/mywebsite.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mywebsite.com/privkey.pem;

    location /  {
        add_header Pragma "no-cache";
        add_header Cache-Control "no-cache, must-revalidate";
        gzip_static on;
        # add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://bam.nr-data.net https://js-agent.newrelic.com \'sha256-BQ4TP+rmuJ4THKLnhrmM7pZ20KRHSVePafL6dhKoHuE=\'; style-src 'self' 'unsafe-inline' https://js-agent.newrelic.com https://bam.nr-data.net; img-src 'self' data: https://img.corp.adobe.com:8443; sandbox allow-forms allow-scripts allow-same-origin allow-popups; report-uri /report-violation; frame-ancestors 'self'; font-src 'self' data:; media-src 'self'; connect-src 'self' https://bam.nr-data.net; frame-src 'self'; object-src 'self'";
        # add_header Strict-Transport-Security "max-age=86400; includeSubDomains";
        proxy_pass http://ui/;
    }

    location ~ ^/gapi(/?)(.*) {
        # remove the gapi from url and redirect
        # set name as gapi because in next.js there is an api route
        proxy_pass http://api/$2$is_args$args;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_send_timeout          600s;
        proxy_read_timeout          600s;
    }
}

In above file, I have defined:

  • an UI
  • an API
  • A load balancer, which will redirect requests to either of them
  • A block for Lets encrypt SSL

Build Nginx Proxy image

docker build -t my-lb .

Docker compose file

version: '2'
services:
  my_api:
    image: my-api:latest
    container_name: "my_api"
    environment:
      - URL=https://my-website
      - NODE_ENV=production
      - DATABASE_HOST=my_mysql
      - DATABASE_PORT=3306
      - DATABASE_PASSWORD=xyz
      - ADMIN_JWT_SECRET=xyz
    volumes:
      - /root/public_files:/usr/src/app/public
    depends_on:
      - my_mysql
    networks:
      - mynetwork
    ports:
      - 1337:1337
  my_ui:
    image: my-ui:latest
    environment:
      - NODE_ENV=production
      - NEXT_TELEMETRY_DISABLED=1
      - NEXTAUTH_URL=https://my-website
      - NEXT_PUBLIC_API_URL=https://my-website/gapi
    depends_on:
      - gyanmail_api
    networks:
      - mynetwork
    ports:
      - 3000:3000
  my_mysql:
    image: mariadb
    restart: always
    container_name: "my_mysql"
    environment:
      - MYSQL_ROOT_PASSWORD=xyz
      - MARIADB_DATABASE=mydb
    volumes:
      - /root/mysql:/var/lib/mysql
    networks:
      - mynetwork
    ports:
      - 3306:3306
  
  my_lb:
    image: my-lb:latest
    container_name: "my_lb"
    networks:
      - mynetwork
    ports:
      - 8090:8090
      - 80:80
      - 443:443
    volumes: 
      - /etc/letsencrypt:/etc/letsencrypt
      - /root/data/certbot/www:/var/www/certbot
    depends_on:
      - mymail_api
      - mymail_ui
networks:
  mynetwork:
    driver: bridge

Run Docker Compose

docker-compose up -d

Enjoy your full working stack.


Similar Posts

Latest Posts