Merge pull request 'fix/docker-config' (#1) from fix/docker-config into main
Some checks failed
Docker Build and Deploy / Build and Push Docker Image (push) Failing after 16s
Docker Build and Deploy / Deploy to VPS (push) Has been skipped

Reviewed-on: #1
This commit is contained in:
ulflow_phattt2901 2025-06-06 14:33:22 +00:00
commit a91e9a74b3
12 changed files with 221 additions and 235 deletions

View File

@ -3,7 +3,10 @@
# This hook runs linters and checks before allowing a commit # This hook runs linters and checks before allowing a commit
echo "Running pre-commit checks..." echo "Running pre-commit checks..."
echo "Current working directory: $(pwd)"
echo "Which golangci-lint: $(which golangci-lint)"
echo "golangci-lint version: $(golangci-lint version)"
cd "$PROJECT_ROOT" || exit 1 # Exit if cd fails
# Check for staged Go files # Check for staged Go files
STAGED_GO_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep "\.go$") STAGED_GO_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep "\.go$")

View File

@ -18,12 +18,18 @@ jobs:
with: with:
go-version: '1.24.3' go-version: '1.24.3'
cache-dependency-path: go.sum cache-dependency-path: go.sum
- name: Cache golangci-lint
uses: actions/cache@v4
with:
path: ~/.cache/golangci-lint
key: ${{ runner.os }}-golangci-lint-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-golangci-lint-
- name: Lint - name: Lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v6
with: with:
version: latest version: latest
args: --timeout=5m args: --timeout=15m
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()

View File

