Add full-stack Hello World app with CI/CD
- Go backend with PostgreSQL connection - Next.js frontend with TypeScript - Docker Compose for local development - Woodpecker CI pipeline for build and push - Kubernetes manifests with Kustomize - ArgoCD application for GitOps deployment Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
12
.env.example
Normal file
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# PostgreSQL
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_NAME=hellodb
|
||||
|
||||
# Backend
|
||||
PORT=8080
|
||||
|
||||
# Frontend
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080
|
||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build
|
||||
.next/
|
||||
out/
|
||||
dist/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Go
|
||||
backend/main
|
||||
*.exe
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
78
.woodpecker.yaml
Normal file
78
.woodpecker.yaml
Normal file
@@ -0,0 +1,78 @@
|
||||
# Woodpecker CI Pipeline
|
||||
# Go Backend + Next.js Frontend + PostgreSQL
|
||||
|
||||
when:
|
||||
- event: [push, pull_request]
|
||||
branch: main
|
||||
|
||||
steps:
|
||||
# 1. Backend Docker image build
|
||||
build-backend:
|
||||
image: docker:24-dind
|
||||
privileged: true
|
||||
commands:
|
||||
- docker build -t hell-world-backend:${CI_COMMIT_SHA:0:8} ./backend
|
||||
- docker tag hell-world-backend:${CI_COMMIT_SHA:0:8} registry.gecore.mn/library/hell-world-backend:${CI_COMMIT_SHA:0:8}
|
||||
- docker tag hell-world-backend:${CI_COMMIT_SHA:0:8} registry.gecore.mn/library/hell-world-backend:latest
|
||||
|
||||
# 2. Frontend Docker image build
|
||||
build-frontend:
|
||||
image: docker:24-dind
|
||||
privileged: true
|
||||
commands:
|
||||
- docker build -t hell-world-frontend:${CI_COMMIT_SHA:0:8} ./frontend
|
||||
- docker tag hell-world-frontend:${CI_COMMIT_SHA:0:8} registry.gecore.mn/library/hell-world-frontend:${CI_COMMIT_SHA:0:8}
|
||||
- docker tag hell-world-frontend:${CI_COMMIT_SHA:0:8} registry.gecore.mn/library/hell-world-frontend:latest
|
||||
|
||||
# 3. Push backend to Harbor registry
|
||||
push-backend:
|
||||
image: docker:24-dind
|
||||
privileged: true
|
||||
commands:
|
||||
- echo "$HARBOR_PASSWORD" | docker login registry.gecore.mn -u "$HARBOR_USER" --password-stdin
|
||||
- docker push registry.gecore.mn/library/hell-world-backend:${CI_COMMIT_SHA:0:8}
|
||||
- docker push registry.gecore.mn/library/hell-world-backend:latest
|
||||
secrets: [harbor_user, harbor_password]
|
||||
when:
|
||||
event: push
|
||||
branch: main
|
||||
|
||||
# 4. Push frontend to Harbor registry
|
||||
push-frontend:
|
||||
image: docker:24-dind
|
||||
privileged: true
|
||||
commands:
|
||||
- echo "$HARBOR_PASSWORD" | docker login registry.gecore.mn -u "$HARBOR_USER" --password-stdin
|
||||
- docker push registry.gecore.mn/library/hell-world-frontend:${CI_COMMIT_SHA:0:8}
|
||||
- docker push registry.gecore.mn/library/hell-world-frontend:latest
|
||||
secrets: [harbor_user, harbor_password]
|
||||
when:
|
||||
event: push
|
||||
branch: main
|
||||
|
||||
# 5. Update Kubernetes manifests with new image tags
|
||||
update-manifests:
|
||||
image: alpine:latest
|
||||
commands:
|
||||
- apk add --no-cache sed git
|
||||
- sed -i "s|image:.*hell-world-backend.*|image: registry.gecore.mn/library/hell-world-backend:${CI_COMMIT_SHA:0:8}|g" manifests/backend-deployment.yaml
|
||||
- sed -i "s|image:.*hell-world-frontend.*|image: registry.gecore.mn/library/hell-world-frontend:${CI_COMMIT_SHA:0:8}|g" manifests/frontend-deployment.yaml
|
||||
- git config user.email "ci@gecore.mn"
|
||||
- git config user.name "Woodpecker CI"
|
||||
- git add manifests/
|
||||
- git commit -m "ci: update images to ${CI_COMMIT_SHA:0:8}" || true
|
||||
- git push origin main || true
|
||||
when:
|
||||
event: push
|
||||
branch: main
|
||||
|
||||
# 6. Deploy notification
|
||||
notify:
|
||||
image: alpine:latest
|
||||
commands:
|
||||
- echo "✅ Build completed for commit ${CI_COMMIT_SHA:0:8}"
|
||||
- echo "📦 Backend: registry.gecore.mn/library/hell-world-backend:${CI_COMMIT_SHA:0:8}"
|
||||
- echo "📦 Frontend: registry.gecore.mn/library/hell-world-frontend:${CI_COMMIT_SHA:0:8}"
|
||||
- echo "🔗 ArgoCD will sync automatically"
|
||||
when:
|
||||
status: [success, failure]
|
||||
126
README.md
126
README.md
@@ -1,2 +1,126 @@
|
||||
# hell-world
|
||||
# Hello World - Go + PostgreSQL + Next.js
|
||||
|
||||
Full-stack Hello World application using Go backend, PostgreSQL database, and Next.js frontend.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── backend/ # Go API server
|
||||
│ ├── main.go
|
||||
│ ├── go.mod
|
||||
│ └── Dockerfile
|
||||
├── frontend/ # Next.js application
|
||||
│ ├── app/
|
||||
│ ├── package.json
|
||||
│ └── Dockerfile
|
||||
├── docker-compose.yml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Quick Start with Docker
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
Services:
|
||||
- Frontend: http://localhost:3000
|
||||
- Backend API: http://localhost:8080
|
||||
- PostgreSQL: localhost:5432
|
||||
|
||||
## Manual Setup
|
||||
|
||||
### Backend (Go)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go mod tidy
|
||||
DB_HOST=localhost DB_PORT=5432 DB_USER=postgres DB_PASSWORD=postgres DB_NAME=hellodb go run main.go
|
||||
```
|
||||
|
||||
### Frontend (Next.js)
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080 npm run dev
|
||||
```
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name hello-postgres \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_DB=hellodb \
|
||||
-p 5432:5432 \
|
||||
postgres:16-alpine
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/hello` | GET | Returns hello message and DB status |
|
||||
| `/api/health` | GET | Health check |
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend**: Go 1.21
|
||||
- **Database**: PostgreSQL 16
|
||||
- **Frontend**: Next.js 14, React 18, TypeScript
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
### Woodpecker CI
|
||||
|
||||
`.woodpecker.yaml` файл нь дараах алхмуудыг гүйцэтгэнэ:
|
||||
|
||||
1. **build-backend** - Go backend Docker image build
|
||||
2. **build-frontend** - Next.js frontend Docker image build
|
||||
3. **push-backend** - Harbor registry рүү backend push
|
||||
4. **push-frontend** - Harbor registry рүү frontend push
|
||||
5. **update-manifests** - Kubernetes manifest дахь image tag шинэчлэх
|
||||
6. **notify** - Deployment notification
|
||||
|
||||
### ArgoCD (GitOps)
|
||||
|
||||
ArgoCD application тохируулах:
|
||||
|
||||
```bash
|
||||
kubectl apply -f argocd-application.yaml -n argocd
|
||||
```
|
||||
|
||||
ArgoCD нь `manifests/` directory-г автоматаар sync хийнэ.
|
||||
|
||||
## Kubernetes Deployment
|
||||
|
||||
### Manifests
|
||||
|
||||
```
|
||||
manifests/
|
||||
├── namespace.yaml # hell-world namespace
|
||||
├── postgres-secret.yaml # Database credentials
|
||||
├── postgres-pvc.yaml # PostgreSQL storage
|
||||
├── postgres-deployment.yaml # PostgreSQL deployment
|
||||
├── postgres-service.yaml # PostgreSQL service
|
||||
├── backend-deployment.yaml # Go backend deployment
|
||||
├── backend-service.yaml # Backend service
|
||||
├── frontend-deployment.yaml # Next.js frontend deployment
|
||||
├── frontend-service.yaml # Frontend service
|
||||
├── ingress.yaml # Ingress with TLS
|
||||
└── kustomization.yaml # Kustomize config
|
||||
```
|
||||
|
||||
### Manual Deploy
|
||||
|
||||
```bash
|
||||
kubectl apply -k manifests/
|
||||
```
|
||||
|
||||
### URLs (Production)
|
||||
|
||||
- Frontend: https://hell-world.gecore.mn
|
||||
- Backend API: https://hell-world-api.gecore.mn
|
||||
|
||||
35
argocd-application.yaml
Normal file
35
argocd-application.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
# ArgoCD Application - GitOps deployment
|
||||
# kubectl apply -f argocd-application.yaml -n argocd
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: hell-world
|
||||
namespace: argocd
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
spec:
|
||||
project: default
|
||||
|
||||
source:
|
||||
# Gitea repository URL
|
||||
repoURL: https://git.gecore.mn/admin/hell-world.git
|
||||
targetRevision: HEAD
|
||||
path: manifests
|
||||
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: hell-world
|
||||
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true # Устгагдсан resource-уудыг автоматаар устгана
|
||||
selfHeal: true # Гараар өөрчилсөн зүйлсийг автоматаар сэргээнэ
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- PruneLast=true
|
||||
retry:
|
||||
limit: 5
|
||||
backoff:
|
||||
duration: 5s
|
||||
factor: 2
|
||||
maxDuration: 3m
|
||||
19
backend/Dockerfile
Normal file
19
backend/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o main .
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/main .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["./main"]
|
||||
5
backend/go.mod
Normal file
5
backend/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module hello-world-backend
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/lib/pq v1.10.9
|
||||
93
backend/main.go
Normal file
93
backend/main.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
DBStatus string `json:"db_status"`
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
func main() {
|
||||
// PostgreSQL connection
|
||||
dbHost := getEnv("DB_HOST", "localhost")
|
||||
dbPort := getEnv("DB_PORT", "5432")
|
||||
dbUser := getEnv("DB_USER", "postgres")
|
||||
dbPassword := getEnv("DB_PASSWORD", "postgres")
|
||||
dbName := getEnv("DB_NAME", "hellodb")
|
||||
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
dbHost, dbPort, dbUser, dbPassword, dbName)
|
||||
|
||||
var err error
|
||||
db, err = sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Could not connect to database: %v", err)
|
||||
} else {
|
||||
defer db.Close()
|
||||
}
|
||||
|
||||
// HTTP handlers
|
||||
http.HandleFunc("/api/hello", corsMiddleware(helloHandler))
|
||||
http.HandleFunc("/api/health", corsMiddleware(healthHandler))
|
||||
|
||||
port := getEnv("PORT", "8080")
|
||||
log.Printf("Server starting on port %s", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||
}
|
||||
|
||||
func helloHandler(w http.ResponseWriter, r *http.Request) {
|
||||
dbStatus := "disconnected"
|
||||
|
||||
if db != nil {
|
||||
err := db.Ping()
|
||||
if err == nil {
|
||||
dbStatus = "connected"
|
||||
}
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Message: "Hello World! - Go + PostgreSQL + Next.js",
|
||||
DBStatus: dbStatus,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
46
docker-compose.yml
Normal file
46
docker-compose.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: hello-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: hellodb
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: hello-backend
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_NAME: hellodb
|
||||
PORT: 8080
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
container_name: hello-frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: http://localhost:8080
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
23
frontend/Dockerfile
Normal file
23
frontend/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
18
frontend/app/layout.tsx
Normal file
18
frontend/app/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Hello World App',
|
||||
description: 'Go + PostgreSQL + Next.js',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
118
frontend/app/page.tsx
Normal file
118
frontend/app/page.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ApiResponse {
|
||||
message: string
|
||||
db_status: string
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [data, setData] = useState<ApiResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080'
|
||||
|
||||
fetch(`${apiUrl}/api/hello`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setData(data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err.message)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<main style={styles.main}>
|
||||
<div style={styles.container}>
|
||||
<h1 style={styles.title}>Hello World!</h1>
|
||||
<p style={styles.subtitle}>Go + PostgreSQL + Next.js</p>
|
||||
|
||||
<div style={styles.card}>
|
||||
{loading && <p>Loading...</p>}
|
||||
{error && <p style={styles.error}>Error: {error}</p>}
|
||||
{data && (
|
||||
<>
|
||||
<p style={styles.message}>{data.message}</p>
|
||||
<p style={styles.status}>
|
||||
Database:
|
||||
<span style={{
|
||||
color: data.db_status === 'connected' ? '#22c55e' : '#ef4444',
|
||||
marginLeft: '8px'
|
||||
}}>
|
||||
{data.db_status}
|
||||
</span>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={styles.techStack}>
|
||||
<span style={styles.badge}>Go</span>
|
||||
<span style={styles.badge}>PostgreSQL</span>
|
||||
<span style={styles.badge}>Next.js</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
const styles: { [key: string]: React.CSSProperties } = {
|
||||
main: {
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
},
|
||||
container: {
|
||||
textAlign: 'center',
|
||||
color: 'white',
|
||||
},
|
||||
title: {
|
||||
fontSize: '4rem',
|
||||
marginBottom: '0.5rem',
|
||||
textShadow: '2px 2px 4px rgba(0,0,0,0.2)',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: '1.25rem',
|
||||
opacity: 0.9,
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
card: {
|
||||
background: 'rgba(255,255,255,0.95)',
|
||||
borderRadius: '12px',
|
||||
padding: '2rem',
|
||||
color: '#333',
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.2)',
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
message: {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 500,
|
||||
},
|
||||
status: {
|
||||
marginTop: '1rem',
|
||||
fontSize: '1rem',
|
||||
},
|
||||
error: {
|
||||
color: '#ef4444',
|
||||
},
|
||||
techStack: {
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
badge: {
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
padding: '0.5rem 1rem',
|
||||
borderRadius: '20px',
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
}
|
||||
6
frontend/next.config.js
Normal file
6
frontend/next.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "hello-world-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
26
frontend/tsconfig.json
Normal file
26
frontend/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
64
manifests/backend-deployment.yaml
Normal file
64
manifests/backend-deployment.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: backend
|
||||
namespace: hell-world
|
||||
labels:
|
||||
app: backend
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: backend
|
||||
spec:
|
||||
containers:
|
||||
- name: backend
|
||||
image: registry.gecore.mn/library/hell-world-backend:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
env:
|
||||
- name: DB_HOST
|
||||
value: postgres
|
||||
- name: DB_PORT
|
||||
value: "5432"
|
||||
- name: DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: POSTGRES_USER
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: POSTGRES_PASSWORD
|
||||
- name: DB_NAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: POSTGRES_DB
|
||||
- name: PORT
|
||||
value: "8080"
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
16
manifests/backend-service.yaml
Normal file
16
manifests/backend-service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: backend
|
||||
namespace: hell-world
|
||||
labels:
|
||||
app: backend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: backend
|
||||
45
manifests/frontend-deployment.yaml
Normal file
45
manifests/frontend-deployment.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: frontend
|
||||
namespace: hell-world
|
||||
labels:
|
||||
app: frontend
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
image: registry.gecore.mn/library/hell-world-frontend:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: http
|
||||
env:
|
||||
- name: NEXT_PUBLIC_API_URL
|
||||
value: "https://hell-world-api.gecore.mn"
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "200m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
16
manifests/frontend-service.yaml
Normal file
16
manifests/frontend-service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontend
|
||||
namespace: hell-world
|
||||
labels:
|
||||
app: frontend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: frontend
|
||||
38
manifests/ingress.yaml
Normal file
38
manifests/ingress.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: hell-world
|
||||
namespace: hell-world
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- hell-world.gecore.mn
|
||||
- hell-world-api.gecore.mn
|
||||
secretName: hell-world-tls
|
||||
rules:
|
||||
# Frontend
|
||||
- host: hell-world.gecore.mn
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: frontend
|
||||
port:
|
||||
number: 3000
|
||||
# Backend API
|
||||
- host: hell-world-api.gecore.mn
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: backend
|
||||
port:
|
||||
number: 8080
|
||||
20
manifests/kustomization.yaml
Normal file
20
manifests/kustomization.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: hell-world
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- postgres-secret.yaml
|
||||
- postgres-pvc.yaml
|
||||
- postgres-deployment.yaml
|
||||
- postgres-service.yaml
|
||||
- backend-deployment.yaml
|
||||
- backend-service.yaml
|
||||
- frontend-deployment.yaml
|
||||
- frontend-service.yaml
|
||||
- ingress.yaml
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/name: hell-world
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
6
manifests/namespace.yaml
Normal file
6
manifests/namespace.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: hell-world
|
||||
labels:
|
||||
app: hell-world
|
||||
56
manifests/postgres-deployment.yaml
Normal file
56
manifests/postgres-deployment.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: hell-world
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: postgres
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: postgres-secret
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "200m"
|
||||
volumeMounts:
|
||||
- name: postgres-data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- postgres
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- postgres
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: postgres-data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-pvc
|
||||
12
manifests/postgres-pvc.yaml
Normal file
12
manifests/postgres-pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-pvc
|
||||
namespace: hell-world
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
storageClassName: local-path
|
||||
10
manifests/postgres-secret.yaml
Normal file
10
manifests/postgres-secret.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-secret
|
||||
namespace: hell-world
|
||||
type: Opaque
|
||||
stringData:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: hellodb
|
||||
16
manifests/postgres-service.yaml
Normal file
16
manifests/postgres-service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: hell-world
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
protocol: TCP
|
||||
name: postgres
|
||||
selector:
|
||||
app: postgres
|
||||
Reference in New Issue
Block a user