From a627afb6c5ee18f1658dd88a2b95b3e20292b86f Mon Sep 17 00:00:00 2001 From: ISMAIL MASSERAN Date: Sun, 25 Oct 2026 15:23:55 +0800 Subject: [PATCH] DONE: implement best practice for cicd, separate docker compose --- .gitea/workflows/deploy-data.yml | 84 +++++++++++++++++++ docker/production/docker-compose.data.yml | 70 ++++++++++++++++ .../production/docker-compose.production.yml | 63 +------------- 3 files changed, 156 insertions(+), 61 deletions(-) create mode 100644 .gitea/workflows/deploy-data.yml create mode 100644 docker/production/docker-compose.data.yml diff --git a/.gitea/workflows/deploy-data.yml b/.gitea/workflows/deploy-data.yml new file mode 100644 index 0000000..eff08a9 --- /dev/null +++ b/.gitea/workflows/deploy-data.yml @@ -0,0 +1,84 @@ +name: Deploy Production Data Services + +# Manual deploy for Postgres and Redis. A backend release does not run this. +on: + workflow_dispatch: + +jobs: + deploy: + runs-on: mykopkb-production + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Deploy data services over SSH + env: + PROD_HOST: ${{ secrets.PROD_HOST }} + PROD_USER: ${{ secrets.PROD_USER }} + PROD_SSH_KEY: ${{ secrets.PROD_SSH_KEY }} + PROD_DEPLOY_DIR: ${{ secrets.PROD_DEPLOY_DIR }} + run: | + set -euo pipefail + + if [ ! -f .env.production ]; then + echo "ERROR: .env.production is missing from the repository checkout" + exit 1 + fi + + if ! command -v ssh >/dev/null 2>&1; then + apt-get update && apt-get install -y openssh-client + fi + + mkdir -p ~/.ssh + umask 077 + if printf '%s' "$PROD_SSH_KEY" | grep -q -- '-----BEGIN'; then + printf '%b\n' "$PROD_SSH_KEY" | tr -d '\r' > ~/.ssh/id_ed25519 + else + printf '%s' "$PROD_SSH_KEY" | tr -d '[:space:]' | base64 -d > ~/.ssh/id_ed25519 + fi + chmod 600 ~/.ssh/id_ed25519 + if ! ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null; then + echo "ERROR: PROD_SSH_KEY is not a readable private key." + exit 1 + fi + ssh-keyscan -H "$PROD_HOST" >> ~/.ssh/known_hosts + + SSH=(ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes "${PROD_USER}@${PROD_HOST}") + + "${SSH[@]}" "bash -s" << EOF + set -euo pipefail + for name in mykopkb-postgres-production mykopkb-redis-production; do + if docker inspect "\$name" >/dev/null 2>&1; then + project=\$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project"}}' "\$name" 2>/dev/null || true) + if [ "\$project" != "mykopkb-data" ]; then + echo "ERROR: \$name is already running under compose project '\${project:-unknown}'." + echo "This workflow does not replace the existing database or Redis containers." + exit 1 + fi + fi + done + EOF + + "${SSH[@]}" "mkdir -p '${PROD_DEPLOY_DIR}/redis-config' '${PROD_DEPLOY_DIR}/docker/postgres/conf.d'" + scp -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes \ + docker/production/docker-compose.data.yml \ + "${PROD_USER}@${PROD_HOST}:${PROD_DEPLOY_DIR}/docker-compose.data.yml" + scp -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes \ + .env.production \ + "${PROD_USER}@${PROD_HOST}:${PROD_DEPLOY_DIR}/.env" + scp -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes \ + docker/production/redis/redis.conf \ + "${PROD_USER}@${PROD_HOST}:${PROD_DEPLOY_DIR}/redis-config/redis.conf" + + "${SSH[@]}" "bash -s" << EOF + set -euo pipefail + chmod 600 "${PROD_DEPLOY_DIR}/.env" + + docker network inspect mykopkb-production-network >/dev/null 2>&1 \ + || docker network create mykopkb-production-network + + cd "${PROD_DEPLOY_DIR}" + docker compose -p mykopkb-data -f docker-compose.data.yml up -d + docker compose -p mykopkb-data -f docker-compose.data.yml ps + EOF diff --git a/docker/production/docker-compose.data.yml b/docker/production/docker-compose.data.yml new file mode 100644 index 0000000..9c6e6b7 --- /dev/null +++ b/docker/production/docker-compose.data.yml @@ -0,0 +1,70 @@ +# Stateful stack. Deploy this only when Postgres or Redis should change. +# The backend reaches these services by the network aliases db and redis. +services: + db: + image: postgres:17 + container_name: mykopkb-postgres-production + restart: unless-stopped + ports: + - "${DB_PORT:-5433}:5432" + environment: + - POSTGRES_USER=${DB_USERNAME} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_DATABASE} + volumes: + - db-data-production:/var/lib/postgresql/data + - ./docker/postgres/conf.d:/etc/postgres/conf.d:ro + networks: + mykopkb-production-network: + aliases: + - db + healthcheck: + test: + [ + "CMD-SHELL", + 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" || exit 1', + ] + interval: 15s + timeout: 10s + retries: 5 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + redis: + image: redis:8 + container_name: mykopkb-redis-production + restart: unless-stopped + volumes: + - redis-data-production:/data + - ./redis-config/redis.conf:/usr/local/etc/redis/redis.conf:ro + command: redis-server /usr/local/etc/redis/redis.conf + networks: + mykopkb-production-network: + aliases: + - redis + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + mykopkb-production-network: + name: mykopkb-production-network + external: true + +volumes: + db-data-production: + name: mykopkb-db-data-production + redis-data-production: + name: mykopkb-redis-data-production diff --git a/docker/production/docker-compose.production.yml b/docker/production/docker-compose.production.yml index 9c729b1..caed282 100644 --- a/docker/production/docker-compose.production.yml +++ b/docker/production/docker-compose.production.yml @@ -1,3 +1,4 @@ +# Application stack. Postgres and Redis live in docker-compose.data.yml. services: backend: image: git.koppkb.com/kopkb/my-kopkb-be:${IMAGE_TAG:-latest} @@ -20,9 +21,6 @@ services: - ${NGINX_CONF:-./docker/production/nginx/nginx.conf}:/etc/nginx/nginx.conf:ro networks: - mykopkb-production-network - depends_on: - redis: - condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost/health"] interval: 30s @@ -35,70 +33,13 @@ services: max-size: "10m" max-file: "3" - db: - image: postgres:17 - container_name: mykopkb-postgres-production - restart: unless-stopped - ports: - - "${DB_PORT:-5433}:5432" - environment: - - POSTGRES_USER=${DB_USERNAME} - - POSTGRES_PASSWORD=${DB_PASSWORD} - - POSTGRES_DB=${DB_DATABASE} - volumes: - - db-data-production:/var/lib/postgresql/data - - ./docker/postgres/conf.d:/etc/postgres/conf.d:ro - networks: - - mykopkb-production-network - healthcheck: - test: - [ - "CMD-SHELL", - 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" || exit 1', - ] - interval: 15s - timeout: 10s - retries: 5 - start_period: 30s - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" - - redis: - image: redis:8 - container_name: mykopkb-redis-production - restart: unless-stopped - volumes: - - redis-data-production:/data - - ./redis-config/redis.conf:/usr/local/etc/redis/redis.conf:ro - command: redis-server /usr/local/etc/redis/redis.conf - networks: - - mykopkb-production-network - healthcheck: - test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 10s - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" - networks: mykopkb-production-network: name: mykopkb-production-network - driver: bridge + external: true volumes: app-storage-production: driver: local app-logs-production: driver: local - db-data-production: - driver: local - redis-data-production: - driver: local