From 1f8642e2302c782617ad5d85fb35b8a0ae88b9a2 Mon Sep 17 00:00:00 2001 From: ismailmasseran Date: Wed, 26 Aug 2026 13:36:18 +0800 Subject: [PATCH] DONE: add cicd for staging (#1) Co-authored-by: ISMAIL MASSERAN Reviewed-on: https://git.koppkb.com/KoPKB/kaunter/pulls/1 --- .dockerignore | 16 ++++ .env.domain | 55 +++++++++++++ .env.staging | 57 +++++++++++++ .gitea/workflows/build.yml | 49 +++++++++++ .gitea/workflows/deploy-staging.yml | 123 ++++++++++++++++++++++++++++ docker-compose.staging.yml | 77 +++++++++++++++++ docker-compose.yml | 43 ++++++++++ docker/nginx/default.conf | 36 ++++++++ docker/php/Dockerfile | 51 ++++++++++++ docker/php/entrypoint.sh | 35 ++++++++ docker/php/php.ini | 4 + docker/php/zz-clear-env.conf | 3 + routes/web.php | 4 + 13 files changed, 553 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.domain create mode 100644 .env.staging create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitea/workflows/deploy-staging.yml create mode 100644 docker-compose.staging.yml create mode 100644 docker-compose.yml create mode 100644 docker/nginx/default.conf create mode 100644 docker/php/Dockerfile create mode 100644 docker/php/entrypoint.sh create mode 100644 docker/php/php.ini create mode 100644 docker/php/zz-clear-env.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..32264f9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.gitea +node_modules +vendor +.env +.env.* +!.env.example +storage/logs +storage/framework/cache +storage/framework/sessions +storage/framework/views +storage/debugbar +npm-debug.log +Dockerfile +docker-compose.yml +docker-compose.staging.yml diff --git a/.env.domain b/.env.domain new file mode 100644 index 0000000..1cfb20c --- /dev/null +++ b/.env.domain @@ -0,0 +1,55 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8= +APP_DEBUG=true +APP_URL=http://localhost +# API_URL=http://api-annurv2.test +API_URL=http://host.docker.internal:8080 +# Server-side Http:: from Docker — same rules as API_URL. Defaults in config/api.php use localhost:8000 which fails inside containers. +API_URL_KOOP=http://host.docker.internal:8080 +API_URL_PKB=http://host.docker.internal:8080 +API_ARRAHNBID=http://lelong.test/api/ + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=kaunter_erahn +DB_USERNAME=kaunteruser +DB_PASSWORD=password + +BROADCAST_DRIVER=pusher +CACHE_DRIVER=file +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +PUSHER_APP_ID=1132718 +PUSHER_APP_KEY=26b0ec2f297f94b7aa9b +PUSHER_APP_SECRET=421b61fb5a8ecce477a6 + +MAIL_MAILER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS=null +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +MYKAD_READER_PORT=8085 +MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED \ No newline at end of file diff --git a/.env.staging b/.env.staging new file mode 100644 index 0000000..2730423 --- /dev/null +++ b/.env.staging @@ -0,0 +1,57 @@ +APP_NAME=Laravel +APP_ENV=staging +APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8= +APP_DEBUG=true +APP_URL=http://172.16.6.200:8091 + +HTTP_PORT=8091 +MYSQL_PUBLISH_PORT=3309 + +API_URL=http://172.16.6.200:8090 +API_URL_KOOP=http://172.16.6.200:8090 +API_URL_PKB=http://172.16.6.200:8090 +API_ARRAHNBID=http://lelong.test/api/ + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=kaunter_erahn +DB_USERNAME=kaunteruser +DB_PASSWORD=password + +BROADCAST_DRIVER=pusher +CACHE_DRIVER=file +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +PUSHER_APP_ID=1132718 +PUSHER_APP_KEY=26b0ec2f297f94b7aa9b +PUSHER_APP_SECRET=421b61fb5a8ecce477a6 + +MAIL_MAILER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS=null +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +MYKAD_READER_PORT=8085 +MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED \ No newline at end of file diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..640e9bb --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,49 @@ +name: Build Docker Image + +on: + push: + branches: + - staging + tags: + - "v*" + workflow_dispatch: + +jobs: + build-frontend: + runs-on: docker + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Docker registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.koppkb.com \ + -u "${{ secrets.REGISTRY_USERNAME }}" \ + --password-stdin + + - name: Build & push frontend image + run: | + set -e + + IMAGE="git.koppkb.com/kopkb/kaunter" + TAGS="-t ${IMAGE}:${{ gitea.sha }}" + + if [ "${{ gitea.ref_type }}" = "tag" ] && echo "${{ gitea.ref_name }}" | grep -q '^v'; then + TAGS="${TAGS} -t ${IMAGE}:${{ gitea.ref_name }}" + fi + + if [ "${{ gitea.ref_name }}" = "staging" ]; then + TAGS="${TAGS} -t ${IMAGE}:staging" + fi + + echo "Building and pushing frontend: ${TAGS}" + docker buildx build \ + --target staging \ + --platform linux/amd64 \ + --cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \ + --cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \ + -f docker/php/Dockerfile \ + ${TAGS} \ + --push . diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml new file mode 100644 index 0000000..30d130c --- /dev/null +++ b/.gitea/workflows/deploy-staging.yml @@ -0,0 +1,123 @@ +name: Deploy to Staging + +on: + workflow_dispatch: + inputs: + image_tag: + description: "Docker image tag to deploy (commit SHA, staging, or v*)" + required: true + default: "staging" + type: string + +env: + APP_IMAGE: git.koppkb.com/kopkb/kaunter + DEPLOY_DIR: /home/arrahn/Project/kaunter + +jobs: + deploy: + runs-on: host + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.image_tag }} + + - name: Login to Docker registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.koppkb.com \ + -u "${{ secrets.REGISTRY_USERNAME }}" \ + --password-stdin + + - name: Verify image exists in registry + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + IMAGE="${APP_IMAGE}:${IMAGE_TAG}" + + echo "Checking if image exists: ${IMAGE}" + if ! docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then + echo "ERROR: Image ${IMAGE} does not exist in registry!" + echo "Run the Build Docker Image workflow first." + exit 1 + fi + echo "✓ ${IMAGE} found" + + - name: Pull Docker image + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + docker pull "${APP_IMAGE}:${IMAGE_TAG}" + echo "✓ Image pulled successfully" + + - name: Sync compose file and deploy + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + + mkdir -p "${DEPLOY_DIR}/docker/nginx" + cp docker-compose.staging.yml "${DEPLOY_DIR}/docker-compose.yml" + cp docker/nginx/default.conf "${DEPLOY_DIR}/docker/nginx/default.conf" + rm -rf "${DEPLOY_DIR}/public" + cp -R public "${DEPLOY_DIR}/public" + + if [ ! -f "${DEPLOY_DIR}/.env.staging" ]; then + if [ -f .env.staging ]; then + cp .env.staging "${DEPLOY_DIR}/.env.staging" + echo "Copied .env.staging from repo (first deploy). Edit ${DEPLOY_DIR}/.env.staging on the server if needed." + else + echo "ERROR: ${DEPLOY_DIR}/.env.staging is missing on the server" + exit 1 + fi + fi + + cd "${DEPLOY_DIR}" + + export APP_IMAGE + export IMAGE_TAG + + docker compose --env-file .env.staging -f docker-compose.yml pull app + docker compose --env-file .env.staging -f docker-compose.yml up -d --no-build --remove-orphans + + echo "✓ Deployed IMAGE_TAG=${IMAGE_TAG}" + + - name: Verify deployment + run: | + set -e + cd "${DEPLOY_DIR}" + + echo "Waiting for services to start..." + sleep 10 + + for SERVICE in app web mysql; do + STATUS=$(docker compose --env-file .env.staging -f docker-compose.yml ps --status running --format '{{.Name}}' "$SERVICE" 2>/dev/null || true) + if [ -z "$STATUS" ]; then + echo "ERROR: ${SERVICE} is not running" + docker compose --env-file .env.staging -f docker-compose.yml ps + docker compose --env-file .env.staging -f docker-compose.yml logs --tail=50 "$SERVICE" || true + exit 1 + fi + echo "✓ ${SERVICE} is running (${STATUS})" + done + + HTTP_PORT=$(grep -E '^HTTP_PORT=' .env.staging 2>/dev/null | cut -d= -f2- | tr -d '"' || true) + HTTP_PORT=${HTTP_PORT:-8091} + + check_health() { + NAME=$1 + URL=$2 + for i in 1 2 3 4 5 6; do + if curl -sf "$URL" > /dev/null; then + echo "✓ ${NAME} health check passed (${URL})" + return 0 + fi + echo "Waiting for ${NAME}... attempt ${i}/6" + sleep 5 + done + echo "ERROR: ${NAME} health check failed (${URL})" + docker compose --env-file .env.staging -f docker-compose.yml logs --tail=50 app web || true + return 1 + } + + check_health "kaunter" "http://127.0.0.1:${HTTP_PORT}/health" diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..073e756 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,77 @@ +# Staging stack — isolated from local docker-compose.yml (8081 / 3307) +# and from api_arrahn staging (8090 / 3308). +# +# Local: +# docker compose --env-file .env.staging -f docker-compose.staging.yml up -d --build +# +# CI (image already in registry): +# APP_IMAGE=git.koppkb.com/kopkb/kaunter IMAGE_TAG= \ +# docker compose --env-file .env.staging -f docker-compose.staging.yml up -d --no-build +# +# Host nginx reverse-proxies the public domain to 127.0.0.1:${HTTP_PORT}. +# See docker/nginx/host-proxy.example.conf +# +# If 8091 or 3309 is already taken, set HTTP_PORT / MYSQL_PUBLISH_PORT in .env.staging. + +name: kaunter_staging + +services: + app: + image: ${APP_IMAGE:-kaunter}:${IMAGE_TAG:-staging} + build: + context: . + dockerfile: docker/php/Dockerfile + target: staging + container_name: kaunter_staging_app + working_dir: /var/www/html + volumes: + - storage_staging:/var/www/html/storage + - ./.env.staging:/var/www/html/.env.staging:ro + env_file: + - .env.staging + environment: + APP_ENV: staging + USE_STAGING_ENV: "true" + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + mysql: + condition: service_healthy + restart: unless-stopped + + web: + image: nginx:1.25-alpine + container_name: kaunter_staging_web + ports: + - "${HTTP_PORT:-8091}:80" + volumes: + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - ./public:/var/www/html/public:ro + depends_on: + - app + restart: unless-stopped + + mysql: + image: mysql:8.0 + container_name: kaunter_staging_mysql + command: --default-authentication-plugin=mysql_native_password + ports: + - "127.0.0.1:${MYSQL_PUBLISH_PORT:-3309}:3306" + environment: + MYSQL_DATABASE: kaunter_erahn + MYSQL_USER: kaunteruser + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: root + volumes: + - mysql_staging_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 20s + restart: unless-stopped + +volumes: + mysql_staging_data: + storage_staging: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ac531a3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + app: + build: + context: . + dockerfile: docker/php/Dockerfile + target: development + working_dir: /var/www/html + volumes: + - ./:/var/www/html:cached + environment: + APP_ENV: local + APP_DEBUG: "true" + # Lets PHP resolve host.docker.internal to the host machine (Linux Docker; Desktop already provides this). + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + - mysql + + web: + image: nginx:1.25-alpine + ports: + - "8081:80" + volumes: + - ./:/var/www/html:cached + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - app + + mysql: + image: mysql:8.0 + command: --default-authentication-plugin=mysql_native_password + ports: + - "3307:3306" + environment: + MYSQL_DATABASE: kaunter_erahn + MYSQL_USER: kaunteruser + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: root + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..223a96c --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name _; + + root /var/www/html/public; + index index.php index.html; + + charset utf-8; + client_max_body_size 50M; + + location / { + try_files $uri $uri/ @php; + } + + location @php { + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_pass app:9000; + fastcgi_index index.php; + fastcgi_read_timeout 120s; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_pass app:9000; + fastcgi_index index.php; + fastcgi_read_timeout 120s; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..b14f579 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,51 @@ +FROM php:7.4-fpm AS base + +WORKDIR /var/www/html + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + git \ + pkg-config \ + unzip \ + libonig-dev \ + libzip-dev \ + libpng-dev \ + libjpeg62-turbo-dev \ + libfreetype6-dev \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j"$(nproc)" \ + pdo_mysql \ + mbstring \ + bcmath \ + zip \ + exif \ + gd \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +COPY docker/php/php.ini /usr/local/etc/php/conf.d/app.ini +COPY docker/php/zz-clear-env.conf /usr/local/etc/php-fpm.d/zz-clear-env.conf +COPY docker/php/entrypoint.sh /usr/local/bin/app-entrypoint.sh + +RUN chmod +x /usr/local/bin/app-entrypoint.sh \ + && usermod -u 1000 www-data && groupmod -g 1000 www-data + +ENTRYPOINT ["app-entrypoint.sh"] +CMD ["php-fpm"] + +FROM base AS staging + +COPY --chown=www-data:www-data . /var/www/html + +RUN composer install --no-interaction --prefer-dist --no-dev --optimize-autoloader --no-scripts \ + && mkdir -p \ + storage/logs \ + storage/framework/cache/data \ + storage/framework/views \ + storage/framework/sessions \ + storage/app/public \ + bootstrap/cache \ + && chown -R www-data:www-data storage bootstrap/cache vendor + +FROM base AS development diff --git a/docker/php/entrypoint.sh b/docker/php/entrypoint.sh new file mode 100644 index 0000000..333adab --- /dev/null +++ b/docker/php/entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -e + +cd /var/www/html + +if [ "${USE_STAGING_ENV:-false}" = "true" ] && [ -f .env.staging ]; then + cp .env.staging .env +fi + +if [ -f composer.json ]; then + if [ -f vendor/autoload.php ] && [ "${USE_STAGING_ENV:-false}" = "true" ]; then + : # vendor is baked into the staging image + elif [ "${USE_STAGING_ENV:-false}" = "true" ]; then + composer install --no-interaction --prefer-dist --no-dev --optimize-autoloader --no-scripts + else + composer install --no-interaction --prefer-dist + fi +fi + +mkdir -p \ + storage/logs \ + storage/framework/cache/data \ + storage/framework/views \ + storage/framework/sessions \ + storage/app/public \ + bootstrap/cache + +if [ -f artisan ]; then + php artisan package:discover --ansi + php artisan storage:link --force >/dev/null 2>&1 || true +fi + +chown -R www-data:www-data storage bootstrap/cache + +exec docker-php-entrypoint "$@" diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 0000000..3e9df60 --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,4 @@ +memory_limit=512M +upload_max_filesize=50M +post_max_size=50M +max_execution_time=120 diff --git a/docker/php/zz-clear-env.conf b/docker/php/zz-clear-env.conf new file mode 100644 index 0000000..edbfb22 --- /dev/null +++ b/docker/php/zz-clear-env.conf @@ -0,0 +1,3 @@ +[www] +; Pass container env vars (from compose env_file) through to PHP workers. +clear_env = no diff --git a/routes/web.php b/routes/web.php index e95c2e4..020f766 100644 --- a/routes/web.php +++ b/routes/web.php @@ -15,6 +15,10 @@ use Illuminate\Support\Facades\Storage; | */ +Route::get('/health', function () { + return response()->json(['status' => 'ok']); +}); + Route::get('/',['as'=>'login','uses'=>'HomeController@index']); Route::get('/testtry',function(ImbangdugaServices $imbangduga){