name: Deploy to Production # Manual production deploy — pulls pre-built images from the registry on: workflow_dispatch: inputs: image_tag: description: "Docker image tag to deploy (commit SHA or version tag, e.g. v1.0.0)" required: true default: "latest" type: string env: BACKEND_IMAGE: git.koppkb.com/kopkb/my-kopkb-be jobs: deploy: runs-on: mykopkb-production steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ inputs.image_tag }} - name: Verify image exists in registry run: | set -e echo "${{ secrets.REGISTRY_PASSWORD }}" | \ docker login git.koppkb.com \ -u "${{ secrets.REGISTRY_USERNAME }}" \ --password-stdin IMAGE="${BACKEND_IMAGE}:${{ inputs.image_tag }}" echo "Checking if image exists: ${IMAGE}" docker manifest inspect "${IMAGE}" > /dev/null echo "✓ ${IMAGE} found" - 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 -euo pipefail if [ ! -f .env.production ]; then echo "ERROR: .env.production is missing from the repository checkout" exit 1 fi if [ ! -f docker/production/nginx/nginx.conf ]; then echo "ERROR: docker/production/nginx/nginx.conf 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 # 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 "Waiting for the backend to start..." sleep 10 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)" 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} 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 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