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