diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml index 0e21d54..66d1002 100644 --- a/.gitea/workflows/deploy-prod.yml +++ b/.gitea/workflows/deploy-prod.yml @@ -12,11 +12,10 @@ on: env: BACKEND_IMAGE: git.koppkb.com/kopkb/my-kopkb-be - DEPLOY_DIR: /home/arrahn/Project jobs: deploy: - runs-on: host + runs-on: mykopkb-production steps: - name: Checkout repository @@ -24,107 +23,133 @@ jobs: with: ref: ${{ inputs.image_tag }} - - name: Login to Docker registry + - name: Verify image exists in registry run: | + set -e echo "${{ secrets.REGISTRY_PASSWORD }}" | \ docker login git.koppkb.com \ -u "${{ secrets.REGISTRY_USERNAME }}" \ --password-stdin - - name: Verify images exist in registry - run: | - set -e - IMAGE_TAG="${{ inputs.image_tag }}" - - IMAGE="${BACKEND_IMAGE}:${IMAGE_TAG}" + IMAGE="${BACKEND_IMAGE}:${{ inputs.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!" - exit 1 - fi + docker manifest inspect "${IMAGE}" > /dev/null echo "✓ ${IMAGE} found" - - name: Pull Docker images + - name: Deploy 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 }} + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + IMAGE_TAG: ${{ inputs.image_tag }} run: | - set -e - IMAGE_TAG="${{ inputs.image_tag }}" - docker pull "${BACKEND_IMAGE}:${IMAGE_TAG}" - echo "✓ Image pulled successfully" + set -euo pipefail - - name: Sync compose file and deploy - run: | - set -e - IMAGE_TAG="${{ inputs.image_tag }}" - - mkdir -p "${DEPLOY_DIR}" - cp docker/production/docker-compose.production.yml "${DEPLOY_DIR}/docker-compose.yml" - - ENV_FILE=".env.production" - if [ ! -f "${ENV_FILE}" ]; then - echo "ERROR: ${ENV_FILE} is missing from the repository checkout" + if [ ! -f .env.production ]; then + echo "ERROR: .env.production is missing from the repository checkout" exit 1 fi - cp "${ENV_FILE}" "${DEPLOY_DIR}/.env" - chmod 600 "${DEPLOY_DIR}/.env" - - NGINX_CONF="$(pwd)/docker/production/nginx/nginx.conf" - if [ ! -f "${NGINX_CONF}" ]; then - echo "ERROR: nginx config not found at ${NGINX_CONF}" + if [ ! -f docker/production/nginx/nginx.conf ]; then + echo "ERROR: docker/production/nginx/nginx.conf is missing from the repository checkout" exit 1 fi - cd "${DEPLOY_DIR}" + if ! command -v ssh >/dev/null 2>&1; then + apt-get update && apt-get install -y openssh-client + fi - export IMAGE_TAG - export NGINX_CONF + mkdir -p ~/.ssh + umask 077 + # Gitea often stores a multiline key as one line. Prefer a base64 + # secret (one line). A raw key that still contains BEGIN is accepted too. + 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." + echo "On the runner, run: base64 -w0 ~/.ssh/mykopkb_prod" + echo "Put that single line in the PROD_SSH_KEY secret." + exit 1 + fi + ssh-keyscan -H "$PROD_HOST" >> ~/.ssh/known_hosts + + SSH=(ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes "${PROD_USER}@${PROD_HOST}") + IMAGE="${BACKEND_IMAGE}:${IMAGE_TAG}" + NGINX_CONF="${PROD_DEPLOY_DIR}/nginx/nginx.conf" + + "${SSH[@]}" "mkdir -p '${PROD_DEPLOY_DIR}/nginx'" + scp -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes \ + docker/production/docker-compose.production.yml \ + "${PROD_USER}@${PROD_HOST}:${PROD_DEPLOY_DIR}/docker-compose.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/nginx/nginx.conf \ + "${PROD_USER}@${PROD_HOST}:${NGINX_CONF}" + + "${SSH[@]}" "bash -s" << EOF + set -euo pipefail + echo "${REGISTRY_PASSWORD}" | docker login git.koppkb.com \ + -u "${REGISTRY_USERNAME}" --password-stdin + + echo "Checking if image exists: ${IMAGE}" + docker manifest inspect "${IMAGE}" > /dev/null + echo "✓ ${IMAGE} found" + + echo "Deploying backend IMAGE=${IMAGE}" + 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}" + export IMAGE_TAG="${IMAGE_TAG}" + export NGINX_CONF="${NGINX_CONF}" docker rm -f mykopkb-be-production || true docker compose -f docker-compose.yml pull backend docker compose -f docker-compose.yml up -d backend - echo "✓ Deployed backend IMAGE_TAG=${IMAGE_TAG}" - - - name: Verify deployment - run: | - set -e - cd "${DEPLOY_DIR}" - - echo "Waiting for services to start..." + echo "Waiting for the backend to start..." sleep 10 - STATUS=$(docker compose -f docker-compose.yml ps --status running --format '{{.Name}}' backend 2>/dev/null || true) - if [ -z "$STATUS" ]; then + STATUS=\$(docker compose -f docker-compose.yml ps --status running --format '{{.Name}}' backend || true) + if [ -z "\$STATUS" ]; then echo "ERROR: backend is not running" docker compose -f docker-compose.yml ps docker compose -f docker-compose.yml logs --tail=50 backend || true exit 1 fi - echo "✓ backend is running (${STATUS})" + echo "✓ backend is running (\$STATUS)" - BE_CONTAINER=$(docker compose -f docker-compose.yml ps -q backend) - if docker exec "$BE_CONTAINER" test -f /var/www/.env; then + BE_CONTAINER=\$(docker compose -f docker-compose.yml ps -q backend) + if docker exec "\$BE_CONTAINER" test -f /var/www/.env; then echo "✓ Backend .env file exists" else echo "WARNING: Backend .env file not found" fi - API_PORT=$(grep -E '^API_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true) - API_PORT=${API_PORT:-8080} + API_PORT=\$(grep -E '^API_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true) + API_PORT=\${API_PORT:-8080} - 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 -f docker-compose.yml logs --tail=50 "${NAME}" || true - return 1 - } + for i in 1 2 3 4 5 6; do + if curl -sf "http://127.0.0.1:\${API_PORT}/health" > /dev/null; then + echo "✓ backend health check passed (http://127.0.0.1:\${API_PORT}/health)" + echo "✓ Deployed backend IMAGE_TAG=${IMAGE_TAG}" + exit 0 + fi + echo "Waiting for backend... attempt \${i}/6" + sleep 5 + done - check_health "backend" "http://127.0.0.1:${API_PORT}/health" + echo "ERROR: backend health check failed (http://127.0.0.1:\${API_PORT}/health)" + docker compose -f docker-compose.yml logs --tail=50 backend || true + exit 1 + EOF