From f0d13cd510caf0c2a205123641d28207de48bb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chu=20V=C4=83n=20H=E1=BA=A3i?= Date: Fri, 5 Dec 2025 11:18:26 +0700 Subject: [PATCH] init --- .dockerignore | 378 +++ .gitea/workflows/deploy.yml | 205 ++ .gitea/workflows/main.yml | 36 + .gitignore | 21 + ansible.cfg | 9 + group_vars/default.yml | 109 + group_vars/port-registry.yml | 11 + group_vars/vault/development.yml | 6 + group_vars/vault/production.yml | 6 + inventory/development.yml | 14 + inventory/production.yml | 14 + playbook.yml | 136 ++ roles/docker/defaults/main.yml | 19 + roles/docker/handlers/main.yml | 86 + roles/docker/tasks/main.yml | 650 ++++++ roles/docker/templates/.env.j2 | 228 ++ .../dockerfiles/expressjs.Dockerfile.j2 | 93 + .../dockerfiles/laravel.Dockerfile.j2 | 106 + .../dockerfiles/nestjs.Dockerfile.j2 | 94 + .../dockerfiles/nextjs.Dockerfile.j2 | 103 + .../dockerfiles/reactjs.Dockerfile.j2 | 189 ++ roles/docker/templates/health-check.sh | 21 + roles/docker/templates/manage-docker.sh.j2 | 1779 +++++++++++++++ .../templates/monitor-container-script.sh | 182 ++ roles/docker/templates/nginx-upstream.conf.j2 | 140 ++ roles/docker/templates/nginx/laravel.conf | 63 + roles/docker/templates/nginx/nginx.conf | 108 + roles/docker/templates/nginx/php.ini | 2023 +++++++++++++++++ roles/docker/templates/nginx/supervisord.conf | 39 + 29 files changed, 6868 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitea/workflows/main.yml create mode 100644 .gitignore create mode 100644 ansible.cfg create mode 100644 group_vars/default.yml create mode 100644 group_vars/port-registry.yml create mode 100644 group_vars/vault/development.yml create mode 100644 group_vars/vault/production.yml create mode 100644 inventory/development.yml create mode 100644 inventory/production.yml create mode 100644 playbook.yml create mode 100644 roles/docker/defaults/main.yml create mode 100644 roles/docker/handlers/main.yml create mode 100644 roles/docker/tasks/main.yml create mode 100644 roles/docker/templates/.env.j2 create mode 100644 roles/docker/templates/dockerfiles/expressjs.Dockerfile.j2 create mode 100644 roles/docker/templates/dockerfiles/laravel.Dockerfile.j2 create mode 100644 roles/docker/templates/dockerfiles/nestjs.Dockerfile.j2 create mode 100644 roles/docker/templates/dockerfiles/nextjs.Dockerfile.j2 create mode 100644 roles/docker/templates/dockerfiles/reactjs.Dockerfile.j2 create mode 100644 roles/docker/templates/health-check.sh create mode 100644 roles/docker/templates/manage-docker.sh.j2 create mode 100644 roles/docker/templates/monitor-container-script.sh create mode 100644 roles/docker/templates/nginx-upstream.conf.j2 create mode 100644 roles/docker/templates/nginx/laravel.conf create mode 100644 roles/docker/templates/nginx/nginx.conf create mode 100644 roles/docker/templates/nginx/php.ini create mode 100644 roles/docker/templates/nginx/supervisord.conf 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..cbcf3a0 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,205 @@ +# 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 + + # ------------------ 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. + SERVER_HOST: + description: 'Target hosts for Ansible (e.g., "user@host")' + required: true + SERVER_PORT: + description: 'Target port for Ansible (e.g., "22")' + required: false + default: '22' + SERVER_USER: + description: 'Target user for Ansible (e.g., "cicd")' + required: false + default: 'cicd' + # SSH private key to connect to the Ansible target host. + SERVER_SSH_PRIVATE_KEY: + description: 'SSH Private Key for Ansible' + required: false + # Password for the target host. + SERVER_PASSWORD: + description: 'Password for Ansible' + required: false + # 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 + # The address of the HashiCorp Vault server. + VAULT_ADDR: + description: 'Vault server address (e.g., https://vault.example.com:8200)' + required: true + # The Vault role to use for authentication. + VAULT_ROLE: + description: 'Vault role for authentication' + required: true + # The path to the secrets in Vault (e.g., "secret/data/my-app my_secret"). + VAULT_SECRETS_PATH: + description: 'Path to the secrets in Vault' + required: true + +jobs: + # ============================================================================ + # JOB 1: BUILD AND PUSH DOCKER IMAGE + # ============================================================================ + build: + runs-on: alpine-latest + # Grant permissions for the workflow to request an OIDC token. + permissions: + id-token: write + contents: read + 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: Import Secrets from HashiCorp Vault + id: vault + uses: hashicorp/vault-action@v2 + with: + url: ${{ secrets.VAULT_ADDR }} + role: ${{ secrets.VAULT_ROLE }} + method: jwt + jwt: ${{ job.id_token }} + secrets: | + ${{ secrets.VAULT_SECRETS_PATH }} env_data | DOTENV_CONTENTS + + - 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 }} + secrets: | + env=${{ steps.vault.outputs.DOTENV_CONTENTS }} + + # ============================================================================ + # JOB 2: DEPLOY WITH ANSIBLE + # ============================================================================ + deploy: + runs-on: alpine-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: | + apk add --no-cache openssh-client-default sshpass python3 py3-pip + pip3 install ansible + ansible-galaxy collection install community.docker + + - name: Setup SSH + if: ${{ secrets.SERVER_SSH_PRIVATE_KEY != '' }} + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SERVER_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 + env: + SERVER_HOST: ${{ secrets.SERVER_HOST }} + SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }} + SERVER_SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_PRIVATE_KEY }} + run: | + auth_method_var="" + if [[ -n "$SERVER_SSH_PRIVATE_KEY" ]]; then + auth_method_var="auth_method=ssh_key ssh_key_path=/root/.ssh/id_rsa" + if [[ -n "$SERVER_PASSWORD" ]]; then + auth_method_var="$auth_method_var server_become_password=$SERVER_PASSWORD" + fi + elif [[ -n "$SERVER_PASSWORD" ]]; then + auth_method_var="auth_method=password server_password=$SERVER_PASSWORD" + auth_method_var="$auth_method_var server_become_password=$SERVER_PASSWORD" + fi + + ansible-playbook playbook.yml \ + -i "$SERVER_HOST," \ + --extra-vars "app_name=${{ inputs.app_name }} \ + deploy_env=${{ inputs.deploy_env }} \ + server_user=${{ inputs.server_user }} \ + server_port=${{ inputs.server_port }} \ + server_host=$SERVER_HOST \ + 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 }} \ + $auth_method_var" \ + --vault-password-file .vault_pass \ No newline at end of file diff --git a/.gitea/workflows/main.yml b/.gitea/workflows/main.yml new file mode 100644 index 0000000..8e234db --- /dev/null +++ b/.gitea/workflows/main.yml @@ -0,0 +1,36 @@ +# This is the main CI/CD workflow for the cms-api application. +# It triggers on pushes of tags matching semantic versioning with environment suffixes. + +name: CI/CD for project + +on: + push: + tags: + - 'v*' # Matches v-prefixed tags like v1.0.0, v1.0.0-dev.1, v1.0.0-stg.1 + +jobs: + # This job calls the reusable deployment workflow. + deploy_service: + # Set the environment name based on the Git tag + environment: ${{ (contains(github.ref_name, '-dev.')) && 'development' || (contains(github.ref_name, '-stg.')) && 'staging' || 'production' }} + + uses: devops/ansible-gitea/.gitea/workflows/deploy.yml@main + + # Provide the specific inputs for this application. + with: + app_name: ${{ github.event.repository.name }} + deploy_env: ${{ (contains(github.ref_name, '-dev.')) && 'development' || (contains(github.ref_name, '-stg.')) && 'staging' || 'production' }} + env_type: ${{ (contains(github.ref_name, 'worker')) && 'worker' || '' }} + + # Pass the required secrets to the reusable workflow. + # These secrets must be configured in the Gitea settings for this repository. + secrets: + SERVER_HOST: ${{ secrets.SERVER_HOST }} + SERVER_PORT: ${{ secrets.SERVER_PORT }} + SERVER_USER: ${{ secrets.SERVER_USER }} + SERVER_SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_PRIVATE_KEY }} + SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }} + VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }} 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/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