@ -1,6 +1,4 @@
# syntax=docker/dockerfile:1.4 # Dockerfile
# Production-ready multi-stage build for server deployment
# Build arguments # Build arguments
ARG GO_VERSION=1.24.3 ARG GO_VERSION=1.24.3
ARG ALPINE_VERSION=3.19 ARG ALPINE_VERSION=3.19
@ -16,7 +14,7 @@ RUN apk add --no-cache git make gcc libc-dev
# Set working directory # Set working directory
WORKDIR /build WORKDIR /build
# Enable Go modules # Enable Go modules and set build environment variables
ENV GO111MODULE=on \ ENV GO111MODULE=on \
CGO_ENABLED=0 \ CGO_ENABLED=0 \
GOOS=linux \ GOOS=linux \
@ -25,37 +23,35 @@ ENV GO111MODULE=on \
# Copy dependency files first for better layer caching # Copy dependency files first for better layer caching
COPY go.mod go.sum ./ COPY go.mod go.sum ./
# Download dependencies using cache mount for efficiency
# Download dependencies
RUN --mount=type=cache,target=/go/pkg/mod \ RUN --mount=type=cache,target=/go/pkg/mod \
go mod download go mod download
# Copy source code # Copy source code
COPY . . COPY . .
# Create the configs directory in the build context (if it exists)
RUN if test -d "configs"; then mkdir -p /build/configs && cp -r configs/* /build/configs/; fi
# Create the configs directory in the build context # Build the application with optimizations and version info
RUN mkdir -p /build/configs && \
cp -r configs/* /build/configs/ 2>/dev/null || :
# Build the application with optimizations
RUN --mount=type=cache,target=/root/.cache/go-build \ RUN --mount=type=cache,target=/root/.cache/go-build \
go build -ldflags="-w -s -X 'main.Version=$(git describe --tags --always 2>/dev/null || echo 'dev')'" \ go build -ldflags="-w -s -X 'main.Version=$(git describe --tags --always 2>/dev/null || echo 'dev')'" \
-o /build/bin/app ./cmd/app -o /build/bin/app ./src/cmd/api # Update path to your main package
# --- Final Stage --- # --- Final Stage ---
FROM alpine:${ALPINE_VERSION} FROM alpine:${ALPINE_VERSION}
# Re-declare ARG to use in this stage # Re-declare ARG to use in this stage
ARG APP_USER=appuser ARG APP_USER
ARG APP_GROUP=appgroup ARG APP_GROUP
ARG APP_HOME=/app ARG APP_HOME=/app
# Set environment variables # Set runtime environment variables
ENV TZ=Asia/Ho_Chi_Minh \ ENV TZ=Asia/Ho_Chi_Minh \
APP_USER=${APP_USER} \ APP_USER=${APP_USER} \
APP_GROUP=${APP_GROUP} \ APP_GROUP=${APP_GROUP} \
APP_HOME=${APP_HOME} APP_HOME=${APP_HOME} \
APP_ENV=production
# Install runtime dependencies # Install runtime dependencies
RUN apk add --no-cache \ RUN apk add --no-cache \
@ -68,7 +64,7 @@ RUN apk add --no-cache \
RUN addgroup -S ${APP_GROUP} && \ RUN addgroup -S ${APP_GROUP} && \
adduser -S -G ${APP_GROUP} -h ${APP_HOME} -D ${APP_USER} adduser -S -G ${APP_GROUP} -h ${APP_HOME} -D ${APP_USER}
# Create necessary directories # Create necessary application directories
RUN mkdir -p ${APP_HOME}/configs ${APP_HOME}/logs ${APP_HOME}/storage RUN mkdir -p ${APP_HOME}/configs ${APP_HOME}/logs ${APP_HOME}/storage
# Switch to app directory # Switch to app directory
@ -99,4 +95,4 @@ EXPOSE 3000
ENV APP_ENV=production ENV APP_ENV=production
# Command to run the application # Command to run the application
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["/sbin/tini", "--"]

View File

@ -86,7 +86,10 @@ clean:
docker-build: docker-build:
@echo "Building Docker image..." @echo "Building Docker image..."
docker build -t ulflow-zee:latest . docker build -t ulflow-zee:latest .
# Build Docker image for development
docker-build-dev:
@echo "Building Docker image for development..."
docker-compose -f docker-compose.dev.yml up --build
# Run application in Docker container # Run application in Docker container
docker-run: docker-run:
@echo "Running application in Docker container..." @echo "Running application in Docker container..."
@ -97,6 +100,13 @@ docker-run:
docker run -p 3000:3000 --env-file .env ulflow-zee:latest docker run -p 3000:3000 --env-file .env ulflow-zee:latest
) )
docker-up-dev:
@echo "Starting all services with Docker Compose for local development..."
docker-compose -f docker-compose.dev.yml up
docker-down-dev:
@echo "Stopping all development services..."
docker-compose -f docker-compose.dev.yml down -v
# Run Docker Compose for local development # Run Docker Compose for local development
docker-compose-up: docker-compose-up:
@echo "Starting all services with Docker Compose for local development..." @echo "Starting all services with Docker Compose for local development..."
@ -106,7 +116,7 @@ docker-compose-up:
# Stop Docker Compose services for local development # Stop Docker Compose services for local development
docker-compose-down: docker-compose-down:
@echo "Stopping all development services..." @echo "Stopping all development services..."
docker-compose down docker-compose down -v
# Run Docker Compose for production # Run Docker Compose for production
docker-compose-prod-up: docker-compose-prod-up:
@ -133,9 +143,7 @@ setup-git-hooks:
@echo Copying prepare-commit-msg hook... @echo Copying prepare-commit-msg hook...
@copy /Y .gitea\hooks\prepare-commit-msg .git\hooks\prepare-commit-msg >nul || (echo Warning: Could not copy prepare-commit-msg hook & exit /b 1) @copy /Y .gitea\hooks\prepare-commit-msg .git\hooks\prepare-commit-msg >nul || (echo Warning: Could not copy prepare-commit-msg hook & exit /b 1)
git config --local core.hooksPath .git/hooks
@echo "Git setup complete!" @echo "Git setup complete!"
# Create git message template # Create git message template
setup-git-message: setup-git-message:
@echo "Creating Git commit message template..." @echo "Creating Git commit message template..."

View File

@ -24,10 +24,14 @@ type HTTPService struct {
} }
func NewHTTPService(cfg *config.Config, db *database.Database) *HTTPService { func NewHTTPService(cfg *config.Config, db *database.Database) *HTTPService {
var gormDB *gorm.DB
if db != nil {
gormDB = db.DB
}
return &HTTPService{ return &HTTPService{
server: http.NewServer(cfg, db.DB), server: http.NewServer(cfg, gormDB), // gormDB can be nil here
cfg: cfg, cfg: cfg,
db: db, db: db, // db itself (the *database.Database wrapper) can also be nil
} }
} }
@ -66,6 +70,7 @@ func (s *HTTPService) Shutdown(ctx context.Context) error {
} }
func main() { func main() {
fmt.Println("Starting application 222...")
// Initialize config loader // Initialize config loader
configLoader := config.NewConfigLoader() configLoader := config.NewConfigLoader()
@ -108,26 +113,44 @@ func main() {
} }
lifecycleMgr := lifecycle.New(shutdownTimeout) lifecycleMgr := lifecycle.New(shutdownTimeout)
// Initialize database connection var gormDB *gorm.DB // Declare gormDB for database connection
db, err := database.NewConnection(&cfg.Database) var dbInstance *database.Database // Declare dbInstance for HTTPService and other potential users
if err != nil {
logger.WithError(err).Fatal("Failed to connect to database") if feature.IsEnabled(feature.EnableDatabase) {
logger.Info("Feature flag 'enable_database' is true. Initializing database connection...")
// Initialize database connection
var dbErr error
gormDB, dbErr = database.NewConnection(&cfg.Database)
if dbErr != nil {
logger.WithError(dbErr).Fatal("Failed to connect to database")
}
// Run database migrations
if err := database.Migrate(cfg.Database); err != nil { // Migrate still needs cfg.Database for path etc.
logger.WithError(err).Fatal("Failed to migrate database")
}
// Register database cleanup on shutdown
lifecycleMgr.Register(&databaseService{db: gormDB})
// Prepare dbInstance for HTTPService
dbInstance = &database.Database{DB: gormDB, Config: &cfg.Database}
} else {
logger.Info("Feature flag 'enable_database' is false. Skipping database initialization.")
// gormDB remains nil
// dbInstance remains nil
// No databaseService is registered with lifecycleMgr if DB is disabled.
} }
// Run database migrations // Initialize HTTP service
if err := database.Migrate(cfg.Database); err != nil { // NewHTTPService and its chain (http.NewServer, http.SetupRouter)
logger.WithError(err).Fatal("Failed to migrate database") // must be able to handle a nil dbInstance or a dbInstance with a nil DB.
} // The NewHTTPService function already checks if its db argument (or db.DB) is nil.
httpService := NewHTTPService(cfg, dbInstance) // dbInstance can be nil here
// Register database cleanup on shutdown
lifecycleMgr.Register(&databaseService{db: db})
// Initialize HTTP service with database
httpService := NewHTTPService(cfg, &database.Database{DB: db})
if httpService == nil { if httpService == nil {
logger.Fatal("Failed to create HTTP service") logger.Fatal("Failed to create HTTP service")
} }
lifecycleMgr.Register(httpService) lifecycleMgr.Register(httpService) // HTTP service is always registered
// Start all services // Start all services
logger.Info("Đang khởi động các dịch vụ...") logger.Info("Đang khởi động các dịch vụ...")

View File

@ -19,11 +19,11 @@ server:
database: database:
driver: "postgres" driver: "postgres"
host: "localhost" host: "postgres"
port: 5432 port: 5432
username: "postgres" username: "postgres"
password: "postgres" password: "postgres"
database: "ulflow" database: "ulflow_zee"
ssl_mode: "disable" ssl_mode: "disable"
max_open_conns: 25 max_open_conns: 25
max_idle_conns: 5 max_idle_conns: 5

59
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,59 @@
# docker-compose.dev.yml (Cho Local Development)
version: '3.8'
services:
api:
build:
context: .
dockerfile: Dockerfile.dev # Sử dụng Dockerfile.dev
container_name: ulflow-api-dev
ports:
- "3000:3000"
volumes:
- .:/app # Bind mount code cho hot reload
- go-modules:/go/pkg/mod
command: air # Chạy công cụ hot reload
env_file:
- ./.env # Load biến môi trường từ file .env
depends_on:
- postgres
networks:
- ulflow-network
restart: "no" # Không tự động restart trong dev
postgres:
image: postgres:15-alpine
container_name: ulflow-postgres-dev
restart: unless-stopped
environment:
# Lấy biến từ file .env
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
ports:
- "5432:5432" # Để truy cập từ host nếu muốn
volumes:
- postgres-dev-data:/var/lib/postgresql/data # Volume riêng cho dev DB
networks:
- ulflow-network
adminer: # Adminer cũng nên có trong dev để quản lý DB dễ dàng
image: adminer:latest
container_name: ulflow-adminer-dev
restart: unless-stopped
ports:
- "8080:8080"
environment:
ADMINER_DEFAULT_SERVER: postgres # Tên service của postgres trong compose này
depends_on:
- postgres
networks:
- ulflow-network
volumes:
postgres-dev-data: # Volume riêng cho dev DB
go-modules: # Volume để cache Go modules cho dev
networks:
ulflow-network:
driver: bridge

View File

@ -1,83 +0,0 @@
version: '3.8'
services:
# API Service - Production Configuration
api:
build:
context: .
dockerfile: Dockerfile
container_name: ulflow-api
restart: always
ports:
- "3000:3000"
env_file:
- .env
environment:
- APP_ENV=production
depends_on:
- postgres
networks:
- ulflow-network
deploy:
resources:
limits:
cpus: '1'
memory: 1G
restart_policy:
condition: on-failure
max_attempts: 3
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# PostgreSQL Database - Production Configuration
postgres:
image: postgres:15-alpine
container_name: ulflow-postgres
restart: always
environment:
POSTGRES_USER: ${DB_USER:-user}
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
POSTGRES_DB: ${DB_NAME:-ulflow_db}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- ulflow-network
deploy:
resources:
limits:
cpus: '1'
memory: 1G
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-user} -d ${DB_NAME:-ulflow_db}"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
container_name: ulflow-nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/ssl:/etc/nginx/ssl
- ./nginx/logs:/var/log/nginx
depends_on:
- api
networks:
- ulflow-network
volumes:
postgres-data:
networks:
ulflow-network:
driver: bridge

View File

@ -1,85 +1,45 @@
# docker-compose.prod.yml (Cho Production / CI/CD)
version: '3.8' version: '3.8'
services: services:
# API Service
api: api:
build: build:
context: . context: .
dockerfile: Dockerfile.local dockerfile: Dockerfile # Sử dụng Dockerfile production
container_name: ulflow-api container_name: ulflow-api-prod
restart: unless-stopped
ports: ports:
- "3000:3000" - "3000:3000"
volumes: # KHÔNG CÓ VOLUMES: - .:/app ở đây!
- .:/app command: /app/app # Chạy binary đã build
- go-modules:/go/pkg/mod # KHÔNG CÓ ENV_FILE Ở ĐÂY, BIẾN MÔI TRƯỜNG SẼ ĐƯỢC CUNG CẤP TỪ BÊN NGOÀI
env_file: restart: always
- .env deploy:
resources:
limits:
cpus: '0.50'
memory: 256M
depends_on: depends_on:
- postgres - postgres
networks: networks:
- ulflow-network - ulflow-network
# PostgreSQL Database
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
container_name: ulflow-postgres container_name: ulflow-postgres-prod
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_USER: ${DB_USER:-user} POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD:-password} POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DB_NAME:-ulflow_db} POSTGRES_DB: ${DATABASE_NAME}
ports: ports:
- "5432:5432" - "5433:5432"
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - postgres-prod-data:/var/lib/postgresql/data # Volume riêng cho prod DB
networks: networks:
- ulflow-network - ulflow-network
# Gitea (Git Server, CI/CD, Registry)
gitea:
image: gitea/gitea:1.21
container_name: ulflow-gitea
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=postgres:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=${DB_USER:-user}
- GITEA__database__PASSWD=${DB_PASSWORD:-password}
ports:
- "3001:3000"
- "2222:22"
volumes:
- gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
- postgres
networks:
- ulflow-network
# Gitea Runner for CI/CD
gitea-runner:
image: gitea/act_runner:latest
container_name: ulflow-gitea-runner
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- gitea-runner-data:/data
depends_on:
- gitea
networks:
- ulflow-network
volumes: volumes:
postgres-data: postgres-prod-data:
gitea-data:
gitea-runner-data:
go-modules:
networks: networks:
ulflow-network: ulflow-network:
driver: bridge driver: bridge

View File

@ -107,6 +107,7 @@ func (l *ViperConfigLoader) loadEnvFile(v *viper.Viper) error {
if err := os.Setenv(key, val); err != nil { if err := os.Setenv(key, val); err != nil {
return fmt.Errorf("failed to set environment variable %s: %w", key, err) return fmt.Errorf("failed to set environment variable %s: %w", key, err)
} }
fmt.Println("Loaded environment variable:", key, "=", val)
} }
return nil return nil

View File

@ -4,6 +4,7 @@ import (
"time" "time"
"zee/internal/adapter/postgres" "zee/internal/adapter/postgres"
"zee/internal/helper/config" "zee/internal/helper/config"
"zee/internal/helper/logger" // Added logger import
"zee/internal/service" "zee/internal/service"
"zee/internal/transport/http/handler" "zee/internal/transport/http/handler"
"zee/internal/transport/http/middleware" "zee/internal/transport/http/middleware"
@ -31,60 +32,72 @@ func SetupRouter(cfg *config.Config, db *gorm.DB) *gin.Engine {
securityCfg := middleware.DefaultSecurityConfig() securityCfg := middleware.DefaultSecurityConfig()
securityCfg.Apply(router) securityCfg.Apply(router)
// Khởi tạo repositories // Khởi tạo các handlers không phụ thuộc DB
userRepo := postgres.NewUserRepository(db)
roleRepo := postgres.NewRoleRepository(db)
// Get JWT configuration from config
jwtSecret := "your-secret-key" // Default fallback
accessTokenExpire := 24 * time.Hour
// Override with config values if available
if cfg.JWT.Secret != "" {
jwtSecret = cfg.JWT.Secret
}
if cfg.JWT.AccessTokenExpire > 0 {
accessTokenExpire = time.Duration(cfg.JWT.AccessTokenExpire) * time.Minute
}
// Khởi tạo services
authSvc := service.NewAuthService(
userRepo,
roleRepo,
jwtSecret,
accessTokenExpire,
)
// Khởi tạo middleware
authMiddleware := middleware.NewAuthMiddleware(authSvc)
_ = authMiddleware // TODO: Use authMiddleware when needed
// Khởi tạo các handlers
healthHandler := handler.NewHealthHandler(cfg) healthHandler := handler.NewHealthHandler(cfg)
authHandler := handler.NewAuthHandler(authSvc)
// Đăng ký các routes // Đăng ký các routes không phụ thuộc DB
// Health check routes (public)
router.GET("/ping", healthHandler.Ping) router.GET("/ping", healthHandler.Ping)
router.GET("/health", healthHandler.HealthCheck) router.GET("/health", healthHandler.HealthCheck)
// Auth routes (public) // Các thành phần và routes phụ thuộc vào DB
authGroup := router.Group("/api/v1/auth") if db != nil {
{ logger.Info("Database connection is available. Initializing DB-dependent services and routes...")
authGroup.POST("/register", authHandler.Register) // Khởi tạo repositories
authGroup.POST("/login", authHandler.Login) userRepo := postgres.NewUserRepository(db)
authGroup.POST("/refresh", authHandler.RefreshToken) roleRepo := postgres.NewRoleRepository(db)
authGroup.POST("/logout", authMiddleware.Authenticate(), authHandler.Logout)
}
// Protected API routes // Get JWT configuration from config
api := router.Group("/api/v1") jwtSecret := "your-secret-key" // Default fallback
api.Use(authMiddleware.Authenticate()) accessTokenExpire := 24 * time.Hour
{
// Ví dụ về protected endpoints // Override with config values if available
// api.GET("/profile", userHandler.GetProfile) if cfg.JWT.Secret != "" {
// api.PUT("/profile", userHandler.UpdateProfile) jwtSecret = cfg.JWT.Secret
}
if cfg.JWT.AccessTokenExpire > 0 {
accessTokenExpire = time.Duration(cfg.JWT.AccessTokenExpire) * time.Minute
}
// Khởi tạo services
authSvc := service.NewAuthService(
userRepo,
roleRepo,
jwtSecret,
accessTokenExpire,
)
// Khởi tạo middleware
authMiddleware := middleware.NewAuthMiddleware(authSvc)
// Khởi tạo các handlers phụ thuộc DB
authHandler := handler.NewAuthHandler(authSvc)
// Auth routes (public)
authGroup := router.Group("/api/v1/auth")
{
authGroup.POST("/register", authHandler.Register)
authGroup.POST("/login", authHandler.Login)
authGroup.POST("/refresh", authHandler.RefreshToken)
authGroup.POST("/logout", authMiddleware.Authenticate(), authHandler.Logout)
}
// Protected API routes
api := router.Group("/api/v1")
api.Use(authMiddleware.Authenticate())
{
// Ví dụ về protected endpoints
// api.GET("/profile", userHandler.GetProfile)
// api.PUT("/profile", userHandler.UpdateProfile)
}
} else {
logger.Info("Database is disabled via feature flag or connection failed. DB-dependent routes (e.g., /api/v1/auth, /api/v1/*) will not be available.")
// Optionally, register placeholder routes that return 503 Service Unavailable
// router.Any("/api/v1/auth/*any", func(c *gin.Context) {
// c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Authentication service is currently disabled"})
// })
// router.Any("/api/v1/*any", func(c *gin.Context) {
// c.JSON(http.StatusServiceUnavailable, gin.H{"error": "API service is currently disabled due to database unavailability"})
// })
} }
return router return router