Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
test-results
playwright-report
.git
.github
49 changes: 49 additions & 0 deletions .github/workflows/docker-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: docker-release
on:
push:
tags:
- "v*"
permissions:
contents: read
env:
IMAGE_NAME: template-typescript-react
jobs:
release:
runs-on: "ubuntu-latest"
timeout-minutes: 10
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://github.com/docker/build-push-action/issues/42#issuecomment-915323168
- name: Set Versions
uses: actions/github-script@v9
id: set_version
with:
script: |
const tag = context.ref.substring(10)
const no_v = tag.replace('v', '')
const dash_index = no_v.lastIndexOf('-')
const no_dash = (dash_index > -1) ? no_v.substring(0, dash_index) : no_v
core.setOutput('tag', tag)
core.setOutput('no-v', no_v)
core.setOutput('no-dash', no_dash)
- name: Build and push tag ${{steps.set_version.outputs.no-dash}}
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
build-args: |
GIT_REVISION=${{ github.sha }}
GIT_TAG=${{steps.set_version.outputs.no-dash}}
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{steps.set_version.outputs.no-dash}}
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
28 changes: 28 additions & 0 deletions .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: docker

on:
push:
branches:
- "main"
- "feature/**"
pull_request:
branches:
- "main"

permissions:
contents: read

jobs:
docker:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # to retrieve git tags
- name: Add to PATH
shell: bash
run: echo "/home/runner/bin" >> "$GITHUB_PATH"
- name: Run CI tests for Docker
shell: bash
run: make ci-test-docker TOOLS_DIR="/home/runner/bin"
45 changes: 45 additions & 0 deletions .github/workflows/ghcr-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: ghcr

on:
push:
tags:
- "v*"

permissions:
packages: write
contents: read

env:
IMAGE_NAME: template-typescript-react

