How to Containerize a React Frontend with Docker, GitHub Actions, and Kubernetes
This guide explains how to containerize a React front‑end application using Docker, set up CI/CD pipelines with GitHub Actions, optimize builds with pnpm caching and buildx for multi‑architecture images, inject environment variables, and deploy the resulting container to Kubernetes.
1. Introduction
Front‑end containerization packages a front‑end application into a container, enabling fast, efficient deployment across environments.
2. Background
With the rise of front‑back separation, front‑end projects have grown in complexity, differing Node.js versions, and lack a single artifact for deployment. Containers simplify deployment, environment variable injection, version rollback, multi‑architecture builds, CI/CD, and DevOps.
3. Using GitHub Actions for CI/CD
GitHub Actions can automate npm publishing. The steps are:
Create
.github/workflows/ci.ymlin the project root.
Obtain an npm token.
Paste the workflow code.
Push to the
masterbranch to trigger the pipeline.
<code>name: CI
on:
push:
branches:
- master
jobs:
build:
# specify OS
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- name: Cache
id: cache-dependencies
uses: actions/cache@v3
with:
path: |
**/node_modules
key: ${{runner.OS}}-${{hashFiles('**/pnpm-lock.yaml')}}
- name: Install pnpm
run: npm install -g [email protected]
- name: Installing Dependencies
if: steps.cache-dependencies.outputs.cache-hit != 'true'
run: pnpm install
- name: Running Build
run: pnpm run build
- name: Running Test
run: pnpm run test-unit
- name: Running Publish
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
</code>4. Building a Front‑end Image with Docker
4.1 Install Docker
Install Docker and verify the version (preferably with buildx support).
<code>docker -v
Docker version 24.0.2, build cb74dfc
</code>4.2 Write a Dockerfile
A typical React project needs
package.json,
npm install, and
npm run build. The Dockerfile uses a multi‑stage build: first a Node builder, then an Nginx image serving the built files.
<code>FROM node:17-buster as builder
WORKDIR /src
COPY ./ /src
RUN npm install -g pnpm \
&& pnpm install \
&& pnpm build
FROM nginx:alpine-slim
RUN mkdir -p /usr/share/nginx/front/dist \
&& rm -rf /etc/nginx/nginx.conf
COPY --from=builder /src/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /src/dist /usr/share/nginx/front/dist
EXPOSE 80
</code>Build and run the image:
<code>docker buildx build -t webapp-demo:v1 .
docker run -d -p 80:80 webapp-demo:v1
</code>4.3 pnpm Cache in Docker
Using multi‑stage builds and cache mounts reduces intermediate layers when
package.jsonchanges. Example Dockerfile with pnpm cache mounts is provided.
<code>FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY . /app
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build
FROM base
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
EXPOSE 8000
CMD ["pnpm", "start"]
</code>4.4 Buildx for Multi‑Architecture Images
Docker
buildxenables building images for different CPU architectures. Create a builder that supports multiple platforms, optionally using a custom buildkit image for faster pulls in China.
<code># Create a builder for domestic environment
docker buildx create --use --name=mybuilder-cn --driver docker-container --driver-opt image=dockerpracticesig/buildkit:master
# List builders
docker buildx ls
# Build and push multi‑arch image
docker buildx build --platform linux/arm,linux/arm64,linux/amd64 -t myusername/hello . --push
</code>If you have a private image accelerator, you can build your own buildkit image based on https://github.com/docker-practice/buildx.
5. Injecting Front‑end Environment Variables via Containers
Common variables include API baseURL, appName, and env.
Micro‑frontend scenarios may require many URLs.
Traditional approaches use domain checks or framework flags.
Containerization allows injecting variables directly into HTML meta tags, which the front‑end reads at runtime.
In monorepos, the build target can be selected via container variables.
Production setups may generate a
default.ymlthat K8s injects into the container.
Example Dockerfile for a production environment (variables are assumed to be already injected):
<code>FROM --platform=${BUILDPLATFORM} hub-dev.rockontrol.com/rk-infrav2/docker.io/library/node:17-bullseye as builder
WORKDIR /src
COPY ./ ./
ARG APP
ARG ENV
ARG PROJECT_GROUP
ARG PROJECT_NAME
ARG PROJECT_VERSION
ARG YARN_NPM_REGISTRY_SERVER
RUN npm install -g --registry=${YARN_NPM_REGISTRY_SERVER} pnpm
RUN pnpm --registry=${YARN_NPM_REGISTRY_SERVER} install
RUN PROJECT_GROUP=${PROJECT_GROUP} PROJECT_VERSION=${PROJECT_VERSION} \
npx devkit build --prod ${APP} ${ENV}
FROM hub-dev.rockontrol.com/rk-infrav2/ghcr.io/zboyco/webrunner:0.0.7
ARG PROJECT_NAME
COPY --from=builder /src/public/${PROJECT_NAME} /app
</code>Shell script that writes environment variables into Nginx configuration and HTML meta tags:
<code>#!/bin/sh
app_config="${APP_CONFIG}"
ext_config=""
for var in $(env | cut -d= -f1); do
if [ "$(echo "$var" | grep '^APP_CONFIG__')" ]; then
trimmed_var=$(echo "$var" | sed 's/^APP_CONFIG__//')
value=$(eval echo "\${$var}")
app_config="${app_config},${trimmed_var}=${value}"
fi
done
export app_config=$(echo "$app_config" | sed 's/^,//')
IFS=","; set -- $app_config
for config in "$@"; do
IFS="="
set -- $config
ext_config="${ext_config} sub_filter '__${1}__' '${2}';\n"
done
sed "s@__EXTENT_CONFIG__@${ext_config}@g" /etc/nginx/conf.d/conf-base.template > /etc/nginx/conf.d/conf.template
envsubst '${PROJECT_VERSION} ${ENV} ${app_config}' < /etc/nginx/conf.d/conf.template > /etc/nginx/conf.d/default.conf
nginx -g 'daemon off;'
</code>Front‑end code to read the injected meta tag:
<code>import appConfig from "../../config";
export function getConfig() {
const defaultAppConfig = {
appName: "",
version: "",
env: "",
baseURL: "",
};
if (import.meta.env.DEV) {
return appConfig;
} else {
const appConfigStr = getMeta("app_config");
if (!appConfigStr) return defaultAppConfig;
return parseEnvVar(appConfigStr);
}
}
function getMeta(metaName) {
const metas = document.getElementsByTagName("meta");
for (let i = 0; i < metas.length; i++) {
if (metas[i].getAttribute("name") === metaName) {
return metas[i].getAttribute("content");
}
}
return "";
}
function parseEnvVar(envVarURL) {
const arrs = envVarURL.split(",");
return arrs.reduce((pre, item) => {
const keyValues = item.split("=");
return {
...pre,
[keyValues[0]]: keyValues[1],
};
}, {});
}
</code>6. Deploying the Front‑end to Kubernetes
Kubernetes automates container orchestration. Create a Deployment and a Service to expose the front‑end.
<code>apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-app
spec:
replicas: 3
selector:
matchLabels:
app: frontend-app
template:
metadata:
labels:
app: frontend-app
spec:
containers:
- name: frontend-app
image: my-frontend-app:latest
ports:
- containerPort: 3000
</code> <code>apiVersion: v1
kind: Service
metadata:
name: frontend-service
spec:
selector:
app: frontend-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancer
</code>Deploy with:
<code>kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
</code>7. React Project Architecture Overview
Core Technologies
Build: Vite
Package manager: pnpm
Language: TypeScript
Framework: React
Routing: react-router
UI library: antd
CSS‑in‑JS: emotion
State: zustand
API generation: OpenAPI
HTTP client: axios
Data fetching: react-query
Hooks: ahooks
Error boundary: react-error-boundary
Logging: sentry‑javascript (not integrated)
Transpilation: Babel
Linter: ESLint
TS lint: typescript‑eslint
Formatter: Prettier
Git hooks: husky
Commit lint: commitlint
OpenAPI‑generated API Functions
<code>// src/core/openapi/index.ts
generateService({
schemaPath: `${appConfig.baseURL}/${urlPath}`,
serversPath: "./src",
requestImportStatement: `/// <reference types="./typings.d.ts" />\nimport request from "@request"`,
namespace: "Api",
});
</code>Using react‑query for Requests
<code>// Example request
export async function HelloGet(params: Api.HelloGetParams, options?: { [key: string]: any }) {
return request<Api.HelloResp>('/gin-demo-server/api/v1/hello', {
method: 'GET',
params: { ...params },
...(options || {}),
});
}
// React component
const { data, isLoading } = useQuery({
queryKey: ["hello", name],
queryFn: () => HelloGet({ name }),
});
</code>8. CLI Repository
Code repository: https://github.com/rookie-luochao/create-vite-app-cli
9. Conclusion
Introduced basic GitHub Action configuration for front‑end npm workflows.
Demonstrated Dockerfile creation and pnpm optimization, including multi‑arch builds with buildx.
Explained usage of environment variables in production containers.
Presented a comprehensive React project technical stack.
Ops Development Stories
Maintained by a like‑minded team, covering both operations and development. Topics span Linux ops, DevOps toolchain, Kubernetes containerization, monitoring, log collection, network security, and Python or Go development. Team members: Qiao Ke, wanger, Dong Ge, Su Xin, Hua Zai, Zheng Ge, Teacher Xia.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.