commit fb6af905cd317690fde0469a7e05762b8ea18fef Author: Chu Văn Hải Date: Fri Dec 5 11:18:26 2025 +0700 init diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..77c17b9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,378 @@ +# ============================================================================ +# .dockerignore - Optimize Docker Build Context +# ============================================================================ +# This file excludes unnecessary files from Docker build context to: +# - Reduce build time +# - Minimize image size +# - Improve cache efficiency +# - Enhance security (exclude sensitive files) +# ============================================================================ + +# ============================================================================ +# VERSION CONTROL +# ============================================================================ +.git +.gitignore +.gitattributes +.gitmodules +.github/ +.gitlab/ +.gitlab-ci.yml +.gitlab-ci.include.yml + +# ============================================================================ +# NODE.JS & PACKAGE MANAGERS +# ============================================================================ +# Dependencies (will be installed in container) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.npm/ +.yarn/ +.pnpm/ +.pnpm-store/ +pnpm-debug.log* + +# Package manager lock files (keep these for consistent installs) +# NOTE: Uncomment if you want to exclude lock files (not recommended) +# package-lock.json +# yarn.lock +# pnpm-lock.yaml + +# Node.js runtime files +.node_repl_history +.node_tmp/ + +# ============================================================================ +# FRAMEWORK-SPECIFIC BUILD OUTPUTS +# ============================================================================ +# Next.js +.next/ +out/ +.vercel/ + +# React.js / Vite +build/ +dist/ + +# NestJS +dist/ + +# Angular +dist/ +.angular/ + +# Nuxt.js +.nuxt/ +.output/ + +# SvelteKit +.svelte-kit/ + +# ============================================================================ +# DEVELOPMENT & TESTING +# ============================================================================ +# Test coverage +coverage/ +.coverage/ +.nyc_output/ +.jest/ + +# Testing +*.test.js.snap +__tests__/ +test/ +tests/ +spec/ +specs/ + +# Storybook +.storybook/ +storybook-static/ + +# Cypress +cypress/videos/ +cypress/screenshots/ +cypress/downloads/ + +# Playwright +playwright-report/ +test-results/ + +# ============================================================================ +# ENVIRONMENT & CONFIGURATION +# ============================================================================ +# Environment files (security risk) +.env +.env.* +!.env.example +!.env.template + +# Local environment +.local +.cache/ + +# Configuration files +.eslintcache +.stylelintcache +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# TypeScript +*.tsbuildinfo +.tscache/ + +# ============================================================================ +# CI/CD & DEPLOYMENT +# ============================================================================ +# CI/CD files +ci/ +.circleci/ +.travis.yml +.appveyor.yml +azure-pipelines.yml +buildspec.yml +cloudbuild.yaml + +# Docker +Dockerfile.* +!Dockerfile +docker-compose*.yml +.dockerignore + +# Kubernetes +k8s/ +kubernetes/ +*.yaml +*.yml +!package.json +!package-lock.json +!yarn.lock +!pnpm-lock.yaml + +# Terraform +*.tf +*.tfstate +*.tfvars +.terraform/ + +# Ansible +inventory/ +playbooks/ +roles/ +ansible.cfg +requirements.yml + +# Helm +charts/ +helm/ + +# ============================================================================ +# DEVELOPMENT TOOLS & EDITORS +# ============================================================================ +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Vim +.vim/ +.vimrc + +# Emacs +.emacs.d/ +.emacs + +# Sublime Text +*.sublime-* + +# WebStorm +.webstorm/ + +# VS Code extensions +.vscode-test/ + +# ============================================================================ +# OPERATING SYSTEM +# ============================================================================ +# macOS +.DS_Store +.AppleDouble +.LSOverride +Icon? +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk + +# Linux +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# ============================================================================ +# LOGS & TEMPORARY FILES +# ============================================================================ +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Temporary folders +tmp/ +temp/ +.tmp/ +.temp/ + +# ============================================================================ +# DOCUMENTATION & ASSETS +# ============================================================================ +# Documentation +docs/ +documentation/ +README*.md +CHANGELOG*.md +CONTRIBUTING*.md +LICENSE* +SECURITY*.md +CODE_OF_CONDUCT*.md + +# Design assets (large files) +*.psd +*.ai +*.sketch +*.fig +*.xd + +# Media files (if not needed in production) +# *.png +# *.jpg +# *.jpeg +# *.gif +# *.svg +# *.ico +# *.webp + +# ============================================================================ +# MONITORING & ANALYTICS +# ============================================================================ +# Bundle analyzers +.webpack-bundle-analyzer/ +bundle-analyzer-report.html + +# Performance monitoring +.lighthouse/ +lighthouse-report.html + +# ============================================================================ +# SECURITY & SECRETS +# ============================================================================ +# Security scan results +security-audit.json +vulnerability-report.json + +# Certificates and keys +*.pem +*.key +*.crt +*.cert +*.p12 +*.pfx + +# Secrets +secrets/ +.secrets/ + +# ============================================================================ +# BACKUP & ARCHIVE +# ============================================================================ +# Backup files +*.bak +*.backup +*.old +*.orig + +# Archive files +*.tar +*.tar.gz +*.zip +*.rar +*.7z + +# ============================================================================ +# CUSTOM PROJECT EXCLUSIONS +# ============================================================================ +# Add your project-specific exclusions here +# Examples: +# assets/raw/ +# scripts/dev/ +# mock-data/ + +# Generated files +# auto-generated/ +# generated/ + +# Large datasets +# data/ +# datasets/ + +# ============================================================================ +# KEEP THESE FILES (Explicitly include important files) +# ============================================================================ +# Ensure these files are included even if excluded above +!package.json +!yarn.lock +!package-lock.json +!pnpm-lock.yaml +!tsconfig.json +!next.config.js +!nuxt.config.js +!vite.config.js +!svelte.config.js +!tailwind.config.js +!postcss.config.js +!babel.config.js +!webpack.config.js +!rollup.config.js +!jest.config.js +!cypress.config.js +!playwright.config.js + +# ============================================================================ +# END OF .dockerignore +# ============================================================================ \ No newline at end of file diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..5457a44 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,152 @@ +# This is a reusable Gitea Actions workflow. +# It's designed to be called by other repositories to standardize the deployment process. +# It performs two main jobs: +# 1. Build: Builds a Docker image from the calling repository's Dockerfile and pushes it to a registry. +# 2. Deploy: Runs the ansible-gitea playbook to deploy the newly built image to a target server. + +name: Reusable CI/CD + +# This workflow is triggered by a call from another workflow. +on: + workflow_call: + # ------------------ INPUTS ------------------ + # These are parameters that the calling workflow must provide. + inputs: + # The name of the application/service being deployed. + # Used to find project-specific variables in ansible. + app_name: + description: 'The application name' + required: true + type: string + # The deployment environment (e.g., staging, production). + deploy_env: + description: 'The deployment environment' + required: true + type: string + # The user on the target server. If not provided, defaults to 'deploy_user'. + server_user: + description: 'User on the target server' + required: false + type: string + default: 'deploy_user' + + # ------------------ SECRETS ------------------ + # These are secrets that the calling workflow must pass. + # They should be configured in the Gitea organization or repository settings. + secrets: + # Address of the target host(s) for Ansible. + ANSIBLE_HOSTS: + description: 'Target hosts for Ansible (e.g., "user@host")' + required: true + # SSH private key to connect to the Ansible target host. + SSH_PRIVATE_KEY: + description: 'SSH Private Key for Ansible' + required: true + # Password for the Ansible vault. + VAULT_PASSWORD: + description: 'Ansible Vault Password' + required: true + # Docker registry username. + DOCKER_USERNAME: + description: 'Docker Registry Username' + required: true + # Docker registry password or token. + DOCKER_PASSWORD: + description: 'Docker Registry Password' + required: true + # The URL of the Docker registry. + DOCKER_REGISTRY: + description: 'Docker Registry URL' + required: true + +jobs: + # ============================================================================ + # JOB 1: BUILD AND PUSH DOCKER IMAGE + # ============================================================================ + build: + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.meta.outputs.version }} + image_name: ${{ secrets.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ inputs.app_name }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.DOCKER_REGISTRY }}/${{ github.repository_owner }}/${{ inputs.app_name }} + tags: | + type=sha,prefix=,format=long + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + # ============================================================================ + # JOB 2: DEPLOY WITH ANSIBLE + # ============================================================================ + deploy: + runs-on: ubuntu-latest + needs: build # This job runs only after the 'build' job succeeds. + steps: + - name: Checkout Application Code + uses: actions/checkout@v4 + + - name: Checkout Ansible Repo + uses: actions/checkout@v4 + with: + # This should be the path to your ansible-gitea repository + repository: ${{ github.repository_owner }}/ansible-gitea + path: .ansible + + - name: Set up Ansible and Dependencies + run: | + sudo apt-get update + sudo apt-get install -y sshpass python3-pip + pip3 install ansible + ansible-galaxy collection install community.docker + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + # Disable strict host key checking for simplicity in CI + echo "StrictHostKeyChecking no" >> ~/.ssh/config + + - name: Setup Ansible Vault + run: | + echo "${{ secrets.VAULT_PASSWORD }}" > .ansible/.vault_pass + + - name: Run Ansible Playbook + working-directory: .ansible + run: | + ansible-playbook playbook.yml \ + -i "${{ secrets.ANSIBLE_HOSTS }}," \ + --extra-vars "app_name=${{ inputs.app_name }} \ + deploy_env=${{ inputs.deploy_env }} \ + server_user=${{ inputs.server_user }} \ + git_repo=${{ github.server_url }}/${{ github.repository }} \ + docker_image_tag=${{ needs.build.outputs.image_tag }} \ + docker_image_name=${{ needs.build.outputs.image_name }} \ + docker_image_full=${{ needs.build.outputs.image_name }}:${{ needs.build.outputs.image_tag }} \ + docker_registry=${{ secrets.DOCKER_REGISTRY }}" \ + --vault-password-file .vault_pass \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a504e5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Ansible +*.retry +.vault_pass + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Local testing +vagrant/ +.vagrant/ + +# Temporary files +/tmp/ansible_ssh_key diff --git a/.gitlab-ci.include.yml b/.gitlab-ci.include.yml new file mode 100644 index 0000000..226a711 --- /dev/null +++ b/.gitlab-ci.include.yml @@ -0,0 +1,1870 @@ +# ============================================================================ +# OPTIMIZED GITLAB CI/CD PIPELINE FOR NODE.JS APPLICATIONS +# ============================================================================ +# Features: +# - Auto-detection: Next.js, NestJS, React.js, Express.js frameworks +# - Flexible Authentication: SSH keys + passwords per environment +# - GitLab Self-hosted Compatible +# - Robust Detection: CI basic → Ansible comprehensive fallback +# - No Auto-Nginx: Manual control with generated templates +# - Single Server: Port management + resource optimization +# - Production Ready: Security, caching, zero-downtime deployment + +include: + - project: '$PROJECT_CI_ANSIBLE_PATH' + ref: 'main' + file: '.gitlab-ci/${CI_PROJECT_NAME}.yml' + rules: + - exists: + - '.gitlab-ci/${CI_PROJECT_NAME}.yml' + +# ============================================================================ +# GLOBAL CONFIGURATION +# ============================================================================ +variables: + # Docker and Build Configuration + DOCKER_DRIVER: overlay2 + DOCKER_BUILDKIT: "1" + BUILDKIT_PROGRESS: plain + + # Auto-detected Application Info + APP_NAME: "${CI_PROJECT_NAME}" + DOCKER_REGISTRY: "${CI_REGISTRY}" + DOCKER_IMAGE: "${CI_REGISTRY_IMAGE}" + DEPLOY_TYPE: "${DEPLOY_TYPE:-docker}" + BUILD_CACHE_ENABLED: "${BUILD_CACHE_ENABLED:-true}" + + # GitLab Self-hosted Detection + GITLAB_HOST: "${CI_SERVER_HOST}" + GITLAB_PORT: "${CI_SERVER_PORT:-443}" + GITLAB_ANSIBLE_USERNAME: "${GITLAB_ANSIBLE_USERNAME:-}" + GITLAB_ANSIBLE_TOKEN: "${GITLAB_ANSIBLE_TOKEN:-}" + + # Ansible Configuration + ANSIBLE_HOST_KEY_CHECKING: "False" + ANSIBLE_STDOUT_CALLBACK: "yaml" + ANSIBLE_FORCE_COLOR: "True" + + # VAULT + VAULT_ENGINE_NAME: "${CI_PROJECT_NAME}" + VAULT_ADDR: "https://vault.forlife.one" + VAULT_USERNAME: "${CI_VAULT_USERNAME}" + VAULT_PASSWORD: "${CI_VAULT_PASSWORD}" + +# Global cache for dependencies +cache: + key: "$CI_COMMIT_REF_SLUG" + paths: + - node_modules/ + - .npm/ + - .yarn/ + - .pnpm-store/ + policy: push + when: on_success + +# Pipeline stages +stages: + - prepare + - generate + - test + - security + - build + - deploy + - verify + - cleanup + +# ============================================================================ +# REUSABLE TEMPLATES +# ============================================================================ + +.docker_template: &docker_template + image: gitlab.forlife.one:5050/share/docker:24-dind + services: + - gitlab.forlife.one:5050/share/docker:24-dind + before_script: + - apk add --no-cache bash + - echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin + - | + echo "⏳ Waiting for Docker daemon..." + for i in {1..10}; do + if docker info > /dev/null 2>&1; then + echo "✅ Docker is ready!" + break + fi + echo "❌ Docker not ready (attempt $i), retrying in 3s..." + sleep 3 + done + +.ansible_template: &ansible_template + image: gitlab.forlife.one:5050/share/docker:ansible-2.13 + before_script: + - apk add --no-cache openssh-client ansible git nodejs npm sshpass wget bash curl jq bc unzip + - curl -fsSL https://releases.hashicorp.com/vault/1.15.3/vault_1.15.3_linux_amd64.zip -o vault.zip + - unzip vault.zip && mv vault /usr/local/bin/ && chmod +x /usr/local/bin/vault + - pip install ansible + + # Setup SSH agent + - eval $(ssh-agent -s) + + # Flexible SSH key setup based on environment + - | + echo "Setting up authentication for environment: ${ENVIRONMENT}" + if [ -n "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" ]; then + echo "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" | tr -d '\r' | ssh-add - + echo "✓ Using Deployment SSH key" + else + echo "ℹ No SSH key found, will use password authentication" + fi + + # Setup SSH configuration + - mkdir -p ~/.ssh && chmod 700 ~/.ssh + - echo "Host *" >> ~/.ssh/config + - echo " StrictHostKeyChecking no" >> ~/.ssh/config + - echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config + - echo " LogLevel ERROR" >> ~/.ssh/config + + # Add known hosts + - | + if [ -n "$DEPLOYMENT_SERVER_HOST" ]; then + ssh-keyscan -H "$DEPLOYMENT_SERVER_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true + fi + # Clone deployment tools - try multiple methods + - | + echo "Attempting to clone deployment tools..." + rm -rf ci + if git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then + echo "♋ Successfully cloned with CI_JOB_TOKEN" + elif [ -n "${GITLAB_ANSIBLE_TOKEN}" ] && git clone https://oauth2:${GITLAB_ANSIBLE_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then + echo "♋ Successfully cloned with GITLAB_ANSIBLE_TOKEN" + fi + + # Install Ansible requirements + #- ansible-galaxy install -r requirements.yml || echo "No requirements.yml found" + +# ============================================================================ +# PREPARE STAGE - ENHANCED DETECTION +# ============================================================================ + +prepare:auto-detect: + stage: prepare + image: gitlab.forlife.one:5050/share/node:18-alpine + before_script: + - apk add --no-cache jq curl + script: + - echo "🔍 Starting auto-detection process..." + + # Enhanced Node.js version detection + - | + echo "Detecting Node.js version..." + NODE_VERSION="" + if [ -f ".nvmrc" ]; then + NODE_VERSION=$(cat .nvmrc | tr -d 'v' | tr -d '\n' | head -1) + if [ -n "$NODE_VERSION" ]; then + echo "NODE_VERSION=${NODE_VERSION}" >> deploy.env + echo "✓ Detected Node.js version from .nvmrc: ${NODE_VERSION}" + else + echo "⚠ Empty .nvmrc file, deferring to Ansible" + fi + elif [ -f "package.json" ] && jq -e '.engines.node' package.json > /dev/null 2>&1; then + RAW_VERSION=$(jq -r '.engines.node' package.json) + NODE_VERSION=$(echo "$RAW_VERSION" | grep -oE '[0-9]+' | head -n 1) + if [ -n "$NODE_VERSION" ] && [ "$NODE_VERSION" != "$RAW_VERSION" ]; then + echo "NODE_VERSION=${NODE_VERSION}" >> deploy.env + echo "✓ Detected Node.js version from package.json: ${NODE_VERSION} (from ${RAW_VERSION})" + else + echo "⚠ Complex Node.js version format, deferring to Ansible: ${RAW_VERSION}" + fi + fi + if [ -n "$NODEJS_VERSION" ]; then + NODE_VERSION=$(echo "$NODEJS_VERSION" | grep -oE '[0-9]+' | head -n 1) + echo "✓ NODE_VERSION: ${NODEJS_VERSION}" + echo "NODE_VERSION=${NODEJS_VERSION}" >> deploy.env + fi + + # Enhanced framework detection + - | + echo "Detecting framework..." + # Detect PHP / Laravel first (composer.json or artisan file) + if [ -f "composer.json" ] || [ -f "artisan" ]; then + if grep -q '"laravel/framework"' composer.json 2>/dev/null || [ -f "artisan" ]; then + FRAMEWORK="laravel" + CONTAINER_PORT="3000" + echo "✓ Detected Laravel (PHP) framework" + fi + elif [ -f "package.json" ]; then + # Check for common frameworks + if jq -e '.dependencies["@nestjs/core"] or .dependencies["@nestjs/common"]' package.json > /dev/null 2>&1; then + FRAMEWORK="nestjs" + CONTAINER_PORT="3000" + echo "✓ Detected NestJS framework" + elif jq -e '.dependencies["next"]' package.json > /dev/null 2>&1; then + FRAMEWORK="nextjs" + CONTAINER_PORT="3000" + echo "✓ Detected Next.js framework" + elif jq -e '.dependencies["react"] and .dependencies["react-scripts"]' package.json > /dev/null 2>&1; then + FRAMEWORK="reactjs" + CONTAINER_PORT="3000" + echo "✓ Detected React.js (CRA) framework" + elif jq -e '.dependencies["react"] and .dependencies["vite"]' package.json > /dev/null 2>&1; then + FRAMEWORK="reactjs-vite" + CONTAINER_PORT="3000" + echo "✓ Detected React.js (Vite) framework" + elif jq -e '.dependencies["express"]' package.json > /dev/null 2>&1; then + FRAMEWORK="expressjs" + CONTAINER_PORT="3000" + echo "✓ Detected Express.js framework" + elif jq -e '.dependencies["fastify"]' package.json > /dev/null 2>&1; then + FRAMEWORK="fastify" + CONTAINER_PORT="3000" + echo "✓ Detected Fastify framework" + elif jq -e '.dependencies["@angular/core"]' package.json > /dev/null 2>&1; then + FRAMEWORK="angular" + CONTAINER_PORT="3000" + echo "✓ Detected Angular framework" + else + FRAMEWORK="" + CONTAINER_PORT="3000" + echo "ℹ Framework will be detected by Ansible (complex dependencies)" + fi + else + FRAMEWORK="" + CONTAINER_PORT="3000" + echo "ℹ No package.json found, framework will be detected by Ansible" + fi + echo "FRAMEWORK=${FRAMEWORK}" >> deploy.env + echo "CONTAINER_PORT=${CONTAINER_PORT}" >> deploy.env + + # Check if this is a frontend project + if [ "${FRAMEWORK}" = "nextjs" ] || [ "${FRAMEWORK}" = "reactjs" ] || [ "${FRAMEWORK}" = "reactjs-vite" ] || [ "${FRAMEWORK}" = "angular" ]; then + export IS_FRONTEND="true" + echo "IS_FRONTEND=true" >> deploy.env + else + echo "IS_FRONTEND=false" >> deploy.env + fi + + # Enhanced package manager detection + - | + echo "Detecting package manager..." + if [ -f "composer.json" ]; then + PACKAGE_MANAGER="composer" + echo "✓ Detected Composer (composer.json found)" + elif [ -f "pnpm-lock.yaml" ]; then + PACKAGE_MANAGER="pnpm" + echo "✓ Detected PNPM (pnpm-lock.yaml found)" + elif [ -f "yarn.lock" ]; then + PACKAGE_MANAGER="yarn" + echo "✓ Detected Yarn (yarn.lock found)" + elif [ -f "package-lock.json" ]; then + PACKAGE_MANAGER="npm" + echo "✓ Detected NPM (package-lock.json found)" + elif [ -f "package.json" ]; then + PACKAGE_MANAGER="npm" + echo "✓ Defaulting to NPM (package.json found)" + else + PACKAGE_MANAGER="" + echo "ℹ Package manager will be detected by Ansible" + fi + echo "PACKAGE_MANAGER=${PACKAGE_MANAGER}" >> deploy.env + + # Enhanced command detection + - | + echo "Detecting build and start commands..." + if [ -f "package.json" ]; then + BUILD_CMD=$(jq -r '.scripts.build // ""' package.json) + START_CMD=$(jq -r '.scripts.start // ""' package.json) + + if [ "$BUILD_CMD" != "" ] && [ "$BUILD_CMD" != "null" ]; then + echo "✓ Detected build command: ${BUILD_CMD}" + else + BUILD_CMD="" + echo "ℹ Build command will be detected by Ansible" + fi + + if [ "$START_CMD" != "" ] && [ "$START_CMD" != "null" ]; then + echo "✓ Detected start command: ${START_CMD}" + else + START_CMD="" + echo "ℹ Start command will be detected by Ansible" + fi + else + BUILD_CMD="" + START_CMD="" + echo "ℹ Build and start commands will be detected by Ansible" + fi + echo "BUILD_COMMAND=${BUILD_CMD}" >> deploy.env + echo "START_COMMAND=${START_CMD}" >> deploy.env + + # Detection summary + - | + echo "" + echo "📋 AUTO-DETECTION SUMMARY" + echo "=========================" + echo "Project: ${CI_PROJECT_NAME}" + echo "Framework: ${FRAMEWORK}" + echo "Node.js: ${NODE_VERSION}" + echo "Package Manager: ${PACKAGE_MANAGER}" + echo "Container Port: ${CONTAINER_PORT}" + echo "Build Command: ${BUILD_CMD}" + echo "Start Command: ${START_CMD}" + echo "" + echo "Note: Complex cases will be handled by Ansible's enhanced detection" + + - echo "🌍 Preparing deployment environment..." + - echo "APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}" >> deploy.env + - echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> deploy.env + - echo "GIT_COMMIT=${CI_COMMIT_SHA}" >> deploy.env + - echo "GIT_BRANCH=${CI_COMMIT_REF_NAME}" >> deploy.env + - echo "GIT_SHORT_SHA=${CI_COMMIT_SHORT_SHA}" >> deploy.env + - echo "PIPELINE_ID=${CI_PIPELINE_ID}" >> deploy.env + + # Pass through detected values with prefixes for Ansible + - echo "CI_DETECTED_NODE_VERSION=${NODE_VERSION:-}" >> deploy.env + - echo "CI_DETECTED_FRAMEWORK=${FRAMEWORK:-}" >> deploy.env + - echo "CI_DETECTED_PACKAGE_MANAGER=${PACKAGE_MANAGER:-}" >> deploy.env + - echo "CI_DETECTED_BUILD_COMMAND=${BUILD_COMMAND:-}" >> deploy.env + - echo "CI_DETECTED_START_COMMAND=${START_COMMAND:-}" >> deploy.env + - echo "CI_DETECTED_CONTAINER_PORT=${CONTAINER_PORT:-3000}" >> deploy.env + + - echo "✓ Environment variables prepared" + + - | + # Detect environment + if echo "$CI_COMMIT_TAG" | grep -E "(dev)"; then + ENV="development" + elif echo "$CI_COMMIT_TAG" | grep -E "(stg)"; then + ENV="staging" + elif echo "$CI_COMMIT_TAG" | grep -E "(prod|^v?[0-9]+\.[0-9]+\.[0-9]+(-worker)?$)"; then + ENV="production" + else + ENV="development" + fi + - echo "ENVIRONMENT=${ENVIRONMENT:-$ENV}" >> deploy.env + artifacts: + reports: + dotenv: deploy.env + expire_in: 2 hours + rules: + - if: $CI_COMMIT_TAG + + +# ============================================================================ +# TEST STAGE - FRAMEWORK AWARE +# ============================================================================ + +# ============================================================================ +# SECURITY STAGE +# ============================================================================ + +# ============================================================================ +# GENERATE STAGE - DYNAMIC DOCKERFILE CREATION +# ============================================================================ + +generate:dockerfile: + <<: *ansible_template + stage: generate + needs: ["prepare:auto-detect", "prepare:frontend-env"] + image: gitlab.forlife.one:5050/share/docker:alpine-latest + before_script: + - apk add --no-cache envsubst git curl jq + script: + - | + echo "Attempting to clone deployment tools..." + rm -rf ci + if git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then + echo "♋ Successfully cloned with CI_JOB_TOKEN" + elif [ -n "${GITLAB_ANSIBLE_TOKEN}" ] && git clone https://oauth2:${GITLAB_ANSIBLE_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then + echo "♋ Successfully cloned with GITLAB_ANSIBLE_TOKEN" + fi + - | + echo "🔧 Generating Dockerfile for ${FRAMEWORK:-nodejs} framework..." + + # Check if Dockerfile already exists + if [ "$FRAMEWORK" = "LARAVEL" ] && [ -f "Dockerfile" ]; then + echo "✅ Dockerfile already exists, using existing one" + echo "DOCKERFILE_GENERATED=false" >> dockerfile.env + exit 0 + fi + + if [ ! -d "ci" ]; then + echo "⚠️ ci folder does not exists, using existing one" + exit 0 + fi + + # Detect environment + if echo "$CI_COMMIT_TAG" | grep -E "(dev)"; then + ENV="development" + elif echo "$CI_COMMIT_TAG" | grep -E "(stg)"; then + ENV="staging" + elif echo "$CI_COMMIT_TAG" | grep -E "(prod|^v?[0-9]+\.[0-9]+\.[0-9]+(-worker)?$)"; then + ENV="production" + else + ENV="development" + fi + + # Determine MEMORY_LIMIT with fallback logic + MEMORY_LIMIT=${MEMORY_LIMIT:-} + + if [ -z "$MEMORY_LIMIT" ]; then + echo "🔍 MEMORY_LIMIT not set in CI variables, checking config files..." + + # Try project-specific config first + PROJECT_CONFIG="ci/projects/${APP_NAME}/group_vars/${ENV}.yml" + if [ -f "$PROJECT_CONFIG" ]; then + echo "📄 Found project config: $PROJECT_CONFIG" + MEMORY_LIMIT=$(grep -A3 "resource_limits:" "$PROJECT_CONFIG" | grep -A2 "${ENV}:" | grep "memory:" | awk '{print $2}' | tr -d '"' | head -n1) + fi + + # Try vault config if still empty + if [ -z "$MEMORY_LIMIT" ]; then + VAULT_CONFIG="ci/group_vars/vault/${ENV}.yml" + if [ -f "$VAULT_CONFIG" ]; then + echo "📄 Found vault config: $VAULT_CONFIG" + MEMORY_LIMIT=$(grep -A3 "resource_limits:" "$VAULT_CONFIG" | grep -A2 "${ENV}:" | grep "memory:" | awk '{print $2}' | tr -d '"' | head -n1) + fi + fi + + # Default fallback + if [ -z "$MEMORY_LIMIT" ]; then + echo "⚙️ No MEMORY_LIMIT found, using default: 2g" + MEMORY_LIMIT="2g" + else + echo "✅ Found MEMORY_LIMIT: $MEMORY_LIMIT" + fi + else + echo "✅ Using MEMORY_LIMIT from CI variables: $MEMORY_LIMIT" + fi + + # Convert memory limit to number (in MB, without unit) + if echo "$MEMORY_LIMIT" | grep -qE '^[0-9]+g$'; then + MEMORY_GB=$(echo "$MEMORY_LIMIT" | sed 's/g$//') + MEMORY_LIMIT_MB=$((MEMORY_GB * 1024)) + echo "📊 Memory limit: ${MEMORY_GB}g = ${MEMORY_LIMIT_MB} (MB)" + elif echo "$MEMORY_LIMIT" | grep -qE '^[0-9]+m$'; then + MEMORY_LIMIT_MB=$(echo "$MEMORY_LIMIT" | sed 's/m$//') + echo "📊 Memory limit: ${MEMORY_LIMIT_MB} (MB)" + elif echo "$MEMORY_LIMIT" | grep -qE '^[0-9]+$'; then + MEMORY_LIMIT_MB=$MEMORY_LIMIT + echo "📊 Memory limit: ${MEMORY_LIMIT_MB} (MB)" + else + echo "⚠️ Invalid MEMORY_LIMIT format: $MEMORY_LIMIT, using default 2048" + MEMORY_LIMIT_MB=2048 + fi + + # Apply 80% factor to MEMORY_LIMIT_MB + MEMORY_LIMIT_MB=$(awk "BEGIN {printf \"%.0f\", $MEMORY_LIMIT_MB * 0.8}") + + APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}} + + # Method 1: Check for local templates first (in ci/roles/docker/templates/dockerfiles/) + TEMPLATE_DIR="ci/roles/docker/templates/dockerfiles" + if [ -d "$TEMPLATE_DIR" ]; then + echo "📁 Found local template directory: $TEMPLATE_DIR" + USE_LOCAL_TEMPLATES=true + else + echo "📥 No local templates, will fetch from ansible repo" + USE_LOCAL_TEMPLATES=false + fi + + # Determine framework-specific settings + FRAMEWORK="${FRAMEWORK:-}" + RUN_CMD="${RUN_CMD:-}" + case "$FRAMEWORK" in + "expressjs"|"fastify") + TEMPLATE_FILE="expressjs.Dockerfile.j2" + DEFAULT_PORT="3000" + + RUN_CMD='["dumb-init", "node", "dist/main"]' + if [[ "$CI_COMMIT_TAG" == *worker* ]]; then + RUN_CMD='["dumb-init", "node", "dist/cli", "sync_queue"]' + fi + ;; + "nestjs") + TEMPLATE_FILE="nestjs.Dockerfile.j2" + DEFAULT_PORT="3000" + + RUN_CMD='["dumb-init", "node", "dist/main"]' + if [[ "$CI_COMMIT_TAG" == *worker* ]]; then + RUN_CMD='["dumb-init", "node", "dist/cli", "sync_queue"]' + fi + ;; + "nextjs") + TEMPLATE_FILE="nextjs.Dockerfile.j2" + DEFAULT_PORT="3000" + + #RUN_CMD='["npm", "start"]' + if [[ "$RUN_CMD" == "" ]]; then + RUN_CMD='["node", "server.js"]' + fi + ;; + "reactjs"|"reactjs-vite"|"angular") + TEMPLATE_FILE="reactjs.Dockerfile.j2" + DEFAULT_PORT="3000" + + RUN_CMD='[ "serve", "-s", "build" ]' + ;; + "laravel") + TEMPLATE_FILE="laravel.Dockerfile.j2" + DEFAULT_PORT="3000" + + RUN_CMD='[ "serve", "-s", "build" ]' + ;; + *) + TEMPLATE_FILE="nodejs.Dockerfile.j2" + DEFAULT_PORT="3000" + + RUN_CMD='["dumb-init", "node", "dist/main"]' + if [[ "$CI_COMMIT_TAG" == *worker* ]]; then + RUN_CMD='["dumb-init", "node", "dist/cli", "sync_queue"]' + fi + ;; + esac + + if [ "${FRAMEWORK}" = "laravel" ]; then + echo "Determining required PHP version based on Laravel version..." + + # 1. Đọc phiên bản Laravel từ composer.json (ví dụ: "^10.0") + LARAVEL_CONSTRAINT=$(jq -r '.require."laravel/framework" // empty' composer.json) + + if [ -z "$LARAVEL_CONSTRAINT" ]; then + echo "Laravel framework version not found in composer.json. Using default PHP 8.1" + PHP_VERSION="8.1" + else + # 2. Lấy số phiên bản chính (ví dụ: từ "^10.0" -> "10") + LARAVEL_MAJOR_VERSION=$(echo $LARAVEL_CONSTRAINT | tr -d '^~>=' | cut -d'.' -f1) + + PHP_VERSION="" + # 3. Ánh xạ phiên bản Laravel sang phiên bản PHP yêu cầu + case "$LARAVEL_MAJOR_VERSION" in + "13") + PHP_VERSION="8.4" + ;; + "12") + PHP_VERSION="8.3" + ;; + "11") + PHP_VERSION="8.2" + ;; + "10") + PHP_VERSION="8.1" + ;; + "9") + PHP_VERSION="8.1" + ;; + "8") + PHP_VERSION="8.1" + ;; + *) + # Nếu không khớp, đặt một giá trị mặc định an toàn và cảnh báo + echo "Warning: Unsupported or unknown Laravel version ($LARAVEL_MAJOR_VERSION). Falling back to PHP 8.1." + PHP_VERSION="8.1" + ;; + esac + + echo "Detected Laravel version: $LARAVEL_MAJOR_VERSION -> Required PHP version: $PHP_VERSION" + fi + fi + + echo "📦 Target template: ${TEMPLATE_FILE}" + echo "🚀 Framework: ${FRAMEWORK}" + echo "🔌 Port: ${CONTAINER_PORT:-$DEFAULT_PORT}" + + # Method 1: Use local templates (if exists) + if [ "$USE_LOCAL_TEMPLATES" = "true" ] && [ -f "$TEMPLATE_DIR/$TEMPLATE_FILE" ]; then + echo "✅ Using local template: $TEMPLATE_DIR/$TEMPLATE_FILE" + TEMPLATE_PATH="$TEMPLATE_DIR/$TEMPLATE_FILE" + # Method 2: Fallback to inline templates + else + echo "⚠️ No template source configured, using inline fallback" + echo "💡 Set ANSIBLE_REPO_URL or TEMPLATE_BASE_URL for external templates" + TEMPLATE_PATH="" + fi + + # Process template if found + if [ -n "$TEMPLATE_PATH" ] && [ -f "$TEMPLATE_PATH" ]; then + echo "🔄 Processing template: $TEMPLATE_PATH" + + if [ -n "${BUILD_COMMAND:-}" ]; then + BUILD_COMMAND=$(echo "$BUILD_COMMAND" | sed 's/&&/\\\&\\\&/g') + fi + if [ -n "${START_COMMAND:-}" ]; then + START_COMMAND=$(echo "$START_COMMAND" | sed 's/&&/\\\&\\\&/g') + fi + + # Set all variables for Jinja2-style replacement + export app_name="${APP_NAME}" + export node_version="${NODE_VERSION:-18}" + export package_manager="${PACKAGE_MANAGER:-npm}" + export build_command="${BUILD_COMMAND:-}" + export start_command="${START_COMMAND:-node index.js}" + export framework_settings_port="${CONTAINER_PORT:-$DEFAULT_PORT}" + export docker_image_tag="${APP_VERSION:-latest}" + export APP_PORT="${framework_settings_port}" + export PHP_VERSION="${PHP_VERSION:-8.1}" + export php_version="${PHP_VERSION:-8.1}" + export MEMORY_LIMIT_MB="${MEMORY_LIMIT_MB:-2048}" + + # Add frontend environment variables if applicable + if [ "${IS_FRONTEND:-false}" = "true" ] && [ -f ".env" ]; then + echo "🌐 Adding frontend environment variables..." + + # Process .env file safely + while IFS='=' read -r key value || [ -n "$key" ]; do + # Skip comments and empty lines + case "$key" in + \#*|'') + continue + ;; + esac + + # Skip empty keys + if [ -z "$key" ]; then + continue + fi + + # Clean the value (remove quotes if present) + case "$value" in + \"*\") + value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') + ;; + \'*\') + value=$(echo "$value" | sed "s/^'//" | sed "s/'$//") + ;; + esac + + export "${key}=${value}" + done < .env + fi + + # Method A: Simple variable substitution (works for basic templates) + echo "🔧 Method A: Simple variable substitution" + sed -e "s|{{ app_name }}|${app_name}|g" \ + -e "s|{{ node_version }}|${node_version}|g" \ + -e "s|{{ package_manager }}|${package_manager}|g" \ + -e "s|{{ build_command }}|${build_command}|g" \ + -e "s|{{ start_command }}|${start_command}|g" \ + -e "s|{{ framework_settings.port }}|${framework_settings_port}|g" \ + -e "s|{{ framework_settings_port }}|${framework_settings_port}|g" \ + -e "s|{{ docker_image_tag }}|${docker_image_tag}|g" \ + -e "s|{{ RUN_CMD }}|${RUN_CMD}|g" \ + -e "s|{{ php_version }}|${PHP_VERSION:-8.1}|g" \ + -e "s|{{ MEMORY_LIMIT_MB }}|${MEMORY_LIMIT_MB:-2048}|g" \ + "$TEMPLATE_PATH" > Dockerfile.tmp + + # Method B: Advanced replacement for complex Jinja2 (if needed) + if grep -q "{%" "$TEMPLATE_PATH"; then + echo "🔧 Method B: Complex Jinja2 detected, using advanced processing" + + # Install j2cli for proper Jinja2 processing (optional) + pip install j2cli 2>/dev/null || { + echo "⚠️ j2cli not available, falling back to sed replacement" + + # Handle {% if %} blocks with sed (basic approach) + sed -i '/{% if start_command and start_command != /,/{% else %}/c\ + # Using detected start command' Dockerfile.tmp + + sed -i '/{% endif %}/d' Dockerfile.tmp + } + + # Alternative: Use j2cli if available + if command -v j2 >/dev/null 2>&1; then + echo "✅ Using j2cli for proper Jinja2 processing" + + # Create variables file + cat > /tmp/vars.json << EOF + { + "app_name": "${app_name}", + "node_version": "${node_version}", + "package_manager": "${package_manager}", + "build_command": "${build_command}", + "start_command": "${start_command}", + "php_version": "${PHP_VERSION:-8.1}", + "framework_settings_port": "${framework_settings_port}", + "framework_settings": { + "port": "${framework_settings_port}" + }, + "docker_image_tag": "${docker_image_tag}", + "RUN_CMD": "${RUN_CMD}", + "MEMORY_LIMIT_MB": "${MEMORY_LIMIT_MB:-2048}" + } + EOF + + j2 "$TEMPLATE_PATH" /tmp/vars.json > Dockerfile.tmp + fi + fi + + # Final processing with envsubst for any remaining ${VAR} patterns + envsubst < Dockerfile.tmp > Dockerfile + rm -f Dockerfile.tmp + + echo "✅ Generated Dockerfile from template: $TEMPLATE_FILE" + + else + echo "🔄 Using inline fallback template for ${FRAMEWORK}" + + # Fallback to previous inline method + case "$FRAMEWORK" in + "expressjs"|"fastify"|*) + cat > Dockerfile << 'EOF' + # Generated Express.js/Node.js Dockerfile + ARG NODE_VERSION=18 + FROM gitlab.forlife.one:5050/share/node:${NODE_VERSION}-alpine AS base + RUN apk add --no-cache libc6-compat curl dumb-init + WORKDIR /app + FROM base AS deps + COPY package*.json yarn.lock* pnpm-lock.yaml* ./ + RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else npm install; fi + FROM base AS builder + COPY --from=deps /app/node_modules ./node_modules + COPY . . + RUN if [ -f package.json ] && grep -q '"build"' package.json; then npm run build; fi + FROM base AS production + RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nodejs + COPY --from=builder --chown=nodejs:nodejs /app . + USER nodejs + EXPOSE 3000 + CMD ["dumb-init", "node", "index.js"] + EOF + ;; + esac + fi + + # Show final Dockerfile preview + echo "📄 Generated Dockerfile preview:" + head -20 Dockerfile + echo "..." + + echo "DOCKERFILE_GENERATED=true" >> dockerfile.env + echo "TEMPLATE_USED=${TEMPLATE_FILE:-inline}" >> dockerfile.env + + artifacts: + paths: + - Dockerfile + reports: + dotenv: dockerfile.env + expire_in: 1 hour + rules: + - if: $CI_COMMIT_TAG && $DEPLOY_TYPE != "pm2" + +generate:dockerignore: + <<: *ansible_template + stage: generate + needs: ["prepare:auto-detect"] + image: gitlab.forlife.one:5050/share/docker:alpine-latest + before_script: + - apk add --no-cache git curl + script: + - | + echo "📁 Fetching .dockerignore file..." + + # Check if project already has .dockerignore + if [ -f ".dockerignore" ]; then + echo "ℹ️ Project already has .dockerignore file" + exit 0 + fi + + ANSIBLE_DOCKERIGNORE="ci/.dockerignore" + if [ -f "$ANSIBLE_DOCKERIGNORE" ]; then + echo "✅ Found .dockerignore in Ansible repo" + cp "$ANSIBLE_DOCKERIGNORE" .dockerignore + echo "DOCKERIGNORE_SOURCE=ansible-repo" >> dockerignore.env + fi + + artifacts: + paths: + - .dockerignore + reports: + dotenv: dockerignore.env + expire_in: 1 hour + when: always + rules: + - if: $CI_COMMIT_TAG && $DEPLOY_TYPE != "pm2" + +# Enhanced prepare stage with frontend env support +prepare:frontend-env: + stage: prepare + needs: ["prepare:auto-detect"] + image: gitlab.forlife.one:5050/share/docker:alpine-latest + before_script: + - apk add --no-cache bash # Đảm bảo có bash + script: + - | + #!/bin/bash + set -e # Exit on error + + echo "🎯 Setting up environment with quote validation..." + + # Detect environment + if echo "$CI_COMMIT_TAG" | grep -E "(dev)"; then + ENV="development" + elif echo "$CI_COMMIT_TAG" | grep -E "(stg)"; then + ENV="staging" + elif echo "$CI_COMMIT_TAG" | grep -E "(prod|^v?[0-9]+\.[0-9]+\.[0-9]+(-worker)?$)"; then + ENV="production" + else + ENV="development" + fi + + echo "📝 Environment: $ENV" + + # Check if it's a frontend framework + is_frontend=false + if [ "${FRAMEWORK}" = "nextjs" ] || [ "${FRAMEWORK}" = "reactjs" ] || [ "${FRAMEWORK}" = "reactjs-vite" ] || [ "${FRAMEWORK}" = "angular" ]; then + is_frontend=true + fi + + if [ "$is_frontend" = "true" ]; then + + if [[ -f "./.gitlab-ci/${ENV}.env" ]]; then + echo "✅ Found: .gitlab-ci/${ENV}.env" + cp "./.gitlab-ci/${ENV}.env" "./.env" + fi + if [[ -f "./.gitlab/ci/${ENV}.sh" ]]; then + bash ".gitlab/ci/${ENV}.sh" + fi + + if [ -f ".env" ]; then + echo "✅ Found: .env" + + # 🔧 ADVANCED QUOTE CLEANING + echo "🧹 Cleaning quotes and special characters..." + + # Step 1: Create working copy + cp ".env" "temp.env" + + # Step 2: Clean problematic content + sed -i 's/\r$//' temp.env # Remove Windows line endings + sed -i '/^$/d' temp.env # Remove empty lines + sed -i '/^[[:space:]]*#/d' temp.env # Remove comments + sed -i 's/^[[:space:]]*//' temp.env # Remove leading spaces + sed -i 's/[[:space:]]*$//' temp.env # Remove trailing spaces + + # Step 3: Initialize frontend.env + cat > frontend.env << EOF + TARGET_ENV=${ENV} + FRAMEWORK=${FRAMEWORK} + IS_FRONTEND=true + EOF + + else + echo "⚠️ No env file: .env" + cat > frontend.env << EOF + TARGET_ENV=${ENV} + FRAMEWORK=${FRAMEWORK} + IS_FRONTEND=false + EOF + fi + + else + echo "⏭️ Backend project" + cat > frontend.env << EOF + TARGET_ENV=${ENV} + FRAMEWORK=${FRAMEWORK} + IS_FRONTEND=false + EOF + fi + + # Final validation + echo "🔍 Validating frontend.env syntax..." + + # Test if file exists and is readable + if [ ! -f "frontend.env" ]; then + echo "❌ frontend.env file not created" + exit 1 + fi + + # Basic syntax check - try to read each line + validation_failed=false + while IFS= read -r line || [ -n "$line" ]; do + if [ -n "$line" ] && ! echo "$line" | grep -q "^[A-Z][A-Z0-9_]*="; then + echo "❌ Invalid line format: $line" + validation_failed=true + fi + done < frontend.env + + if [ "$validation_failed" = "true" ]; then + echo "❌ Syntax validation failed" + echo "📋 File contents:" + cat frontend.env + exit 1 + fi + + echo "✅ Syntax validation passed" + echo "📋 Final preview:" + head -10 frontend.env | sed 's/=.*/=***/' + + artifacts: + paths: + - .env + - frontend.env # Đảm bảo cả 2 file đều được lưu + reports: + dotenv: frontend.env + expire_in: 2 hours + + rules: + - if: $CI_COMMIT_TAG + +# ============================================================================ +# BUILD STAGE - FRAMEWORK OPTIMIZED +# ============================================================================ + +build:docker: + <<: *docker_template + stage: build + needs: ["prepare:auto-detect", "prepare:frontend-env", "generate:dockerfile", "generate:dockerignore"] + retry: + max: 1 + when: + - script_failure + - runner_system_failure + before_script: + - apk add --no-cache bash envsubst curl jq unzip + - curl -fsSL https://releases.hashicorp.com/vault/1.15.3/vault_1.15.3_linux_amd64.zip -o vault.zip + - unzip vault.zip && mv vault /usr/local/bin/ && chmod +x /usr/local/bin/vault && rm -f vault.zip + - echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin + - | + echo "⏳ Waiting for Docker daemon..." + for i in {1..10}; do + if docker info > /dev/null 2>&1; then + echo "✅ Docker is ready!" + break + fi + echo "❌ Docker not ready (attempt $i), retrying in 3s..." + sleep 3 + done + + # Nếu hết retry mà vẫn fail thì exit để GitLab tự retry job + if ! docker info > /dev/null 2>&1; then + echo "❌ Docker vẫn chưa sẵn sàng sau nhiều lần thử. Thoát để GitLab retry job." + + MESSAGE="🚨 CI job *failed* in project \`$CI_PROJECT_PATH\` at stage \`$CI_JOB_STAGE\`!\n\nDocker daemon is not ready on runner \`$CI_RUNNER_DESCRIPTION\`.\nCommit: \`$CI_COMMIT_SHORT_SHA\`\nJob URL: $CI_JOB_URL" + + curl -s -X POST https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage \ + -d chat_id="${TELEGRAM_CHAT_ID}" \ + -d parse_mode=Markdown \ + -d text="$MESSAGE" + + exit 1 + fi + script: + - | + echo "Attempting to clone deployment tools..." + rm -rf ci + if git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then + echo "♋ Successfully cloned with CI_JOB_TOKEN" + elif [ -n "${GITLAB_ANSIBLE_TOKEN}" ] && git clone https://oauth2:${GITLAB_ANSIBLE_TOKEN}@gitlab.forlife.one/forlife/devops/ansible-deploy.git ci; then + echo "♋ Successfully cloned with GITLAB_ANSIBLE_TOKEN" + fi + - | + #!/bin/bash + set -e # Exit on error + + echo "🐳 Building Docker image with framework optimization..." + echo "Framework: ${FRAMEWORK:-}" + echo "Node.js: ${NODE_VERSION:-}" + echo "Package Manager: ${PACKAGE_MANAGER:-}" + echo "Environment: ${ENVIRONMENT:-development}" + + # Check Dockerfile status + if [ "${DOCKERFILE_GENERATED:-false}" == "true" ]; then + echo "✅ Using auto-generated Dockerfile for ${FRAMEWORK}" + elif [ -f "Dockerfile" ]; then + echo "✅ Using existing Dockerfile from repository" + else + echo "❌ No Dockerfile available - this should not happen" + exit 1 + fi + + if [ "${FRAMEWORK}" = "nextjs" ] && [ ! -f "next.config.ts" ] && [ ! -f "next.config.js" ]; then + echo "❌ Không tìm thấy next.config.js hoặc next.config.ts trong dự án Next.js." + echo "❌ Hãy thêm next.config.js/ts và cấu hình output standalone để build Docker image đúng cách." + exit 1 + fi + + if [ "${FRAMEWORK}" = "expressjs" ]; then + if [ ! -d "src/public" ]; then + mkdir -p "src/public" + fi + fi + + if [ "${FRAMEWORK}" = "expressjs" ] || [ "${FRAMEWORK}" = "nestjs" ]; then + if [ ! -f "src/main.ts" ] && [ -f "src/index.ts" ]; then + cp "src/index.ts" "src/main.ts" + fi + fi + + if [[ "$IS_FRONTEND" == "true" ]]; then + cd ci + CI_PROJECT_DIR="projects/${APP_NAME}" + if [[ -f "../.gitlab-ci/${ENVIRONMENT}.env" ]]; then + echo "✅ Found: .gitlab-ci/${ENVIRONMENT}.env" + cp "../.gitlab-ci/${ENVIRONMENT}.env" "../.env" + fi + + if [[ -f "../.gitlab/ci/${ENVIRONMENT}.sh" ]]; then + cd ../ && bash ".gitlab/ci/${ENVIRONMENT}.sh" && cd ci + fi + + if [ ! -d "../public" ]; then + mkdir -p "../public" + fi + cd ../ + fi + + export VAULT_ADDR="${VAULT_ADDR}" + + VAULT_PATH="${VAULT_ENGINE_NAME}/${ENVIRONMENT}" + export VAULT_PATH="${VAULT_PATH}" + + VAULT_RESPONSE=$(curl --silent --request POST \ + --data "{\"password\": \"${VAULT_PASSWORD}\"}" \ + "${VAULT_ADDR}/v1/auth/userpass/login/${VAULT_USERNAME}") + + #echo "🔐 Vault login response:" + #echo "$VAULT_RESPONSE" | jq + + VAULT_TOKEN=$(echo "$VAULT_RESPONSE" | jq -r '.auth.client_token') + + if [[ "$VAULT_TOKEN" == "null" || -z "$VAULT_TOKEN" ]]; then + echo "❌ Failed to get Vault token" + exit 1 + fi + + export VAULT_TOKEN="${VAULT_TOKEN}" + + VAULT_OUTPUT=$(vault kv get -format=json "$VAULT_PATH" 2>/dev/null || echo "") + + # Kiểm tra nếu không lấy được VAULT_OUTPUT + if [[ -z "$VAULT_OUTPUT" ]] || ! echo "$VAULT_OUTPUT" | jq empty >/dev/null 2>&1; then + echo "⚠️ Failed to get or parse Vault response for ${VAULT_PATH}" + else + SECRETS_JSON=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | @json') + + if [[ -n "$SECRETS_JSON" && "$SECRETS_JSON" != "null" ]]; then + echo "🔐 Vault setting for ${VAULT_PATH} will be used!" + + # Dùng jq để chuyển đổi JSON sang định dạng KEY="VALUE" + # to_entries[]: Chuyển object {"k1":"v1", "k2":"v2"} thành các dòng {"key":"k1", "value":"v1"} + # "\(.key)=\"\(.value)\"": Định dạng mỗi dòng thành KEY="VALUE" + #SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\"\(.value)\""') + SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"') + + # Kiểm tra xem có secret nào được tìm thấy không + if [[ -n "$SECRETS_TO_ENV" ]]; then + # Ghi các secret đã được định dạng vào file .env + echo "$SECRETS_TO_ENV" > .env + fi + else + echo "⚠️ No secrets found at ${VAULT_PATH}" + fi + fi + + APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}} + + # Setup Docker Buildx with unique name + BUILDER_NAME="builder-${CI_PIPELINE_ID}-${CI_JOB_ID}" + docker buildx create --use --name "$BUILDER_NAME" || true + + # ======================================================================== + # FRAMEWORK-SPECIFIC BUILD ARGS PREPARATION + # ======================================================================== + + echo "🔧 Preparing build arguments..." + + # Initialize build args + BUILD_ARGS="" + + # Add base build args one by one + BUILD_ARGS="${BUILD_ARGS} --build-arg NODE_VERSION='${NODE_VERSION:-18}'" + BUILD_ARGS="${BUILD_ARGS} --build-arg PACKAGE_MANAGER='${PACKAGE_MANAGER:-npm}'" + BUILD_ARGS="${BUILD_ARGS} --build-arg APP_PORT='${CONTAINER_PORT:-3000}'" + BUILD_ARGS="${BUILD_ARGS} --build-arg NODE_ENV='production'" + BUILD_ARGS="${BUILD_ARGS} --build-arg APP_VERSION='${APP_VERSION}'" + BUILD_ARGS="${BUILD_ARGS} --build-arg FRAMEWORK='${FRAMEWORK:-nodejs}'" + + # Add optional build args if they have values + if [ -n "${BUILD_COMMAND:-}" ]; then + ESCAPED=$(echo "$START_COMMAND" | sed 's/"/\\"/g' | sed "s/'/'\"'\"'/g") + BUILD_ARGS="${BUILD_ARGS} --build-arg BUILD_COMMAND='${ESCAPED}'" + fi + if [ -n "${START_COMMAND:-}" ]; then + ESCAPED=$(echo "$START_COMMAND" | sed 's/"/\\"/g' | sed "s/'/'\"'\"'/g") + #BUILD_ARGS="${BUILD_ARGS} --build-arg START_COMMAND='${ESCAPED}'" + fi + if [ -n "${BUILD_DATE:-}" ]; then + BUILD_ARGS="${BUILD_ARGS} --build-arg BUILD_DATE='${BUILD_DATE}'" + fi + if [ -n "${GIT_COMMIT:-}" ]; then + BUILD_ARGS="${BUILD_ARGS} --build-arg GIT_COMMIT='${GIT_COMMIT}'" + fi + if [ -n "${PIPELINE_ID:-}" ]; then + BUILD_ARGS="${BUILD_ARGS} --build-arg PIPELINE_ID='${PIPELINE_ID}'" + fi + + # Add frontend environment variables if applicable + if [ "${IS_FRONTEND:-false}" = "true" ] && [ -f ".env" ]; then + echo "🌐 Adding frontend environment variables..." + + # Process .env file safely + while IFS='=' read -r key value || [ -n "$key" ]; do + # Skip comments and empty lines + case "$key" in + \#*|'') + continue + ;; + esac + + # Skip empty keys + if [ -z "$key" ]; then + continue + fi + + # Skip keys that are already in base build args to avoid duplicates + case "$key" in + NODE_VERSION|PACKAGE_MANAGER|APP_PORT|PORT|NODE_ENV|APP_VERSION|FRAMEWORK|BUILD_COMMAND|START_COMMAND|BUILD_DATE|GIT_COMMIT|PIPELINE_ID) + echo "⚠️ Skipped duplicate: ${key} (already in base args)" + continue + ;; + esac + + # Clean the value (remove quotes if present) + case "$value" in + \"*\") + value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') + ;; + \'*\') + value=$(echo "$value" | sed "s/^'//" | sed "s/'$//") + ;; + esac + + # Add to build args with proper quoting + BUILD_ARGS="${BUILD_ARGS} --build-arg ${key}='${value}'" + echo "✓ Added build arg: ${key}=${value:0:30}..." + + done < .env + fi + + # Debug: Show actual BUILD_ARGS content + echo "🔍 DEBUG: BUILD_ARGS content:" + echo "BUILD_ARGS='${BUILD_ARGS}'" + echo "🔍 DEBUG: First few build args:" + echo "${BUILD_ARGS}" | tr ' ' '\n' | head -10 + + # ======================================================================== + # DOCKER BUILD EXECUTION + # ======================================================================== + + echo "🚀 Starting Docker build with ${FRAMEWORK} optimization..." + + # Setup cache configuration + CACHE_CONFIG="" + if [ "${BUILD_CACHE_ENABLED:-true}" == "true" ]; then + if [ -n "${FRAMEWORK:-}" ]; then + CACHE_CONFIG="--cache-from type=registry,ref=${DOCKER_IMAGE}:cache-${FRAMEWORK} --cache-to type=registry,ref=${DOCKER_IMAGE}:cache-${FRAMEWORK},mode=max" + else + CACHE_CONFIG="--cache-from type=registry,ref=${DOCKER_IMAGE}:cache --cache-to type=registry,ref=${DOCKER_IMAGE}:cache,mode=max" + fi + else + CACHE_CONFIG="--no-cache" + fi + + # Create output file first (in case of failure) - SIMPLE dotenv format + echo "SKIP_DOCKER_BUILD=false" > docker_build.env + echo "BUILD_SUCCESS=false" >> docker_build.env + echo "DOCKER_IMAGE_BUILT=${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}" >> docker_build.env + + echo "🔨 Executing Docker build command..." + echo "🔍 DEBUG: Command length: ${#BUILD_ARGS} characters" + + # Write build args to a file to avoid command line length issues + echo "📝 Writing build args to file for docker buildx..." + echo "$BUILD_ARGS" | tr ' ' '\n' | grep -E "^--build-arg" > /tmp/build_args.txt + echo "📝 Build args file created: $(wc -l < /tmp/build_args.txt) lines" + + # Show sample of build args file + echo "🔍 Sample build args:" + head -5 /tmp/build_args.txt + + echo "▶️ Starting docker buildx build..." + + # Set conditional tag + LATEST_TAG="" + if [ "${ENVIRONMENT:-development}" == "production" ]; then + LATEST_TAG="--tag '${DOCKER_IMAGE}:${APP_VERSION}-latest'" + fi + + # Kiểm tra nếu có .env thì thêm vào BUILD_SECRET + if [ -f .env ]; then + echo "✅ Found .env, using as Docker secret" + BUILD_SECRET="--secret id=env,src=.env" + else + echo "⚠️ .env not found, skipping --secret" + BUILD_SECRET="" + fi + + if [ "${FRAMEWORK}" = "laravel" ]; then + cp -r ci/roles/docker/templates/nginx ./docker + fi + + if [[ "$FRAMEWORK" == "expressjs" ]]; then + if [ ! -d "src/public" ]; then + mkdir -p "src/public" + fi + fi + if [[ "$FRAMEWORK" == "nextjs" ]] && [[ ! -f "../next.config.js" ]]; then + cat > "../next.config.js" < docker_build.env + echo "BUILD_SUCCESS=false" >> docker_build.env + echo "DOCKER_IMAGE_BUILT=" >> docker_build.env + rm -f /tmp/build_args.txt + exit 1 + } + + # Build successful - update dotenv file + echo "SKIP_DOCKER_BUILD=false" > docker_build.env + echo "BUILD_SUCCESS=true" >> docker_build.env + echo "DOCKER_IMAGE_BUILT=${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}" >> docker_build.env + + # Cleanup + rm -f /tmp/build_args.txt + + echo "✅ Docker image built successfully:" + echo " - ${DOCKER_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}" + if [ "${ENVIRONMENT:-development}" == "production" ]; then + echo " - ${DOCKER_IMAGE}:${APP_VERSION}-latest" + fi + + # Cleanup + docker buildx rm "$BUILDER_NAME" || true + +# artifacts: +# reports: +# dotenv: docker_build.env +# expire_in: 1 hour +# when: always + + rules: + - if: $CI_COMMIT_TAG && $DEPLOY_TYPE != "pm2" + +# ============================================================================ +# ADDITIONAL JOBS FOR ENVIRONMENT-SPECIFIC BUILDS +# ============================================================================ + +# Build for specific environments (if needed) +#build:docker:development: +# extends: build:docker +# variables: +# ENVIRONMENT: "development" +# # NextJS development-specific variables +# NEXT_PUBLIC_API_URL: "${NEXT_PUBLIC_API_URL_DEV:-http://localhost:3001}" +# NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL_DEV:-http://localhost:3000}" +# # React development-specific variables +# REACT_APP_API_URL: "${REACT_APP_API_URL_DEV:-http://localhost:3001}" +# # Build optimizations for development +# OPTIMIZE_IMAGES: "false" +# COMPRESS_ASSETS: "false" +# ENABLE_BUNDLE_ANALYZER: "false" +# rules: +# - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-dev\.\d+$/ && $DEPLOY_TYPE != "pm2" + +# ============================================================================ +# DEPLOYMENT STAGES - FLEXIBLE AUTHENTICATION +# ============================================================================ + +.deploy_template: &deploy_template + <<: *ansible_template + needs: ["prepare:auto-detect", "prepare:frontend-env", "build:docker"] + script: + - | + APP_VERSION=${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}} + + echo "🚀 Starting deployment with auto-scaling" + echo "App: ${APP_NAME}" + echo "Version: ${APP_VERSION}" + echo "Framework: ${FRAMEWORK:-}" + echo "Environment: ${ENVIRONMENT:-unknown}" + echo "Node.js: ${NODE_VERSION:-}" + echo "Host: ${DEPLOYMENT_SERVER_HOST:-not-set}:${DEPLOYMENT_SERVER_PORT:-not-set}" + echo "User: ${DEPLOYMENT_SERVER_USERNAME:-not-set}" + echo "Target Replicas: ${DEPLOYMENT_REPLICAS:-1}" + echo "Deployment Strategy: ${DEPLOYMENT_STRATEGY:-rolling}" + + # Validate required variables + if [ -z "$DEPLOYMENT_SERVER_HOST" ]; then + echo "❌ DEPLOYMENT_SERVER_HOST is required" + exit 1 + fi + + cd ci + CI_PROJECT_DIR="projects/${APP_NAME}" + if [ ! -d "$CI_PROJECT_DIR" ]; then + echo "📁 Folder '$CI_PROJECT_DIR' does not exist. Creating..." + mkdir -p "$CI_PROJECT_DIR/templates" + fi + if [[ ! -f "$CI_PROJECT_DIR/vars.yml" ]]; then + touch "$CI_PROJECT_DIR/vars.yml" + fi + if [ ! -d "$CI_PROJECT_DIR/templates" ]; then + mkdir -p "$CI_PROJECT_DIR/templates" + fi + + INVENTORY_FILE="inventory/${ENVIRONMENT}.yml" + + # Check if inventory file exists + if [[ ! -f "$INVENTORY_FILE" ]]; then + echo "Error: Inventory file not found: $INVENTORY_FILE" + exit 1 + fi + + AUTH_METHOD=${AUTH_METHOD:-password} + if [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" ]] || [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_PATH" ]]; then + AUTH_METHOD="ssh_key" + fi + + ENV_TAG=${ENV_TAG:-""} + if [[ "$ENV_TAG" == "worker" ]]; then + ENV_TAG="-${ENV_TAG}" + fi + + ANSIBLE_CMD="ansible-playbook -i $INVENTORY_FILE playbook.yml" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"app_name=${APP_NAME} deploy_env=${ENVIRONMENT} gitlab_ci_token=${CI_JOB_TOKEN} git_repo=${CI_REPOSITORY_URL} git_tag=${CI_COMMIT_TAG} git_sha=${CI_COMMIT_SHA} git_short_sha=${CI_COMMIT_SHORT_SHA}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_user=${DEPLOYMENT_SERVER_USERNAME} auth_method=${AUTH_METHOD:-password} deploy_strategy=${DEPLOY_TYPE:-docker} env_tag=${ENV_TAG}\"" + + # Auto-scaling configuration + if [[ -n "${DEPLOYMENT_REPLICAS:-}" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"replicas=${DEPLOYMENT_REPLICAS:-1}\"" + fi + if [[ -n "${DEPLOYMENT_MAX_REPLICAS:-}" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"max_replicas=${DEPLOYMENT_MAX_REPLICAS:-9}\"" + fi + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"deployment_strategy=${DEPLOYMENT_STRATEGY:-rolling}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"health_check_timeout=${HEALTH_CHECK_TIMEOUT:-30}\"" + + if [ "${ENVIRONMENT}" = "production" ]; then + if [[ -n "${CPU_LIMIT:-}" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"container_cpu_limit=${CPU_LIMIT}\"" + fi + + if [[ -n "${MEMORY_LIMIT:-}" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"container_memory_limit=${MEMORY_LIMIT}\"" + fi + fi + + # Add authentication and other parameters + if [[ -n "$DEPLOYMENT_SERVER_HOST" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_host=${DEPLOYMENT_SERVER_HOST}\"" + fi + + if [[ -n "$DEPLOYMENT_SERVER_PORT" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_port=${DEPLOYMENT_SERVER_PORT}\"" + fi + + if [[ "$AUTH_METHOD" == "password" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"server_password=${DEPLOYMENT_SERVER_PASSWORD} server_become_password=${DEPLOYMENT_SERVER_PASSWORD}\"" + elif [[ "$AUTH_METHOD" == "ssh_key" ]]; then + if [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_PATH" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"ssh_key_path=${DEPLOYMENT_SERVER_SSH_KEY_PATH}\"" + fi + if [[ -n "$DEPLOYMENT_SERVER_SSH_KEY_CONTENT" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"ssh_key_content=${DEPLOYMENT_SERVER_SSH_KEY_CONTENT}\"" + fi + fi + + if [[ -n "$GIT_USER" && -n "$GIT_TOKEN" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"git_username=$GIT_USER git_token=$GIT_TOKEN\"" + fi + + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"is_frontend=${IS_FRONTEND}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_registry=${CI_REGISTRY}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_project_path=${CI_PROJECT_PATH}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_image_name=${CI_REGISTRY_IMAGE}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_image_tag=${APP_VERSION}-${CI_COMMIT_SHORT_SHA}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"docker_image_full=${CI_REGISTRY_IMAGE}:${APP_VERSION}-${CI_COMMIT_SHORT_SHA}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"node_version=${NODE_VERSION:-}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"framework=${FRAMEWORK:-}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"package_manager=${PACKAGE_MANAGER:-}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"build_command=${BUILD_COMMAND:-}\"" + #ANSIBLE_CMD=" $ANSIBLE_CMD -e \"start_command=${START_COMMAND:-}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"container_port=${CONTAINER_PORT:-3000}\"" + ANSIBLE_CMD=" $ANSIBLE_CMD -e \"pipeline_id=${PIPELINE_ID}\"" + + export VAULT_ADDR="${VAULT_ADDR}" + + VAULT_PATH="${VAULT_ENGINE_NAME}/${ENVIRONMENT}" + export VAULT_PATH="${VAULT_PATH}" + + VAULT_RESPONSE=$(curl --silent --request POST \ + --data "{\"password\": \"${VAULT_PASSWORD}\"}" \ + "${VAULT_ADDR}/v1/auth/userpass/login/${VAULT_USERNAME}") + + #echo "🔐 Vault login response:" + #echo "$VAULT_RESPONSE" | jq + + VAULT_TOKEN=$(echo "$VAULT_RESPONSE" | jq -r '.auth.client_token') + + if [[ "$VAULT_TOKEN" == "null" || -z "$VAULT_TOKEN" ]]; then + echo "❌ Failed to get Vault token" + exit 1 + fi + + export VAULT_TOKEN="${VAULT_TOKEN}" + + VAULT_OUTPUT=$(vault kv get -format=json "$VAULT_PATH" 2>/dev/null || echo "") + + # Kiểm tra nếu không lấy được VAULT_OUTPUT + if [[ -z "$VAULT_OUTPUT" ]] || ! echo "$VAULT_OUTPUT" | jq empty >/dev/null 2>&1; then + echo "⚠️ Failed to get or parse Vault response for ${VAULT_PATH}" + else + SECRETS_JSON=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | @json') + + if [[ -n "$SECRETS_JSON" && "$SECRETS_JSON" != "null" ]]; then + echo "🔐 Vault setting for ${VAULT_PATH} will be used!" + + # Dùng jq để chuyển đổi JSON sang định dạng KEY="VALUE" + # to_entries[]: Chuyển object {"k1":"v1", "k2":"v2"} thành các dòng {"key":"k1", "value":"v1"} + # "\(.key)=\"\(.value)\"": Định dạng mỗi dòng thành KEY="VALUE" + #SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\"\(.value)\""') + SECRETS_TO_ENV=$(echo "$VAULT_OUTPUT" | jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"') + + # Kiểm tra xem có secret nào được tìm thấy không + if [[ -n "$SECRETS_TO_ENV" ]]; then + # Ghi các secret đã được định dạng vào file .env + echo "$SECRETS_TO_ENV" > "$CI_PROJECT_DIR/templates/.env.j2" + fi + fi + fi + + if [[ -n "$EXTRA_VARS" ]]; then + ANSIBLE_CMD=" $ANSIBLE_CMD --extra-vars '$EXTRA_VARS'" + fi + + echo $ANSIBLE_CMD + eval $ANSIBLE_CMD + + # --extra-vars '{ + # "database_host": "'$DEV_DATABASE_HOST'", + # "database_user": "'$DEV_DATABASE_USER'", + # "database_password": "'$DEV_DATABASE_PASSWORD'", + # "database_name": "myapp_dev", + # "redis_host": "'$DEV_REDIS_HOST'", + # "jwt_secret": "'$DEV_JWT_SECRET'", + # "app_url": "http://'$DEV_SERVER_HOST':3000" + # }' + + +# Development deployment +deploy:development: + extends: .deploy_template + stage: deploy + environment: + name: development + variables: + ENVIRONMENT: "development" + DEPLOYMENT_SERVER_HOST: "$DEVELOPMENT_SERVER_HOST" + DEPLOYMENT_SERVER_PORT: "$DEVELOPMENT_SERVER_PORT" + DEPLOYMENT_SERVER_USERNAME: "$DEVELOPMENT_SERVER_USERNAME" + DEPLOYMENT_SERVER_PASSWORD: "$DEVELOPMENT_SERVER_PASSWORD" + DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$DEVELOPMENT_SERVER_SSH_KEY_CONTENT" + DEPLOYMENT_SERVER_SSH_KEY_PATH: "$DEVELOPMENT_SERVER_SSH_KEY_PATH" + + # Auto-scaling configuration for development + DEPLOYMENT_REPLICAS: "$DEVELOPMENT_REPLICAS" + DEPLOYMENT_MAX_REPLICAS: "$DEVELOPMENT_MAX_REPLICAS" + DEPLOYMENT_STRATEGY: "$DEVELOPMENT_DEPLOYMENT_STRATEGY" + HEALTH_CHECK_TIMEOUT: "$DEVELOPMENT_HEALTH_CHECK_TIMEOUT" + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-dev\.\d+$/ + +# Staging deployment +deploy:staging: + extends: .deploy_template + stage: deploy + environment: + name: staging + variables: + ENVIRONMENT: "staging" + DEPLOYMENT_SERVER_HOST: "$STAGING_SERVER_HOST" + DEPLOYMENT_SERVER_PORT: "$STAGING_SERVER_PORT" + DEPLOYMENT_SERVER_USERNAME: "$STAGING_SERVER_USERNAME" + DEPLOYMENT_SERVER_PASSWORD: "$STAGING_SERVER_PASSWORD" + DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$STAGING_SERVER_SSH_KEY_CONTENT" + DEPLOYMENT_SERVER_SSH_KEY_PATH: "$STAGING_SERVER_SSH_KEY_PATH" + + # Auto-scaling configuration for staging + DEPLOYMENT_REPLICAS: "$STAGING_REPLICAS" + DEPLOYMENT_MAX_REPLICAS: "$STAGING_MAX_REPLICAS" + DEPLOYMENT_STRATEGY: "$STAGING_DEPLOYMENT_STRATEGY" + HEALTH_CHECK_TIMEOUT: "$STAGING_HEALTH_CHECK_TIMEOUT" + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-stg\.\d+$/ + +# Production deployment +deploy:production: + extends: .deploy_template + stage: deploy + environment: + name: production + variables: + ENVIRONMENT: "production" + DEPLOYMENT_SERVER_HOST: "$PRODUCTION_SERVER_HOST" + DEPLOYMENT_SERVER_PORT: "$PRODUCTION_SERVER_PORT" + DEPLOYMENT_SERVER_USERNAME: "$PRODUCTION_SERVER_USERNAME" + DEPLOYMENT_SERVER_PASSWORD: "$PRODUCTION_SERVER_PASSWORD" + DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$PRODUCTION_SERVER_SSH_KEY_CONTENT" + DEPLOYMENT_SERVER_SSH_KEY_PATH: "$PRODUCTION_SERVER_SSH_KEY_PATH" + + # Auto-scaling configuration for production + DEPLOYMENT_REPLICAS: "$PRODUCTION_REPLICAS" + DEPLOYMENT_MAX_REPLICAS: "$PRODUCTION_MAX_REPLICAS" + DEPLOYMENT_STRATEGY: "$PRODUCTION_DEPLOYMENT_STRATEGY" + HEALTH_CHECK_TIMEOUT: "$PRODUCTION_HEALTH_CHECK_TIMEOUT" + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ + when: manual + allow_failure: false + +# Development deployment for worker +deploy:development:worker: + extends: .deploy_template + stage: deploy + environment: + name: development + variables: + ENVIRONMENT: "development" + DEPLOYMENT_SERVER_HOST: "$DEVELOPMENT_SERVER_HOST" + DEPLOYMENT_SERVER_PORT: "$DEVELOPMENT_SERVER_PORT" + DEPLOYMENT_SERVER_USERNAME: "$DEVELOPMENT_SERVER_USERNAME" + DEPLOYMENT_SERVER_PASSWORD: "$DEVELOPMENT_SERVER_PASSWORD" + DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$DEVELOPMENT_SERVER_SSH_KEY_CONTENT" + DEPLOYMENT_SERVER_SSH_KEY_PATH: "$DEVELOPMENT_SERVER_SSH_KEY_PATH" + ENV_TAG: "worker" + + # Worker-specific auto-scaling (typically needs more replicas) + DEPLOYMENT_REPLICAS: "$DEVELOPMENT_WORKER_REPLICAS" + DEPLOYMENT_MAX_REPLICAS: "$DEVELOPMENT_WORKER_MAX_REPLICAS" + DEPLOYMENT_STRATEGY: "$DEVELOPMENT_WORKER_DEPLOYMENT_STRATEGY" + HEALTH_CHECK_TIMEOUT: "$DEVELOPMENT_HEALTH_CHECK_TIMEOUT" + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-dev\.\d+\-worker$/ + +# Staging deployment for worker +deploy:staging:worker: + extends: .deploy_template + stage: deploy + environment: + name: staging + variables: + ENVIRONMENT: "staging" + DEPLOYMENT_SERVER_HOST: "$STAGING_SERVER_HOST" + DEPLOYMENT_SERVER_PORT: "$STAGING_SERVER_PORT" + DEPLOYMENT_SERVER_USERNAME: "$STAGING_SERVER_USERNAME" + DEPLOYMENT_SERVER_PASSWORD: "$STAGING_SERVER_PASSWORD" + DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$STAGING_SERVER_SSH_KEY_CONTENT" + DEPLOYMENT_SERVER_SSH_KEY_PATH: "$STAGING_SERVER_SSH_KEY_PATH" + ENV_TAG: "worker" + + # Worker-specific auto-scaling (typically needs more replicas) + DEPLOYMENT_REPLICAS: "$STAGING_WORKER_REPLICAS" + DEPLOYMENT_MAX_REPLICAS: "$STAGING_WORKER_MAX_REPLICAS" + DEPLOYMENT_STRATEGY: "$STAGING_WORKER_DEPLOYMENT_STRATEGY" + HEALTH_CHECK_TIMEOUT: "$STAGING_HEALTH_CHECK_TIMEOUT" + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-stg\.\d+\-worker$/ + +# Production deployment for worker +deploy:production:worker: + extends: .deploy_template + stage: deploy + environment: + name: production + variables: + ENVIRONMENT: "production" + DEPLOYMENT_SERVER_HOST: "$PRODUCTION_SERVER_HOST" + DEPLOYMENT_SERVER_PORT: "$PRODUCTION_SERVER_PORT" + DEPLOYMENT_SERVER_USERNAME: "$PRODUCTION_SERVER_USERNAME" + DEPLOYMENT_SERVER_PASSWORD: "$PRODUCTION_SERVER_PASSWORD" + DEPLOYMENT_SERVER_SSH_KEY_CONTENT: "$PRODUCTION_SERVER_SSH_KEY_CONTENT" + DEPLOYMENT_SERVER_SSH_KEY_PATH: "$PRODUCTION_SERVER_SSH_KEY_PATH" + ENV_TAG: "worker" + + # Production worker auto-scaling + DEPLOYMENT_REPLICAS: "$PRODUCTION_WORKER_REPLICAS" + DEPLOYMENT_MAX_REPLICAS: "$PRODUCTION_WORKER_MAX_REPLICAS" + DEPLOYMENT_STRATEGY: "$PRODUCTION_WORKER_DEPLOYMENT_STRATEGY" + HEALTH_CHECK_TIMEOUT: "$PRODUCTION_HEALTH_CHECK_TIMEOUT" + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+\-worker$/ + when: manual + allow_failure: false + +# ============================================================================ +# VERIFICATION STAGE +# ============================================================================ + +#verify:deployment: +# stage: verify +# image: gitlab.forlife.one:5050/share/docker:alpine-latest +# needs: +# - job: deploy:development +# optional: true +# - job: deploy:staging +# optional: true +# before_script: +# - apk add --no-cache curl jq bc +# script: +# - | +# echo "🔍 Verifying deployment..." +# +# # Determine which environment was deployed +# if [ "$CI_COMMIT_BRANCH" = "dev" ]; then +# VERIFY_HOST="${DEVELOPMENT_HOST}" +# VERIFY_PORT="3001" +# elif [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then +# VERIFY_HOST="${STAGING_HOST}" +# VERIFY_PORT="3002" +# else +# echo "ℹ No verification needed for this branch" +# exit 0 +# fi +# +# echo "Verifying: http://${VERIFY_HOST}:${VERIFY_PORT}" +# +# # Basic connectivity test +# response=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "http://${VERIFY_HOST}:${VERIFY_PORT}/" || echo "000") +# if [ "$response" = "200" ] || [ "$response" = "301" ] || [ "$response" = "302" ]; then +# echo "✅ Basic connectivity test passed (HTTP $response)" +# else +# echo "❌ Basic connectivity test failed (HTTP $response)" +# exit 1 +# fi +# +# # Performance test +# load_time=$(curl -s -o /dev/null -w "%{time_total}" --max-time 15 "http://${VERIFY_HOST}:${VERIFY_PORT}/" || echo "999") +# echo "⏱️ Response time: ${load_time}s" +# +# if [ "$(echo "$load_time < 5.0" | bc)" = "1" ]; then +# echo "✅ Performance test passed" +# else +# echo "⚠️ Performance test warning: Response time is high (${load_time}s)" +# fi +# +# echo "✅ Deployment verification completed successfully" +# +# rules: +# - if: $CI_COMMIT_BRANCH == "dev" +# - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH +# allow_failure: true + +# ============================================================================ +# CLEANUP STAGE +# ============================================================================ + +cleanup:images: + <<: *docker_template + stage: cleanup + script: + - | + echo "🧹 Cleaning up old Docker images..." + + # Remove images older than 7 days + docker image prune -af --filter "until=168h" || true + + # Keep only last 5 versions per framework + if [ "${FRAMEWORK:-}" != "" ]; then + echo "Cleaning up old ${FRAMEWORK} images..." + # This would require additional logic to clean specific framework images + fi + + echo "✅ Cleanup completed" + + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + when: delayed + start_in: 2 hours + allow_failure: true + +# ============================================================================ +# MANUAL OPERATIONS +# ============================================================================ + +manual:rollback: + <<: *ansible_template + stage: deploy + script: + - | + echo "🔄 Rolling back ${APP_NAME} in ${ENVIRONMENT} environment" + echo "Target version: ${ROLLBACK_VERSION:-previous}" + echo "Target replicas: ${ROLLBACK_REPLICAS:-current}" + + cd ci + ansible-playbook \ + -i inventory/${ENVIRONMENT}.yml \ + playbook.yml \ + -e app_name="${APP_NAME}" \ + -e deploy_env="${ENVIRONMENT}" \ + -e rollback_version="${ROLLBACK_VERSION:-}" \ + -e rollback_replicas="${ROLLBACK_REPLICAS:-}" \ + -e force_rollback=true \ + --tags rollback + + echo "✅ Rollback completed" + + variables: + ENVIRONMENT: "production" + ROLLBACK_VERSION: "$ROLLBACK_VERSION" + ROLLBACK_REPLICAS: "$ROLLBACK_REPLICAS" + + when: manual + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/ && $CI_COMMIT_BRANCH == "main_rollback" + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+\-worker$/ && $CI_COMMIT_BRANCH == "main_rollback_worker" + allow_failure: false + +manual:emergency-scale: + <<: *ansible_template + stage: deploy + script: + - | + echo "🚨 Emergency scaling ${APP_NAME} to ${TARGET_REPLICAS} replicas" + echo "Environment: ${SCALE_ENVIRONMENT}" + + # Direct script execution for emergency scaling + ssh ${DEPLOYMENT_SERVER_USERNAME}@${DEPLOYMENT_SERVER_HOST} \ + "/home/${DEPLOYMENT_SERVER_USERNAME}/scripts/manage-${APP_NAME}-${SCALE_ENVIRONMENT}.sh scale ${TARGET_REPLICAS}" + + # Show status after scaling + ssh ${DEPLOYMENT_SERVER_USERNAME}@${DEPLOYMENT_SERVER_HOST} \ + "/home/${DEPLOYMENT_SERVER_USERNAME}/scripts/manage-${APP_NAME}-${SCALE_ENVIRONMENT}.sh status" + + variables: + SCALE_ENVIRONMENT: "$SCALE_ENVIRONMENT" + TARGET_REPLICAS: "$TARGET_REPLICAS" + DEPLOYMENT_SERVER_HOST: "$PRODUCTION_SERVER_HOST" + DEPLOYMENT_SERVER_USERNAME: "$PRODUCTION_SERVER_USERNAME" + + when: manual + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/ && $CI_COMMIT_BRANCH == "main_scale" + allow_failure: true + +# ============================================================================ +# NOTIFICATIONS +# ============================================================================ +notify:slack: + stage: .post + image: gitlab.forlife.one:5050/share/docker:alpine-latest + before_script: + - apk add --no-cache curl jq + script: + - | + echo "📢 Sending deployment notification with auto-scaling info..." + + # Determine status and emoji + if [ "$CI_JOB_STATUS" = "success" ]; then + color="good" + emoji="🎉" + status_text="SUCCESS" + else + color="danger" + emoji="❌" + status_text="FAILED" + fi + + # Determine environment + if echo "$CI_COMMIT_TAG" | grep -E "v[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+"; then + env_name="Development" + replicas="${DEV_REPLICAS:-2}" + strategy="${DEV_DEPLOYMENT_STRATEGY:-rolling}" + elif echo "$CI_COMMIT_TAG" | grep -E "v[0-9]+\.[0-9]+\.[0-9]+-stg\.[0-9]+"; then + env_name="Staging" + replicas="${STG_REPLICAS:-3}" + strategy="${STG_DEPLOYMENT_STRATEGY:-rolling}" + elif echo "$CI_COMMIT_TAG" | grep -E "v[0-9]+\.[0-9]+\.[0-9]+"; then + env_name="Production" + replicas="${PROD_REPLICAS:-5}" + strategy="${PROD_DEPLOYMENT_STRATEGY:-rolling}" + else + env_name="Unknown" + replicas="2" + strategy="rolling" + fi + + # Send Slack notification + curl -X POST -H 'Content-type: application/json' \ + --data "{ + \"text\": \"${emoji} Auto-Scaling Deployment ${status_text}\", + \"attachments\": [{ + \"color\": \"${color}\", + \"fields\": [ + {\"title\": \"Application\", \"value\": \"${APP_NAME}\", \"short\": true}, + {\"title\": \"Environment\", \"value\": \"${env_name}\", \"short\": true}, + {\"title\": \"Framework\", \"value\": \"${FRAMEWORK:-Node.js}\", \"short\": true}, + {\"title\": \"Version\", \"value\": \"${APP_VERSION}\", \"short\": true}, + {\"title\": \"Replicas\", \"value\": \"${replicas}\", \"short\": true}, + {\"title\": \"Strategy\", \"value\": \"${strategy}\", \"short\": true}, + {\"title\": \"Node.js\", \"value\": \"${NODE_VERSION:-auto-detected}\", \"short\": true}, + {\"title\": \"Branch\", \"value\": \"${CI_COMMIT_REF_NAME}\", \"short\": true}, + {\"title\": \"Commit\", \"value\": \"${CI_COMMIT_SHORT_SHA}\", \"short\": true}, + {\"title\": \"Pipeline\", \"value\": \"<${CI_PIPELINE_URL}|#${CI_PIPELINE_ID}>\", \"short\": true} + ], + \"footer\": \"GitLab CI/CD Auto-Scaling\", + \"ts\": $(date +%s) + }] + }" \ + "${SLACK_WEBHOOK_URL}" || echo "Failed to send Slack notification" + + rules: + - if: $SLACK_WEBHOOK_URL + when: always + allow_failure: true + +# ============================================================================ +# GITLAB CI VARIABLES CONFIGURATION EXAMPLES +# ============================================================================ + +# Example GitLab CI/CD Variables for auto-scaling: +# +# Development Environment: +# DEV_REPLICAS=2 +# DEV_MAX_REPLICAS=4 +# DEV_DEPLOYMENT_STRATEGY=rolling +# DEV_HEALTH_CHECK_TIMEOUT=30 +# +# Staging Environment: +# STG_REPLICAS=3 +# STG_MAX_REPLICAS=5 +# STG_DEPLOYMENT_STRATEGY=rolling +# STG_HEALTH_CHECK_TIMEOUT=30 +# +# Production Environment: +# PROD_REPLICAS=5 +# PROD_MAX_REPLICAS=10 +# PROD_DEPLOYMENT_STRATEGY=rolling +# PROD_HEALTH_CHECK_TIMEOUT=45 +# +# Worker Applications: +# DEV_WORKER_REPLICAS=3 +# DEV_WORKER_MAX_REPLICAS=6 +# PROD_WORKER_REPLICAS=8 +# PROD_WORKER_MAX_REPLICAS=15 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..921015a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,4 @@ +include: + - project: '$PROJECT_CI_ANSIBLE_PATH' + ref: 'main' + file: '.gitlab-ci.include.yml' diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..5140fd2 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +host_key_checking = False +timeout = 30 +deprecation_warnings = False +interpreter_python = auto_silent + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no diff --git a/group_vars/default.yml b/group_vars/default.yml new file mode 100644 index 0000000..c1cfaa0 --- /dev/null +++ b/group_vars/default.yml @@ -0,0 +1,109 @@ +--- +port_registry_file: "group_vars/port-registry.yml" + +# Thông tin triển khai +deploy_env: production # Môi trường triển khai: development hoặc production +app_name: "App" # Tên ứng dụng (sẽ được ghi đè) +app_port: 3000 # Port mặc định +node_env: "{{ deploy_env }}" # NODE_ENV +nodejs_version: "18" # Phiên bản Node.js mặc định +framework: expressjs # Framework (sẽ được ghi đè): expressjs, nestjs, nextjs, reactjs + +# Thông tin Git +git_repo: "" # Repo URL (bắt buộc ghi đè) +git_tag: "latest" # Tag/branch để checkout +git_username: "" # Username để clone repo (nếu private) +git_token: "" # Token để clone repo (nếu private) + +# Thông tin triển khai +deploy_strategy: "docker" # Phương thức triển khai (pm2, systemd, etc) +app_base_dir: "/var/www" # Thư mục cơ sở +app_dir: "{{ app_base_dir }}/{{ app_name }}-{{ deploy_env }}{{ env_tag }}" +deploy_dir: "{{ app_dir }}-{{ git_tag }}" +current_dir: "{{ app_dir }}-current" +keep_releases: 3 # Số lượng phiên bản cần giữ lại + +# Các lệnh ứng dụng +app_install_cmd: "npm ci" # Lệnh cài đặt dependencies +app_build_cmd: "npm run build" # Lệnh build ứng dụng +app_start_cmd: "" # Lệnh khởi động ứng dụng +app_start_cmds: # Lệnh khởi động ứng dụng theo framework + expressjs: "./dist/main.js" + nestjs: "./dist/main.js" + reactjs: "serve" + nextjs: "node_modules/next/dist/bin/next" +app_start_args: # Tham so cho lệnh khởi động ứng dụng + expressjs: "" + nestjs: "" + reactjs: "-s build" + nextjs: "start" +#app_start_cmd_override_worker: "./dist/cli.js sync_queue" #override start command for worker + +# Thông tin server +server_host: "" # Server IP/hostname (bắt buộc ghi đè) +server_port: "22" # Server Port +server_user: "" # Username SSH (bắt buộc ghi đè) +server_password: "" # Password SSH (để trống nếu dùng SSH key) +server_become_password: "" # Password sudo (để trống nếu dùng SSH key) +ssh_key_path: "" # Đường dẫn SSH private key file (để trống nếu dùng ssh_key_content) +ssh_key_content: "" # Nội dung SSH private key (từ GitLab CI/CD variables) +auth_method: "password" # Phương thức xác thực: "password" hoặc "ssh_key" + +# Environment-specific overrides +environment_config: + development: + short_key: "dev" + log_level: "debug" + enable_swagger: true + enable_hot_reload: true + database_logging: true + pm2_instances: 1 + pm2_max_memory: "1G" + production: + short_key: "prod" + log_level: "warn" + enable_swagger: false + enable_hot_reload: false + database_logging: false + pm2_instances: 1 + pm2_max_memory: "2G" + +# Path Configurations +entities_dir: "src/api/models" +migrations_dir: "src/database/migrations" +seeding_factories: "src/database/factories/**/*{.ts,.js}" +seeding_seeds: "src/database/seeds/**/*{.ts,.js}" +controllers_dir: "/api/controllers/**/*Controller{.ts,.js}" +cron_dir: "/api/cron-jobs/**/*Job{.ts,.js}" +middlewares_dir: "/infrastructure/middlewares/**/*{.ts,.js}" +subscribers_dir: "/api/subscribers/**/*Subscriber{.ts,.js}" +events_dir: "/api/events/**/*{.ts,.js}" +resolvers_dir: "/api/resolvers/**/*Resolver{.ts,.js}" + +# Health Check Configuration +health_check: + enabled: false + endpoint: "" + timeout: 5000 + interval: 30000 + +# Security Configuration +security: + cors_origins: "*" + helmet_enabled: true + rate_limit_enabled: true + request_size_limit: "50mb" + trust_proxy: true + +# Custom NPM Scripts +custom_scripts: + enabled: false + +# Deployment Configuration +pm2_restart_delay: 5000 + +# Auto-scaling configuration +replicas: 1 +max_replicas: 9 +health_check_timeout: 30 +deployment_timeout: 300 diff --git a/group_vars/port-registry.yml b/group_vars/port-registry.yml new file mode 100644 index 0000000..d66edc2 --- /dev/null +++ b/group_vars/port-registry.yml @@ -0,0 +1,11 @@ +# port-registry.yml +projects: + tarot-fe: + development: 3002 + production: 3002 + staging: 13002 + + tarot-be: + development: 3003 + production: 3003 + staging: 13003 diff --git a/group_vars/vault/development.yml b/group_vars/vault/development.yml new file mode 100644 index 0000000..329404e --- /dev/null +++ b/group_vars/vault/development.yml @@ -0,0 +1,6 @@ +--- +# default resource_limits +resource_limits: + development: + memory: "2g" + cpu: "1.0" diff --git a/group_vars/vault/production.yml b/group_vars/vault/production.yml new file mode 100644 index 0000000..b4dfe8c --- /dev/null +++ b/group_vars/vault/production.yml @@ -0,0 +1,6 @@ +--- +# default resource_limits +resource_limits: + production: + memory: "4g" + cpu: "2.0" diff --git a/inventory/development.yml b/inventory/development.yml new file mode 100644 index 0000000..62dbdbf --- /dev/null +++ b/inventory/development.yml @@ -0,0 +1,14 @@ +all: + hosts: + dev_server: + ansible_host: "{{ server_host if server_host is defined and server_host != '' else '10.252.231.175' }}" + ansible_port: "{{ server_port if server_port is defined and server_port != '' else '22' }}" + ansible_user: "{{ server_user }}" + # Cấu hình SSH linh động - password hoặc SSH key + ansible_password: "{{ server_password if auth_method == 'password' else omit }}" + ansible_ssh_private_key_file: "{{ ssh_key_path if (auth_method == 'ssh_key' and ssh_key_path != '') else omit }}" + ansible_become_password: "{{ server_become_password if auth_method == 'password' else omit }}" + ansible_sudo_pass: "{{ server_become_password if auth_method == 'password' else omit }}" + vars: + # Load vault variables + # vault_file: "defaults/vault/development.yml" \ No newline at end of file diff --git a/inventory/production.yml b/inventory/production.yml new file mode 100644 index 0000000..5f04898 --- /dev/null +++ b/inventory/production.yml @@ -0,0 +1,14 @@ +all: + hosts: + prod_server: + ansible_host: "{{ server_host if server_host is defined and server_host != '' else '10.252.252.10' }}" + ansible_port: "{{ server_port if server_port is defined and server_port != '' else '22' }}" + ansible_user: "{{ server_user }}" + # Cấu hình SSH linh động - password hoặc SSH key + ansible_password: "{{ server_password if auth_method == 'password' else omit }}" + ansible_ssh_private_key_file: "{{ ssh_key_path if (auth_method == 'ssh_key' and ssh_key_path != '') else omit }}" + ansible_become_password: "{{ server_become_password if auth_method == 'password' else omit }}" + ansible_sudo_pass: "{{ server_become_password if auth_method == 'password' else omit }}" + vars: + # Load vault variables + # vault_file: "defaults/vault/production.yml" \ No newline at end of file diff --git a/playbook.yml b/playbook.yml new file mode 100644 index 0000000..71eb86b --- /dev/null +++ b/playbook.yml @@ -0,0 +1,136 @@ +--- +- name: Deploy Application with Auto-Scaling + hosts: "{{ target_hosts | default('all') }}" + become: yes + become_method: sudo + vars_files: + - group_vars/default.yml + - "group_vars/vault/{{ deploy_env }}.yml" + + vars: + # Deployment configuration + deploy_strategy: "{{ deploy_strategy | default('docker') }}" + docker_image_tag: "{{ docker_image_tag | default('latest') }}" + + # Environment detection + is_production: "{{ deploy_env == 'production' }}" + is_staging: "{{ deploy_env == 'staging' }}" + is_development: "{{ deploy_env == 'development' }}" + + pre_tasks: + - name: Validate required variables + assert: + that: + - app_name is defined and app_name != "" + - git_repo is defined and git_repo != "" + - server_user is defined and server_user != "" + - deploy_env is defined and deploy_env != "" + fail_msg: "Missing required variables: app_name, git_repo, server_user, and deploy_env must be defined" + + - name: Validate auto-scaling configuration + assert: + that: + - replicas | int >= 1 + - replicas | int <= max_replicas | int + - deployment_strategy in ['rolling', 'blue-green', 'recreate'] + fail_msg: "Invalid auto-scaling configuration. Replicas must be between 1 and {{ max_replicas }}, and deployment_strategy must be one of: rolling, blue-green, recreate" + + - name: Load project-specific variables + include_vars: "projects/{{ app_name }}/vars.yml" + when: + - lookup('file', 'projects/' + app_name + '/vars.yml', errors='ignore') + + - name: Override deployment strategy from project vars + set_fact: + deploy_strategy: "{{ deploy_strategy | default('docker') }}" + + - name: Ensure port registry exists + file: + path: "{{ port_registry_file }}" + state: touch + delegate_to: localhost + run_once: true + + - name: Initialize port registry if empty + copy: + content: "projects: {}" + dest: "{{ port_registry_file }}" + force: no + delegate_to: localhost + run_once: true + + tasks: + # ============================================================================ + # DOCKER DEPLOYMENT WITH AUTO-SCALING + # ============================================================================ + - name: Include docker role with auto-scaling + include_role: + name: docker + when: deploy_strategy in ['simple-docker', 'docker'] + + # ============================================================================ + # POST-DEPLOYMENT VERIFICATION + # ============================================================================ + - name: Wait for deployment to settle + pause: + seconds: 10 + when: deploy_strategy in ['simple-docker', 'docker'] + + # ============================================================================ + # CLEANUP AND MAINTENANCE + # ============================================================================ + - name: Cleanup old Docker images + shell: | + sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh clean + become_user: "{{ server_user }}" + when: + - deploy_strategy in ['simple-docker', 'docker'] + - cleanup_old_images | default(true) + ignore_errors: yes + + # ============================================================================ + # DEPLOYMENT SUMMARY + # ============================================================================ + - name: Generate deployment summary + set_fact: + deployment_summary: + application: "{{ app_name }}" + environment: "{{ deploy_env }}{{ env_tag | default('') }}" + version: "{{ docker_image_tag }}" + replicas: "{{ replicas }}" + strategy: "{{ deployment_strategy }}" + deploy_strategy: "{{ deploy_strategy }}" + timestamp: "{{ ansible_date_time.iso8601 }}" + duration: "{{ ansible_play_duration | default('unknown') }}" + + - name: Display deployment summary + debug: + msg: + - "🎉 DEPLOYMENT SUMMARY" + - "====================" + - "Application: {{ deployment_summary.application }}" + - "Environment: {{ deployment_summary.environment }}" + - "Version: {{ deployment_summary.version }}" + - "Replicas: {{ deployment_summary.replicas }}" + - "Strategy: {{ deployment_summary.strategy }}" + - "Timestamp: {{ deployment_summary.timestamp }}" + + - name: Save deployment record + copy: + content: "{{ deployment_summary | to_nice_yaml }}" + dest: "/home/{{ server_user }}/ci/logs/{{ app_name }}/{{ deploy_env }}/deployment-{{ ansible_date_time.epoch }}.yml" + when: save_deployment_record | default(true) + + # ============================================================================ + # HANDLERS + # ============================================================================ + handlers: + - name: reload nginx + service: + name: nginx + state: reloaded + when: ansible_service_mgr == "systemd" + + # ============================================================================ + # POST TASKS + # ============================================================================ diff --git a/roles/docker/defaults/main.yml b/roles/docker/defaults/main.yml new file mode 100644 index 0000000..81cab86 --- /dev/null +++ b/roles/docker/defaults/main.yml @@ -0,0 +1,19 @@ +--- +# Docker configuration +service_name: "{{ app_name }}-{{ deploy_env }}{{ env_tag }}" +#docker_registry: "{{ ansible_env.CI_REGISTRY | default('gitlab.forlife.one') }}" +#docker_project_path: "{{ ansible_env.CI_PROJECT_PATH | default('') }}" +#docker_image_tag: "{{ ansible_env.CI_COMMIT_TAG | default('latest') }}" +#docker_image_full: "{{ ansible_env.CI_REGISTRY_IMAGE }}:{ ansible_env.CI_COMMIT_TAG | default(docker_image_tag) }" +#docker_image_name: "{{ ansible_env.CI_REGISTRY_IMAGE }}" +docker_image_rollback: "{{ app_name }}:{{ previous_version.stdout | default('latest') }}" + +# Directory configuration +deploy_dir: "{{ '/home/' + server_user + '/ci/deployments/' + app_name + '/' + deploy_env }}" +current_dir: "{{ '/home/' + server_user + '/ci/deployments/' + app_name + '/current' }}" + +# GitLab CI integration +#gitlab_ci_user: "{{ ansible_env.CI_REGISTRY_USER | default('gitlab-ci-token') }}" + +# Health Check Configuration +health_path: "" diff --git a/roles/docker/handlers/main.yml b/roles/docker/handlers/main.yml new file mode 100644 index 0000000..fe28b19 --- /dev/null +++ b/roles/docker/handlers/main.yml @@ -0,0 +1,86 @@ +--- +# ============================================================================ +# NGINX HANDLERS FOR AUTO-SCALING DEPLOYMENT +# ============================================================================ +# if nginx install by aaPanel: run command > mkdir -p /etc/nginx/conf.d/ +# if nginx install by aaPanel: run command > ln -fs /www/server/nginx/sbin/nginx /usr/bin/nginx +# if nginx install by aaPanel: run command > echo "map $http_upgrade $connection_upgrade { default upgrade; '' close; }" > /www/server/panel/vhost/nginx/0.websocket.conf +# if nginx install by aaPanel: run command > echo "include /etc/nginx/conf.d/*.conf;" > /www/server/panel/vhost/nginx/forlife-upstream.conf + +- name: test nginx configuration + command: nginx -t + become: yes + listen: test nginx configuration + +- name: reload nginx + shell: | + # Test nginx configuration + nginx -t && /etc/init.d/nginx reload + + # Wait for reload + sleep 3 + become: yes + when: ansible_service_mgr == "systemd" + listen: reload nginx + +- name: webapplication purge cache + shell: | + filepath="/www/server/scripts/cloudflare-purge-cache.sh" + if [ -f "$filepath" ]; then bash "$filepath"; fi + become: yes + when: ansible_service_mgr == "systemd" + listen: webapplication purge cache + +- name: restart nginx + shell: | + # Test nginx configuration + nginx -t && /etc/init.d/nginx restart + + # Wait for reload + sleep 3 + become: yes + when: ansible_service_mgr == "systemd" + listen: restart nginx + +- name: validate nginx upstream + shell: | + # Test nginx configuration + nginx -t && /etc/init.d/nginx reload + + # Wait for reload + sleep 3 + become: yes + ignore_errors: true + listen: validate nginx upstream + +# service: +# name: nginx +# state: reloaded +# become: yes +# when: ansible_service_mgr == "systemd" +# listen: reload nginx + +#- name: restart nginx +# service: +# name: nginx +# state: restarted +# become: yes +# when: ansible_service_mgr == "systemd" +# listen: restart nginx + +#- name: validate nginx upstream +# shell: | +# # Test nginx configuration +# nginx -t && nginx -s reload +# +# # Wait for reload +# sleep 3 +# +# # Test upstream connectivity +# if command -v curl >/dev/null 2>&1; then +# echo "Testing upstream connectivity..." +# curl -f -s --max-time 10 http://localhost/health || echo "⚠️ Upstream health check skipped" +# fi +# become: yes +# ignore_errors: true +# listen: validate nginx upstream diff --git a/roles/docker/tasks/main.yml b/roles/docker/tasks/main.yml new file mode 100644 index 0000000..f0b079e --- /dev/null +++ b/roles/docker/tasks/main.yml @@ -0,0 +1,650 @@ +--- +# ============================================================================ +# ENHANCED DOCKER DEPLOYMENT WITH AUTO-SCALING SUPPORT +# ============================================================================ + +# ============================================================================ +# SYSTEM VALIDATION AND REQUIREMENTS CHECK +# ============================================================================ + +- name: Validate deployment environment + debug: + msg: + - "=== AUTO-SCALING DEPLOYMENT VALIDATION ===" + - "Target OS: {{ ansible_distribution }} {{ ansible_distribution_version }}" + - "Architecture: {{ ansible_architecture }}" + - "Available memory: {{ ansible_memtotal_mb }}MB" + - "Available disk: {{ ansible_mounts[0].size_available / 1024 / 1024 / 1024 | round(2) }}GB" + - "Target replicas: {{ docker_replicas | default(replicas | default(1)) }}" + - "Deployment strategy: {{ docker_deployment_strategy | default(deployment_strategy | default('rolling')) }}" + +- name: Check minimum system requirements for auto-scaling + assert: + that: + - ansible_memtotal_mb >= (docker_replicas | default(1) | int * 512) + - (ansible_mounts[0].size_available / 1024 / 1024 / 1024) >= 5 + - docker_replicas | default(1) | int <= docker_max_replicas | default(9) | int + fail_msg: | + Insufficient system resources for {{ docker_replicas | default(1) }} replicas. + Need at least {{ docker_replicas | default(1) | int * 512 }}MB RAM and 5GB disk space. + Replicas ({{ docker_replicas | default(1) }}) cannot exceed max_replicas ({{ docker_max_replicas | default(9) }}). + success_msg: "✅ System requirements met for auto-scaling deployment." + +# ============================================================================ +# PROJECT-SPECIFIC CONFIGURATION LOADING +# ============================================================================ +- name: Load project-specific vars if exists + block: + - name: Check if project-specific variables file exists + stat: + path: "projects/{{ app_name }}/vars.yml" + register: project_vars_file + + - name: Load project-specific variables + include_vars: + file: "projects/{{ app_name }}/vars.yml" + when: project_vars_file.stat.exists + + - name: Check if project-specific variables by environment file exists + stat: + path: "projects/{{ app_name }}/group_vars/{{ deploy_env }}.yml" + register: project_vars_env_file + + - name: Load project-specific variables by environment + include_vars: + file: "projects/{{ app_name }}/group_vars/{{ deploy_env }}.yml" + when: project_vars_env_file.stat.exists + delegate_to: localhost + run_once: true + +- name: Load port registry + include_vars: + file: "{{ port_registry_file | default('group_vars/port_registry.yml') }}" + name: port_registry + delegate_to: localhost + run_once: true + when: port_registry_file is defined + +# ============================================================================ +# DEPLOYMENT CONFIGURATION SETUP +# ============================================================================ + +- name: Set deployment environment tag + set_fact: + deploy_env_tag: "{{ deploy_env }}{{ env_tag | default('') }}" + +- name: Set auto-scaling configuration + set_fact: + # Auto-scaling parameters + final_replicas: "{{ docker_replicas | default(replicas | default(1)) }}" + final_max_replicas: "{{ docker_max_replicas | default(max_replicas | default(9)) }}" + final_deployment_strategy: "{{ docker_deployment_strategy | default(deployment_strategy | default('rolling')) }}" + final_health_check_timeout: "{{ docker_health_check_timeout | default(health_check_timeout | default(30)) }}" + final_deployment_timeout: "{{ docker_deployment_timeout | default(deployment_timeout | default(300)) }}" + + # Port configuration + final_app_port: "{{ port_registry.projects[app_name][deploy_env_tag] | default(3000) }}" + final_container_port: "{{ container_port | default(3000) }}" + + # Resource configuration + final_memory_limit: "{{ container_memory_limit | default(resource_limits[deploy_env].memory | default('1g')) }}" + final_cpu_limit: "{{ container_cpu_limit | default(resource_limits[deploy_env].cpu | default('1.0')) }}" + + # Network configuration + final_docker_network: "{{ docker_network_name | default(app_name + '-' + deploy_env + '-network') }}" + final_restart_policy: "{{ container_restart_policy | default('unless-stopped') }}" + +- name: Display auto-scaling deployment configuration + debug: + msg: + - "🚀 AUTO-SCALING DEPLOYMENT CONFIGURATION" + - "========================================" + - "Application: {{ app_name }}" + - "Environment: {{ deploy_env_tag }}" + - "Framework: {{ framework | default('unknown') }}" + - "Docker Image: {{ docker_image_name }}:{{ docker_image_tag }}" + - "Target Replicas: {{ final_replicas }}" + - "Max Replicas: {{ final_max_replicas }}" + - "Deployment Strategy: {{ final_deployment_strategy }}" + - "Base Port: {{ final_app_port }} (Pattern: {{ final_app_port }}{{ replica_number | default(1) }})" + - "Container Port: {{ final_container_port }}" + - "Memory Limit: {{ final_memory_limit }}" + - "CPU Limit: {{ final_cpu_limit }}" + - "Health Check Timeout: {{ final_health_check_timeout }}s" + - "Deployment Timeout: {{ final_deployment_timeout }}s" + +# ============================================================================ +# PRE-DEPLOYMENT VALIDATION +# ============================================================================ + +- name: Validate auto-scaling deployment parameters + assert: + that: + - app_name is defined and app_name != "" + - deploy_env in ['development', 'staging', 'production'] + - docker_image_tag is defined and docker_image_tag != "" + - final_replicas | int >= 1 + - final_replicas | int <= final_max_replicas | int + - final_deployment_strategy in ['rolling', 'blue-green', 'recreate'] + - docker_registry is defined + fail_msg: "Missing or invalid auto-scaling deployment parameters" + success_msg: "✅ Auto-scaling deployment parameters validated" + tags: [validate] + +# ============================================================================ +# DIRECTORY AND ENVIRONMENT SETUP +# ============================================================================ + +- name: Create deployment directories + file: + path: "{{ item }}" + state: directory + owner: "{{ server_user }}" + group: "{{ server_user }}" + mode: '0755' + loop: + - "/home/{{ server_user }}/scripts" + - "/home/{{ server_user }}/ci" + - "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}" + - "/home/{{ server_user }}/ci/data/{{ app_name }}/{{ deploy_env }}" + - "/home/{{ server_user }}/ci/logs/{{ app_name }}/{{ deploy_env }}" + - "/home/{{ server_user }}/ci/backups/{{ app_name }}/{{ deploy_env }}" + - "/home/{{ server_user }}/ci/deployments/{{ app_name }}/{{ deploy_env }}" + +- name: Create environment file from vault if secrets_dict is defined + copy: + dest: "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env" + content: | + {% for key, value in secrets_dict.items() %} + {{ key }}={{ value }} + {% endfor %} + when: secrets_dict is defined + +- name: Load project-specific env if exists + block: + - name: Check if project-specific .env template exists by environment + stat: + path: "projects/{{ app_name }}/templates/.env.{{ deploy_env }}.j2" + register: custom_env_template_by_env + delegate_to: localhost + run_once: true + + - name: Create environment file from project template by environment + template: + src: "projects/{{ app_name }}/templates/.env.{{ deploy_env }}.j2" + dest: "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env" + owner: "{{ server_user }}" + group: "{{ server_user }}" + mode: '0600' + backup: yes + when: env_file_enabled | default(true) and custom_env_template_by_env.stat.exists + + - name: Check if project-specific .env template exists + stat: + path: "projects/{{ app_name }}/templates/.env.j2" + register: custom_env_template + delegate_to: localhost + run_once: true + + - name: Create environment file from project template + template: + src: "projects/{{ app_name }}/templates/.env.j2" + dest: "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env" + owner: "{{ server_user }}" + group: "{{ server_user }}" + mode: '0600' + backup: yes + when: env_file_enabled | default(true) and custom_env_template.stat.exists and not custom_env_template_by_env.stat.exists + + - name: Create environment file with default template + template: + src: .env.j2 + dest: "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env" + owner: "{{ server_user }}" + group: "{{ server_user }}" + mode: '0600' + backup: yes + when: not custom_env_template_by_env.stat.exists and not custom_env_template.stat.exists + vars: + # Pass auto-scaling configuration to template + auto_scaling_config: + replicas: "{{ final_replicas }}" + max_replicas: "{{ final_max_replicas }}" + deployment_strategy: "{{ final_deployment_strategy }}" + base_port: "{{ final_app_port }}" + container_port: "{{ final_container_port }}" + tags: [environment] + when: secrets_dict is not defined + +# ============================================================================ +# DOCKER ENVIRONMENT SETUP +# ============================================================================ + +- name: Setup Docker environment for auto-scaling + block: + - name: Check if Docker is installed + shell: | + command -v docker + register: docker_check + failed_when: false + changed_when: false + + - name: Install Docker (Debian/Ubuntu) + apt: + name: docker.io + state: present + update_cache: yes + become: yes + when: + - docker_check.rc != 0 + - ansible_os_family == "Debian" + + - name: Install Docker (CentOS/RHEL) + yum: + name: docker + state: present + become: yes + when: + - docker_check.rc != 0 + - ansible_os_family == "RedHat" + + # Kiểm tra xem Python đã có Docker SDK chưa + - name: Check if Docker SDK for Python is installed + command: python3 -c "import docker" + register: docker_sdk_check + failed_when: false + changed_when: false + + # Cài Docker SDK bằng pip nếu chưa có + - name: Install Docker SDK for Python via apt (nếu không muốn dùng pip) + apt: + name: python3-docker + state: present + become: yes + when: + - ansible_os_family == "Debian" + - docker_sdk_check.rc != 0 + + - name: Ensure Docker service is started and enabled + service: + name: docker + state: started + enabled: true + + # - name: Create Docker network for auto-scaling deployment + # docker_network: + # name: "{{ final_docker_network }}" + # state: present + # driver: bridge + # ipam_config: + # - subnet: "172.{{ (final_app_port | string)[-2:] | int + 20 }}.0.0/16" + + - name: Gather Docker networks on host + shell: docker network inspect $(docker network ls -q) 2>/dev/null || echo '[]' + register: docker_networks_raw + changed_when: false + failed_when: false + + - name: Parse Docker networks JSON + set_fact: + docker_networks_info: "{{ docker_networks_raw.stdout | from_json }}" + + - name: Extract existing subnets (as strings) from docker networks + set_fact: + existing_subnets: >- + {{ + docker_networks_info | default([]) + | selectattr('IPAM', 'defined') + | selectattr('IPAM.Config', 'defined') + | map(attribute='IPAM.Config') + | select + | map('first') + | selectattr('Subnet', 'defined') + | map(attribute='Subnet') + | list + }} + + - name: Debug existing subnets + debug: + var: existing_subnets + + - name: Get used second-octets from existing subnets (172.X.0.0/16 only) + set_fact: + used_seconds: >- + {%- set result = [] -%} + {%- for subnet in existing_subnets -%} + {%- if subnet.startswith('172.') -%} + {%- set parts = subnet.split('.') -%} + {%- if parts | length >= 2 -%} + {%- set _ = result.append(parts[1] | int) -%} + {%- endif -%} + {%- endif -%} + {%- endfor -%} + {{ result }} + + - name: Debug used second octets + debug: + var: used_seconds + + - name: Compute start octet from final_app_port (bounded 20..250) + set_fact: + start_octet: >- + {{ + (((final_app_port | string)[-2:] | int) + 20) + if (((final_app_port | string)[-2:] | int) + 20) <= 250 + else 250 + }} + + - name: Build candidate second-octet list (start -> 250 then 20 -> start-1) + set_fact: + candidate_list: >- + {{ + (range(start_octet | int, 251) | list) + (range(20, start_octet | int) | list) + }} + + - name: Debug candidate list + debug: + msg: "Candidates: {{ candidate_list }}, Used: {{ used_seconds }}" + + - name: Select first unused second-octet + set_fact: + chosen_second: >- + {{ + candidate_list + | map('int') + | reject('in', used_seconds) + | list + | first + }} + + - name: Fail if couldn't find free subnet octet + fail: + msg: "No available 172.X.0.0/16 subnet found in candidate space (20..250). Existing used: {{ used_seconds }}" + when: chosen_second is not defined + + - name: Build chosen subnet + set_fact: + chosen_subnet: "172.{{ chosen_second }}.0.0/16" + + - name: Debug chosen subnet + debug: + msg: "Creating Docker network '{{ final_docker_network }}' with subnet {{ chosen_subnet }}" + + - name: Create Docker network for auto-scaling deployment + community.docker.docker_network: + name: "{{ final_docker_network }}" + state: present + driver: bridge + ipam_config: + - subnet: "{{ chosen_subnet }}" + become: true + + + + - name: Pull Docker image for auto-scaling deployment + docker_image: + name: "{{ docker_image_full | default(docker_image_name + ':' + docker_image_tag) }}" + source: pull + force_source: true + register: docker_pull_result + + tags: [docker] + +# ============================================================================ +# ENHANCED DOCKER MANAGEMENT SCRIPT GENERATION +# ============================================================================ + +- name: Generate enhanced Docker management script with auto-scaling + template: + src: manage-docker.sh.j2 + dest: "/home/{{ server_user }}/scripts/manage-{{ service_name }}.sh" + owner: "{{ server_user }}" + group: "{{ server_user }}" + mode: '0755' + backup: yes + vars: + # Container configuration + server_user: "{{ server_user }}" + container_name: "{{ service_name }}" + framework: "{{ framework }}" + + # Auto-scaling configuration + default_replicas: "{{ final_replicas }}" + max_replicas: "{{ final_max_replicas }}" + health_check_timeout: "{{ final_health_check_timeout }}" + deployment_timeout: "{{ final_deployment_timeout }}" + + # Port configuration + app_port: "{{ final_app_port }}" + container_port: "{{ final_container_port }}" + + # Resource configuration + container_restart_policy: "{{ final_restart_policy }}" + container_memory_limit: "{{ final_memory_limit }}" + container_cpu_limit: "{{ final_cpu_limit }}" + + # Network configuration + docker_network_name: "{{ final_docker_network }}" + +# ============================================================================ +# AUTO-SCALING DEPLOYMENT EXECUTION +# ============================================================================ + +- name: Check current deployment state + shell: | + if [ -f "/home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh" ]; then + sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh status 2>/dev/null || echo "no-containers" + else + echo "no-script" + fi + become_user: "{{ server_user }}" + register: current_deployment_state + changed_when: false + +- name: Display current deployment state + debug: + msg: "{{ current_deployment_state.stdout_lines }}" + when: current_deployment_state.stdout_lines is defined + +- name: Ensure Docker is installed and user has permissions + block: + - name: Add user to docker group + user: + name: "{{ server_user }}" + groups: docker + append: yes + become: yes + + - name: Reset SSH connection for group changes + meta: reset_connection + + - name: Test Docker access + command: docker version + register: docker_test + failed_when: docker_test.rc != 0 + +- name: Execute smart deployment with auto-scaling + shell: | + sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh deploy {{ replicas | default(1) }} {{ docker_image_tag }} rolling + async: 600 + poll: 10 # Poll every 10 seconds instead of 0 + register: deployment_result + become_user: "{{ server_user }}" + +- name: Check deployment result + fail: + msg: "Deployment failed: {{ deployment_result.stderr | default('Unknown error') }}" + when: + - deployment_result.rc is defined + - deployment_result.rc != 0 + +# ============================================================================ +# POST-DEPLOYMENT VERIFICATION +# ============================================================================ + +- name: Wait for auto-scaling deployment to settle + pause: + seconds: 5 + tags: [health] + +- name: Verify auto-scaling deployment completion + shell: | + sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh health + become_user: "{{ server_user }}" + register: auto_scaling_health_check + retries: 5 + delay: 15 + until: auto_scaling_health_check.rc == 0 + tags: [health] + +- name: Get final auto-scaling deployment status + shell: | + sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh status + become_user: "{{ server_user }}" + register: final_auto_scaling_status + changed_when: false + tags: [health] + +- name: Extract auto-scaling deployment metrics + set_fact: + auto_scaling_metrics: + running_containers: "{{ final_auto_scaling_status.stdout | regex_search('Running Containers: (\\d+)', '\\1') | first | default('0') }}" + current_version: "{{ final_auto_scaling_status.stdout | regex_search('Current Version: ([^\\n]+)', '\\1') | first | default('unknown') }}" + base_port: "{{ final_auto_scaling_status.stdout | regex_search('Base Port: (\\d+)', '\\1') | first | default('0') }}" + +# - name: Validate auto-scaling deployment metrics +# assert: +# that: +# - auto_scaling_metrics.running_containers | int == final_replicas | int +# - auto_scaling_metrics.current_version == docker_image_tag +# fail_msg: | +# Auto-scaling deployment validation failed: +# Expected: {{ final_replicas }} replicas with version {{ docker_image_tag }} +# Actual: {{ auto_scaling_metrics.running_containers }} replicas with version {{ auto_scaling_metrics.current_version }} +# success_msg: "✅ Auto-scaling deployment validation passed" + +# ============================================================================ +# LOAD BALANCER INTEGRATION +# ============================================================================ +- name: Get active ports from script + shell: | + sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh deploy_active_ports + become_user: "{{ server_user }}" + register: active_ports_result + when: env_tag | default('') != '-worker' + +- name: Convert ports to clean list + set_fact: + active_ports_list: "{{ active_ports_result.stdout.split() }}" + when: env_tag | default('') != '-worker' and active_ports_result.stdout | length > 0 + +- name: Generate nginx upstream configuration + template: + src: nginx-upstream.conf.j2 + dest: "/etc/nginx/conf.d/{{ app_name }}-{{ deploy_env }}-upstream.conf" + backup: yes + vars: + upstream_name: "{{ app_name | regex_replace('[^A-Za-z0-9_]', '_') }}_{{ deploy_env }}_backend" + base_port: "{{ final_app_port }}" + replica_count: "{{ final_replicas }}" + load_balancing_method: "{{ nginx_config.load_balancing_method | default('least_conn') }}" + max_fails: "{{ nginx_config.max_fails | default(1) }}" + fail_timeout: "{{ nginx_config.fail_timeout | default(10) }}" + active_ports: "{{ active_ports_list }}" + when: + - update_load_balancer | default(true) + - env_tag | default('') != '-worker' + notify: + - test nginx configuration + - reload nginx + - webapplication purge cache + tags: [loadbalancer] + +# ============================================================================ +# DEPLOYMENT SUCCESS SUMMARY +# ============================================================================ + +- name: Display auto-scaling deployment success + debug: + msg: + - "🎉 AUTO-SCALING DEPLOYMENT SUCCESS" + - "==================================" + - "✅ Successfully deployed {{ app_name }} ({{ framework | default('unknown') }})" + - "🔢 Replicas: {{ auto_scaling_metrics.running_containers }}/{{ final_replicas }}" + - "🏷️ Version: {{ auto_scaling_metrics.current_version }}" + - "🚀 Strategy: {{ final_deployment_strategy }}" + - "🌐 Base Port: {{ auto_scaling_metrics.base_port }}" + - "🏥 Health Status: All containers healthy" + - "⚖️ Load Balancer: Updated with {{ final_replicas }} targets" + - "" + - "🔧 Management Commands:" + - " Status: ./scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh status" + - " Scale: ./scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh scale " + - " Logs: ./scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh logs" + - " Health: ./scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh health" + tags: [info] + +# ============================================================================ +# CLEANUP AND MAINTENANCE +# ============================================================================ + +- name: Cleanup old Docker images (keep last 5 versions) + shell: | + # Enhanced cleanup for auto-scaling deployment + sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh clean + become_user: "{{ server_user }}" + ignore_errors: true + when: cleanup_old_images | default(true) + tags: [cleanup] + +# ============================================================================ +# DEPLOYMENT RECORD AND AUDIT TRAIL +# ============================================================================ + +- name: Create auto-scaling deployment record + copy: + content: | + # Auto-Scaling Deployment Record + timestamp: "{{ ansible_date_time.iso8601 }}" + application: "{{ app_name }}" + environment: "{{ deploy_env }}{{ env_tag | default('') }}" + version: "{{ docker_image_tag }}" + replicas: {{ final_replicas }} + max_replicas: {{ final_max_replicas }} + strategy: "{{ final_deployment_strategy }}" + base_port: "{{ final_app_port }}" + framework: "{{ framework | default('unknown') }}" + node_version: "{{ node_version | default('unknown') }}" + status: "success" + duration: "{{ auto_scaling_deployment_result.delta | default('unknown') }}" + operator: "{{ ansible_user_id }}" + git_commit: "{{ git_sha | default('unknown') }}" + git_tag: "{{ git_tag | default('unknown') }}" + memory_limit: "{{ final_memory_limit }}" + cpu_limit: "{{ final_cpu_limit }}" + docker_network: "{{ final_docker_network }}" + health_check_timeout: "{{ final_health_check_timeout }}" + deployment_timeout: "{{ final_deployment_timeout }}" + load_balancer_updated: {{ update_load_balancer | default(true) }} + management_script: "/home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh" + dest: "/home/{{ server_user }}/ci/logs/{{ app_name }}/{{ deploy_env }}/deployment-{{ ansible_date_time.epoch }}.yml" + owner: "{{ server_user }}" + group: "{{ server_user }}" + mode: '0644' + tags: [logging] + +- name: Register auto-scaling deployment in system log + lineinfile: + path: "/var/log/microservices-deployments.log" + line: "{{ ansible_date_time.iso8601 }} | {{ app_name }} | {{ deploy_env }} | {{ docker_image_tag }} | {{ git_tag }} | AUTO-SCALE-SUCCESS | {{ final_replicas }}@{{ auto_scaling_metrics.base_port }} | {{ framework | default('unknown') }}" + create: true + mode: '0644' + tags: [logging] + +- name: Cleanup old backups file + shell: | + ls -1t /home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env.*~ 2>/dev/null | tail -n +4 | xargs -r rm -- + ls -1t /home/{{ server_user }}/scripts/manage-{{ service_name }}.sh.*~ 2>/dev/null | tail -n +4 | xargs -r rm -- + ls -1t /etc/nginx/conf.d/{{ app_name }}-{{ deploy_env }}-upstream.conf.*~ 2>/dev/null | tail -n +4 | xargs -r rm -- + args: + warn: false \ No newline at end of file diff --git a/roles/docker/templates/.env.j2 b/roles/docker/templates/.env.j2 new file mode 100644 index 0000000..8e8fc53 --- /dev/null +++ b/roles/docker/templates/.env.j2 @@ -0,0 +1,228 @@ +# ============================================================================ +# AUTO-GENERATED ENVIRONMENT FILE FOR AUTO-SCALING DEPLOYMENT +# Application: {{ app_name }} +# Environment: {{ deploy_env }}{{ env_tag | default('') }} +# Generated: {{ ansible_date_time.iso8601 }} +# Ansible User: {{ ansible_user_id }} +# ============================================================================ + +# Application Configuration +NODE_ENV={{ deploy_env }} +APP_NAME={{ app_name }} +APP_VERSION={{ docker_image_tag }} +DEPLOY_ENV={{ deploy_env }} +INSTANCE_ID={{ app_name }}-{{ deploy_env }} + +# Port Configuration +PORT={{ final_container_port | default(container_port) | default(3000) }} +HOST=0.0.0.0 + +# Auto-Scaling Configuration +{% if auto_scaling_config is defined %} +REPLICAS={{ auto_scaling_config.replicas }} +MAX_REPLICAS={{ auto_scaling_config.max_replicas }} +DEPLOYMENT_STRATEGY={{ auto_scaling_config.deployment_strategy }} +BASE_PORT={{ auto_scaling_config.base_port }} +CONTAINER_PORT={{ auto_scaling_config.container_port }} +{% else %} +REPLICAS={{ final_replicas | default(2) }} +MAX_REPLICAS={{ final_max_replicas | default(5) }} +DEPLOYMENT_STRATEGY={{ final_deployment_strategy | default('rolling') }} +BASE_PORT={{ final_app_port | default(3000) }} +CONTAINER_PORT={{ final_container_port | default(3000) }} +{% endif %} + +# Framework Configuration +{% if framework is defined %} +FRAMEWORK={{ framework }} +{% endif %} + +{% if node_version is defined %} +NODE_VERSION={{ node_version }} +{% endif %} + +{% if package_manager is defined %} +PACKAGE_MANAGER={{ package_manager }} +{% endif %} + +# Docker Configuration +DOCKER_NETWORK={{ final_docker_network | default(app_name + '-' + deploy_env + '-network') }} +RESTART_POLICY={{ final_restart_policy | default('unless-stopped') }} +MEMORY_LIMIT={{ final_memory_limit | default('1g') }} +CPU_LIMIT={{ final_cpu_limit | default('1.0') }} + +# Health Check Configuration +{% if health_check is defined %} +HEALTH_CHECK_PATH={{ health_check.path | default('/health') }} +HEALTH_CHECK_TIMEOUT={{ health_check.timeout | default(10) }} +HEALTH_CHECK_INTERVAL={{ health_check.interval | default(30) }} +HEALTH_CHECK_RETRIES={{ health_check.retries | default(3) }} +{% else %} +HEALTH_CHECK_PATH={{ health_path | default('/health') }} +HEALTH_CHECK_TIMEOUT=10 +HEALTH_CHECK_INTERVAL=30 +HEALTH_CHECK_RETRIES=3 +{% endif %} + +# Git Information +{% if git_tag is defined %} +GIT_TAG={{ git_tag }} +{% endif %} +{% if git_sha is defined %} +GIT_COMMIT={{ git_sha }} +{% endif %} +{% if git_branch is defined %} +GIT_BRANCH={{ git_branch }} +{% endif %} + +# Build Information +BUILD_DATE={{ ansible_date_time.iso8601 }} +PIPELINE_ID={{ pipeline_id | default('unknown') }} + +# Environment-Specific Variables +{% if environment_variables is defined and environment_variables[deploy_env] is defined %} +{% for key, value in environment_variables[deploy_env].items() %} +{{ key }}={{ value }} +{% endfor %} +{% endif %} + +# Legacy env_vars support (backward compatibility) +{% if env_vars is defined %} +{% for key, value in env_vars.items() %} +{{ key }}={{ value }} +{% endfor %} +{% endif %} + +# Project-Specific Variables +{% if project_env_vars is defined and project_env_vars[deploy_env] is defined %} +{% for key, value in project_env_vars[deploy_env].items() %} +{{ key }}={{ value }} +{% endfor %} +{% endif %} + +# Database Configuration (if applicable) +{% if database_config is defined %} +{% for key, value in database_config.items() %} +{{ key | upper }}={{ value }} +{% endfor %} +{% endif %} + +# Queue Configuration (if applicable) +{% if queue_config is defined %} +{% for key, value in queue_config.items() %} +{{ key | upper }}={{ value }} +{% endfor %} +{% endif %} + +# Monitoring Configuration +{% if monitoring is defined %} +{% if monitoring.enable_metrics %} +ENABLE_METRICS=true +METRICS_PORT={{ monitoring.metrics_port | default(9090) }} +METRICS_PATH={{ monitoring.metrics_path | default('/metrics') }} +{% endif %} +{% if monitoring.enable_tracing %} +ENABLE_TRACING=true +JAEGER_ENDPOINT={{ monitoring.jaeger_endpoint | default('http://jaeger:14268/api/traces') }} +{% endif %} +{% if monitoring.log_aggregation %} +LOG_AGGREGATION=true +LOG_FORMAT={{ monitoring.log_format | default('json') }} +{% endif %} +{% endif %} + +# Load Balancer Information +LB_UPSTREAM_NAME={{ app_name }}_{{ deploy_env }}_backend +LB_METHOD={{ nginx_config.load_balancing_method | default('least_conn') }} + +# Service Discovery +SERVICE_NAME={{ app_name }} +SERVICE_VERSION={{ docker_image_tag }} +SERVICE_ENVIRONMENT={{ deploy_env }} + +# Auto-scaling Metadata for Application +AUTOSCALE_ENABLED=true +AUTOSCALE_TARGET_REPLICAS={{ final_replicas | default(2) }} +AUTOSCALE_MAX_REPLICAS={{ final_max_replicas | default(5) }} +AUTOSCALE_STRATEGY={{ final_deployment_strategy | default('rolling') }} + +# Deployment Metadata +DEPLOYMENT_TIMESTAMP={{ ansible_date_time.epoch }} +DEPLOYMENT_USER={{ ansible_user_id }} +DEPLOYMENT_HOST={{ ansible_host }} + +# Security Configuration +{% if security_config is defined %} +{% for key, value in security_config.items() %} +{{ key | upper }}={{ value }} +{% endfor %} +{% endif %} + +# Feature Flags +{% if feature_flags is defined %} +{% for key, value in feature_flags.items() %} +FEATURE_{{ key | upper }}={{ value }} +{% endfor %} +{% endif %} + +# Next.js Specific (if framework is nextjs) +{% if framework == 'nextjs' %} +NEXT_TELEMETRY_DISABLED=1 +{% if environment_variables is defined and environment_variables[deploy_env] is defined %} +{% for key, value in environment_variables[deploy_env].items() %} +{% if key.startswith('NEXT_PUBLIC_') %} +{{ key }}={{ value }} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} + +# React Specific (if framework is reactjs) +{% if framework == 'reactjs' or framework == 'reactjs-vite' %} +{% if environment_variables is defined and environment_variables[deploy_env] is defined %} +{% for key, value in environment_variables[deploy_env].items() %} +{% if key.startswith('REACT_APP_') %} +{{ key }}={{ value }} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} + +# NestJS Specific (if framework is nestjs) +{% if framework == 'nestjs' %} +NEST_DEBUG=false +{% if deploy_env == 'development' %} +NEST_DEBUG=true +{% endif %} +{% endif %} + +# Express Specific (if framework is expressjs) +{% if framework == 'expressjs' %} +EXPRESS_ENV={{ deploy_env }} +TRUST_PROXY=true +{% endif %} + +# Worker Specific (if it's a worker deployment) +{% if env_tag is defined and 'worker' in env_tag %} +WORKER_CONCURRENCY={{ worker_config.concurrency | default(5) }} +WORKER_TIMEOUT={{ worker_config.timeout | default(30000) }} +QUEUE_NAME={{ worker_config.queue_name | default('default') }} +QUEUE_RETRY_ATTEMPTS={{ worker_config.retry_attempts | default(3) }} +{% endif %} + +# ============================================================================ +# AUTO-SCALING DEPLOYMENT INFORMATION +# ============================================================================ +# This file was auto-generated for auto-scaling deployment +# Management script: /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh +# +# Scale command: ./manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh scale +# Status command: ./manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh status +# Health command: ./manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh health +# Logs command: ./manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh logs +# +# Port Pattern: {{ final_app_port | default(3000) }}{replica_number} +# Current Replicas: {{ final_replicas | default(2) }} +# Max Replicas: {{ final_max_replicas | default(5) }} +# Strategy: {{ final_deployment_strategy | default('rolling') }} +# ============================================================================ diff --git a/roles/docker/templates/dockerfiles/expressjs.Dockerfile.j2 b/roles/docker/templates/dockerfiles/expressjs.Dockerfile.j2 new file mode 100644 index 0000000..7c0ca23 --- /dev/null +++ b/roles/docker/templates/dockerfiles/expressjs.Dockerfile.j2 @@ -0,0 +1,93 @@ +# Express.js Optimized Dockerfile +# Auto-generated for {{ app_name }} +# Node.js Version: {{ node_version }} +# Package Manager: {{ package_manager }} + +# ============================================================================ +# Build arguments +# ============================================================================ +ARG NODE_VERSION={{ node_version }} +ARG PACKAGE_MANAGER={{ package_manager }} +ARG BUILD_COMMAND={{ build_command }} +ARG APP_PORT={{ framework_settings_port }} +ARG NODE_ENV=production + +# ============================================================================ +# Base stage +# ============================================================================ +FROM gitlab.forlife.one:5050/share/node:${NODE_VERSION}-alpine AS base + +# Install dependencies for native modules and health checks +RUN apk update && apk add imagemagick && apk add graphicsmagick && apk add tmux && apk add ffmpeg + +RUN apk add --no-cache \ + libc6-compat \ + python3 \ + make \ + g++ \ + curl \ + dumb-init + +# ============================================================================ +# Production stage +# ============================================================================ +FROM base AS production + +COPY . ./app + +WORKDIR /app + +RUN npm install + +# Build arguments +ARG GIT_COMMIT=unknown +ARG BUILD_DATE=unknown + +# Set build environment +ENV NODE_ENV=production + +# Create build info endpoint +RUN mkdir -p src/common && \ + echo 'export const BUILD_INFO = {"version":"{{ docker_image_tag }}","commit":"'${GIT_COMMIT}'","buildDate":"'${BUILD_DATE}'","framework":"expressjs","nodeVersion":"{{ node_version }}"}' > src/common/build-info.ts + +# Build app +RUN npm run build + +# Create non-root user +RUN addgroup --system --gid 1001 expressjs && adduser --system --uid 1001 expressjs + +# Ensure dist folder is owned by expressjs user +RUN chown -R expressjs:expressjs /app/dist + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=${APP_PORT} +ENV APP_PORT=${APP_PORT} +ENV NODE_OPTIONS="--max-old-space-size={{ MEMORY_LIMIT_MB }}" + +RUN echo '#!/bin/sh\n\ +status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${APP_PORT}/health)\n\ +if [ "$status" -eq 200 ] || [ "$status" -eq 404 ]; then exit 0; fi\n\ +\n\ +status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${APP_PORT}/)\n\ +if [ "$status" -eq 200 ] || [ "$status" -eq 404 ]; then exit 0; fi\n\ +\n\ +exit 1' > /health-check.sh && \ + chmod +x /health-check.sh + +# Create logs directory +RUN mkdir -p ./logs && chown -R expressjs:expressjs ./logs +RUN ([ -d "./src/public" ] && cp -r ./src/public ./dist && chown -R expressjs:expressjs ./dist/public) || true + +# Switch to non-root user +USER expressjs + +# Expose port +EXPOSE ${APP_PORT} + +# Health check +#HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 CMD /health-check.sh + +# Start the application with dumb-init for proper signal handling +#CMD ["dumb-init", "node", "dist/main"] +CMD {{ RUN_CMD }} diff --git a/roles/docker/templates/dockerfiles/laravel.Dockerfile.j2 b/roles/docker/templates/dockerfiles/laravel.Dockerfile.j2 new file mode 100644 index 0000000..edd899b --- /dev/null +++ b/roles/docker/templates/dockerfiles/laravel.Dockerfile.j2 @@ -0,0 +1,106 @@ +# Define a build-time variable for the PHP version, with a default of 8.1 +ARG PHP_VERSION={{ php_version }} + +# Stage 1: Build Stage (Optional but good practice) +# Use the PHP_VERSION argument in the FROM instruction +FROM gitlab.forlife.one:5050/share/php:${PHP_VERSION}-fpm-alpine AS base + +# Set working directory inside the container +WORKDIR /var/www/html + +# ----------------------------------------------- +# 1. INSTALL SYSTEM DEPENDENCIES & PHP EXTENSIONS +# ----------------------------------------------- +# Added redis extension installation via PECL. +RUN apk update && apk add --no-cache \ + git \ + curl \ + libpng-dev \ + libjpeg-turbo-dev \ + libwebp-dev \ + freetype-dev \ + zip \ + unzip \ + libzip-dev \ + oniguruma-dev \ + autoconf \ + g++ \ + make \ + nginx \ + supervisor \ + dcron \ + postgresql-dev \ + libxml2-dev \ + && pecl install redis \ + && docker-php-ext-enable redis \ + # Configure and install other extensions + && docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \ + && docker-php-ext-install -j$(nproc) gd pdo_mysql pdo_pgsql opcache bcmath exif pcntl sockets zip mbstring dom \ + # Clean up + && rm -rf /var/cache/apk/* + +# Setup Laravel scheduler cron job to run every minute +# This cron job will be run by the 'www-data' user +RUN echo "* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1" > /etc/crontabs/www-data + +# ============================================================================ +# Production stage +# ============================================================================ +FROM base AS production + +WORKDIR /var/www/html + +# ----------------------------------------------- +# 2. APPLICATION SETUP & DEPENDENCIES +# ----------------------------------------------- + +# Install Composer v2, suitable for modern PHP versions +#COPY --from=composer:latest /usr/bin/composer /usr/bin/composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Copy the Laravel application files +COPY . . + +# Install Composer dependencies (Assuming composer.lock is present) +RUN composer install --no-dev --optimize-autoloader --no-interaction + +# Optimize Laravel +RUN php artisan optimize && \ + php artisan route:clear && \ + php artisan route:cache && \ + php artisan config:clear && \ + php artisan config:cache && \ + php artisan view:clear && \ + php artisan view:cache + +#RUN php artisan storage:link + +# remove this line if you do not want to run migrations on each build +#RUN php artisan migrate --force + +# Set appropriate permissions for Laravel (www-data is the default FPM user on Alpine) +RUN chown -R www-data:www-data /var/www/html \ + && chmod -R 775 storage bootstrap/cache +RUN ([ -d "public/cms/backend/uploads" ] && chmod -R 775 public/cms/backend/uploads) || true + +# ----------------------------------------------- +# 3. CONFIGURATION & ENTRYPOINT (THE FIX) +# ----------------------------------------------- +RUN mkdir -p /run/nginx \ + && chown -R www-data:www-data /run/nginx \ + && chown -R www-data:www-data /var/lib/nginx + +# Copy Nginx Configuration (Assuming it's in the 'docker' subdirectory) +# Nginx listens on port 80 and proxies PHP requests to FPM (port 9000). +COPY docker/nginx.conf /etc/nginx/nginx.conf +COPY docker/laravel.conf /etc/nginx/http.d/site.conf +COPY docker/php.ini /usr/local/etc/php/php.ini + +# Copy Supervisor Configuration (Assuming it's in the 'docker' subdirectory) +# Supervisor manages both Nginx and PHP-FPM processes. +COPY docker/supervisord.conf /etc/supervisord.conf + +EXPOSE 3000 + +# Start Supervisor, which runs Nginx, PHP-FPM, and Cron +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] \ No newline at end of file diff --git a/roles/docker/templates/dockerfiles/nestjs.Dockerfile.j2 b/roles/docker/templates/dockerfiles/nestjs.Dockerfile.j2 new file mode 100644 index 0000000..98e3758 --- /dev/null +++ b/roles/docker/templates/dockerfiles/nestjs.Dockerfile.j2 @@ -0,0 +1,94 @@ +# Express.js Optimized Dockerfile +# Auto-generated for {{ app_name }} +# Node.js Version: {{ node_version }} +# Package Manager: {{ package_manager }} + +# ============================================================================ +# Build arguments +# ============================================================================ +ARG NODE_VERSION={{ node_version }} +ARG PACKAGE_MANAGER={{ package_manager }} +ARG BUILD_COMMAND={{ build_command }} +ARG APP_PORT={{ framework_settings_port }} +ARG NODE_ENV=production + +# ============================================================================ +# Base stage +# ============================================================================ +FROM gitlab.forlife.one:5050/share/node:${NODE_VERSION}-alpine AS base + +# Install dependencies for native modules and health checks +RUN apk update && apk add imagemagick && apk add graphicsmagick && apk add tmux && apk add ffmpeg + +RUN apk add --no-cache \ + libc6-compat \ + python3 \ + make \ + g++ \ + curl \ + dumb-init + +# ============================================================================ +# Production stage +# ============================================================================ +FROM base AS production + +COPY . ./app + +WORKDIR /app + +RUN yarn install + +# Build arguments +ARG GIT_COMMIT=unknown +ARG BUILD_DATE=unknown + +# Set build environment +ENV NODE_ENV=production + +# Create build info endpoint +RUN mkdir -p src/common && \ + echo 'export const BUILD_INFO = {"version":"{{ docker_image_tag }}","commit":"'${GIT_COMMIT}'","buildDate":"'${BUILD_DATE}'","framework":"nestjs","nodeVersion":"{{ node_version }}"}' > src/common/build-info.ts + +# Build app +RUN npm run build + +# Create non-root user +RUN addgroup --system --gid 1001 nestjs && adduser --system --uid 1001 nestjs + +# Ensure dist folder is owned by nestjs user +RUN chown -R nestjs:nestjs /app/dist + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=${APP_PORT} +ENV APP_PORT=${APP_PORT} +ENV NODE_OPTIONS="--max-old-space-size={{ MEMORY_LIMIT_MB }}" + +# Create health check script +RUN echo '#!/bin/sh\n\ +status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${APP_PORT}/health)\n\ +if [ "$status" -eq 200 ] || [ "$status" -eq 404 ]; then exit 0; fi\n\ +\n\ +status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${APP_PORT}/)\n\ +if [ "$status" -eq 200 ] || [ "$status" -eq 404 ]; then exit 0; fi\n\ +\n\ +exit 1' > /health-check.sh && \ + chmod +x /health-check.sh + +# Create logs directory +RUN mkdir -p ./logs && chown -R nestjs:nestjs ./logs +RUN ([ -d "./src/public" ] && cp -r ./src/public ./dist && chown -R nestjs:nestjs ./dist/public) || true + +# Switch to non-root user +USER nestjs + +# Expose port +EXPOSE ${APP_PORT} + +# Health check +#HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 CMD /health-check.sh + +# Start the application with dumb-init for proper signal handling +#CMD ["dumb-init", "node", "dist/main"] +CMD {{ RUN_CMD }} diff --git a/roles/docker/templates/dockerfiles/nextjs.Dockerfile.j2 b/roles/docker/templates/dockerfiles/nextjs.Dockerfile.j2 new file mode 100644 index 0000000..1aceefd --- /dev/null +++ b/roles/docker/templates/dockerfiles/nextjs.Dockerfile.j2 @@ -0,0 +1,103 @@ +# Next.js Optimized Dockerfile with Dynamic Build Args +# Auto-generated for {{ app_name }} +# Node.js Version: {{ node_version }} +# Package Manager: {{ package_manager }} + +# ============================================================================ +# Build arguments - Base Configuration +# ============================================================================ +ARG NODE_VERSION={{ node_version }} +ARG PACKAGE_MANAGER={{ package_manager }} +ARG BUILD_COMMAND={{ build_command }} +ARG START_COMMAND={{ start_command }} +ARG APP_PORT={{ framework_settings_port }} +ARG NODE_ENV=production +ARG APP_VERSION={{ docker_image_tag }} +ARG FRAMEWORK=nextjs + +# ============================================================================ +# Base stage - Alpine for smaller size +# ============================================================================ +FROM gitlab.forlife.one:5050/share/node:${NODE_VERSION}-alpine AS base + +# Install dependencies +RUN apk add --no-cache \ + libc6-compat \ + python3 \ + make \ + g++ curl + +# Install dependencies only when needed +FROM base AS deps + +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \ + else {{ package_manager }} install; \ + fi + +#RUN {{ package_manager }} install + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +# ENV NEXT_TELEMETRY_DISABLED=1 + +RUN yarn build + +# If using npm comment out above and use below instead +# RUN npm run build + +# Production image, copy all the files and run next +FROM base AS production +WORKDIR /app + +ENV NODE_ENV=production +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_OPTIONS="--max-old-space-size={{ MEMORY_LIMIT_MB }}" + + +# Create health check script +RUN echo '#!/bin/sh\n\ +status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${APP_PORT}/health)\n\ +if [ "$status" -eq 200 ] || [ "$status" -eq 404 ]; then exit 0; fi\n\ +\n\ +status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${APP_PORT}/)\n\ +if [ "$status" -eq 200 ] || [ "$status" -eq 404 ]; then exit 0; fi\n\ +\n\ +exit 1' > /health-check.sh && \ + chmod +x /health-check.sh + +# You only need to copy next.config.js if you are NOT using the default configuration +# COPY --from=builder /app/next.config.js ./ +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +EXPOSE 3000 + +ENV HOSTNAME=0.0.0.0 +ENV PORT=3000 +ENV APP_PORT=3000 + +# Health check +#HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 CMD /health-check.sh + +#CMD ["node", "server.js"] +CMD {{ RUN_CMD }} diff --git a/roles/docker/templates/dockerfiles/reactjs.Dockerfile.j2 b/roles/docker/templates/dockerfiles/reactjs.Dockerfile.j2 new file mode 100644 index 0000000..dc67fd8 --- /dev/null +++ b/roles/docker/templates/dockerfiles/reactjs.Dockerfile.j2 @@ -0,0 +1,189 @@ +# React.js Optimized Dockerfile (No Nginx) +# Auto-generated for {{ app_name }} +# Node.js Version: {{ node_version }} +# Package Manager: {{ package_manager }} + +# ============================================================================ +# Build arguments +# ============================================================================ +ARG NODE_VERSION={{ node_version }} +ARG PACKAGE_MANAGER={{ package_manager }} +ARG BUILD_COMMAND={{ build_command }} +ARG APP_PORT={{ framework_settings_port }} + +# ============================================================================ +# Base stage +# ============================================================================ +FROM gitlab.forlife.one:5050/share/node:${NODE_VERSION}-alpine AS base + +# Install dependencies +RUN apk add --no-cache \ + libc6-compat \ + python3 \ + make \ + g++ curl + +WORKDIR /app + +# ============================================================================ +# Dependencies stage +# ============================================================================ +FROM base AS deps + +# Copy package files +COPY package*.json yarn.lock* pnpm-lock.yaml* ./ + +# Install dependencies +#RUN yarn install +RUN {{ package_manager }} install + +# ============================================================================ +# Builder stage +# ============================================================================ +FROM base AS builder + +# Copy dependencies +COPY --from=deps /app/node_modules ./node_modules + +# Copy source code +COPY . . + +# Build arguments for runtime configuration +ARG GIT_COMMIT=unknown +ARG BUILD_DATE=unknown +ARG REACT_APP_VERSION={{ docker_image_tag }} + +# Set build environment +ENV NODE_ENV=production +ENV GENERATE_SOURCEMAP=false +ENV REACT_APP_VERSION=${REACT_APP_VERSION} +ENV REACT_APP_BUILD_DATE=${BUILD_DATE} +ENV REACT_APP_GIT_COMMIT=${GIT_COMMIT} + +# Create build info file +RUN echo '{"version":"{{ docker_image_tag }}","commit":"'${GIT_COMMIT}'","buildDate":"'${BUILD_DATE}'","framework":"reactjs","nodeVersion":"{{ node_version }}"}' > public/build-info.json + +# Require BuildKit enabled +# Mount secret nếu tồn tại, copy vào thư mục app +RUN --mount=type=secret,id=env,target=/run/secrets/env \ + cp /run/secrets/env .env || echo ".env not found" + +# ============================================================================ +# Build the application +# ============================================================================ +RUN \ + if [ -f yarn.lock ]; then \ + yarn build; \ + elif [ -f package-lock.json ]; then \ + npm run build; \ + elif [ -f pnpm-lock.yaml ]; then \ + pnpm build; \ + else \ + npm run build; \ + fi + +# ============================================================================ +# Production stage with serve +# ============================================================================ +FROM node:${NODE_VERSION}-alpine AS production + +# Install curl for health checks +RUN apk add --no-cache curl + +WORKDIR /app + +# Install serve globally for serving static files +RUN npm install -g serve + +# Copy built React app +COPY --from=builder /app/build ./build + +# Create package.json for serving +RUN echo '{ \ + "name": "{{ app_name }}-server", \ + "version": "{{ docker_image_tag }}", \ + "scripts": { \ + "start": "serve -s build -l 3000" \ + }, \ + "dependencies": { \ + "serve": "latest" \ + } \ +}' > package.json + +# Create serve.json configuration +RUN echo '{ \ + "public": "build", \ + "rewrites": [ \ + { "source": "**", "destination": "/index.html" } \ + ], \ + "headers": [ \ + { \ + "source": "**/*.@(jpg|jpeg|gif|png|svg|ico|css|js)", \ + "headers": [ \ + { \ + "key": "Cache-Control", \ + "value": "public, max-age=31536000, immutable" \ + } \ + ] \ + }, \ + { \ + "source": "**", \ + "headers": [ \ + { \ + "key": "X-Content-Type-Options", \ + "value": "nosniff" \ + }, \ + { \ + "key": "X-Frame-Options", \ + "value": "DENY" \ + }, \ + { \ + "key": "X-XSS-Protection", \ + "value": "1; mode=block" \ + } \ + ] \ + } \ + ] \ +}' > serve.json + +# Create health check endpoint +RUN mkdir -p build && echo 'healthy' > build/health.txt + +# Create health check script +RUN echo '#!/bin/sh\n\ +status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${APP_PORT}/health)\n\ +if [ "$status" -eq 200 ] || [ "$status" -eq 404 ]; then exit 0; fi\n\ +\n\ +status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${APP_PORT}/)\n\ +if [ "$status" -eq 200 ] || [ "$status" -eq 404 ]; then exit 0; fi\n\ +\n\ +exit 1' > /health-check.sh && \ + chmod +x /health-check.sh + +# Create non-root user +RUN addgroup --system --gid 1001 reactapp && \ + adduser --system --uid 1001 reactapp + +# Set proper permissions +RUN chown -R reactapp:reactapp /app + +# Switch to non-root user +USER reactapp + +# Expose port +EXPOSE 3000 + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3000 +ENV APP_PORT=3000 +ENV NODE_OPTIONS="--max-old-space-size={{ MEMORY_LIMIT_MB }}" + +# Health check +#HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 CMD /health-check.sh + +# Start the application +#CMD ["npm", "start"] + +#CMD [ "serve", "-s", "build" ] +CMD {{ RUN_CMD }} diff --git a/roles/docker/templates/health-check.sh b/roles/docker/templates/health-check.sh new file mode 100644 index 0000000..13b3412 --- /dev/null +++ b/roles/docker/templates/health-check.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -e # Thoát ngay lập tức nếu có lệnh nào thất bại + +echo "Checking /health endpoint..." +health_status=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/health") + +if [ "$health_status" -eq 200 ] || [ "$health_status" -eq 404 ]; then + echo "/health check passed with status ${health_status}" + exit 0 +fi + +echo "/health check failed with status ${health_status}. Checking / endpoint..." +root_status=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/") + +if [ "$root_status" -eq 200 ] || [ "$root_status" -eq 404 ]; then + echo "/ check passed with status ${root_status}" + exit 0 +fi + +echo "/ check failed with status ${root_status}. Health check failed overall." +exit 1 \ No newline at end of file diff --git a/roles/docker/templates/manage-docker.sh.j2 b/roles/docker/templates/manage-docker.sh.j2 new file mode 100644 index 0000000..0fdd44c --- /dev/null +++ b/roles/docker/templates/manage-docker.sh.j2 @@ -0,0 +1,1779 @@ +#!/bin/bash + +# ============================================================================ +# ENHANCED DOCKER MANAGEMENT SCRIPT WITH AUTO-SCALING +# Application: {{ app_name }} +# Environment: {{ deploy_env }}{{ env_tag | default('') }} +# Generated: {{ ansible_date_time.iso8601 }} +# ============================================================================ + +set -eo pipefail + +# ============================================================================ +# CONFIGURATION & ENVIRONMENT DETECTION +# ============================================================================ + +# Extract APP_NAME and ENVIRONMENT from script name +SCRIPT_NAME=$(basename "$0") +SCRIPT_NAME="${SCRIPT_NAME/-worker/}" +if [[ "$SCRIPT_NAME" =~ manage-(.+)-(.+)\.sh ]]; then + APP_NAME="${BASH_REMATCH[1]}" + ENVIRONMENT="${BASH_REMATCH[2]}" +else + echo "❌ Error: Could not extract APP_NAME and ENVIRONMENT from script name: $SCRIPT_NAME" + echo "Expected format: manage--.sh" + exit 1 +fi + +# Default configuration +FRAMEWORK={{ framework }} +DEFAULT_REPLICAS={{ default_replicas | default(2) }} +MAX_REPLICAS={{ max_replicas | default(5) }} +HEALTH_CHECK_TIMEOUT={{ health_check_timeout | default(60) }} +DEPLOYMENT_TIMEOUT={{ deployment_timeout | default(300) }} +DEFAULT_LOG_LINES=100 + +HOME="/home/{{ server_user }}" +# Environment-specific configuration +CONFIG_DIR="$HOME/ci/configs/${APP_NAME}/${ENVIRONMENT}" +DATA_DIR="$HOME/ci/data/${APP_NAME}/${ENVIRONMENT}" +BACKUP_DIR="$HOME/ci/backups/${APP_NAME}/${ENVIRONMENT}" +LOG_DIR="$HOME/ci/logs/${APP_NAME}/${ENVIRONMENT}" + +# Docker configuration +DOCKER_REGISTRY="{{ docker_registry | default('registry.gitlab.com') }}" +DOCKER_IMAGE="{{ docker_image_name }}" +DOCKER_NETWORK="{{ docker_network_name }}" +CONTAINER_PREFIX="{{ container_name }}" +CURRENT_TAG="{{ docker_image_tag }}" + +# Port management +BASE_PORT={{ app_port }} +CONTAINER_PORT={{ container_port }} +BINDING_PORT=0 + +# Resource configuration +RESTART_POLICY="{{ container_restart_policy | default('unless-stopped') }}" +MEMORY_LIMIT="{{ container_memory_limit | default('') }}" +CPU_LIMIT="{{ container_cpu_limit | default('') }}" + +CONTAINER_MAPS_FILE="/var/lib/container_maps.json" +CONTAINER_LOCK_FILE="/var/lib/container_maps.lock" +declare -A IS_CONTAINER_MAP_NEW +# Khởi tạo associative array +declare -A container_map +NGINX_CONF_DIR="/etc/nginx/conf.d" +NGINX_CONF_FILE="$NGINX_CONF_DIR/$CONTAINER_PREFIX-upstream.conf" + +# Ensure directories exist +mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$BACKUP_DIR" "$LOG_DIR" + +IS_WORKER=0 +if [[ "$CONTAINER_PREFIX" == *worker* ]]; then + IS_WORKER=1 +fi + +# ============================================================================ +# UTILITY FUNCTIONS +# ============================================================================ + +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_DIR/manage.log" +} + +log_error() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ❌ ERROR: $*" | tee -a "$LOG_DIR/manage.log" >&2 +} + +log_success() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ✅ SUCCESS: $*" | tee -a "$LOG_DIR/manage.log" +} + +log_warning() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ⚠️ WARNING: $*" | tee -a "$LOG_DIR/manage.log" +} + +log_info() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] ℹ️ INFO: $*" | tee -a "$LOG_DIR/manage.log" +} + +# ============================================================================ +# CONTAINER LOG ARCHIVAL FUNCTIONS +# ============================================================================ + +# Archive running container logs to timestamped files +archive_running_container_logs() { + {% raw %} + local container_logs_dir="$LOG_DIR/archives" + mkdir -p "$container_logs_dir" + + local running_containers + running_containers=$(get_running_containers) + + if [[ -z "$running_containers" ]]; then + return 0 + fi + + # Get current timestamp for all logs in this batch + local batch_timestamp=$(date +%Y%m%d-%H%M%S) + + echo "$running_containers" | while read -r container; do + if [[ -z "$container" ]]; then + continue + fi + + # Extract container prefix (everything before the last -) + # e.g., gemba-api-1-40001 → gemba-api + local container_prefix_name=$(echo "$container" | sed 's/-[0-9]*$//') + + # Create subdirectory per prefix + local prefix_log_dir="$container_logs_dir/$container_prefix_name" + mkdir -p "$prefix_log_dir" + + # Log file: container_name_timestamp.log + local log_file="$prefix_log_dir/${container}_${batch_timestamp}.log" + + # Capture running logs from container + docker logs "$container" > "$log_file" 2>&1 + + CONTAINER_ID_FULL=$(docker inspect --format='{{.Id}}' "$container" 2>/dev/null) + CONTAINERLOG_PATH="/var/lib/docker/containers/${CONTAINER_ID_FULL}/${CONTAINER_ID_FULL}-json.log" + sudo sh -c "cat /dev/null > $CONTAINERLOG_PATH" + + echo "[$(date +'%Y-%m-%d %H:%M:%S')] 📝 Container logs saved: $log_file" >> "$LOG_DIR/manage.log" + done + {% endraw %} +} + +# Rotate container logs (keep only last N logs per container) +rotate_container_runtime_logs() { + local container_logs_dir="$LOG_DIR/archives" + local max_logs_per_container=30 # Keep last 30 log files per container + + if [[ ! -d "$container_logs_dir" ]]; then + return 0 + fi + + # For each container prefix directory + for prefix_dir in "$container_logs_dir"/*; do + if [[ ! -d "$prefix_dir" ]]; then + continue + fi + + # For each container in this prefix directory + for container_logs in "$prefix_dir"/*; do + if [[ ! -d "$container_logs" ]]; then + # Count log files for this container pattern + local container_name=$(basename "$container_logs" | sed 's/_[0-9]*\.[0-9]*\.log$//') + local log_count=$(ls "$prefix_dir"/${container_name}_*.log 2>/dev/null | wc -l) + + if [[ $log_count -gt $max_logs_per_container ]]; then + local remove_count=$((log_count - max_logs_per_container)) + # Remove oldest logs (keep newest) + ls -t "$prefix_dir"/${container_name}_*.log 2>/dev/null | tail -n $remove_count | xargs -r rm -f + fi + fi + done + + # Also cleanup at prefix level + local total_logs=$(ls "$prefix_dir"/*.log 2>/dev/null | wc -l) + if [[ $total_logs -gt $((max_logs_per_container * 10)) ]]; then + # If too many logs, remove oldest ones + local remove_count=$((total_logs - max_logs_per_container * 10)) + ls -t "$prefix_dir"/*.log 2>/dev/null | tail -n $remove_count | xargs -r rm -f + fi + done +} + +# Setup cron job to periodically archive container logs (one-time for all apps) +setup_container_log_archive_cron() { + local cron_file="/etc/cron.d/docker-container-logs-archive" + local script_path="$HOME/scripts/archive-all-container-logs.sh" + + # Check if cron job already exists + if [[ -f "$cron_file" ]]; then + return 0 + fi + + log_info "Setting up global container log archive cron job (every 15 minutes)" + + # Create archive script + mkdir -p "$HOME/scripts" + cat > "$script_path" << SCRIPT_EOF +#!/bin/bash +# Global container log archival script for all applications +for manage_script in $HOME/scripts/manage-*.sh; do + if [[ -f "\$manage_script" && -x "\$manage_script" ]]; then + "\$manage_script" archive-logs 2>/dev/null || true + fi +done +SCRIPT_EOF + + chmod 755 "$script_path" + + # Create cron file (one-time, global for all apps) + sudo tee "$cron_file" > /dev/null << CRON_EOF +# Global Docker container log archival (runs every 15 minutes) +*/15 * * * * root "$script_path" >/dev/null 2>&1 +CRON_EOF + + sudo chmod 644 "$cron_file" + + log_success "Global cron job created: $cron_file" + log_success "Archive script created: $script_path" +} +setup_container_log_archive_cron + +# ==== Function: Check and install jq ==== +check_and_install_jq() { + if ! command -v jq &>/dev/null; then + echo "[INFO] jq not found. Installing..." + if command -v apt-get &>/dev/null; then + sudo apt-get update -y && sudo apt-get install -y jq + elif command -v yum &>/dev/null; then + sudo yum install -y jq + elif command -v brew &>/dev/null; then + brew install jq + else + echo "[ERROR] Could not detect package manager to install jq." + exit 1 + fi + echo "[INFO] jq installed successfully." + fi +} +check_and_install_jq + +show_active_ports() { + {% raw %} + mapfile -t containers < <(docker ps --format '{{.Names}}' | grep "^${CONTAINER_PREFIX}") + + # Nếu vẫn rỗng thì thông báo lỗi + if [ ${#containers[@]} -eq 0 ]; then + echo "❌ No containers found with prefix: $CONTAINER_PREFIX" >&2 + return 1 + fi + + local ports=() + for c in "${containers[@]}"; do + local port=$(docker port "$c" 3000/tcp | awk -F: '{print $2}' | xargs) + if [ -n "$port" ]; then + ports+=("$port") + fi + done + + # Nếu vẫn rỗng sau khi lấy port thì báo lỗi + if [ ${#ports[@]} -eq 0 ]; then + echo "❌ No ports found for containers: ${containers[*]}" >&2 + return 1 + fi + {% endraw %} + # In tất cả port cách nhau bằng dấu cách + echo "${ports[@]}" +} + +# Get port for specific replica using base port + replica number pattern +get_replica_port() { + local base_port=$1 + local replica_number=$2 + + # Create port by concatenating base_port + replica_number + local port="${base_port}${replica_number}" + + echo $port +} + +# Get random port +get_random_port() { + local port + local max_attempts=100 + local attempt=0 + + while [[ $attempt -lt $max_attempts ]]; do + port=$(( RANDOM % (65500 - 40000 + 1) + 40000 )) # Random từ 40000-65500 + + # Check if port is available (both netstat and Docker) + if is_port_available "$port"; then + echo "$port" + return 0 + fi + + attempt=$((attempt + 1)) + done + + # Fallback: find next available port from 40000 + log_warning "Random port selection exhausted, finding next available..." + get_next_available_port 40000 +} + +# Check if port is available +is_port_available() { + local port=$1 + {% raw %} + + # Check if port is in use by network stack + if netstat -tuln 2>/dev/null | grep -qE ":$port\s"; then + return 1 # Port is busy + fi + # Check if port is reserved by any Docker container (running or stopped) + if docker ps -a --format '{{.Ports}}' 2>/dev/null | grep -qE "0\.0\.0\.0:$port->|:::$port->"; then + return 1 # Port is reserved by Docker + fi + # Double check with lsof if available + if command -v lsof &>/dev/null; then + if lsof -i tcp:"$port" &>/dev/null 2>&1; then + return 1 + fi + fi + + {% endraw %} + return 0 # Port is available +} + +# Get next available port (fallback method) +get_next_available_port() { + local start_port=$1 + local port=$start_port + local max_port=$((start_port + 10000)) + + while [[ $port -lt $max_port ]]; do + if is_port_available $port; then + echo $port + return 0 + fi + ((port++)) + done + + log_error "No available port found starting from $start_port" + return 1 +} + +remove_timestamp() { [[ $1 =~ ^(.*-[0-9]+)-([0-9]+)$ ]] && echo "${BASH_REMATCH[1]}" || echo "$1"; } + +# Get all containers for this app +get_app_containers() { + docker ps -a --filter "name=${CONTAINER_PREFIX}" --format "{{ "{{" }}.Names{{ "}}" }}" | sort +} + +# Get running containers only +get_running_containers() { + docker ps --filter "name=${CONTAINER_PREFIX}" --format "{{ "{{" }}.Names{{ "}}" }}" | sort +} + +# Get container count +get_container_count() { + docker ps --filter "name=${CONTAINER_PREFIX}" --quiet | wc -l +} + +# Hàm kiểm tra và giải phóng port +check_and_free_port() { + local port="$1" + local container_old=${2:-} + + # Validate port + if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then + echo "❌ Port không hợp lệ: $port" + fi + + # Tìm PID process đang giữ port + local pids + pids=$(lsof -ti tcp:"$port" 2>/dev/null | tr '\n' ' ' | xargs) + + if [ -n "$pids" ]; then + # Kiểm tra container nào publish port này + local container_name + container_name=$(docker ps --format '{{ "{{" }}.Names{{ "}}" }}' --filter "publish=$port" | head -n 1) + + if [ -n "$container_name" ]; then + container_old="$container_name" + else + echo "🛑 Kill process PID(s): $pids (đang dùng port $port)" + kill -9 $pids 2>/dev/null || true + fi + fi + + if [ -n "$container_old" ] && [ "$container_old" != "" ]; then + if docker ps -a --format '{{ "{{" }}.Names{{ "}}" }}' | grep -qx "$container_old"; then + echo "🛑 Dừng container $container_old (đang dùng port $port)" + docker stop "$container_old" >/dev/null 2>&1 || true + docker rm "$container_old" >/dev/null 2>&1 || true + fi + fi +} + +# Health check for a container +health_check() { + local container_name=$1 + local timeout=${HEALTH_CHECK_TIMEOUT:-30} + if [ "$FRAMEWORK" = "reactjs" ] || [ "$FRAMEWORK" = "laravel" ] || [ "$FRAMEWORK" = "magento" ] || [ "$FRAMEWORK" = "wordpress" ]; then + timeout=180 + fi + local max_attempts=$((timeout / 5)) + [ "$max_attempts" -gt 5 ] && max_attempts=5 + local sleep_time=$((timeout / max_attempts)) + + if [[ "$IS_WORKER" -eq 1 ]]; then + return 0 + fi + + log_info "Performing health check for $container_name (timeout: ${timeout}s)" + for ((i=1; i<=max_attempts; i++)); do + # Lấy mã trạng thái HTTP từ endpoint /health + health_status=$(docker exec "$container_name" curl -s -o /dev/null -w "%{http_code}" http://localhost:${CONTAINER_PORT}) + + # Kiểm tra nếu mã trạng thái là 200 hoặc 404 + if [[ "$health_status" -eq 200 || "$health_status" -eq 404 ]]; then + log_success "Health check passed for $container_name on /health with status $health_status (attempt $i/$max_attempts)" + return 0 + fi + + if [[ $i -lt $max_attempts ]]; then + # Cung cấp thêm thông tin về lỗi để dễ debug + log_info "Health check attempt $i/$max_attempts failed (statuses: $health_status), retrying in ${sleep_time}s..." + sleep $sleep_time + fi + done + + log_error "Kiểm tra trạng thái máy chủ ở container $container_name thất bại sau $max_attempts lần thử" + + #local docker_logs=`docker logs --tail 10 "$container_name"` + #echo $docker_logs + #log_error $docker_logs + return 1 +} + +# Get current image version +get_current_version() { + # Tìm container đang chạy có prefix + local container_name + container_name=$(docker ps --filter "name=^${CONTAINER_PREFIX}" --format '{{ "{{" }}.Names{{ "}}" }}' | head -n 1) + + if [ -n "$container_name" ]; then + docker inspect "$container_name" --format '{{ "{{" }}index .Config.Labels "version"{{ "}}" }}' 2>/dev/null || echo "unknown" + else + echo "none" + fi +} + +# Get available versions +get_available_versions() { + docker images "${DOCKER_IMAGE}" --format "{{ "{{" }}.Tag{{ "}}" }}" | grep -v "latest" | sort -V -r | head -10 +} + +# Backup current configuration +backup_config() { + local backup_file="$BACKUP_DIR/config-$(date +%Y%m%d-%H%M%S).tar.gz" + + log_info "Creating configuration backup: $backup_file" + + if [[ -d "$CONFIG_DIR" ]] && [[ -n "$(ls -A "$CONFIG_DIR" 2>/dev/null)" ]]; then + tar -czf "$backup_file" -C "$CONFIG_DIR" . 2>/dev/null + log_success "Configuration backed up to: $backup_file" + + # Keep only last 10 backups + ls -t "$BACKUP_DIR"/config-*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f + else + log_info "No configuration files to backup" + fi +} + +# Create Docker network if it doesn't exist +ensure_network() { + if ! docker network ls | grep -q "$DOCKER_NETWORK"; then + log_info "Creating Docker network: $DOCKER_NETWORK" + docker network create "$DOCKER_NETWORK" || { + log_info "Network creation failed or network already exists" + } + fi +} + +# Show port mapping information +show_port_mapping() { + local replicas=${1:-$DEFAULT_REPLICAS} + + echo "🔌 PORT MAPPING PREVIEW" + echo "======================" + echo "Base Port: $BASE_PORT" + echo "Pattern: ${base_port}${replica_number:-1}" + echo "" + echo "Replica Mapping:" + for ((i=1; i<=replicas; i++)); do + local container_name="${CONTAINER_PREFIX}-${i}" + local port=$(get_replica_port "$BASE_PORT" "$i") + printf " %-25s → localhost:%-6s → container:%-6s\n" "$container_name" "$port" "$CONTAINER_PORT" + done + echo "" +} + +# Start a single container +start_single_container() { + local container_name=$1 + local image_tag=${2:-$CURRENT_TAG} + local port=${3:-""} + + if [[ "$IS_WORKER" -ne 1 ]]; then + # Get port if not provided + if [[ -z "$port" ]]; then + local replica_number="1" + if [[ "$container_name" =~ -([0-9]+)$ ]]; then + replica_number="${BASH_REMATCH[1]}" + fi + + # Use base port + replica number pattern (e.g., 3003 + 1 = 30031) + port=$(get_replica_port "$BASE_PORT" "$replica_number") + fi + + # Check if calculated port is available + if ! is_port_available "$port"; then + log_warning "Calculated port $port is busy, finding alternative..." + port=$(get_random_port) + BINDING_PORT=$port + fi + fi + + log_info "Starting container: $container_name with image ${DOCKER_IMAGE}:${image_tag} on port $port" + + ensure_network + + # Build docker run command + local docker_cmd="docker run -d --ulimit nofile=65535:65535 -e TZ=Asia/Ho_Chi_Minh" + docker_cmd+=" --name $container_name" + if [[ "$FRAMEWORK" == "nextjs" && "$container_name" != *rolling* ]]; then + mkdir -p /var/cache/nginx/ 2>/dev/null + #create volume if not exists + local docker_volume="${APP_NAME}-${ENVIRONMENT}" + #local docker_volume="$container_name" + local docker_volume_cmd="docker volume inspect $docker_volume > /dev/null 2>&1 || docker volume create $docker_volume" + eval "$docker_volume_cmd" + docker_cmd+=" --mount type=volume,source=$docker_volume,target=/app/.next/cache/images" + elif [[ "$APP_NAME" == "kamgift-cms" ]]; then + mkdir -p /var/cache/nginx/ 2>/dev/null + + # Create volume for uploads + local docker_volume_uploads="${APP_NAME}-${ENVIRONMENT}" + local docker_volume_cmd_uploads="docker volume inspect $docker_volume_uploads > /dev/null 2>&1 || docker volume create $docker_volume_uploads" + eval "$docker_volume_cmd_uploads" + + # Create volume for storage + local docker_volume_storage="${APP_NAME}-${ENVIRONMENT}-storage" + local docker_volume_cmd_storage="docker volume inspect $docker_volume_storage > /dev/null 2>&1 || docker volume create $docker_volume_storage" + eval "$docker_volume_cmd_storage" + + docker_cmd+=" --mount type=volume,source=$docker_volume_uploads,target=/var/www/html/public/cms/backend/uploads" + docker_cmd+=" --mount type=volume,source=$docker_volume_storage,target=/var/www/html/storage" + elif [[ "$FRAMEWORK" == "laravel" ]]; then + mkdir -p /var/cache/nginx/ 2>/dev/null + #create volume if not exists + local docker_volume="${APP_NAME}-${ENVIRONMENT}" + #local docker_volume="$container_name" + local docker_volume_cmd="docker volume inspect $docker_volume > /dev/null 2>&1 || docker volume create $docker_volume" + eval "$docker_volume_cmd" + docker_cmd+=" --mount type=volume,source=$docker_volume,target=/var/www/html/storage" + fi + docker_cmd+=" --network $DOCKER_NETWORK" + docker_cmd+=" --restart $RESTART_POLICY" + + # Add environment file if exists + if [[ -f "$CONFIG_DIR/.env" ]]; then + if [[ "$FRAMEWORK" == "laravel" ]]; then + docker_cmd+=" --env-file $CONFIG_DIR/.env" + fi + # docker_cmd+=" --env-file $CONFIG_DIR/.env" + log_info "Using environment file: $CONFIG_DIR/.env" + else + log_warning "No .env file found in $CONFIG_DIR" + fi + + # Add resource limits + if [[ -n "$MEMORY_LIMIT" ]]; then + docker_cmd+=" --memory $MEMORY_LIMIT" + fi + if [[ -n "$CPU_LIMIT" ]]; then + docker_cmd+=" --cpus $CPU_LIMIT" + fi + + # Add labels + docker_cmd+=" --label app=$APP_NAME" + docker_cmd+=" --label environment=$ENVIRONMENT" + docker_cmd+=" --label version=$image_tag" + + # Add health check + + if [[ "$IS_WORKER" -ne 1 ]]; then + docker_cmd+=" --health-cmd=\"curl -s -o /dev/null -w '%{http_code}' http://localhost:${CONTAINER_PORT}/ | grep -qE '^(200|404)$' || exit 1\"" + docker_cmd+=" --health-interval=10s" + docker_cmd+=" --health-timeout=10s" + docker_cmd+=" --health-start-period=10s" + docker_cmd+=" --health-retries=3" + fi + + local docker_cmd_eval=$docker_cmd + if [[ "$IS_WORKER" -ne 1 ]]; then + docker_cmd_eval+=" -p ${port}:${CONTAINER_PORT}" + fi + + docker_cmd_eval+=" ${DOCKER_IMAGE}:${image_tag}" + + #log_info "$docker_cmd_eval" + + # Stop old container (frees up original port) + if [[ "$IS_WORKER" -eq 1 ]]; then + container_name_original="${container_name%-*}" + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_name_original}$"; then + docker stop "$container_name_original" &>/dev/null || true + docker rm "$container_name_original" &>/dev/null || true + fi + fi + + # Execute command + if eval "$docker_cmd_eval"; then + log_success "Container started: $container_name on port $port" + BINDING_PORT=0 + return 0 + else + log_error "Failed to start container: $container_name" + + # Check if it's a port conflict error and retry with random port + if docker logs "$container_name" 2>&1 | grep -q "address already in use"; then + log_warning "Port conflict detected, removing failed container and retrying with random port..." + docker rm -f "$container_name" &>/dev/null || true + + # Retry up to 3 times with random ports + local retry_count=0 + local max_retries=1 + + while [[ $retry_count -lt $max_retries ]]; do + retry_count=$((retry_count + 1)) + local random_port=$(get_random_port) + log_info "Retry $retry_count/$max_retries with random port: $random_port" + + local docker_cmd_eval=$docker_cmd + if [[ "$IS_WORKER" -ne 1 ]]; then + docker_cmd_eval+=" -p ${random_port}:${CONTAINER_PORT}" + fi + + docker_cmd_eval+=" ${DOCKER_IMAGE}:${image_tag}" + + if eval "$docker_cmd_eval"; then + log_success "Container started on retry with port $random_port: $container_name" + BINDING_PORT=$random_port + return 0 + else + log_warning "Retry $retry_count failed, removing container..." + docker rm -f "$container_name" &>/dev/null || true + fi + done + + log_error "All retry attempts failed for container: $container_name" + fi + + BINDING_PORT=0 + return 1 + fi +} + +# Smart deployment with auto-scaling +deploy_with_auto_scale() { + local target_replicas=${1:-$DEFAULT_REPLICAS} + local image_tag=${2:-$CURRENT_TAG} + local strategy=${3:-"rolling"} + local is_restart=${4:-"0"} + local restart_container=${5:-} + + log "" + log_info "🚀 Starting smart deployment with auto-scaling" + log_info "Target: $target_replicas replicas, Version: $image_tag, Strategy: $strategy" + + # Get current state + local current_count + current_count=$(get_container_count) + local current_version + current_version=$(get_current_version) + + log_info "Current state: $current_count replicas running version $current_version" + + if [[ "$is_restart" != "1" ]]; then + # Login to registry if token available + if [[ -n "{{ gitlab_ci_token | default('') }}" ]]; then + log_info "Logging into GitLab Container Registry..." + echo "{{ gitlab_ci_token }}" | docker login gitlab.forlife.one:5050 -u gitlab-ci-token --password-stdin >/dev/null 2>&1 + fi + + # Pull new image first + log_info "Pulling new image: ${DOCKER_IMAGE}:${image_tag}" + if ! docker pull "${DOCKER_IMAGE}:${image_tag}"; then + log_error "Failed to pull image: ${DOCKER_IMAGE}:${image_tag}" + rm -f "$CONTAINER_LOCK_FILE" + return 1 + fi + fi + + # Determine deployment strategy + if [[ $current_count -eq 0 ]]; then + # Fresh deployment + log_info "Fresh deployment: starting $target_replicas containers" + docker_start "$target_replicas" "$image_tag" + elif [[ "$current_version" != "$image_tag" || "$is_restart" == "1" ]]; then + # Version update with scaling + log_info "Version update: $current_version → $image_tag with scaling to $target_replicas" + + case "$strategy" in + "rolling") + deploy_rolling_with_scale "$target_replicas" "$image_tag" "$is_restart" "$restart_container" + ;; + "blue-green") + deploy_blue_green_with_scale "$target_replicas" "$image_tag" + ;; + "recreate") + deploy_recreate_with_scale "$target_replicas" "$image_tag" + ;; + *) + log_error "Unknown deployment strategy: $strategy" + rm -f "$CONTAINER_LOCK_FILE" + return 1 + ;; + esac + elif [[ $current_count -ne $target_replicas ]]; then + # Only scaling needed (same version) + log_info "Scaling only: $current_count → $target_replicas replicas (same version)" + docker_scale "$target_replicas" + else + deploy_recreate_with_scale "$target_replicas" "$image_tag" + # No changes needed + # log_success "No deployment needed: $current_count replicas already running version $image_tag" + # return 0 + fi + + # Verify final state + local final_count + final_count=$(get_container_count) + local final_version + final_version=$(get_current_version) + + #if [[ $final_count -eq $target_replicas ]] && [[ "$final_version" == "$image_tag" ]]; then + if [[ "$final_version" == "$image_tag" ]]; then + log_success "✅ Deployment completed successfully: $final_count replicas running $final_version" + if [[ "$is_restart" != "1" ]]; then + # Show final status + echo "" + docker_status + fi + rm -f "$CONTAINER_LOCK_FILE" + return 0 + else + rm -f "$CONTAINER_LOCK_FILE" + log_error "❌ Deployment verification failed: expected $target_replicas/$image_tag, got $final_count/$final_version" + return 1 + fi +} + +# Rolling deployment with scaling +deploy_rolling_with_scale_old() { + local target_replicas=$1 + local image_tag=$2 + local is_restart=${3:-"0"} + local restart_container=${4:-} + local current_count + current_count=$(get_container_count) + LOCK_DIR="/tmp/container_restart_locks" && mkdir -p "$LOCK_DIR" + + log_info "Rolling deployment with scaling: $current_count → $target_replicas replicas" + + # Phase 1: Update existing containers with zero-downtime + local existing_containers + existing_containers=$(get_running_containers) + + if [[ -n "$existing_containers" ]]; then + log_info "Phase 1: Rolling update of existing containers" + local container_rolling_checked="?" + + echo "$existing_containers" | while read -r container; do + if [ "$is_restart" = "1" ]; then + container_rolling_checked="OK" + + if [ "$container" != "$restart_container" ]; then + continue + fi + fi + local timestamp=$(date +%s) + # Get replica number from container name + local replica_num="1" + if [[ "$container" =~ -([0-9]+)$ ]]; then + replica_num="${BASH_REMATCH[1]}" + fi + + # Get original port + local original_port + original_port=$(get_replica_port "$BASE_PORT" "$replica_num") + + # Find next available port in rolling range (BASE_PORT + 2000 to BASE_PORT + 2999) + local rolling_port + rolling_port=$(get_next_available_port "$((BASE_PORT + 2000))") + + + if [[ "$IS_WORKER" -ne 1 ]]; then + if [[ -z "$rolling_port" ]]; then + log_error "Cannot find available rolling port for $container" + return 1 + fi + fi + + # Start new container with temp name on rolling port + local temp_name="${container}-rolling" + log_info "🔄 Rolling update: $container → $temp_name (port $rolling_port → $original_port)" + + if [[ "$container_rolling_checked" == "OK" ]] || start_single_container "$temp_name" "$image_tag" "$rolling_port"; then + if [[ "$container_rolling_checked" == "OK" ]] || health_check "$temp_name"; then + log_info "✅ New container healthy, switching traffic..." + + date +%s > "$LOCK_DIR/$container.lock" + # Stop old container (frees up original port) + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container}$"; then + docker stop "$container" &>/dev/null || true + docker rm "$container" &>/dev/null || true + fi + + if [[ "$container_rolling_checked" != "OK" ]]; then + # Stop temp container + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${temp_name}$"; then + docker stop "$temp_name" &>/dev/null || true + docker rm "$temp_name" &>/dev/null || true + fi + fi + + container_rolling_checked="OK" + # Start new container with original name and port + if start_single_container "$container" "$image_tag" "$original_port"; then + if health_check "$container"; then + rm -f "$LOCK_DIR/$container.lock" + log_success "✅ Rolling update completed: $container" + else + log_error "Health check failed for final container: $container" + return 1 + fi + else + log_error "Failed to start final container: $container" + return 1 + fi + else + log_error "Không thể bắt đầu container mới, giữ nguyên container phiên bản cũ !!!" + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${temp_name}$"; then + docker stop "$temp_name" &>/dev/null || true + docker rm "$temp_name" &>/dev/null || true + fi + return 1 + fi + else + log_error "Failed to start rolling container: $temp_name" + return 1 + fi + + log_info "⏳ Waiting before next container update..." + sleep 5 # Brief pause between updates + done + fi + + # Phase 2: Scale up if needed + if [[ $target_replicas -gt $current_count ]]; then + log_info "Phase 2: Scaling up from $current_count to $target_replicas" + + for ((i=$((current_count + 1)); i<=target_replicas; i++)); do + local container_name="${CONTAINER_PREFIX}-${i}" + local port + port=$(get_replica_port "$BASE_PORT" "$i") + + if start_single_container "$container_name" "$image_tag" "$port"; then + if ! health_check "$container_name"; then + log_error "Health check failed for new container: $container_name" + return 1 + fi + log_success "✅ Scaled up: $container_name on port $port" + else + return 1 + fi + + sleep 2 + done + fi + + # Phase 3: Scale down if needed + if [[ $target_replicas -lt $current_count ]]; then + log_info "Phase 3: Scaling down from $current_count to $target_replicas" + + local scale_down_num=$((current_count - target_replicas)) + mapfile -t containers < <(docker ps --format '{% raw %}{{.Names}}{% endraw %}' | grep "^${CONTAINER_PREFIX}") + for container_name in "${containers[@]}"; do + if (( scale_down_num <= 0 )); then + log_info "No more containers to scale down" + break + fi + + oldport=$(docker port $container_name 3000/tcp | awk -F: '{print $2}' | xargs) + sed -i "\|server localhost:${oldport}|d" $NGINX_CONF_FILE + nginx -t && /etc/init.d/nginx reload + + scale_down_num=$((scale_down_num - 1)) + log_info "Removing container: $container_name" + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_name}$"; then + docker stop "$container_name" &>/dev/null || true + docker rm "$container_name" &>/dev/null || true + fi + done + fi + + log_success "🎉 Rolling deployment with scaling completed successfully" +} + +deploy_rolling_with_scale() { + local target_replicas=$1 + local image_tag=$2 + local is_restart=${3:-"0"} + local restart_container=${4:-} + local current_count + current_count=$(get_container_count) + local current_version + current_version=$(get_current_version) + LOCK_DIR="/tmp/container_restart_locks" && mkdir -p "$LOCK_DIR" + + log_info "Rolling deployment with scaling: $current_count → $target_replicas replicas" + + # Scale down + if [[ $target_replicas -lt $current_count ]]; then + log_info "Scaling down from $current_count to $target_replicas" + + local scale_down_num=$((current_count - target_replicas)) + mapfile -t containers < <(docker ps --format '{% raw %}{{.Names}}{% endraw %}' | grep "^${CONTAINER_PREFIX}") + for container_name in "${containers[@]}"; do + if (( scale_down_num <= 0 )); then + log_info "No more containers to scale down" + break + fi + + oldport=$(docker port $container_name 3000/tcp | awk -F: '{print $2}' | xargs) + sed -i "\|server localhost:${oldport}|d" $NGINX_CONF_FILE + nginx -t && /etc/init.d/nginx reload + + scale_down_num=$((scale_down_num - 1)) + log_info "Removing container: $container_name" + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_name}$"; then + docker stop "$container_name" &>/dev/null || true + docker rm "$container_name" &>/dev/null || true + fi + done + fi + + # Update existing containers with zero-downtime + local existing_containers + existing_containers=$(get_running_containers) + local existing_updated_count=0 + + if [[ -n "$existing_containers" ]]; then + log_info "Rolling update of existing containers" + local container_rolling_checked="?" + + echo "$existing_containers" | while read -r container; do + existing_updated_count=$((existing_updated_count + 1)) + if [[ $existing_updated_count -gt $target_replicas ]]; then + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container}$"; then + docker stop "$container" &>/dev/null || true + docker rm "$container" &>/dev/null || true + fi + continue + fi + + local container_old="$container" + container="$(remove_timestamp "$container_old")" + if [ "$is_restart" = "1" ]; then + container_rolling_checked="OK" + if [ "$container_old" != "$restart_container" ]; then + continue + fi + fi + + # Get old port trước khi tạo container mới + local oldport="" + # if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_old}$"; then + # oldport=$(docker port "$container_old" 3000/tcp 2>/dev/null | awk -F: '{print $2}' | xargs) + # fi + + local timestamp=$(date +%s) + local replica_num="1" + if [[ "$container" =~ -([0-9]+)$ ]]; then + replica_num="${BASH_REMATCH[1]}" + fi + + # local original_port + # original_port=$(get_replica_port "$BASE_PORT" "$replica_num") + + # local rolling_port + # rolling_port=$(get_next_available_port "$((BASE_PORT + 2000))") + local random_port=$(get_random_port) + + if [[ "$IS_WORKER" -ne 1 ]]; then + if [[ -z "$random_port" ]]; then + log_error "Cannot find available rolling port for $container" + return 1 + fi + fi + + local container_new="${container}-${random_port}" + date +%s > "$LOCK_DIR/$container.lock" + + log_info "🔄 Rolling update: $container → $container_new (new port: $random_port)" + + if start_single_container "$container_new" "$image_tag" "$random_port"; then + if [[ "$BINDING_PORT" -ne 0 ]]; then + random_port=${BINDING_PORT} + fi + if [[ "$IS_WORKER" -ne 1 ]]; then + if health_check "$container_new"; then + oldport=$(docker port $container_old 3000/tcp | awk -F: '{print $2}' | xargs) + newport=${random_port} + log_info "✅ New container healthy, switching traffic..." + # Stop old container (frees up original port) + if [[ -n "$oldport" ]]; then + sed -i "\|server localhost:${oldport}|d" $NGINX_CONF_FILE + fi + sed -i "\|server localhost:|a\ server localhost:${newport};" $NGINX_CONF_FILE + nginx -t && /etc/init.d/nginx reload + # remove old container + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_old}$"; then + docker stop "$container_old" &>/dev/null || true + docker rm "$container_old" &>/dev/null || true + fi + + log_success "✅ Rolling update completed: $container" + else + log_error "Health check failed for final container: $container" + return 1 + fi + else + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_old}$"; then + docker stop "$container_old" &>/dev/null || true + docker rm "$container_old" &>/dev/null || true + fi + fi + else + rm -f "$LOCK_DIR/$container.lock" + log_error "Không thể bắt đầu container mới, giữ nguyên container phiên bản cũ !!!" + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_new}$"; then + docker stop "$container_new" &>/dev/null || true + docker rm "$container_new" &>/dev/null || true + fi + return 1 + fi + + log_info "⏳ Waiting before next container update..." + sleep 2 + done + fi + + # Scale up + if [[ $target_replicas -gt $current_count ]]; then + log_info "Scaling up from $current_count to $target_replicas" + i=$((current_count + 1)) + while [[ $i -le $target_replicas ]]; do + local port + port=$(get_random_port) + local timestamp + timestamp=$(date +%s) + local container="${CONTAINER_PREFIX}-${i}" + local container_name="${container}-${port}" + + date +%s > "$LOCK_DIR/$container.lock" + if start_single_container "$container_name" "$image_tag" "$port"; then + if ! health_check "$container_name"; then + log_error "Health check failed for new container: $container_name" + return 1 + fi + log_success "✅ Scaled up: $container_name on port $port" + else + return 1 + fi + + sleep 2 + i=$((i + 1)) # tự tăng i + done + fi + + log_success "🎉 Rolling deployment with scaling completed successfully" +} + +# Blue-green deployment with scaling +deploy_blue_green_with_scale() { + local target_replicas=$1 + local image_tag=$2 + + log_info "Blue-green deployment with scaling: $target_replicas replicas" + + # Create green environment + local green_containers=() + log_info "Creating green environment with $target_replicas containers" + + for ((i=1; i<=target_replicas; i++)); do + local container_name="${CONTAINER_PREFIX}-green-${i}" + local port + port=$(get_replica_port "$((BASE_PORT + 1000))" "$i") # Use different port range for green + + if start_single_container "$container_name" "$image_tag" "$port"; then + if health_check "$container_name"; then + green_containers+=("$container_name") + log_success "✅ Green container ready: $container_name on port $port" + else + log_error "Health check failed for green container: $container_name" + # Cleanup green containers + for green_container in "${green_containers[@]}"; do + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${green_container}$"; then + docker stop "$green_container" || true + docker rm "$green_container" || true + fi + done + return 1 + fi + else + return 1 + fi + done + + # Switch traffic (rename containers) + log_info "Switching traffic to green environment" + + # Stop and remove all blue containers + local blue_containers + blue_containers=$(get_running_containers) + if [[ -n "$blue_containers" ]]; then + echo "$blue_containers" | while read -r blue_container; do + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${blue_container}$"; then + docker stop "$blue_container" || true + docker rm "$blue_container" || true + fi + done + fi + + # Rename green containers to blue + for ((i=1; i<=target_replicas; i++)); do + local green_name="${CONTAINER_PREFIX}-green-${i}" + local blue_name="${CONTAINER_PREFIX}-${i}" + local original_port + original_port=$(get_replica_port "$BASE_PORT" "$i") + + # Stop green container and restart with correct port + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${green_name}$"; then + docker stop "$green_name" || true + docker rm "$green_name" || true + fi + + # Start with final name and port + start_single_container "$blue_name" "$image_tag" "$original_port" + done + + log_success "Blue-green deployment with scaling completed" +} + +# Recreate deployment with scaling +deploy_recreate_with_scale() { + local target_replicas=$1 + local image_tag=$2 + + log_info "Recreate deployment with scaling: $target_replicas replicas" + + # Stop all existing containers + docker_stop + + # Remove all containers + local containers + containers=$(get_app_containers) + if [[ -n "$containers" ]]; then + echo "$containers" | while read -r container; do + docker rm "$container" &>/dev/null || true + done + fi + + # Start new containers + docker_start "$target_replicas" "$image_tag" + + log_success "Recreate deployment with scaling completed" +} + +function docker_start() { + local replicas=${1:-$DEFAULT_REPLICAS} + local image_tag=${2:-$CURRENT_TAG} + + log_info "Starting $replicas containers with image tag: $image_tag" + + for ((i=1; i<=replicas; i++)); do + # local port=$(get_replica_port "$BASE_PORT" "$i") + local port=$(get_random_port) + local container_name="${CONTAINER_PREFIX}-${i}-${port}" + # check_and_free_port $port $container_name + if start_single_container "$container_name" "$image_tag" "$port"; then + echo "" + fi; + sleep 5 + done + + log_success "All containers started successfully" +} + +function docker_stop() { + local containers + containers=$(get_running_containers) + + if [[ -z "$containers" ]]; then + log_info "No running containers to stop" + return 0 + fi + + log_info "Stopping containers..." + + echo "$containers" | while read -r container; do + log_info "Stopping container: $container" + docker stop "$container" || log_warning "Failed to stop container: $container" + done + + log_success "All containers stopped" +} + +function docker_restart() { + local replicas=${1:-$(get_container_count)} + local image_tag=${2:-$(get_current_version)} + local strategy=${3:-"rolling"} + local restart_container=${4:-} + + if [[ $replicas -eq 0 ]]; then + replicas=$DEFAULT_REPLICAS + fi + + if [[ $replicas -gt $DEFAULT_REPLICAS ]]; then + replicas=$DEFAULT_REPLICAS + fi + + if [[ "$image_tag" == "unknown" || "$image_tag" == "none" ]]; then + image_tag=$CURRENT_TAG + log_info "Using current deployment tag: $image_tag" + fi + + log_info "Restarting with $replicas replicas, image tag: $image_tag, strategy: $strategy" + + # Use smart deployment with auto-scaling + if [[ "$restart_container" != "" ]]; then + deploy_with_auto_scale "$replicas" "$image_tag" "$strategy" "1" "$restart_container" + else + deploy_with_auto_scale "$replicas" "$image_tag" "$strategy" "1" + fi +} + +function docker_restart_single() { + local restart_container=${1:-} + local replicas=${2:-$(get_container_count)} + local image_tag=${3:-$(get_current_version)} + local strategy=${4:-"rolling"} + + # Use smart deployment with auto-scaling + docker_restart "$replicas" "$image_tag" "$strategy" "$restart_container" + #docker restart "$restart_container" +} + +function docker_scale() { + local target_replicas=$1 + + if [[ -z "$target_replicas" || ! "$target_replicas" =~ ^[0-9]+$ ]]; then + log_error "Usage: scale " + return 1 + fi + + if [[ $target_replicas -gt $MAX_REPLICAS ]]; then + log_error "Cannot scale beyond maximum replicas: $MAX_REPLICAS" + return 1 + fi + + local current_count + current_count=$(get_container_count) + + log_info "Scaling from $current_count to $target_replicas replicas" + + if [[ $target_replicas -eq $current_count ]]; then + log_info "Already at target replica count: $target_replicas" + return 0 + fi + + local current_version + current_version=$(get_current_version) + + if [[ "$current_version" == "none" && $target_replicas -gt 0 ]]; then + log_error "No containers running, use 'start' command instead" + return 1 + fi + + if [[ $target_replicas -gt $current_count ]]; then + # Scale up + log_info "Scaling up from $current_count to $target_replicas" + + for ((i=$((current_count + 1)); i<=target_replicas; i++)); do + local port=$(get_random_port) + local timestamp=$(date +%s) + local container="${CONTAINER_PREFIX}-${i}" + local container_name="${container}-${port}" + + date +%s > "$LOCK_DIR/$container.lock" + + if start_single_container "$container_name" "$current_version" "$port"; then + if ! health_check "$container_name"; then + log_error "Health check failed for new container: $container_name" + return 1 + fi + log_success "Container $container_name started on port $port" + else + return 1 + fi + done + else + # Scale down + log_info "Scaling down from $current_count to $target_replicas" + + local scale_down_num=$((current_count - target_replicas)) + mapfile -t containers < <(docker ps --format '{% raw %}{{.Names}}{% endraw %}' | grep "^${CONTAINER_PREFIX}") + for container_name in "${containers[@]}"; do + if (( scale_down_num <= 0 )); then + log_info "No more containers to scale down" + break + fi + + oldport=$(docker port $container_name 3000/tcp | awk -F: '{print $2}' | xargs) + sed -i "\|server localhost:${oldport}|d" $NGINX_CONF_FILE + nginx -t && /etc/init.d/nginx reload + + scale_down_num=$((scale_down_num - 1)) + log_info "Removing container: $container_name" + if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_name}$"; then + docker stop "$container_name" &>/dev/null || true + docker rm "$container_name" &>/dev/null || true + fi + done + fi + + log_success "Scaling completed: $(get_container_count) replicas running" +} + +function docker_rollback() { + local target_version=${1:-""} + local replicas=${2:-$(get_container_count)} + + if [[ $replicas -eq 0 ]]; then + replicas=$DEFAULT_REPLICAS + fi + + log_info "Rolling back application (replicas: $replicas)" + + if [[ -n "$target_version" ]]; then + log_info "Rolling back to specific version: $target_version" + + # Check if version exists + if ! docker images "${DOCKER_IMAGE}:${target_version}" --quiet | head -1 &>/dev/null; then + log_info "Pulling rollback image: ${DOCKER_IMAGE}:${target_version}" + if ! docker pull "${DOCKER_IMAGE}:${target_version}"; then + log_error "Failed to pull rollback image: ${DOCKER_IMAGE}:${target_version}" + return 1 + fi + fi + else + # Find previous version + local available_versions + available_versions=$(get_available_versions) + + if [[ -z "$available_versions" ]]; then + log_error "No previous versions available for rollback" + return 1 + fi + + # Get the second version (first is current) + target_version=$(echo "$available_versions" | sed -n '2p') + + if [[ -z "$target_version" ]]; then + target_version=$(echo "$available_versions" | head -1) + log_info "Only one version available, using: $target_version" + else + log_info "Rolling back to previous version: $target_version" + fi + fi + + # Create backup before rollback + backup_config + + # Perform rollback + deploy_with_auto_scale "$replicas" "$target_version" "rolling" + + if [[ $? -eq 0 ]]; then + log_success "Rollback completed to version: $target_version" + else + log_error "Rollback failed" + return 1 + fi +} + +function docker_status() { + local containers + containers=$(get_app_containers) + + echo "📊 APPLICATION STATUS" + echo "=====================" + echo "App: $APP_NAME" + echo "Environment: $ENVIRONMENT" + echo "Current Version: $(get_current_version)" + echo "Running Containers: $(get_container_count)" + echo "Base Port: $BASE_PORT (Pattern: ${BASE_PORT}${replica_number:-1})" + echo "Container Port: $CONTAINER_PORT" + echo "" + + if [[ -z "$containers" ]]; then + echo "❌ No containers found" + echo "" + echo "📋 PORT MAPPING PREVIEW (if containers were running):" + show_port_mapping 3 + return 0 + fi + + echo "📋 CONTAINER DETAILS" + echo "====================" + printf "%-25s %-15s %-20s %-10s %-15s\n" "NAME" "STATUS" "VERSION" "PORT" "HEALTH" + echo "$(printf '%.0s-' {1..95})" + + echo "$containers" | while read -r container; do + local status + status=$(docker inspect "$container" --format '{{ "{{" }}.State.Status{{ "}}" }}' 2>/dev/null || echo "unknown") + + local version + version=$(docker inspect "$container" --format '{{ "{{" }}index .Config.Labels "version"{{ "}}" }}' 2>/dev/null || echo "unknown") + + local port + port=$(docker port "$container" 2>/dev/null | grep "${CONTAINER_PORT}/tcp" | cut -d':' -f2 || echo "none") + + local health="unknown" + if [[ "$status" == "running" ]]; then + if health_check "$container" 5; then + health="healthy" + else + health="unhealthy" + fi + fi + + printf "%-25s %-15s %-20s %-10s %-15s\n" "$container" "$status" "$version" "$port" "$health" + done + + echo "" + echo "📡 LOAD BALANCER TARGETS" + echo "========================" + echo "$containers" | while read -r container; do + local port + port=$(docker port "$container" 2>/dev/null | grep "${CONTAINER_PORT}/tcp" | cut -d':' -f2 || echo "none") + if [[ "$port" != "none" ]]; then + echo " server localhost:$port;" + fi + done +} + +function docker_logs() { + local lines=${1:-$DEFAULT_LOG_LINES} + local follow=${2:-false} + local containers + containers=$(get_running_containers) + + if [[ -z "$containers" ]]; then + log_error "No running containers found" + return 1 + fi + + log_info "Viewing logs (last $lines lines, follow: $follow)" + + if [[ "$follow" == "true" ]]; then + # Follow logs from all containers + echo "$containers" | xargs -I {} sh -c 'echo "=== {} ===" && docker logs -f --tail '"$lines"' {} &' + wait + else + # Show logs from all containers + echo "$containers" | while read -r container; do + echo "=== $container ===" + docker logs --tail "$lines" "$container" 2>&1 + echo "" + done + fi +} + +function docker_health() { + local containers + containers=$(get_running_containers) + + if [[ -z "$containers" ]]; then + log_error "No running containers found" + return 1 + fi + + local all_healthy=true + echo "$containers" | while read -r container; do + if health_check "$container"; then + log_success "Health check passed: $container" + else + log_error "Health check failed: $container" + all_healthy=false + fi + done + + if [[ "$all_healthy" == "true" ]]; then + log_success "All containers are healthy" + return 0 + else + log_error "Some containers are unhealthy" + return 1 + fi +} + +function docker_clean() { + echo "Cleaning up Docker images and containers..." + + # Remove stopped containers + docker container prune -f + + # Remove unused images + docker image prune -a -f + + # Xóa volume không còn dùng + docker volume prune -f + + # Xóa network không còn dùng + docker network prune -f + + # delete unused all + #docker system prune -a --volumes -f + + # Remove old versions (keep last 5) + local old_images + old_images=$(docker images "${DOCKER_IMAGE}" --format "{{ "{{" }}.Tag{{ "}}" }}" | grep -v "latest" | sort -V -r | tail -n +6) + + if [[ -n "$old_images" ]]; then + echo "$old_images" | while read -r tag; do + log_info "Removing old image: ${DOCKER_IMAGE}:${tag}" + docker rmi "${DOCKER_IMAGE}:${tag}" &>/dev/null || true + done + fi + + log_success "Cleanup completed" +} + +function docker_info() { + echo "🔧 DOCKER CONTAINER INFORMATION" + echo "===============================" + echo "- Container Base Name: $CONTAINER_PREFIX" + echo "- Image: $DOCKER_IMAGE" + echo "- Current Tag: $CURRENT_TAG" + echo "- Host Port Base: $BASE_PORT" + echo "- Port Pattern: ${BASE_PORT}${replica_number:-1}" + echo "- Container Port: $CONTAINER_PORT" + echo "- Memory Limit: ${MEMORY_LIMIT:-unlimited}" + echo "- CPU Limit: ${CPU_LIMIT:-unlimited}" + echo "- Environment: $ENVIRONMENT" + echo "- Docker Network: $DOCKER_NETWORK" + echo "- Default Replicas: $DEFAULT_REPLICAS" + echo "- Max Replicas: $MAX_REPLICAS" + echo "- Docker Version: $(docker --version)" + echo "" + + # Show port mapping preview + show_port_mapping $DEFAULT_REPLICAS + + echo "📋 AVAILABLE VERSIONS" + echo "====================" + local versions + versions=$(get_available_versions) + if [[ -n "$versions" ]]; then + echo "$versions" + else + echo "No versions found" + fi + echo "" + + echo "🌐 NETWORK INFORMATION" + echo "=====================" + if docker network ls | grep -q "$DOCKER_NETWORK"; then + docker network inspect "$DOCKER_NETWORK" --format '{{ "{{" }}.Name{{ "}}" }}: {{ "{{" }}.Driver{{ "}}" }} ({{ "{{" }}.Scope{{ "}}" }})' + else + echo "Network $DOCKER_NETWORK not found" + fi + echo "" + + docker_status +} + +function show_help() { + cat << EOF +🚀 Enhanced Docker Management Script with Auto-Scaling +====================================================== +App: $APP_NAME +Environment: $ENVIRONMENT +Port Pattern: ${BASE_PORT}${replica_number:-1} + +USAGE: + $0 [COMMAND] [OPTIONS] + +DEPLOYMENT COMMANDS: + deploy [replicas] [version] [strategy] + Smart deployment with auto-scaling (recommended for CI/CD) + - replicas: Target number of replicas (default: $DEFAULT_REPLICAS) + - version: Docker image version (default: $CURRENT_TAG) + - strategy: rolling|blue-green|recreate (default: rolling) + + start [replicas] [version] [strategy] + Alias for deploy command + + restart [replicas] [version] [strategy] + Restart with smart deployment and auto-scaling + + restart_single [container_name] + Restart with smart deployment and auto-scaling + +MANUAL OPERATIONS: + stop + Stop all running containers + + scale + Manual scale to specified number of replicas (max: $MAX_REPLICAS) + + rollback [version] [replicas] + Rollback to previous or specific version + +MONITORING COMMANDS: + status + Show application status and health + + ports [replicas] + Show port mapping preview for specified replicas + + logs [lines] [follow] + View container logs + + archive-logs + Archive running container logs to timestamped files organized by prefix + (Usually called automatically by cron job every 15 minutes) + + health + Perform health check on all containers + + clean + Remove old images and containers + + info + Show detailed container and environment information + + help + Show this help message + +PORT ALLOCATION: + Base Port: $BASE_PORT + Pattern: ${base_port}${replica_number:-1} + + Examples: + - 1 replica: ${BASE_PORT}1 ($(get_replica_port "$BASE_PORT" "1")) + - 2 replicas: ${BASE_PORT}1, ${BASE_PORT}2 ($(get_replica_port "$BASE_PORT" "1"), $(get_replica_port "$BASE_PORT" "2")) + - 3 replicas: ${BASE_PORT}1, ${BASE_PORT}2, ${BASE_PORT}3 ($(get_replica_port "$BASE_PORT" "1"), $(get_replica_port "$BASE_PORT" "2"), $(get_replica_port "$BASE_PORT" "3")) + +EXAMPLES: + $0 deploy 5 v1.2.0 rolling + $0 scale 8 + $0 ports 4 + $0 rollback v1.1.0 2 + $0 logs 100 true + +EOF +} + +# ============================================================================ +# MAIN SCRIPT EXECUTION +# ============================================================================ + +function docker_manage() { + case "$1" in + deploy|start) + local replicas=${2:-$DEFAULT_REPLICAS} + local image_tag=${3:-$CURRENT_TAG} + local strategy=${4:-"rolling"} + date +%s > "$CONTAINER_LOCK_FILE" + deploy_with_auto_scale "$replicas" "$image_tag" "$strategy" + ;; + stop) + docker_stop + ;; + restart) + local replicas=${2:-$(get_container_count)} + local image_tag=${3:-$(get_current_version)} + local strategy=${4:-"rolling"} + docker_restart "$replicas" "$image_tag" "$strategy" + ;; + restart_single) + local restart_container=${2:-} + docker_restart_single "$restart_container" + ;; + scale) + if [[ -z "$2" ]]; then + log_error "Usage: $0 scale " + exit 1 + fi + docker_scale "$2" + ;; + rollback) + docker_rollback "$2" "$3" + ;; + status) + docker_status + ;; + ports) + local replicas=${2:-$DEFAULT_REPLICAS} + if [[ ! "$replicas" =~ ^[0-9]+$ ]] || [[ $replicas -lt 1 ]] || [[ $replicas -gt $MAX_REPLICAS ]]; then + log_error "Invalid replica count. Must be between 1 and $MAX_REPLICAS" + exit 1 + fi + show_port_mapping "$replicas" + ;; + deploy_active_ports) + show_active_ports + ;; + logs) + docker_logs "$2" "$3" + ;; + health) + docker_health + ;; + shell) + local container_num=${2:-1} + local container_name="${CONTAINER_PREFIX}-${container_num}" + echo "Entering shell of $container_name container..." + if docker ps --filter "name=$container_name" --quiet | head -1 &>/dev/null; then + docker exec -it "$container_name" /bin/sh + else + log_error "Container $container_name is not running" + exit 1 + fi + ;; + clean) + docker_clean + ;; + info) + docker_info + ;; + archive-logs) + archive_running_container_logs + rotate_container_runtime_logs + ;; + help|--help|-h) + show_help + ;; + *) + echo "❌ Unknown command: $1" + echo "" + show_help + exit 1 + ;; + esac +} + +# Execute main function with all arguments +docker_manage "$@" + +exit $? diff --git a/roles/docker/templates/monitor-container-script.sh b/roles/docker/templates/monitor-container-script.sh new file mode 100644 index 0000000..15ac6a5 --- /dev/null +++ b/roles/docker/templates/monitor-container-script.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# method 1: crontab with parallel block +#* * * * * flock -n /var/lock/monitor-container-script.lock /www/server/scripts/monitor-container-script.sh >>/var/log/monitor-container-script.log 2>&1 +set -Eeuo pipefail + +# method 2: crontab with parallel block +# only use method 1 or method 2 +# Khóa tiến trình (FD 9) +#~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ +exec 9>/var/lock/monitor-container-script.lock +if ! flock -n 9; then + echo "[$(date '+%F %T')] ⚠️ Another run is in progress. Exit." + exit 0 +fi +trap 'flock -u 9' EXIT +#~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ + +# === Cấu hình chung === +CONTAINER_PREFIX="webapplication-production-" +CONTAINER_PORT=3000 # cổng bên trong container (không cần dùng ở đây) +HOST="localhost" +TIMEOUT=3 +SLEEP_TIME=10 +COOLDOWN=180 # giây + +CPU_LIMIT=80 +MEM_LIMIT=90 +MANAGE_SCRIPT="/home/forlifeci/scripts/manage-webapplication-production.sh restart_single" + +CONTAINER_LOCK_FILE="/var/lib/container_maps.lock" +LOCK_DIR="/tmp/container_restart_locks" && mkdir -p "$LOCK_DIR/logs" + +remove_timestamp() { [[ $1 =~ ^(.*-[0-9]+)-([0-9]+)$ ]] && echo "${BASH_REMATCH[1]}" || echo "$1"; } + +check_container_health() { + local container=$1 + local cpu mem + + echo "📡 $(date '+%Y-%m-%d %H:%M:%S') - Kiểm tra container... $container" + + #cpu=$(docker stats --no-stream --format "{{.CPUPerc}}" "$container" | tr -d '%') + #mem=$(docker stats --no-stream --format "{{.MemPerc}}" "$container" | tr -d '%') + + #echo "Kiểm tra CPU / RAM quá giới hạn -> $container" + #if (( $(echo "$cpu > $CPU_LIMIT" | bc -l) )) || (( $(echo "$mem > $MEM_LIMIT" | bc -l) )); then + # echo "⚠️ $container quá tải (CPU: $cpu%, RAM: $mem%)" + # return 1 + #fi + + #echo "Kiểm tra port 3000 bên trong container -> $container" + if ! docker exec "$container" sh -c "nc -z -w 2 127.0.0.1 3000" >/dev/null 2>&1; then + echo "❌ $container không phản hồi trên port 3000" + return 1 + fi + + return 0 +} + +# === Telegram Config === +TELEGRAM_BOT_TOKEN="7837210802:AAHFlwXOdm3PExgIbADJ_TstNoXwJ_Aq7g4" +TELEGRAM_CHAT_ID="-1002468192435" +THREAD_ID="10248" + +# === Hàm gửi cảnh báo Telegram === +send_telegram_alert() { + local container=$1 + local port=$2 + local fileName=${3:-"log"} + local now=$(date '+%Y-%m-%d %H:%M:%S') + local msg="🚨 *CẢNH BÁO* 🚨 + +Container: \`$container\` +Port: \`$port\` +Thời gian: *$now* +Hành động: \`docker restart\` + +➡️ Container không phản hồi, đã được khởi động lại. + +http://42.118.103.30/logs/$container.restart.$fileName.html +" + + curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \ + -d chat_id="$TELEGRAM_CHAT_ID" \ + -d parse_mode="Markdown" \ + -d message_thread_id="$THREAD_ID" \ + -d text="$msg" > /dev/null +} + +# === Hàm kiểm tra port === +#check_port() { +# local port=$1 +# nc -z -w $TIMEOUT $HOST $port +#} + +check_port() { + local port=$1 + curl -s --max-time "$TIMEOUT" "http://$HOST:$port" -o /dev/null +} + +should_skip_check() { + local container=$1 + local lock_file="$LOCK_DIR/$container.lock" + + if [[ -f "$lock_file" ]]; then + local last_restart + last_restart=$(cat "$lock_file") + local now + now=$(date +%s) + if (( now - last_restart < COOLDOWN )); then + return 0 # bỏ qua + else + rm -f "$lock_file" # hết hạn cooldown + fi + fi + return 1 +} + +mark_restart() { + local container=$1 + date +%s > "$LOCK_DIR/$container.lock" +} + + +get_docker_binding_port() { + local container=$1 + echo $(docker port "$container" 3000/tcp | awk -F: '{print $2}' | xargs) +} + +containers=$(docker ps --format '{{.Names}}' | grep "^$CONTAINER_PREFIX") +# === Vòng lặp kiểm tra === +#while true; do + +for container in $containers; do + if [[ -f $CONTAINER_LOCK_FILE ]]; then + break + fi + + container_locked=$(remove_timestamp "$container") + + # 🔹 Bỏ qua nếu container đang trong cooldown + if should_skip_check "$container_locked"; then + echo "⏩ Bỏ qua $container_locked (đang cooldown sau khi restart)" + continue + fi + + host_port=$(get_docker_binding_port "$container") + + if [ -z "$host_port" ]; then + echo "⚠ $container không map port 3000/tcp, bỏ qua." + continue + fi + + echo ""; + echo "👉 Kiểm tra $container tại $HOST:$host_port..." + + if ! check_port $host_port; then + echo "⚠️ $container KHÔNG phản hồi. Chờ 5s để xác nhận..." + sleep 5 + if ! check_port $host_port; then + echo "❌ $container vẫn không phản hồi! Restart & gửi Telegram..." + logName=$(date +%s) + mark_restart "$container_locked" # đánh dấu cooldown + #docker restart "$container" + restart=`bash $MANAGE_SCRIPT $container` + logPath="$LOCK_DIR/logs/$container.restart.$logName.html" + if [ ! -f "$logPath" ]; then + echo "" > "$logPath" + fi + echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
" >> "$logPath" + printf "%s" "
$restart
" >> "$logPath" + send_telegram_alert "$container" "$host_port" "$logName" + else + echo "✅ $container đã phản hồi lại sau khi chờ." + fi + else + echo "✅ $container phản hồi OK." + fi +done + +# sleep $SLEEP_TIME +#done +#/home/forlifeci/scripts/manage-webapplication-production.sh restart_single $container diff --git a/roles/docker/templates/nginx-upstream.conf.j2 b/roles/docker/templates/nginx-upstream.conf.j2 new file mode 100644 index 0000000..6ed8cbb --- /dev/null +++ b/roles/docker/templates/nginx-upstream.conf.j2 @@ -0,0 +1,140 @@ +# ============================================================================ +# AUTO-GENERATED NGINX UPSTREAM CONFIGURATION +# Application: {{ app_name }} +# Environment: {{ deploy_env }}{{ env_tag | default('') }} +# Generated: {{ ansible_date_time.iso8601 }} +# Replicas: {{ replica_count }} +# ============================================================================ + +{% if framework is defined and framework == "nextjs" %} +map $arg_url ${{app_name | regex_replace('[^A-Za-z0-9_]', '_')}}_clean_url { + ~^(?.+)%3Fv%3D\d+$ $base; # chỉ có ?v=xxx + ~^(?.+)%3Fv%3D\d+%26(?.+)$ $base?$rest; # có ?v=xxx&... → giữ lại phần sau + default $arg_url; # mặc định giữ nguyên +} +{% endif %} + +upstream {{ upstream_name | regex_replace('[^A-Za-z0-9_]', '_') }} { + # Load balancing method + {% if load_balancing_method is defined %} + {{ load_balancing_method }}; + {% else %} + least_conn; + {% endif %} + + # Health check configuration + {% if health_check_interval is defined %} + health_check interval={{ health_check_interval }} fails={{ health_check_fails | default(1) }} passes={{ health_check_passes | default(2) }}; + {% endif %} + + # Upstream servers (auto-generated from active ports) + {% for port in active_ports %} + server localhost:{{ port }} weight={{ server_weight | default(1) }} max_fails={{ max_fails | default(1) }} fail_timeout={{ fail_timeout | default(10) }}s; + {% endfor %} + + # Backup server (optional) + {% if backup_server is defined %} + server {{ backup_server }} backup; + {% endif %} + + # Keep-alive connections + keepalive {{ keepalive_connections | default(64) }}; +} + +{% if framework is defined and framework == "nextjs" %} +proxy_cache_path /var/cache/nginx/{{app_name | regex_replace('[^A-Za-z0-9_]', '_')}}_cache levels=1:2 keys_zone={{app_name | regex_replace('[^A-Za-z0-9_]', '_')}}_cache:512m inactive=30d max_size=100g use_temp_path=off; + +#levels=1:2: Tạo cây thư mục theo hash key để phân bổ file. +#keys_zone=tkl_web_img_cache:512m: Tăng RAM dùng cho metadata (1MB chứa được ~8000 keys) → 2G = ~16 triệu keys. +#inactive=30d: Nếu file không được truy cập trong 30 ngày → bị xóa. +#max_size=100g: Giới hạn tối đa 100GB cache disk. +#use_temp_path=off: Ghi thẳng vào cache thay vì ghi tạm → hiệu quả hơn. +{% endif %} + +server { + listen {{ base_port }}; + server_name localhost; + + {% if framework is defined and framework == "nextjs" %} + + location ~* /_next/image { + access_log off; + proxy_pass http://{{ upstream_name | regex_replace('[^A-Za-z0-9_]', '_') }}; # upstream load balancing + + proxy_cache {{app_name | regex_replace('[^A-Za-z0-9_]', '_')}}_cache; + proxy_cache_key "url=${{app_name | regex_replace('[^A-Za-z0-9_]', '_')}}_clean_url&w=$arg_w&q=$arg_q"; + proxy_cache_valid 200 180d; + proxy_cache_use_stale error timeout updating http_502 http_503 http_504; + proxy_next_upstream error timeout http_502 http_503 http_504; + proxy_next_upstream_tries 3; + + # Chống cache stampede (bottleneck) + proxy_cache_lock on; + proxy_cache_lock_timeout 10s; + proxy_cache_lock_age 5s; + + # Timeout & buffering + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + send_timeout 60s; + + # Ignore upstream cache headers (Next.js gửi max-age + vary) + proxy_ignore_headers Cache-Control Vary; + proxy_hide_header Set-Cookie; + proxy_set_header Connection $connection_upgrade; + proxy_http_version 1.1; + + # Chuẩn hóa Accept để tránh tạo nhiều cache key khác nhau + proxy_set_header Accept "image/webp,image/*,*/*;q=0.8"; + + add_header X-Cache-Status $upstream_cache_status; + #add_header X-Cache-Key "url=${{app_name | regex_replace('[^A-Za-z0-9_]', '_')}}_clean_url&w=$arg_w&q=$arg_q"; + + # Keepalive settings (optional) + keepalive_timeout 64; + } + + {% endif %} + + location / { + access_log off; + + proxy_pass http://{{ upstream_name | regex_replace('[^A-Za-z0-9_]', '_') }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_http_version 1.1; + proxy_cache off; + + proxy_next_upstream error timeout http_502 http_503 http_504; + proxy_next_upstream_tries 3; + + # Timeout & buffering + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + send_timeout 60s; + + proxy_buffering on; + client_max_body_size 256M; + + # Keepalive settings (optional) + keepalive_timeout 64; + } + +} + +# ============================================================================ +# CONFIGURATION METADATA (for debugging) +# ============================================================================ +# Configuration Details: +# - Base Port: {{ base_port }} +# - Load Balancing: {{ load_balancing_method | default('least_conn') }} +# - Max Fails: {{ max_fails | default(1) }} +# - Fail Timeout: {{ fail_timeout | default(10) }}s +# - Health Check: {{ 'enabled' if enable_health_check_endpoint | default(true) else 'disabled' }} +# ============================================================================ diff --git a/roles/docker/templates/nginx/laravel.conf b/roles/docker/templates/nginx/laravel.conf new file mode 100644 index 0000000..b8af527 --- /dev/null +++ b/roles/docker/templates/nginx/laravel.conf @@ -0,0 +1,63 @@ +server { + listen 3000; + server_name localhost; + root /var/www/html/public; # Đây là thư mục public của Laravel + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options "nosniff"; + + index index.html index.htm index.php; + charset utf-8; + + #access_log /dev/stdout; + #error_log /dev/stdout; + + location ~\.(yml|env|example|log|prod|dev|local|production)$ { + deny all; + return 404; + } + + location ~* \.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|mp4|vtt)$ { + access_log off; + log_not_found off; + expires 30d; + add_header Access-Control-Allow-Origin *; + + } + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + error_page 404 /index.php; + + location ~ "^(.+\.php)($|/)" { + + index index.php index.html index.htm admin.php; + fastcgi_index index.php; + fastcgi_split_path_info ^(.+\.php)(.*)$; + fastcgi_pass 127.0.0.1:9000; + include fastcgi_params; + fastcgi_param HTTPS off; + send_timeout 120; + fastcgi_read_timeout 120; + client_max_body_size 20m; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} \ No newline at end of file diff --git a/roles/docker/templates/nginx/nginx.conf b/roles/docker/templates/nginx/nginx.conf new file mode 100644 index 0000000..23e65e0 --- /dev/null +++ b/roles/docker/templates/nginx/nginx.conf @@ -0,0 +1,108 @@ +#user nginx; +user www-data; + +# Set number of worker processes automatically based on number of CPU cores. +worker_processes auto; + +# Enables the use of JIT for regular expressions to speed-up their processing. +pcre_jit on; + +# Configures default error logger. +error_log /var/log/nginx/error.log warn; + +# Includes files with directives to load dynamic modules. +include /etc/nginx/modules/*.conf; + +# Include files with config snippets into the root context. +include /etc/nginx/conf.d/*.conf; + +events { + # The maximum number of simultaneous connections that can be opened by + # a worker process. + worker_connections 1024; +} + +http { + # Includes mapping of file name extensions to MIME types of responses + # and defines the default type. + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Name servers used to resolve names of upstream servers into addresses. + # It's also needed when using tcpsocket and udpsocket in Lua modules. + #resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001]; + + # Don't tell nginx version to the clients. Default is 'on'. + server_tokens off; + + # Specifies the maximum accepted body size of a client request, as + # indicated by the request header Content-Length. If the stated content + # length is greater than this size, then the client receives the HTTP + # error code 413. Set to 0 to disable. Default is '1m'. + client_max_body_size 100m; + + # Sendfile copies data between one FD and other from within the kernel, + # which is more efficient than read() + write(). Default is off. + sendfile on; + + # Causes nginx to attempt to send its HTTP response head in one packet, + # instead of using partial frames. Default is 'off'. + tcp_nopush on; + + + # Enables the specified protocols. Default is TLSv1 TLSv1.1 TLSv1.2. + # TIP: If you're not obligated to support ancient clients, remove TLSv1.1. + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + + # Path of the file with Diffie-Hellman parameters for EDH ciphers. + # TIP: Generate with: `openssl dhparam -out /etc/ssl/nginx/dh2048.pem 2048` + #ssl_dhparam /etc/ssl/nginx/dh2048.pem; + + # Specifies that our cipher suits should be preferred over client ciphers. + # Default is 'off'. + ssl_prefer_server_ciphers on; + + # Enables a shared SSL cache with size that can hold around 8000 sessions. + # Default is 'none'. + ssl_session_cache shared:SSL:2m; + + # Specifies a time during which a client may reuse the session parameters. + # Default is '5m'. + ssl_session_timeout 1h; + + # Disable TLS session tickets (they are insecure). Default is 'on'. + ssl_session_tickets off; + + + # Enable gzipping of responses. + gzip on; + + # Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'. + #gzip_vary on; + + + # Helper variable for proxying websockets. + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + + # Specifies the main log format. + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + # Sets the path, format, and configuration for a buffered log write. + access_log /var/log/nginx/access.log main; + + real_ip_header X-Forwarded-For; + real_ip_recursive on; + set_real_ip_from 0.0.0.0/0; + + #access_log /dev/stdout; + #error_log /dev/stdout; + + # Includes virtual hosts configs. + include /etc/nginx/http.d/*.conf; +} \ No newline at end of file diff --git a/roles/docker/templates/nginx/php.ini b/roles/docker/templates/nginx/php.ini new file mode 100644 index 0000000..28e9e18 --- /dev/null +++ b/roles/docker/templates/nginx/php.ini @@ -0,0 +1,2023 @@ +[PHP] + +;;;;;;;;;;;;;;;;;;; +; About php.ini ; +;;;;;;;;;;;;;;;;;;; +; PHP's initialization file, generally called php.ini, is responsible for +; configuring many of the aspects of PHP's behavior. + +; PHP attempts to find and load this configuration from a number of locations. +; The following is a summary of its search order: +; 1. SAPI module specific location. +; 2. The PHPRC environment variable. (As of PHP 5.2.0) +; 3. A number of predefined registry keys on Windows (As of PHP 5.2.0) +; 4. Current working directory (except CLI) +; 5. The web server's directory (for SAPI modules), or directory of PHP +; (otherwise in Windows) +; 6. The directory from the --with-config-file-path compile time option, or the +; Windows directory (C:\windows or C:\winnt) +; See the PHP docs for more specific information. +; http://php.net/configuration.file + +; The syntax of the file is extremely simple. Whitespace and lines +; beginning with a semicolon are silently ignored (as you probably guessed). +; Section headers (e.g. [Foo]) are also silently ignored, even though +; they might mean something in the future. + +; Directives following the section heading [PATH=/www/mysite] only +; apply to PHP files in the /www/mysite directory. Directives +; following the section heading [HOST=www.example.com] only apply to +; PHP files served from www.example.com. Directives set in these +; special sections cannot be overridden by user-defined INI files or +; at runtime. Currently, [PATH=] and [HOST=] sections only work under +; CGI/FastCGI. +; http://php.net/ini.sections + +; Directives are specified using the following syntax: +; directive = value +; Directive names are *case sensitive* - foo=bar is different from FOO=bar. +; Directives are variables used to configure PHP or PHP extensions. +; There is no name validation. If PHP can't find an expected +; directive because it is not set or is mistyped, a default value will be used. + +; The value can be a string, a number, a PHP constant (e.g. E_ALL or M_PI), one +; of the INI constants (On, Off, True, False, Yes, No and None) or an expression +; (e.g. E_ALL & ~E_NOTICE), a quoted string ("bar"), or a reference to a +; previously set variable or directive (e.g. ${foo}) + +; Expressions in the INI file are limited to bitwise operators and parentheses: +; | bitwise OR +; ^ bitwise XOR +; & bitwise AND +; ~ bitwise NOT +; ! boolean NOT + +; Boolean flags can be turned on using the values 1, On, True or Yes. +; They can be turned off using the values 0, Off, False or No. + +; An empty string can be denoted by simply not writing anything after the equal +; sign, or by using the None keyword: + +; foo = ; sets foo to an empty string +; foo = None ; sets foo to an empty string +; foo = "None" ; sets foo to the string 'None' + +; If you use constants in your value, and these constants belong to a +; dynamically loaded extension (either a PHP extension or a Zend extension), +; you may only use these constants *after* the line that loads the extension. + +;;;;;;;;;;;;;;;;;;; +; About this file ; +;;;;;;;;;;;;;;;;;;; +; PHP comes packaged with two INI files. One that is recommended to be used +; in production environments and one that is recommended to be used in +; development environments. + +; php.ini-production contains settings which hold security, performance and +; best practices at its core. But please be aware, these settings may break +; compatibility with older or less security conscience applications. We +; recommending using the production ini in production and testing environments. + +; php.ini-development is very similar to its production variant, except it's +; much more verbose when it comes to errors. We recommending using the +; development version only in development environments as errors shown to +; application users can inadvertently leak otherwise secure information. + +; This is php.ini-production INI file. + +;;;;;;;;;;;;;;;;;;; +; Quick Reference ; +;;;;;;;;;;;;;;;;;;; +; The following are all the settings which are different in either the production +; or development versions of the INIs with respect to PHP's default behavior. +; Please see the actual settings later in the document for more details as to why +; we recommend these changes in PHP's behavior. + +; display_errors +; Default Value: On +; Development Value: On +; Production Value: Off + +; display_startup_errors +; Default Value: Off +; Development Value: On +; Production Value: Off + +; error_reporting +; Default Value: E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED +; Development Value: E_ALL +; Production Value: E_ALL & ~E_DEPRECATED & ~E_STRICT +; Production Value: E_ALL & ~E_DEPRECATED +; html_errors +; Default Value: On +; Development Value: On +; Production value: On + +; log_errors +; Default Value: Off +; Development Value: On +; Production Value: On + +; max_input_time +; Default Value: -1 (Unlimited) +; Development Value: 60 (60 seconds) +; Production Value: 60 (60 seconds) + +; output_buffering +; Default Value: Off +; Development Value: 4096 +; Production Value: 4096 + +; register_argc_argv +; Default Value: On +; Development Value: Off +; Production Value: Off + +; request_order +; Default Value: None +; Development Value: "GP" +; Production Value: "GP" + +; session.gc_divisor +; Default Value: 100 +; Development Value: 1000 +; Production Value: 1000 + +; session.hash_bits_per_character +; Default Value: 4 +; Development Value: 5 +; Production Value: 5 + +; short_open_tag +; Default Value: On +; Development Value: Off +; Production Value: Off + +; track_errors +; Default Value: Off +; Development Value: On +; Production Value: Off + +; url_rewriter.tags +; Default Value: "a=href,area=href,frame=src,form=,fieldset=" +; Development Value: "a=href,area=href,frame=src,input=src,form=fakeentry" +; Production Value: "a=href,area=href,frame=src,input=src,form=fakeentry" + +; variables_order +; Default Value: "EGPCS" +; Development Value: "GPCS" +; Production Value: "GPCS" + +;;;;;;;;;;;;;;;;;;;; +; php.ini Options ; +;;;;;;;;;;;;;;;;;;;; +; Name for user-defined php.ini (.htaccess) files. Default is ".user.ini" +;user_ini.filename = ".user.ini" + +; To disable this feature set this option to empty value +;user_ini.filename = + +; TTL for user-defined php.ini files (time-to-live) in seconds. Default is 300 seconds (5 minutes) +;user_ini.cache_ttl = 300 + +;;;;;;;;;;;;;;;;;;;; +; Language Options ; +;;;;;;;;;;;;;;;;;;;; + +; Enable the PHP scripting language engine under Apache. +; http://php.net/engine +engine = On + +; This directive determines whether or not PHP will recognize code between +; tags as PHP source which should be processed as such. It is +; generally recommended that should be used and that this feature +; should be disabled, as enabling it may result in issues when generating XML +; documents, however this remains supported for backward compatibility reasons. +; Note that this directive does not control the tags. +; http://php.net/asp-tags +asp_tags = Off + +; The number of significant digits displayed in floating point numbers. +; http://php.net/precision +precision = 14 + +; Output buffering is a mechanism for controlling how much output data +; (excluding headers and cookies) PHP should keep internally before pushing that +; data to the client. If your application's output exceeds this setting, PHP +; will send that data in chunks of roughly the size you specify. +; Turning on this setting and managing its maximum buffer size can yield some +; interesting side-effects depending on your application and web server. +; You may be able to send headers and cookies after you've already sent output +; through print or echo. You also may see performance benefits if your server is +; emitting less packets due to buffered output versus PHP streaming the output +; as it gets it. On production servers, 4096 bytes is a good setting for performance +; reasons. +; Note: Output buffering can also be controlled via Output Buffering Control +; functions. +; Possible Values: +; On = Enabled and buffer is unlimited. (Use with caution) +; Off = Disabled +; Integer = Enables the buffer and sets its maximum size in bytes. +; Note: This directive is hardcoded to Off for the CLI SAPI +; Default Value: Off +; Development Value: 4096 +; Production Value: 4096 +; http://php.net/output-buffering +output_buffering = 4096 + +; You can redirect all of the output of your scripts to a function. For +; example, if you set output_handler to "mb_output_handler", character +; encoding will be transparently converted to the specified encoding. +; Setting any output handler automatically turns on output buffering. +; Note: People who wrote portable scripts should not depend on this ini +; directive. Instead, explicitly set the output handler using ob_start(). +; Using this ini directive may cause problems unless you know what script +; is doing. +; Note: You cannot use both "mb_output_handler" with "ob_iconv_handler" +; and you cannot use both "ob_gzhandler" and "zlib.output_compression". +; Note: output_handler must be empty if this is set 'On' !!!! +; Instead you must use zlib.output_handler. +; http://php.net/output-handler +;output_handler = + +; Transparent output compression using the zlib library +; Valid values for this option are 'off', 'on', or a specific buffer size +; to be used for compression (default is 4KB) +; Note: Resulting chunk size may vary due to nature of compression. PHP +; outputs chunks that are few hundreds bytes each as a result of +; compression. If you prefer a larger chunk size for better +; performance, enable output_buffering in addition. +; Note: You need to use zlib.output_handler instead of the standard +; output_handler, or otherwise the output will be corrupted. +; http://php.net/zlib.output-compression +zlib.output_compression = Off + +; http://php.net/zlib.output-compression-level +;zlib.output_compression_level = -1 + +; You cannot specify additional output handlers if zlib.output_compression +; is activated here. This setting does the same as output_handler but in +; a different order. +; http://php.net/zlib.output-handler +;zlib.output_handler = + +; Implicit flush tells PHP to tell the output layer to flush itself +; automatically after every output block. This is equivalent to calling the +; PHP function flush() after each and every call to print() or echo() and each +; and every HTML block. Turning this option on has serious performance +; implications and is generally recommended for debugging purposes only. +; http://php.net/implicit-flush +; Note: This directive is hardcoded to On for the CLI SAPI +implicit_flush = Off + +; The unserialize callback function will be called (with the undefined class' +; name as parameter), if the unserializer finds an undefined class +; which should be instantiated. A warning appears if the specified function is +; not defined, or if the function doesn't include/implement the missing class. +; So only set this entry, if you really want to implement such a +; callback-function. +unserialize_callback_func = + +; When floats & doubles are serialized store serialize_precision significant +; digits after the floating point. The default value ensures that when floats +; are decoded with unserialize, the data will remain the same. +serialize_precision = 17 + +; open_basedir, if set, limits all file operations to the defined directory +; and below. This directive makes most sense if used in a per-directory +; or per-virtualhost web server configuration file. +; http://php.net/open-basedir +;open_basedir = + +; This directive allows you to disable certain functions for security reasons. +; It receives a comma-delimited list of function names. +; http://php.net/disable-functions +disable_functions = + +; This directive allows you to disable certain classes for security reasons. +; It receives a comma-delimited list of class names. +; http://php.net/disable-classes +disable_classes = + +; Colors for Syntax Highlighting mode. Anything that's acceptable in +; would work. +; http://php.net/syntax-highlighting +;highlight.string = #DD0000 +;highlight.comment = #FF9900 +;highlight.keyword = #007700 +;highlight.default = #0000BB +;highlight.html = #000000 + +; If enabled, the request will be allowed to complete even if the user aborts +; the request. Consider enabling it if executing long requests, which may end up +; being interrupted by the user or a browser timing out. PHP's default behavior +; is to disable this feature. +; http://php.net/ignore-user-abort +;ignore_user_abort = On + +; Determines the size of the realpath cache to be used by PHP. This value should +; be increased on systems where PHP opens many files to reflect the quantity of +; the file operations performed. +; http://php.net/realpath-cache-size +;realpath_cache_size = 16k + +; Duration of time, in seconds for which to cache realpath information for a given +; file or directory. For systems with rarely changing files, consider increasing this +; value. +; http://php.net/realpath-cache-ttl +;realpath_cache_ttl = 120 + +; Enables or disables the circular reference collector. +; http://php.net/zend.enable-gc +zend.enable_gc = On + +; If enabled, scripts may be written in encodings that are incompatible with +; the scanner. CP936, Big5, CP949 and Shift_JIS are the examples of such +; encodings. To use this feature, mbstring extension must be enabled. +; Default: Off +;zend.multibyte = Off + +; Allows to set the default encoding for the scripts. This value will be used +; unless "declare(encoding=...)" directive appears at the top of the script. +; Only affects if zend.multibyte is set. +; Default: "" +;zend.script_encoding = + +;;;;;;;;;;;;;;;;; +; Miscellaneous ; +;;;;;;;;;;;;;;;;; + +; Decides whether PHP may expose the fact that it is installed on the server +; (e.g. by adding its signature to the Web server header). It is no security +; threat in any way, but it makes it possible to determine whether you use PHP +; on your server or not. +; http://php.net/expose-php +expose_php = Off + +;;;;;;;;;;;;;;;;;;; +; Resource Limits ; +;;;;;;;;;;;;;;;;;;; + +; Maximum execution time of each script, in seconds +; http://php.net/max-execution-time +; Note: This directive is hardcoded to 0 for the CLI SAPI +max_execution_time = 120 + +; Maximum amount of time each script may spend parsing request data. It's a good +; idea to limit this time on productions servers in order to eliminate unexpectedly +; long running scripts. +; Note: This directive is hardcoded to -1 for the CLI SAPI +; Default Value: -1 (Unlimited) +; Development Value: 60 (60 seconds) +; Production Value: 60 (60 seconds) +; http://php.net/max-input-time +max_input_time = 120 + +; Maximum input variable nesting level +; http://php.net/max-input-nesting-level +;max_input_nesting_level = 64 + +; How many GET/POST/COOKIE input variables may be accepted +; max_input_vars = 1000 +max_input_vars = 3000 + +; Maximum amount of memory a script may consume (128MB) +; http://php.net/memory-limit +memory_limit = 1024M + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Error handling and logging ; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +; This directive informs PHP of which errors, warnings and notices you would like +; it to take action for. The recommended way of setting values for this +; directive is through the use of the error level constants and bitwise +; operators. The error level constants are below here for convenience as well as +; some common settings and their meanings. +; By default, PHP is set to take action on all errors, notices and warnings EXCEPT +; those related to E_NOTICE and E_STRICT, which together cover best practices and +; recommended coding standards in PHP. For performance reasons, this is the +; recommend error reporting setting. Your production server shouldn't be wasting +; resources complaining about best practices and coding standards. That's what +; development servers and development settings are for. +; Note: The php.ini-development file has this setting as E_ALL. This +; means it pretty much reports everything which is exactly what you want during +; development and early testing. +; +; Error Level Constants: +; E_ALL - All errors and warnings (includes E_STRICT as of PHP 5.4.0) +; E_ERROR - fatal run-time errors +; E_RECOVERABLE_ERROR - almost fatal run-time errors +; E_WARNING - run-time warnings (non-fatal errors) +; E_PARSE - compile-time parse errors +; E_NOTICE - run-time notices (these are warnings which often result +; from a bug in your code, but it's possible that it was +; intentional (e.g., using an uninitialized variable and +; relying on the fact it's automatically initialized to an +; empty string) +; E_STRICT - run-time notices, enable to have PHP suggest changes +; to your code which will ensure the best interoperability +; and forward compatibility of your code +; E_CORE_ERROR - fatal errors that occur during PHP's initial startup +; E_CORE_WARNING - warnings (non-fatal errors) that occur during PHP's +; initial startup +; E_COMPILE_ERROR - fatal compile-time errors +; E_COMPILE_WARNING - compile-time warnings (non-fatal errors) +; E_USER_ERROR - user-generated error message +; E_USER_WARNING - user-generated warning message +; E_USER_NOTICE - user-generated notice message +; E_DEPRECATED - warn about code that will not work in future versions +; of PHP +; E_USER_DEPRECATED - user-generated deprecation warnings +; +; Common Values: +; E_ALL (Show all errors, warnings and notices including coding standards.) +; E_ALL & ~E_NOTICE (Show all errors, except for notices) +; E_ALL & ~E_NOTICE & ~E_STRICT (Show all errors, except for notices and coding standards warnings.) +; E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR (Show only errors) +; Default Value: E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED +; Development Value: E_ALL +; Production Value: E_ALL & ~E_DEPRECATED & ~E_STRICT +; http://php.net/error-reporting +;error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT +error_reporting = E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR + +; This directive controls whether or not and where PHP will output errors, +; notices and warnings too. Error output is very useful during development, but +; it could be very dangerous in production environments. Depending on the code +; which is triggering the error, sensitive information could potentially leak +; out of your application such as database usernames and passwords or worse. +; It's recommended that errors be logged on production servers rather than +; having the errors sent to STDOUT. +; Possible Values: +; Off = Do not display any errors +; stderr = Display errors to STDERR (affects only CGI/CLI binaries!) +; On or stdout = Display errors to STDOUT +; Default Value: On +; Development Value: On +; Production Value: Off +; http://php.net/display-errors +display_errors = Off + +; The display of errors which occur during PHP's startup sequence are handled +; separately from display_errors. PHP's default behavior is to suppress those +; errors from clients. Turning the display of startup errors on can be useful in +; debugging configuration problems. But, it's strongly recommended that you +; leave this setting off on production servers. +; Default Value: Off +; Development Value: On +; Production Value: Off +; http://php.net/display-startup-errors +display_startup_errors = Off + +; Besides displaying errors, PHP can also log errors to locations such as a +; server-specific log, STDERR, or a location specified by the error_log +; directive found below. While errors should not be displayed on productions +; servers they should still be monitored and logging is a great way to do that. +; Default Value: Off +; Development Value: On +; Production Value: On +; http://php.net/log-errors +log_errors = On + +; Set maximum length of log_errors. In error_log information about the source is +; added. The default is 1024 and 0 allows to not apply any maximum length at all. +; http://php.net/log-errors-max-len +log_errors_max_len = 1024 + +; Do not log repeated messages. Repeated errors must occur in same file on same +; line unless ignore_repeated_source is set true. +; http://php.net/ignore-repeated-errors +ignore_repeated_errors = Off + +; Ignore source of message when ignoring repeated messages. When this setting +; is On you will not log errors with repeated messages from different files or +; source lines. +; http://php.net/ignore-repeated-source +ignore_repeated_source = Off + +; If this parameter is set to Off, then memory leaks will not be shown (on +; stdout or in the log). This has only effect in a debug compile, and if +; error reporting includes E_WARNING in the allowed list +; http://php.net/report-memleaks +report_memleaks = On + +; This setting is on by default. +;report_zend_debug = 0 + +; Store the last error/warning message in $php_errormsg (boolean). Setting this value +; to On can assist in debugging and is appropriate for development servers. It should +; however be disabled on production servers. +; Default Value: Off +; Development Value: On +; Production Value: Off +; http://php.net/track-errors +track_errors = Off + +; Turn off normal error reporting and emit XML-RPC error XML +; http://php.net/xmlrpc-errors +;xmlrpc_errors = 0 + +; An XML-RPC faultCode +;xmlrpc_error_number = 0 + +; When PHP displays or logs an error, it has the capability of formatting the +; error message as HTML for easier reading. This directive controls whether +; the error message is formatted as HTML or not. +; Note: This directive is hardcoded to Off for the CLI SAPI +; Default Value: On +; Development Value: On +; Production value: On +; http://php.net/html-errors +html_errors = On + +; If html_errors is set to On *and* docref_root is not empty, then PHP +; produces clickable error messages that direct to a page describing the error +; or function causing the error in detail. +; You can download a copy of the PHP manual from http://php.net/docs +; and change docref_root to the base URL of your local copy including the +; leading '/'. You must also specify the file extension being used including +; the dot. PHP's default behavior is to leave these settings empty, in which +; case no links to documentation are generated. +; Note: Never use this feature for production boxes. +; http://php.net/docref-root +; Examples +;docref_root = "/phpmanual/" + +; http://php.net/docref-ext +;docref_ext = .html + +; String to output before an error message. PHP's default behavior is to leave +; this setting blank. +; http://php.net/error-prepend-string +; Example: +;error_prepend_string = "" + +; String to output after an error message. PHP's default behavior is to leave +; this setting blank. +; http://php.net/error-append-string +; Example: +;error_append_string = "" + +; Log errors to specified file. PHP's default behavior is to leave this value +; empty. +; http://php.net/error-log +; Example: +;error_log = php_errors.log +; Log errors to syslog (Event Log on Windows). +;error_log = syslog + +;windows.show_crt_warning +; Default value: 0 +; Development value: 0 +; Production value: 0 + +;;;;;;;;;;;;;;;;; +; Data Handling ; +;;;;;;;;;;;;;;;;; + +; The separator used in PHP generated URLs to separate arguments. +; PHP's default setting is "&". +; http://php.net/arg-separator.output +; Example: +;arg_separator.output = "&" + +; List of separator(s) used by PHP to parse input URLs into variables. +; PHP's default setting is "&". +; NOTE: Every character in this directive is considered as separator! +; http://php.net/arg-separator.input +; Example: +;arg_separator.input = ";&" + +; This directive determines which super global arrays are registered when PHP +; starts up. G,P,C,E & S are abbreviations for the following respective super +; globals: GET, POST, COOKIE, ENV and SERVER. There is a performance penalty +; paid for the registration of these arrays and because ENV is not as commonly +; used as the others, ENV is not recommended on productions servers. You +; can still get access to the environment variables through getenv() should you +; need to. +; Default Value: "EGPCS" +; Development Value: "GPCS" +; Production Value: "GPCS"; +; http://php.net/variables-order +variables_order = "GPCS" + +; This directive determines which super global data (G,P,C,E & S) should +; be registered into the super global array REQUEST. If so, it also determines +; the order in which that data is registered. The values for this directive are +; specified in the same manner as the variables_order directive, EXCEPT one. +; Leaving this value empty will cause PHP to use the value set in the +; variables_order directive. It does not mean it will leave the super globals +; array REQUEST empty. +; Default Value: None +; Development Value: "GP" +; Production Value: "GP" +; http://php.net/request-order +request_order = "GP" + +; This directive determines whether PHP registers $argv & $argc each time it +; runs. $argv contains an array of all the arguments passed to PHP when a script +; is invoked. $argc contains an integer representing the number of arguments +; that were passed when the script was invoked. These arrays are extremely +; useful when running scripts from the command line. When this directive is +; enabled, registering these variables consumes CPU cycles and memory each time +; a script is executed. For performance reasons, this feature should be disabled +; on production servers. +; Note: This directive is hardcoded to On for the CLI SAPI +; Default Value: On +; Development Value: Off +; Production Value: Off +; http://php.net/register-argc-argv +register_argc_argv = Off + +; When enabled, the ENV, REQUEST and SERVER variables are created when they're +; first used (Just In Time) instead of when the script starts. If these +; variables are not used within a script, having this directive on will result +; in a performance gain. The PHP directive register_argc_argv must be disabled +; for this directive to have any affect. +; http://php.net/auto-globals-jit +auto_globals_jit = On + +; Whether PHP will read the POST data. +; This option is enabled by default. +; Most likely, you won't want to disable this option globally. It causes $_POST +; and $_FILES to always be empty; the only way you will be able to read the +; POST data will be through the php://input stream wrapper. This can be useful +; to proxy requests or to process the POST data in a memory efficient fashion. +; http://php.net/enable-post-data-reading +;enable_post_data_reading = Off + +; Maximum size of POST data that PHP will accept. +; Its value may be 0 to disable the limit. It is ignored if POST data reading +; is disabled through enable_post_data_reading. +; http://php.net/post-max-size +post_max_size = 128M + +; Automatically add files before PHP document. +; http://php.net/auto-prepend-file +auto_prepend_file = + +; Automatically add files after PHP document. +; http://php.net/auto-append-file +auto_append_file = + +; By default, PHP will output a character encoding using +; the Content-type: header. To disable sending of the charset, simply +; set it to be empty. +; +; PHP's built-in default is text/html +; http://php.net/default-mimetype +default_mimetype = "text/html" + +; PHP's default character set is set to empty. +; http://php.net/default-charset +;default_charset = "UTF-8" + +; Always populate the $HTTP_RAW_POST_DATA variable. PHP's default behavior is +; to disable this feature. If post reading is disabled through +; enable_post_data_reading, $HTTP_RAW_POST_DATA is *NOT* populated. +; http://php.net/always-populate-raw-post-data +;always_populate_raw_post_data = On + +;;;;;;;;;;;;;;;;;;;;;;;;; +; Paths and Directories ; +;;;;;;;;;;;;;;;;;;;;;;;;; + +; UNIX: "/path1:/path2" +;include_path = ".:/php/includes" +; +; Windows: "\path1;\path2" +;include_path = ".;c:\php\includes" +; +; PHP's default setting for include_path is ".;/path/to/php/pear" +; http://php.net/include-path + +; The root of the PHP pages, used only if nonempty. +; if PHP was not compiled with FORCE_REDIRECT, you SHOULD set doc_root +; if you are running php as a CGI under any web server (other than IIS) +; see documentation for security issues. The alternate is to use the +; cgi.force_redirect configuration below +; http://php.net/doc-root +doc_root = + +; The directory under which PHP opens the script using /~username used only +; if nonempty. +; http://php.net/user-dir +user_dir = + +; Directory in which the loadable extensions (modules) reside. +; http://php.net/extension-dir +; extension_dir = "./" +; On windows: +; extension_dir = "ext" + +; Directory where the temporary files should be placed. +; Defaults to the system default (see sys_get_temp_dir) +; sys_temp_dir = "/tmp" + +; Whether or not to enable the dl() function. The dl() function does NOT work +; properly in multithreaded servers, such as IIS or Zeus, and is automatically +; disabled on them. +; http://php.net/enable-dl +enable_dl = Off + +; cgi.force_redirect is necessary to provide security running PHP as a CGI under +; most web servers. Left undefined, PHP turns this on by default. You can +; turn it off here AT YOUR OWN RISK +; **You CAN safely turn this off for IIS, in fact, you MUST.** +; http://php.net/cgi.force-redirect +;cgi.force_redirect = 1 + +; if cgi.nph is enabled it will force cgi to always sent Status: 200 with +; every request. PHP's default behavior is to disable this feature. +;cgi.nph = 1 + +; if cgi.force_redirect is turned on, and you are not running under Apache or Netscape +; (iPlanet) web servers, you MAY need to set an environment variable name that PHP +; will look for to know it is OK to continue execution. Setting this variable MAY +; cause security issues, KNOW WHAT YOU ARE DOING FIRST. +; http://php.net/cgi.redirect-status-env +;cgi.redirect_status_env = + +; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP's +; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok +; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting +; this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting +; of zero causes PHP to behave as before. Default is 1. You should fix your scripts +; to use SCRIPT_FILENAME rather than PATH_TRANSLATED. +; http://php.net/cgi.fix-pathinfo +;cgi.fix_pathinfo=1 + +; FastCGI under IIS (on WINNT based OS) supports the ability to impersonate +; security tokens of the calling client. This allows IIS to define the +; security context that the request runs under. mod_fastcgi under Apache +; does not currently support this feature (03/17/2002) +; Set to 1 if running under IIS. Default is zero. +; http://php.net/fastcgi.impersonate +;fastcgi.impersonate = 1 + +; Disable logging through FastCGI connection. PHP's default behavior is to enable +; this feature. +;fastcgi.logging = 0 + +; cgi.rfc2616_headers configuration option tells PHP what type of headers to +; use when sending HTTP response code. If it's set 0 PHP sends Status: header that +; is supported by Apache. When this option is set to 1 PHP will send +; RFC2616 compliant header. +; Default is zero. +; http://php.net/cgi.rfc2616-headers +;cgi.rfc2616_headers = 0 + +;;;;;;;;;;;;;;;; +; File Uploads ; +;;;;;;;;;;;;;;;; + +; Whether to allow HTTP file uploads. +; http://php.net/file-uploads +file_uploads = On + +; Temporary directory for HTTP uploaded files (will use system default if not +; specified). +; http://php.net/upload-tmp-dir +;upload_tmp_dir = + +; Maximum allowed size for uploaded files. +; http://php.net/upload-max-filesize +upload_max_filesize = 50M + +; Maximum number of files that can be uploaded via a single request +max_file_uploads = 20 + +;;;;;;;;;;;;;;;;;; +; Fopen wrappers ; +;;;;;;;;;;;;;;;;;; + +; Whether to allow the treatment of URLs (like http:// or ftp://) as files. +; http://php.net/allow-url-fopen +allow_url_fopen = On + +; Whether to allow include/require to open URLs (like http:// or ftp://) as files. +; http://php.net/allow-url-include +allow_url_include = Off + +; Define the anonymous ftp password (your email address). PHP's default setting +; for this is empty. +; http://php.net/from +;from="john@doe.com" + +; Define the User-Agent string. PHP's default setting for this is empty. +; http://php.net/user-agent +;user_agent="PHP" + +; Default timeout for socket based streams (seconds) +; http://php.net/default-socket-timeout +default_socket_timeout = 30 + +; If your scripts have to deal with files from Macintosh systems, +; or you are running on a Mac and need to deal with files from +; unix or win32 systems, setting this flag will cause PHP to +; automatically detect the EOL character in those files so that +; fgets() and file() will work regardless of the source of the file. +; http://php.net/auto-detect-line-endings +;auto_detect_line_endings = Off + +;;;;;;;;;;;;;;;;;;;;;; +; Dynamic Extensions ; +;;;;;;;;;;;;;;;;;;;;;; + +; If you wish to have an extension loaded automatically, use the following +; syntax: +; +; extension=modulename.extension +; +; For example, on Windows: +; +; extension=msql.dll +; +; ... or under UNIX: +; +; extension=msql.so +; +; ... or with a path: +; +; extension=/path/to/extension/msql.so +; +; If you only provide the name of the extension, PHP will look for it in its +; default extension directory. +; +; Windows Extensions +; Note that ODBC support is built in, so no dll is needed for it. +; Note that many DLL files are located in the extensions/ (PHP 4) ext/ (PHP 5) +; extension folders as well as the separate PECL DLL download (PHP 5). +; Be sure to appropriately set the extension_dir directive. +; +;extension=php_bz2.dll +;extension=php_curl.dll +;extension=php_fileinfo.dll +;extension=php_gd2.dll +;extension=php_gettext.dll +;extension=php_gmp.dll +;extension=php_intl.dll +;extension=php_imap.dll +;extension=php_interbase.dll +;extension=php_ldap.dll +;extension=php_mbstring.dll +;extension=php_exif.dll ; Must be after mbstring as it depends on it +;extension=php_mysql.dll +;extension=php_mysqli.dll +;extension=php_oci8.dll ; Use with Oracle 10gR2 Instant Client +;extension=php_oci8_11g.dll ; Use with Oracle 11gR2 Instant Client +;extension=php_openssl.dll +;extension=php_pdo_firebird.dll +;extension=php_pdo_mysql.dll +;extension=php_pdo_oci.dll +;extension=php_pdo_odbc.dll +;extension=php_pdo_pgsql.dll +;extension=php_pdo_sqlite.dll +;extension=php_pgsql.dll +;extension=php_pspell.dll +;extension=php_shmop.dll + +; The MIBS data available in the PHP distribution must be installed. +; See http://www.php.net/manual/en/snmp.installation.php +;extension=php_snmp.dll + +;extension=php_soap.dll +;extension=php_sockets.dll +;extension=php_sqlite3.dll +;extension=php_sybase_ct.dll +;extension=php_tidy.dll +;extension=php_xmlrpc.dll +;extension=php_xsl.dll + +;;;;;;;;;;;;;;;;;;; +; Module Settings ; +;;;;;;;;;;;;;;;;;;; + +[CLI Server] +; Whether the CLI web server uses ANSI color coding in its terminal output. +cli_server.color = On + +[Date] +; Defines the default timezone used by the date functions +; http://php.net/date.timezone +date.timezone = "Asia/Ho_Chi_Minh" + +; http://php.net/date.default-latitude +;date.default_latitude = 31.7667 + +; http://php.net/date.default-longitude +;date.default_longitude = 35.2333 + +; http://php.net/date.sunrise-zenith +;date.sunrise_zenith = 90.583333 + +; http://php.net/date.sunset-zenith +;date.sunset_zenith = 90.583333 + +[filter] +; http://php.net/filter.default +;filter.default = unsafe_raw + +; http://php.net/filter.default-flags +;filter.default_flags = + +[iconv] +;iconv.input_encoding = ISO-8859-1 +;iconv.internal_encoding = ISO-8859-1 +;iconv.output_encoding = ISO-8859-1 + +[intl] +;intl.default_locale = +; This directive allows you to produce PHP errors when some error +; happens within intl functions. The value is the level of the error produced. +; Default is 0, which does not produce any errors. +;intl.error_level = E_WARNING + +[sqlite] +; http://php.net/sqlite.assoc-case +;sqlite.assoc_case = 0 + +[sqlite3] +;sqlite3.extension_dir = + +[Pcre] +;PCRE library backtracking limit. +; http://php.net/pcre.backtrack-limit +;pcre.backtrack_limit=100000 + +;PCRE library recursion limit. +;Please note that if you set this value to a high number you may consume all +;the available process stack and eventually crash PHP (due to reaching the +;stack size limit imposed by the Operating System). +; http://php.net/pcre.recursion-limit +;pcre.recursion_limit=100000 + +[Pdo] +; Whether to pool ODBC connections. Can be one of "strict", "relaxed" or "off" +; http://php.net/pdo-odbc.connection-pooling +;pdo_odbc.connection_pooling=strict + +;pdo_odbc.db2_instance_name + +[Pdo_mysql] +; If mysqlnd is used: Number of cache slots for the internal result set cache +; http://php.net/pdo_mysql.cache_size +pdo_mysql.cache_size = 2000 + +; Default socket name for local MySQL connects. If empty, uses the built-in +; MySQL defaults. +; http://php.net/pdo_mysql.default-socket +pdo_mysql.default_socket=/var/lib/mysql/mysql.sock + +[Phar] +; http://php.net/phar.readonly +;phar.readonly = On + +; http://php.net/phar.require-hash +;phar.require_hash = On + +;phar.cache_list = + +[mail function] +; For Win32 only. +; http://php.net/smtp +SMTP = localhost +; http://php.net/smtp-port +smtp_port = 25 + +; For Win32 only. +; http://php.net/sendmail-from +;sendmail_from = me@example.com + +; For Unix only. You may supply arguments as well (default: "sendmail -t -i"). +; http://php.net/sendmail-path +;sendmail_path = + +; Force the addition of the specified parameters to be passed as extra parameters +; to the sendmail binary. These parameters will always replace the value of +; the 5th parameter to mail(). +;mail.force_extra_parameters = + +; Add X-PHP-Originating-Script: that will include uid of the script followed by the filename +mail.add_x_header = On + +; The path to a log file that will log all mail() calls. Log entries include +; the full path of the script, line number, To address and headers. +;mail.log = +; Log mail to syslog (Event Log on Windows). +;mail.log = syslog + +[SQL] +; http://php.net/sql.safe-mode +sql.safe_mode = Off + +[ODBC] +; http://php.net/odbc.default-db +;odbc.default_db = Not yet implemented + +; http://php.net/odbc.default-user +;odbc.default_user = Not yet implemented + +; http://php.net/odbc.default-pw +;odbc.default_pw = Not yet implemented + +; Controls the ODBC cursor model. +; Default: SQL_CURSOR_STATIC (default). +;odbc.default_cursortype + +; Allow or prevent persistent links. +; http://php.net/odbc.allow-persistent +odbc.allow_persistent = On + +; Check that a connection is still valid before reuse. +; http://php.net/odbc.check-persistent +odbc.check_persistent = On + +; Maximum number of persistent links. -1 means no limit. +; http://php.net/odbc.max-persistent +odbc.max_persistent = -1 + +; Maximum number of links (persistent + non-persistent). -1 means no limit. +; http://php.net/odbc.max-links +odbc.max_links = -1 + +; Handling of LONG fields. Returns number of bytes to variables. 0 means +; passthru. +; http://php.net/odbc.defaultlrl +odbc.defaultlrl = 4096 + +; Handling of binary data. 0 means passthru, 1 return as is, 2 convert to char. +; See the documentation on odbc_binmode and odbc_longreadlen for an explanation +; of odbc.defaultlrl and odbc.defaultbinmode +; http://php.net/odbc.defaultbinmode +odbc.defaultbinmode = 1 + +;birdstep.max_links = -1 + +[Interbase] +; Allow or prevent persistent links. +ibase.allow_persistent = 1 + +; Maximum number of persistent links. -1 means no limit. +ibase.max_persistent = -1 + +; Maximum number of links (persistent + non-persistent). -1 means no limit. +ibase.max_links = -1 + +; Default database name for ibase_connect(). +;ibase.default_db = + +; Default username for ibase_connect(). +;ibase.default_user = + +; Default password for ibase_connect(). +;ibase.default_password = + +; Default charset for ibase_connect(). +;ibase.default_charset = + +; Default timestamp format. +ibase.timestampformat = "%Y-%m-%d %H:%M:%S" + +; Default date format. +ibase.dateformat = "%Y-%m-%d" + +; Default time format. +ibase.timeformat = "%H:%M:%S" + +[MySQL] +; Allow accessing, from PHP's perspective, local files with LOAD DATA statements +; http://php.net/mysql.allow_local_infile +mysql.allow_local_infile = On + +; Allow or prevent persistent links. +; http://php.net/mysql.allow-persistent +mysql.allow_persistent = On + +; If mysqlnd is used: Number of cache slots for the internal result set cache +; http://php.net/mysql.cache_size +mysql.cache_size = 2000 + +; Maximum number of persistent links. -1 means no limit. +; http://php.net/mysql.max-persistent +mysql.max_persistent = -1 + +; Maximum number of links (persistent + non-persistent). -1 means no limit. +; http://php.net/mysql.max-links +mysql.max_links = -1 + +; Default port number for mysql_connect(). If unset, mysql_connect() will use +; the $MYSQL_TCP_PORT or the mysql-tcp entry in /etc/services or the +; compile-time value defined MYSQL_PORT (in that order). Win32 will only look +; at MYSQL_PORT. +; http://php.net/mysql.default-port +mysql.default_port = + +; Default socket name for local MySQL connects. If empty, uses the built-in +; MySQL defaults. +; http://php.net/mysql.default-socket +mysql.default_socket =/var/lib/mysql/mysql.sock + +; Default host for mysql_connect() (doesn't apply in safe mode). +; http://php.net/mysql.default-host +mysql.default_host = + +; Default user for mysql_connect() (doesn't apply in safe mode). +; http://php.net/mysql.default-user +mysql.default_user = + +; Default password for mysql_connect() (doesn't apply in safe mode). +; Note that this is generally a *bad* idea to store passwords in this file. +; *Any* user with PHP access can run 'echo get_cfg_var("mysql.default_password") +; and reveal this password! And of course, any users with read access to this +; file will be able to reveal the password as well. +; http://php.net/mysql.default-password +mysql.default_password = + +; Maximum time (in seconds) for connect timeout. -1 means no limit +; http://php.net/mysql.connect-timeout +mysql.connect_timeout = 60 + +; Trace mode. When trace_mode is active (=On), warnings for table/index scans and +; SQL-Errors will be displayed. +; http://php.net/mysql.trace-mode +mysql.trace_mode = Off + +[MySQLi] + +; Maximum number of persistent links. -1 means no limit. +; http://php.net/mysqli.max-persistent +mysqli.max_persistent = -1 + +; Allow accessing, from PHP's perspective, local files with LOAD DATA statements +; http://php.net/mysqli.allow_local_infile +;mysqli.allow_local_infile = On + +; Allow or prevent persistent links. +; http://php.net/mysqli.allow-persistent +mysqli.allow_persistent = On + +; Maximum number of links. -1 means no limit. +; http://php.net/mysqli.max-links +mysqli.max_links = -1 + +; If mysqlnd is used: Number of cache slots for the internal result set cache +; http://php.net/mysqli.cache_size +mysqli.cache_size = 2000 + +; Default port number for mysqli_connect(). If unset, mysqli_connect() will use +; the $MYSQL_TCP_PORT or the mysql-tcp entry in /etc/services or the +; compile-time value defined MYSQL_PORT (in that order). Win32 will only look +; at MYSQL_PORT. +; http://php.net/mysqli.default-port +mysqli.default_port = 3306 + +; Default socket name for local MySQL connects. If empty, uses the built-in +; MySQL defaults. +; http://php.net/mysqli.default-socket +mysqli.default_socket =/var/lib/mysql/mysql.sock + +; Default host for mysql_connect() (doesn't apply in safe mode). +; http://php.net/mysqli.default-host +mysqli.default_host = + +; Default user for mysql_connect() (doesn't apply in safe mode). +; http://php.net/mysqli.default-user +mysqli.default_user = + +; Default password for mysqli_connect() (doesn't apply in safe mode). +; Note that this is generally a *bad* idea to store passwords in this file. +; *Any* user with PHP access can run 'echo get_cfg_var("mysqli.default_pw") +; and reveal this password! And of course, any users with read access to this +; file will be able to reveal the password as well. +; http://php.net/mysqli.default-pw +mysqli.default_pw = + +; Allow or prevent reconnect +mysqli.reconnect = Off + +[mysqlnd] +; Enable / Disable collection of general statistics by mysqlnd which can be +; used to tune and monitor MySQL operations. +; http://php.net/mysqlnd.collect_statistics +mysqlnd.collect_statistics = On + +; Enable / Disable collection of memory usage statistics by mysqlnd which can be +; used to tune and monitor MySQL operations. +; http://php.net/mysqlnd.collect_memory_statistics +mysqlnd.collect_memory_statistics = Off + +; Size of a pre-allocated buffer used when sending commands to MySQL in bytes. +; http://php.net/mysqlnd.net_cmd_buffer_size +;mysqlnd.net_cmd_buffer_size = 2048 + +; Size of a pre-allocated buffer used for reading data sent by the server in +; bytes. +; http://php.net/mysqlnd.net_read_buffer_size +;mysqlnd.net_read_buffer_size = 32768 + +[OCI8] + +; Connection: Enables privileged connections using external +; credentials (OCI_SYSOPER, OCI_SYSDBA) +; http://php.net/oci8.privileged-connect +;oci8.privileged_connect = Off + +; Connection: The maximum number of persistent OCI8 connections per +; process. Using -1 means no limit. +; http://php.net/oci8.max-persistent +;oci8.max_persistent = -1 + +; Connection: The maximum number of seconds a process is allowed to +; maintain an idle persistent connection. Using -1 means idle +; persistent connections will be maintained forever. +; http://php.net/oci8.persistent-timeout +;oci8.persistent_timeout = -1 + +; Connection: The number of seconds that must pass before issuing a +; ping during oci_pconnect() to check the connection validity. When +; set to 0, each oci_pconnect() will cause a ping. Using -1 disables +; pings completely. +; http://php.net/oci8.ping-interval +;oci8.ping_interval = 60 + +; Connection: Set this to a user chosen connection class to be used +; for all pooled server requests with Oracle 11g Database Resident +; Connection Pooling (DRCP). To use DRCP, this value should be set to +; the same string for all web servers running the same application, +; the database pool must be configured, and the connection string must +; specify to use a pooled server. +;oci8.connection_class = + +; High Availability: Using On lets PHP receive Fast Application +; Notification (FAN) events generated when a database node fails. The +; database must also be configured to post FAN events. +;oci8.events = Off + +; Tuning: This option enables statement caching, and specifies how +; many statements to cache. Using 0 disables statement caching. +; http://php.net/oci8.statement-cache-size +;oci8.statement_cache_size = 20 + +; Tuning: Enables statement prefetching and sets the default number of +; rows that will be fetched automatically after statement execution. +; http://php.net/oci8.default-prefetch +;oci8.default_prefetch = 100 + +; Compatibility. Using On means oci_close() will not close +; oci_connect() and oci_new_connect() connections. +; http://php.net/oci8.old-oci-close-semantics +;oci8.old_oci_close_semantics = Off + +[PostgreSQL] +; Allow or prevent persistent links. +; http://php.net/pgsql.allow-persistent +pgsql.allow_persistent = On + +; Detect broken persistent links always with pg_pconnect(). +; Auto reset feature requires a little overheads. +; http://php.net/pgsql.auto-reset-persistent +pgsql.auto_reset_persistent = Off + +; Maximum number of persistent links. -1 means no limit. +; http://php.net/pgsql.max-persistent +pgsql.max_persistent = -1 + +; Maximum number of links (persistent+non persistent). -1 means no limit. +; http://php.net/pgsql.max-links +pgsql.max_links = -1 + +; Ignore PostgreSQL backends Notice message or not. +; Notice message logging require a little overheads. +; http://php.net/pgsql.ignore-notice +pgsql.ignore_notice = 0 + +; Log PostgreSQL backends Notice message or not. +; Unless pgsql.ignore_notice=0, module cannot log notice message. +; http://php.net/pgsql.log-notice +pgsql.log_notice = 0 + +[Sybase-CT] +; Allow or prevent persistent links. +; http://php.net/sybct.allow-persistent +sybct.allow_persistent = On + +; Maximum number of persistent links. -1 means no limit. +; http://php.net/sybct.max-persistent +sybct.max_persistent = -1 + +; Maximum number of links (persistent + non-persistent). -1 means no limit. +; http://php.net/sybct.max-links +sybct.max_links = -1 + +; Minimum server message severity to display. +; http://php.net/sybct.min-server-severity +sybct.min_server_severity = 10 + +; Minimum client message severity to display. +; http://php.net/sybct.min-client-severity +sybct.min_client_severity = 10 + +; Set per-context timeout +; http://php.net/sybct.timeout +;sybct.timeout= + +;sybct.packet_size + +; The maximum time in seconds to wait for a connection attempt to succeed before returning failure. +; Default: one minute +;sybct.login_timeout= + +; The name of the host you claim to be connecting from, for display by sp_who. +; Default: none +;sybct.hostname= + +; Allows you to define how often deadlocks are to be retried. -1 means "forever". +; Default: 0 +;sybct.deadlock_retry_count= + +[bcmath] +; Number of decimal digits for all bcmath functions. +; http://php.net/bcmath.scale +bcmath.scale = 0 + +[browscap] +; http://php.net/browscap +;browscap = extra/browscap.ini + +[Session] +; Handler used to store/retrieve data. +; http://php.net/session.save-handler +session.save_handler = files + +; Argument passed to save_handler. In the case of files, this is the path +; where data files are stored. Note: Windows users have to change this +; variable in order to use PHP's session functions. +; +; The path can be defined as: +; +; session.save_path = "N;/path" +; +; where N is an integer. Instead of storing all the session files in +; /path, what this will do is use subdirectories N-levels deep, and +; store the session data in those directories. This is useful if you +; or your OS have problems with lots of files in one directory, and is +; a more efficient layout for servers that handle lots of sessions. +; +; NOTE 1: PHP will not create this directory structure automatically. +; You can use the script in the ext/session dir for that purpose. +; NOTE 2: See the section on garbage collection below if you choose to +; use subdirectories for session storage +; +; The file storage module creates files using mode 600 by default. +; You can change that by using +; +; session.save_path = "N;MODE;/path" +; +; where MODE is the octal representation of the mode. Note that this +; does not overwrite the process's umask. +; http://php.net/session.save-path +session.save_path = "/tmp" +;session.save_path = "tcp://127.0.0.1:11211" + +; Whether to use strict session mode. +; Strict session mode does not accept uninitialized session ID and regenerate +; session ID if browser sends uninitialized session ID. Strict mode protects +; applications from session fixation via session adoption vulnerability. It is +; disabled by default for maximum compatibility, but enabling it is encouraged. +; https://wiki.php.net/rfc/strict_sessions +session.use_strict_mode = 0 + +; Whether to use cookies. +; http://php.net/session.use-cookies +session.use_cookies = 1 + +; http://php.net/session.cookie-secure +;session.cookie_secure = + +; This option forces PHP to fetch and use a cookie for storing and maintaining +; the session id. We encourage this operation as it's very helpful in combating +; session hijacking when not specifying and managing your own session id. It is +; not the end all be all of session hijacking defense, but it's a good start. +; http://php.net/session.use-only-cookies +session.use_only_cookies = 1 + +; Name of the session (used as cookie name). +; http://php.net/session.name +session.name = PHPSESSID + +; Initialize session on request startup. +; http://php.net/session.auto-start +session.auto_start = 0 + +; Lifetime in seconds of cookie or, if 0, until browser is restarted. +; http://php.net/session.cookie-lifetime +session.cookie_lifetime = 0 + +; The path for which the cookie is valid. +; http://php.net/session.cookie-path +session.cookie_path = / + +; The domain for which the cookie is valid. +; http://php.net/session.cookie-domain +session.cookie_domain = + +; Whether or not to add the httpOnly flag to the cookie, which makes it inaccessible to browser scripting languages such as JavaScript. +; http://php.net/session.cookie-httponly +session.cookie_httponly = + +; Handler used to serialize data. php is the standard serializer of PHP. +; http://php.net/session.serialize-handler +session.serialize_handler = php + +; Defines the probability that the 'garbage collection' process is started +; on every session initialization. The probability is calculated by using +; gc_probability/gc_divisor. Where session.gc_probability is the numerator +; and gc_divisor is the denominator in the equation. Setting this value to 1 +; when the session.gc_divisor value is 100 will give you approximately a 1% chance +; the gc will run on any give request. +; Default Value: 1 +; Development Value: 1 +; Production Value: 1 +; http://php.net/session.gc-probability +session.gc_probability = 1 + +; Defines the probability that the 'garbage collection' process is started on every +; session initialization. The probability is calculated by using the following equation: +; gc_probability/gc_divisor. Where session.gc_probability is the numerator and +; session.gc_divisor is the denominator in the equation. Setting this value to 1 +; when the session.gc_divisor value is 100 will give you approximately a 1% chance +; the gc will run on any give request. Increasing this value to 1000 will give you +; a 0.1% chance the gc will run on any give request. For high volume production servers, +; this is a more efficient approach. +; Default Value: 100 +; Development Value: 1000 +; Production Value: 1000 +; http://php.net/session.gc-divisor +session.gc_divisor = 1000 + +; After this number of seconds, stored data will be seen as 'garbage' and +; cleaned up by the garbage collection process. +; http://php.net/session.gc-maxlifetime +session.gc_maxlifetime = 1440 + +; NOTE: If you are using the subdirectory option for storing session files +; (see session.save_path above), then garbage collection does *not* +; happen automatically. You will need to do your own garbage +; collection through a shell script, cron entry, or some other method. +; For example, the following script would is the equivalent of +; setting session.gc_maxlifetime to 1440 (1440 seconds = 24 minutes): +; find /path/to/sessions -cmin +24 -type f | xargs rm + +; Check HTTP Referer to invalidate externally stored URLs containing ids. +; HTTP_REFERER has to contain this substring for the session to be +; considered as valid. +; http://php.net/session.referer-check +session.referer_check = + +; How many bytes to read from the file. +; http://php.net/session.entropy-length +;session.entropy_length = 32 + +; Specified here to create the session id. +; http://php.net/session.entropy-file +; Defaults to /dev/urandom +; On systems that don't have /dev/urandom but do have /dev/arandom, this will default to /dev/arandom +; If neither are found at compile time, the default is no entropy file. +; On windows, setting the entropy_length setting will activate the +; Windows random source (using the CryptoAPI) +;session.entropy_file = /dev/urandom + +; Set to {nocache,private,public,} to determine HTTP caching aspects +; or leave this empty to avoid sending anti-caching headers. +; http://php.net/session.cache-limiter +session.cache_limiter = nocache + +; Document expires after n minutes. +; http://php.net/session.cache-expire +session.cache_expire = 180 + +; trans sid support is disabled by default. +; Use of trans sid may risk your users security. +; Use this option with caution. +; - User may send URL contains active session ID +; to other person via. email/irc/etc. +; - URL that contains active session ID may be stored +; in publicly accessible computer. +; - User may access your site with the same session ID +; always using URL stored in browser's history or bookmarks. +; http://php.net/session.use-trans-sid +session.use_trans_sid = 0 + +; Select a hash function for use in generating session ids. +; Possible Values +; 0 (MD5 128 bits) +; 1 (SHA-1 160 bits) +; This option may also be set to the name of any hash function supported by +; the hash extension. A list of available hashes is returned by the hash_algos() +; function. +; http://php.net/session.hash-function +session.hash_function = 0 + +; Define how many bits are stored in each character when converting +; the binary hash data to something readable. +; Possible values: +; 4 (4 bits: 0-9, a-f) +; 5 (5 bits: 0-9, a-v) +; 6 (6 bits: 0-9, a-z, A-Z, "-", ",") +; Default Value: 4 +; Development Value: 5 +; Production Value: 5 +; http://php.net/session.hash-bits-per-character +session.hash_bits_per_character = 5 + +; The URL rewriter will look for URLs in a defined set of HTML tags. +; form/fieldset are special; if you include them here, the rewriter will +; add a hidden field with the info which is otherwise appended +; to URLs. If you want XHTML conformity, remove the form entry. +; Note that all valid entries require a "=", even if no value follows. +; Default Value: "a=href,area=href,frame=src,form=,fieldset=" +; Development Value: "a=href,area=href,frame=src,input=src,form=fakeentry" +; Production Value: "a=href,area=href,frame=src,input=src,form=fakeentry" +; http://php.net/url-rewriter.tags +url_rewriter.tags = "a=href,area=href,frame=src,input=src,form=fakeentry" + +; Enable upload progress tracking in $_SESSION +; Default Value: On +; Development Value: On +; Production Value: On +; http://php.net/session.upload-progress.enabled +;session.upload_progress.enabled = On + +; Cleanup the progress information as soon as all POST data has been read +; (i.e. upload completed). +; Default Value: On +; Development Value: On +; Production Value: On +; http://php.net/session.upload-progress.cleanup +;session.upload_progress.cleanup = On + +; A prefix used for the upload progress key in $_SESSION +; Default Value: "upload_progress_" +; Development Value: "upload_progress_" +; Production Value: "upload_progress_" +; http://php.net/session.upload-progress.prefix +;session.upload_progress.prefix = "upload_progress_" + +; The index name (concatenated with the prefix) in $_SESSION +; containing the upload progress information +; Default Value: "PHP_SESSION_UPLOAD_PROGRESS" +; Development Value: "PHP_SESSION_UPLOAD_PROGRESS" +; Production Value: "PHP_SESSION_UPLOAD_PROGRESS" +; http://php.net/session.upload-progress.name +;session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" + +; How frequently the upload progress should be updated. +; Given either in percentages (per-file), or in bytes +; Default Value: "1%" +; Development Value: "1%" +; Production Value: "1%" +; http://php.net/session.upload-progress.freq +;session.upload_progress.freq = "1%" + +; The minimum delay between updates, in seconds +; Default Value: 1 +; Development Value: 1 +; Production Value: 1 +; http://php.net/session.upload-progress.min-freq +;session.upload_progress.min_freq = "1" + +[MSSQL] +; Allow or prevent persistent links. +mssql.allow_persistent = On + +; Maximum number of persistent links. -1 means no limit. +mssql.max_persistent = -1 + +; Maximum number of links (persistent+non persistent). -1 means no limit. +mssql.max_links = -1 + +; Minimum error severity to display. +mssql.min_error_severity = 10 + +; Minimum message severity to display. +mssql.min_message_severity = 10 + +; Compatibility mode with old versions of PHP 3.0. +mssql.compatibility_mode = Off + +; Connect timeout +;mssql.connect_timeout = 5 + +; Query timeout +;mssql.timeout = 60 + +; Valid range 0 - 2147483647. Default = 4096. +;mssql.textlimit = 4096 + +; Valid range 0 - 2147483647. Default = 4096. +;mssql.textsize = 4096 + +; Limits the number of records in each batch. 0 = all records in one batch. +;mssql.batchsize = 0 + +; Specify how datetime and datetim4 columns are returned +; On => Returns data converted to SQL server settings +; Off => Returns values as YYYY-MM-DD hh:mm:ss +;mssql.datetimeconvert = On + +; Use NT authentication when connecting to the server +mssql.secure_connection = Off + +; Specify max number of processes. -1 = library default +; msdlib defaults to 25 +; FreeTDS defaults to 4096 +;mssql.max_procs = -1 + +; Specify client character set. +; If empty or not set the client charset from freetds.conf is used +; This is only used when compiled with FreeTDS +;mssql.charset = "ISO-8859-1" + +[Assertion] +; Assert(expr); active by default. +; http://php.net/assert.active +;assert.active = On + +; Issue a PHP warning for each failed assertion. +; http://php.net/assert.warning +;assert.warning = On + +; Don't bail out by default. +; http://php.net/assert.bail +;assert.bail = Off + +; User-function to be called if an assertion fails. +; http://php.net/assert.callback +;assert.callback = 0 + +; Eval the expression with current error_reporting(). Set to true if you want +; error_reporting(0) around the eval(). +; http://php.net/assert.quiet-eval +;assert.quiet_eval = 0 + +[COM] +; path to a file containing GUIDs, IIDs or filenames of files with TypeLibs +; http://php.net/com.typelib-file +;com.typelib_file = + +; allow Distributed-COM calls +; http://php.net/com.allow-dcom +;com.allow_dcom = true + +; autoregister constants of a components typlib on com_load() +; http://php.net/com.autoregister-typelib +;com.autoregister_typelib = true + +; register constants casesensitive +; http://php.net/com.autoregister-casesensitive +;com.autoregister_casesensitive = false + +; show warnings on duplicate constant registrations +; http://php.net/com.autoregister-verbose +;com.autoregister_verbose = true + +; The default character set code-page to use when passing strings to and from COM objects. +; Default: system ANSI code page +;com.code_page= + +[mbstring] +; language for internal character representation. +; http://php.net/mbstring.language +;mbstring.language = Japanese + +; internal/script encoding. +; Some encoding cannot work as internal encoding. +; (e.g. SJIS, BIG5, ISO-2022-*) +; http://php.net/mbstring.internal-encoding +;mbstring.internal_encoding = UTF-8 + +; http input encoding. +; http://php.net/mbstring.http-input +;mbstring.http_input = UTF-8 + +; http output encoding. mb_output_handler must be +; registered as output buffer to function +; http://php.net/mbstring.http-output +;mbstring.http_output = pass + +; enable automatic encoding translation according to +; mbstring.internal_encoding setting. Input chars are +; converted to internal encoding by setting this to On. +; Note: Do _not_ use automatic encoding translation for +; portable libs/applications. +; http://php.net/mbstring.encoding-translation +;mbstring.encoding_translation = Off + +; automatic encoding detection order. +; auto means +; http://php.net/mbstring.detect-order +;mbstring.detect_order = auto + +; substitute_character used when character cannot be converted +; one from another +; http://php.net/mbstring.substitute-character +;mbstring.substitute_character = none + +; overload(replace) single byte functions by mbstring functions. +; mail(), ereg(), etc are overloaded by mb_send_mail(), mb_ereg(), +; etc. Possible values are 0,1,2,4 or combination of them. +; For example, 7 for overload everything. +; 0: No overload +; 1: Overload mail() function +; 2: Overload str*() functions +; 4: Overload ereg*() functions +; http://php.net/mbstring.func-overload +;mbstring.func_overload = 0 + +; enable strict encoding detection. +;mbstring.strict_detection = On + +; This directive specifies the regex pattern of content types for which mb_output_handler() +; is activated. +; Default: mbstring.http_output_conv_mimetype=^(text/|application/xhtml\+xml) +;mbstring.http_output_conv_mimetype= + +[gd] +; Tell the jpeg decode to ignore warnings and try to create +; a gd image. The warning will then be displayed as notices +; disabled by default +; http://php.net/gd.jpeg-ignore-warning +;gd.jpeg_ignore_warning = 0 + +[exif] +; Exif UNICODE user comments are handled as UCS-2BE/UCS-2LE and JIS as JIS. +; With mbstring support this will automatically be converted into the encoding +; given by corresponding encode setting. When empty mbstring.internal_encoding +; is used. For the decode settings you can distinguish between motorola and +; intel byte order. A decode setting cannot be empty. +; http://php.net/exif.encode-unicode +;exif.encode_unicode = ISO-8859-15 + +; http://php.net/exif.decode-unicode-motorola +;exif.decode_unicode_motorola = UCS-2BE + +; http://php.net/exif.decode-unicode-intel +;exif.decode_unicode_intel = UCS-2LE + +; http://php.net/exif.encode-jis +;exif.encode_jis = + +; http://php.net/exif.decode-jis-motorola +;exif.decode_jis_motorola = JIS + +; http://php.net/exif.decode-jis-intel +;exif.decode_jis_intel = JIS + +[Tidy] +; The path to a default tidy configuration file to use when using tidy +; http://php.net/tidy.default-config +;tidy.default_config = /usr/local/lib/php/default.tcfg + +; Should tidy clean and repair output automatically? +; WARNING: Do not use this option if you are generating non-html content +; such as dynamic images +; http://php.net/tidy.clean-output +tidy.clean_output = Off + +[soap] +; Enables or disables WSDL caching feature. +; http://php.net/soap.wsdl-cache-enabled +soap.wsdl_cache_enabled=1 + +; Sets the directory name where SOAP extension will put cache files. +; http://php.net/soap.wsdl-cache-dir +soap.wsdl_cache_dir="/tmp" + +; (time to live) Sets the number of second while cached file will be used +; instead of original one. +; http://php.net/soap.wsdl-cache-ttl +soap.wsdl_cache_ttl=86400 + +; Sets the size of the cache limit. (Max. number of WSDL files to cache) +soap.wsdl_cache_limit = 5 + +[sysvshm] +; A default size of the shared memory segment +;sysvshm.init_mem = 10000 + +[ldap] +; Sets the maximum number of open links or -1 for unlimited. +ldap.max_links = -1 + +[mcrypt] +; For more information about mcrypt settings see http://php.net/mcrypt-module-open + +; Directory where to load mcrypt algorithms +; Default: Compiled in into libmcrypt (usually /usr/local/lib/libmcrypt) +;mcrypt.algorithms_dir= + +; Directory where to load mcrypt modes +; Default: Compiled in into libmcrypt (usually /usr/local/lib/libmcrypt) +;mcrypt.modes_dir= + +[dba] +;dba.default_handler= + +[opcache] +; Determines if Zend OPCache is enabled +;opcache.enable=0 + +; Determines if Zend OPCache is enabled for the CLI version of PHP +;opcache.enable_cli=0 + +; The OPcache shared memory storage size. +;opcache.memory_consumption=64 + +; The amount of memory for interned strings in Mbytes. +;opcache.interned_strings_buffer=4 + +; The maximum number of keys (scripts) in the OPcache hash table. +; Only numbers between 200 and 100000 are allowed. +;opcache.max_accelerated_files=2000 + +; The maximum percentage of "wasted" memory until a restart is scheduled. +;opcache.max_wasted_percentage=5 + +; When this directive is enabled, the OPcache appends the current working +; directory to the script key, thus eliminating possible collisions between +; files with the same name (basename). Disabling the directive improves +; performance, but may break existing applications. +;opcache.use_cwd=1 + +; When disabled, you must reset the OPcache manually or restart the +; webserver for changes to the filesystem to take effect. +;opcache.validate_timestamps=1 + +; How often (in seconds) to check file timestamps for changes to the shared +; memory storage allocation. ("1" means validate once per second, but only +; once per request. "0" means always validate) +;opcache.revalidate_freq=2 + +; Enables or disables file search in include_path optimization +;opcache.revalidate_path=0 + +; If disabled, all PHPDoc comments are dropped from the code to reduce the +; size of the optimized code. +;opcache.save_comments=1 + +; If disabled, PHPDoc comments are not loaded from SHM, so "Doc Comments" +; may be always stored (save_comments=1), but not loaded by applications +; that don't need them anyway. +;opcache.load_comments=1 + +; If enabled, a fast shutdown sequence is used for the accelerated code +;opcache.fast_shutdown=0 + +; Allow file existence override (file_exists, etc.) performance feature. +;opcache.enable_file_override=0 + +; A bitmask, where each bit enables or disables the appropriate OPcache +; passes +;opcache.optimization_level=0xffffffff + +;opcache.inherited_hack=1 +;opcache.dups_fix=0 + +; The location of the OPcache blacklist file (wildcards allowed). +; Each OPcache blacklist file is a text file that holds the names of files +; that should not be accelerated. The file format is to add each filename +; to a new line. The filename may be a full path or just a file prefix +; (i.e., /var/www/x blacklists all the files and directories in /var/www +; that start with 'x'). Line starting with a ; are ignored (comments). +;opcache.blacklist_filename= + +; Allows exclusion of large files from being cached. By default all files +; are cached. +;opcache.max_file_size=0 + +; Check the cache checksum each N requests. +; The default value of "0" means that the checks are disabled. +;opcache.consistency_checks=0 + +; How long to wait (in seconds) for a scheduled restart to begin if the cache +; is not being accessed. +;opcache.force_restart_timeout=180 + +; OPcache error_log file name. Empty string assumes "stderr". +;opcache.error_log= + +; All OPcache errors go to the Web server log. +; By default, only fatal errors (level 0) or errors (level 1) are logged. +; You can also enable warnings (level 2), info messages (level 3) or +; debug messages (level 4). +;opcache.log_verbosity_level=1 + +; Preferred Shared Memory back-end. Leave empty and let the system decide. +;opcache.preferred_memory_model= + +; Protect the shared memory from unexpected writing during script execution. +; Useful for internal debugging only. +;opcache.protect_memory=0 + +[curl] +; A default value for the CURLOPT_CAINFO option. This is required to be an +; absolute path. +;curl.cainfo = + +; Local Variables: +; tab-width: 4 +; End: +;; this is an example, it won't work unless properly configured into php.ini +[xcache-common] +;; non-Windows example: +;extension = xcache.so +;; Windows example: +; extension = php_xcache.dll + +[xcache.admin] +xcache.admin.enable_auth = On + +; use http://xcache.lighttpd.net/demo/cacher/mkpassword.php to generate your encrypted password +xcache.admin.user = "mOo" +xcache.admin.pass = "md5 encrypted password" + +[xcache] +; ini only settings, all the values here is default unless explained + +; select low level shm implemenation +xcache.shm_scheme = "mmap" +; to disable: xcache.size=0 +; to enable : xcache.size=64M etc (any size > 0) and your system mmap allows +xcache.size = 512M +; set to cpu count (cat /proc/cpuinfo |grep -c processor) +xcache.count = 32 +; just a hash hints, you can always store count(items) > slots +xcache.slots = 32K +; ttl of the cache item, 0=forever +xcache.ttl = 0 +; interval of gc scanning expired items, 0=no scan, other values is in seconds +xcache.gc_interval = 0 + +; same as aboves but for variable cache +xcache.var_size = 4M +xcache.var_count = 1 +xcache.var_slots = 8K +; default value for $ttl parameter of xcache_*() functions +xcache.var_ttl = 0 +; hard limit ttl that cannot be exceed by xcache_*() functions. 0=unlimited +xcache.var_maxttl = 12000 +xcache.var_gc_interval = 1800 + +; mode:0, const string specified by xcache.var_namespace +; mode:1, $_SERVER[xcache.var_namespace] +; mode:2, uid or gid (specified by xcache.var_namespace) +xcache.var_namespace_mode = 0 +xcache.var_namespace = "" + +; N/A for /dev/zero +xcache.readonly_protection = Off +; for *nix, xcache.mmap_path is a file path, not directory. (auto create/overwrite) +; Use something like "/tmp/xcache" instead of "/dev/*" if you want to turn on ReadonlyProtection +; different process group of php won't share the same /tmp/xcache +; for win32, xcache.mmap_path=anonymous map name, not file path +xcache.mmap_path = "/dev/zero" + + +; Useful when XCache crash. leave it blank(disabled) or "/tmp/phpcore/" (writable by php) +xcache.coredump_directory = "" +; Windows only. leave it as 0 (default) until you're told by XCache dev +xcache.coredump_type = 0 + +; disable cache after crash +xcache.disable_on_crash = Off + +; enable experimental documented features for each release if available +xcache.experimental = Off + +; per request settings. can ini_set, .htaccess etc +xcache.cacher = On +xcache.stat = On +xcache.optimizer = Off + +[xcache.coverager] +; enabling this feature will impact performance +; enabled only if xcache.coverager == On && xcache.coveragedump_directory == "non-empty-value" + +; per request settings. can ini_set, .htaccess etc +; enable coverage data collecting and xcache_coverager_start/stop/get/clean() functions +xcache.coverager = Off +xcache.coverager_autostart = On + +; set in php ini file only +; make sure it's readable (open_basedir is checked) by coverage viewer script +xcache.coveragedump_directory = "" diff --git a/roles/docker/templates/nginx/supervisord.conf b/roles/docker/templates/nginx/supervisord.conf new file mode 100644 index 0000000..9951ca0 --- /dev/null +++ b/roles/docker/templates/nginx/supervisord.conf @@ -0,0 +1,39 @@ +[supervisord] +nodaemon=true +# (Tùy chọn) Ngăn Supervisor tự tạo log file của chính nó +logfile=/dev/null +logfile_maxbytes=0 + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=true +autorestart=true +priority=10 +# Chuyển hướng stdout của Nginx ra stdout của Supervisor +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +# Chuyển hướng stderr của Nginx ra stderr của Supervisor +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:php-fpm] +command=/usr/local/sbin/php-fpm --nodaemonize --fpm-config /usr/local/etc/php-fpm.conf +autostart=true +autorestart=true +priority=5 +# Chuyển hướng stdout của PHP-FPM ra stdout của Supervisor +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +# Chuyển hướng stderr của PHP-FPM ra stderr của Supervisor +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:queue-work] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/html/artisan queue:work +user=www-data +autostart=true +autorestart=true +numprocs=1 +redirect_stderr=true +stdout_logfile=/dev/stdout