jobs:
ghcr:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v6
id: meta
with:
images: ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
- uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
build-args: |
GIT_REVISION=${{ github.sha }}
GIT_TAG=${{ github.ref_name }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
32 changes: 32 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,35 @@ preview: ## preview production build
.PHONY: update
update: ## update dependencies
pnpm update --latest

# ---
# Docker
# ---
DOCKER_REPO_NAME ?= ks6088ts
DOCKER_IMAGE_NAME ?= template-typescript-react
DOCKER_FILE ?= docker/Dockerfile
TRIVY_VERSION ?= 0.69.3

.PHONY: docker-build
docker-build: ## build Docker image
docker build -f $(DOCKER_FILE) \
-t $(DOCKER_REPO_NAME)/$(DOCKER_IMAGE_NAME):$(GIT_TAG) \
--build-arg GIT_REVISION=$(GIT_REVISION) \
--build-arg GIT_TAG=$(GIT_TAG) \
.

.PHONY: docker-run
docker-run: ## run Docker container (serves on http://localhost:8080)
docker run --rm -p 8080:80 $(DOCKER_REPO_NAME)/$(DOCKER_IMAGE_NAME):$(GIT_TAG)

.PHONY: docker-lint
docker-lint: ## lint Dockerfile
docker run --rm -i hadolint/hadolint < $(DOCKER_FILE)

.PHONY: docker-scan
docker-scan: ## scan Docker image
@which trivy || curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b $(TOOLS_DIR) v$(TRIVY_VERSION)
trivy image $(DOCKER_REPO_NAME)/$(DOCKER_IMAGE_NAME):$(GIT_TAG)

.PHONY: ci-test-docker
ci-test-docker: docker-lint docker-build docker-scan ## run CI test for Docker (build only, no run)
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
[![test](https://github.com/ks6088ts/template-typescript-react/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/ks6088ts/template-typescript-react/actions/workflows/test.yaml?query=branch%3Amain)
[![e2e-test](https://github.com/ks6088ts/template-typescript-react/actions/workflows/e2e-test.yaml/badge.svg?branch=main)](https://github.com/ks6088ts/template-typescript-react/actions/workflows/e2e-test.yaml?query=branch%3Amain)
[![docker](https://github.com/ks6088ts/template-typescript-react/actions/workflows/docker.yaml/badge.svg?branch=main)](https://github.com/ks6088ts/template-typescript-react/actions/workflows/docker.yaml?query=branch%3Amain)
[![ghcr](https://github.com/ks6088ts/template-typescript-react/actions/workflows/ghcr-release.yaml/badge.svg)](https://github.com/ks6088ts/template-typescript-react/actions/workflows/ghcr-release.yaml)
[![docker-release](https://github.com/ks6088ts/template-typescript-react/actions/workflows/docker-release.yaml/badge.svg)](https://github.com/ks6088ts/template-typescript-react/actions/workflows/docker-release.yaml)
[![github-pages](https://github.com/ks6088ts/template-typescript-react/actions/workflows/github-pages.yaml/badge.svg?branch=main)](https://github.com/ks6088ts/template-typescript-react/actions/workflows/github-pages.yaml?query=branch%3Amain)
[![release](https://github.com/ks6088ts/template-typescript-react/actions/workflows/release.yaml/badge.svg)](https://github.com/ks6088ts/template-typescript-react/actions/workflows/release.yaml)
[![GitHub release](https://img.shields.io/github/v/release/ks6088ts/template-typescript-react?logo=github&label=release)](https://github.com/ks6088ts/template-typescript-react/releases/latest)
[![Docker Hub](https://img.shields.io/docker/v/ks6088ts/template-typescript-react?logo=docker&label=Docker%20Hub&sort=semver)](https://hub.docker.com/r/ks6088ts/template-typescript-react)

![React](https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=white)
![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)
Expand Down Expand Up @@ -72,6 +76,49 @@ pnpm format

A `Makefile` wraps common workflows — run `make help` to list targets (e.g. `make ci-test`, `make e2e`, `make ci-test-e2e`).

## Docker

Build and run the production nginx image locally:

```bash
# Build the production image
make docker-build

# Run the container (http://localhost:8080)
make docker-run
```

You can also start the same image with Docker Compose:

```bash
docker compose -f docker/compose.yaml up --build web
```

For Docker-focused CI checks, run:

```bash
make ci-test-docker
```

### Published images

Pushing a `v*` tag keeps the existing release asset flow and also publishes multi-arch (`linux/amd64`, `linux/arm64`) images to both registries:

| Registry | Image | Workflow |
| --- | --- | --- |
| GitHub Container Registry | `ghcr.io/ks6088ts/template-typescript-react` | [ghcr-release.yaml](.github/workflows/ghcr-release.yaml) |
| Docker Hub | `ks6088ts/template-typescript-react` | [docker-release.yaml](.github/workflows/docker-release.yaml) |

```bash
# Pull and run from GitHub Container Registry
docker run --rm -p 8080:80 ghcr.io/ks6088ts/template-typescript-react:latest

# Pull and run from Docker Hub
docker run --rm -p 8080:80 ks6088ts/template-typescript-react:latest
```

The Docker Hub workflow requires two repository secrets: `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` (a Docker Hub access token).

## Testing

Two E2E suites run headless by default — Vitest browser mode and Playwright:
Expand Down Expand Up @@ -102,6 +149,7 @@ src/ # React app source
__tests__/e2e/ # Vitest browser E2E tests
playwright/ # Playwright smoke tests
docker/ # OTel Collector + Grafana LGTM stack
Dockerfile # Production nginx image for built SPA assets
docs/ # Detailed documentation
.github/workflows/ # CI/CD pipelines
```
21 changes: 21 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# syntax=docker/dockerfile:1

# ---- build stage ----
FROM node:24.10.0-alpine AS build
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

# ---- runtime stage ----
FROM nginx:alpine AS runtime
ARG GIT_REVISION="0000000"
ARG GIT_TAG="x.x.x"
LABEL org.opencontainers.image.revision="${GIT_REVISION}" \
org.opencontainers.image.version="${GIT_TAG}" \
org.opencontainers.image.source="https://github.com/ks6088ts/template-typescript-react"
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
11 changes: 3 additions & 8 deletions docker/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
services:
web:
# Serve the pre-built assets in ../dist with nginx.
# Build them first: `pnpm build` (or `make build`), then `docker compose up web`.
image: nginx:alpine
build:
context: ..
dockerfile: docker/Dockerfile
ports:
- "8080:80"
volumes:
# Production build output from Vite (read-only).
- ../dist:/usr/share/nginx/html:ro
# SPA-friendly nginx config (falls back to index.html).
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro

otel-collector:
image: otel/opentelemetry-collector-contrib:latest
Expand Down