diff --git a/.gitea/workflows/build-and-deploy.yml b/.gitea/workflows/build-and-deploy.yml index 194ee08..75f54a8 100644 --- a/.gitea/workflows/build-and-deploy.yml +++ b/.gitea/workflows/build-and-deploy.yml @@ -1,4 +1,4 @@ -name: Build & Deploy +name: Build & Push on: push: @@ -291,198 +291,3 @@ jobs: docker push '$IMAGE_LATEST' fi " - - deploy: - name: Deploy to VPS - runs-on: ubuntu-latest - needs: [build-image] - env: - DOCKER_IMAGE: ${{ needs.build-image.outputs.docker_image }} - IMAGE_REPOSITORY: ${{ needs.build-image.outputs.image_repository }} - steps: - - name: Checkout repository - shell: bash - env: - GITEA_SERVER_URL: ${{ gitea.server_url }} - GITEA_REPOSITORY: ${{ gitea.repository }} - GITEA_SHA: ${{ gitea.sha }} - CHECKOUT_TOKEN: ${{ gitea.token }} - run: | - set -euo pipefail - if ! command -v git >/dev/null 2>&1; then - echo "::error::git must be available in the runner image; this workflow cannot install git while runner DNS is unavailable." - exit 1 - fi - - WORKSPACE="${GITHUB_WORKSPACE:-$PWD}" - mkdir -p "$WORKSPACE" - cd "$WORKSPACE" - - SERVER_URL="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPOSITORY="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - SHA="${GITEA_SHA:-${GITHUB_SHA:-}}" - if [ -z "$SERVER_URL" ] || [ -z "$REPOSITORY" ] || [ -z "$SHA" ]; then - echo "::error::Missing repository checkout context" - exit 1 - fi - - REPOSITORY_URL="${SERVER_URL}/${REPOSITORY}.git" - if [ ! -d .git ]; then - git init - git remote add origin "$REPOSITORY_URL" - fi - - git config --global --add safe.directory "$WORKSPACE" - if [ -n "${CHECKOUT_TOKEN:-}" ]; then - git -c "http.extraHeader=Authorization: token ${CHECKOUT_TOKEN}" fetch --no-tags --depth=1 origin "$SHA" - else - git fetch --no-tags --depth=1 origin "$SHA" - fi - git checkout --force FETCH_HEAD - - - name: Check deploy credentials - id: check-creds - env: - VPS_HOST: ${{ secrets.VPS_IP }} - VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }} - VPS_SSH_KEY_B64: ${{ secrets.VPS_SSH_KEY_B64 }} - run: | - VPS_HOST="${VPS_HOST:-$DEPLOY_SSH_HOST}" - if { [ -z "$VPS_SSH_KEY" ] && [ -z "$VPS_SSH_KEY_B64" ]; } || \ - [ -z "$VPS_HOST" ] || \ - [ -z "${{ secrets.VPS_USER }}" ]; then - echo "VPS secrets not fully configured — skipping deploy" - echo "skip=true" >> "$GITHUB_OUTPUT" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Install SSH client - if: steps.check-creds.outputs.skip == 'false' - run: | - if ! command -v ssh >/dev/null 2>&1 || ! command -v ssh-keygen >/dev/null 2>&1 || ! command -v scp >/dev/null 2>&1; then - echo "::error::ssh, ssh-keygen, and scp must be available in the runner image; this workflow cannot install openssh-client while runner DNS is unavailable." - exit 1 - fi - - - name: Set up SSH key - if: steps.check-creds.outputs.skip == 'false' - env: - VPS_HOST: ${{ secrets.VPS_IP }} - VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }} - VPS_SSH_KEY_B64: ${{ secrets.VPS_SSH_KEY_B64 }} - run: | - VPS_HOST="${VPS_HOST:-$DEPLOY_SSH_HOST}" - if [ -z "$VPS_HOST" ]; then - echo "::error::VPS_IP secret or DEPLOY_SSH_HOST is required" - exit 1 - fi - echo "Using SSH host $VPS_HOST" - mkdir -p ~/.ssh && chmod 700 ~/.ssh - if [ -n "$VPS_SSH_KEY_B64" ]; then - if ! printf '%s' "$VPS_SSH_KEY_B64" | tr -d '[:space:]' | base64 -d > ~/.ssh/id_rsa 2>/tmp/vps_ssh_key_decode.err; then - if [ -n "$VPS_SSH_KEY" ]; then - echo "::warning::VPS_SSH_KEY_B64 is not valid base64; using VPS_SSH_KEY instead" - printf '%b\n' "$VPS_SSH_KEY" | tr -d '\r' > ~/.ssh/id_rsa - else - echo "::error::VPS_SSH_KEY_B64 is not valid base64. Store a base64-encoded private key there, or set VPS_SSH_KEY to the raw private key." - exit 1 - fi - fi - else - printf '%b\n' "$VPS_SSH_KEY" | tr -d '\r' > ~/.ssh/id_rsa - fi - chmod 600 ~/.ssh/id_rsa - key_header="$(head -n 1 ~/.ssh/id_rsa || true)" - key_size="$(wc -c < ~/.ssh/id_rsa | tr -d ' ')" - case "$key_header" in - "-----BEGIN "*PRIVATE*" KEY-----") ;; - ssh-*|"ecdsa-"*|"sk-"*) - echo "::error::Decoded SSH key looks like a public key, not a private key. Encode the private key file, not the .pub file." - exit 1 - ;; - *) - echo "::error::Decoded SSH key does not start with a private key header. Decoded byte count: $key_size." - exit 1 - ;; - esac - if ! keygen_error="$(ssh-keygen -y -f ~/.ssh/id_rsa 2>&1 >/dev/null)"; then - echo "::error::SSH private key is not readable by ssh-keygen: $keygen_error. Use an unencrypted private key or configure a CI-specific deploy key." - exit 1 - fi - ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.pub - key_fingerprint="$(ssh-keygen -lf ~/.ssh/id_rsa.pub | awk '{print $2}')" - echo "Loaded deploy key fingerprint: $key_fingerprint" - echo "Deploy public key: $(cat ~/.ssh/id_rsa.pub)" - touch ~/.ssh/known_hosts - ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true - chmod 644 ~/.ssh/known_hosts - - - name: Test SSH authentication - if: steps.check-creds.outputs.skip == 'false' - env: - VPS_HOST: ${{ secrets.VPS_IP }} - run: | - VPS_HOST="${VPS_HOST:-$DEPLOY_SSH_HOST}" - if [ -z "$VPS_HOST" ]; then - echo "::error::VPS_IP secret or DEPLOY_SSH_HOST is required" - exit 1 - fi - SSH_OPTIONS="-i $HOME/.ssh/id_rsa -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=$HOME/.ssh/known_hosts" - key_fingerprint="$(ssh-keygen -lf "$HOME/.ssh/id_rsa.pub" | awk '{print $2}')" - echo "Testing SSH authentication to $VPS_HOST with deploy key fingerprint $key_fingerprint" - if ! ssh $SSH_OPTIONS "${{ secrets.VPS_USER }}@$VPS_HOST" "printf 'ssh authenticated as '; whoami"; then - echo "::error::SSH authentication failed. Verify VPS_USER matches the account that has deploy key fingerprint $key_fingerprint in ~/.ssh/authorized_keys on $VPS_HOST." - exit 1 - fi - - - name: Sync deployment assets - if: steps.check-creds.outputs.skip == 'false' - env: - VPS_HOST: ${{ secrets.VPS_IP }} - run: | - VPS_HOST="${VPS_HOST:-$DEPLOY_SSH_HOST}" - if [ -z "$VPS_HOST" ]; then - echo "::error::VPS_IP secret or DEPLOY_SSH_HOST is required" - exit 1 - fi - SSH_OPTIONS="-i $HOME/.ssh/id_rsa -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=$HOME/.ssh/known_hosts" - ssh $SSH_OPTIONS "${{ secrets.VPS_USER }}@$VPS_HOST" \ - "mkdir -p '$DEPLOY_ROOT/scripts' '$DEPLOY_ROOT/docker/pgmanage' '$DEPLOY_ROOT/docker/registry/auth' '$DEPLOY_ROOT/dynamic'" - scp $SSH_OPTIONS docker-compose.production.yml \ - docker-compose.portainer.production.yml \ - docker-compose.registry.production.yml \ - docker-compose.registry.local.yml \ - traefik.yaml \ - "${{ secrets.VPS_USER }}@$VPS_HOST:$DEPLOY_ROOT/" - scp $SSH_OPTIONS scripts/docker-prod-common.sh \ - scripts/docker-prod-deploy.sh \ - scripts/docker-prod-up-registry.sh \ - scripts/docker-registry-local-up.sh \ - "${{ secrets.VPS_USER }}@$VPS_HOST:$DEPLOY_ROOT/scripts/" - scp $SSH_OPTIONS docker/pgmanage/override.py \ - "${{ secrets.VPS_USER }}@$VPS_HOST:$DEPLOY_ROOT/docker/pgmanage/" - - - name: Run deploy script on VPS - if: steps.check-creds.outputs.skip == 'false' - env: - VPS_HOST: ${{ secrets.VPS_IP }} - run: | - VPS_HOST="${VPS_HOST:-$DEPLOY_SSH_HOST}" - if [ -z "$VPS_HOST" ]; then - echo "::error::VPS_IP secret or DEPLOY_SSH_HOST is required" - exit 1 - fi - SSH_OPTIONS="-i $HOME/.ssh/id_rsa -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=$HOME/.ssh/known_hosts" - REGISTRY_PASSWORD_B64="$(printf '%s' "${{ secrets.REGISTRY_PASSWORD }}" | base64 | tr -d '\n')" - ssh $SSH_OPTIONS "${{ secrets.VPS_USER }}@$VPS_HOST" " - set -e - cd '$DEPLOY_ROOT' - APP_IMAGE='$DEPLOY_REGISTRY_HOST/$IMAGE_REPOSITORY' \ - APP_VERSION='${{ gitea.sha }}' \ - IMAGE_TAG='${{ gitea.sha }}' \ - REGISTRY_HOST='$DEPLOY_REGISTRY_HOST' \ - REGISTRY_USER='${{ secrets.REGISTRY_USERNAME }}' \ - REGISTRY_PASSWORD=\$(printf '%s' '$REGISTRY_PASSWORD_B64' | base64 -d) \ - bash scripts/docker-prod-deploy.sh - " diff --git a/docs/DOCKER.md b/docs/DOCKER.md index 12d8ae2..b86b430 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -162,6 +162,14 @@ Open `.env.docker.production` and fill in every value. The minimum required secr | `RESEND_API_KEY` | Resend API key (or configure SMTP vars instead) | | `PGMANAGE_DOMAIN` | Hostname for pgManage, e.g. `pgmanage.rentaldrivego.ma` | +For Gitea Actions deploys, store the completed production env file as the `ENV_DOCKER_PRODUCTION_B64` secret: + +```bash +base64 < .env.docker.production | tr -d '\n' +``` + +Paste that single-line output into the Gitea secret. During deploy, the workflow writes it to `/opt/rentaldrivego/.env.docker.production` on the VPS with `600` permissions before running `scripts/docker-prod-deploy.sh`. + Production now derives `DATABASE_URL` inside the app container from `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD` when `DATABASE_URL_FROM_POSTGRES=true`. That avoids Prisma auth failures when the database password contains reserved URL characters such as `@`, `:`, or `/`. The example file uses `rentaldrivego.ma` for the carplace and public site. The dashboard and admin panel are routed under that same host at `/dashboard` and `/admin`.