They say you need to do something several hundred times before you are an expert. This time I’ll be deploying a SPA using nginx with static content. Eventually I would love to check these out from an existing repository; eventually I will get there.

This is for a fresh project. So I’ll walk through the whole process, or at least until I get distracted.

Content Skelton

I suppose I need someting to dpeloy. I’ll just use the base react generator.

$ create-react-app maurice-dashboard

Not exactly delivering any value yet beyond the build system interface. Time to setup the Jenkins Pipeline to build a simple container.

pipeline {
    agent any
    stages {
        stage('Unit tests in a docker container'){
            agent {
                docker { image 'node:13.12.0' }
            steps {
                sh 'yarn install'
                sh 'yarn test --watchAll=false'
        stage('Build Docker container'){
            steps {
                sh "docker build . -f ./.cd/Dockerfile --tag docker.artifacts.internal/maurice-dashboard:${env.BUILD_NUMBER}"
        stage('Publish') {
            steps {
                sh "docker push docker.artifacts.internal/maurice-dashboard:${env.BUILD_NUMBER}"
            steps {
                sh ".cd/"
    post {
        success {
          slackSend (color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")

        failure {
          slackSend (color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
          sh "docker rmi docker.artifacts.internal/maurice-dashboard:${env.BUILD_NUMBER}"

I do not like the drift in the Dockerfile versus int eh Jenkinsfile pipeline for testing, however at this time I do not have a good solution to use the same file. The Dockerfile for producing the SPA static content is below.

FROM node:13.12.0 AS builder

COPY . /app
RUN chown -R node:node /app

USER node
ENV NODE_ENV production
RUN yarn install --production
RUN yarn build

FROM nginx:1.17.9
COPY --from=builder /app/build /usr/share/nginx/html
COPY .cd/k8s-nginx.conf /etc/nginx/conf.d/default.conf

My current nginx.conf file is pretty simple:

server {
  listen       80;
  access_log off;
  error_log off;

  location / {
      root   /usr/share/nginx/html;
      index  index.html index.htm;
      try_files $uri /index.html;

The file is really a placeholder for pushing changes into the Kubernetes clusters in the future. For now it just updates the integration environment via Helm.


helm update --namespace maurice dashboard --set tag=${env.BUILD_NUMBER} -f integ.yaml

The integ.yaml file contains environment specific setup. In this case it is just setting the imagePullSecret. For future implementations this may provide additional overrides or details.