Complete Guide: CI/CD for a Java Web App on Azure with GitHub Actions (No Docker)

Build a Java JAR with Maven and deploy directly to Azure App Service using GitHub Actions and OIDC (no Docker, no passwords).

Jun 18, 20265 min read

Complete Guide: CI/CD for a Java Web App on Azure with GitHub Actions (No Docker) This guide documents a working setup that builds a Java JAR with Maven and deploys it straight to an <strong>Azure App Service Web App</strong> using <strong>GitHub Actions</strong> — <strong>no container</strong>, <strong>no Dockerfile</strong>, just the JAR file pushed directly to an <strong>Azure Linux Java runtime</strong>. It’s written so you (or anyone on your team) can follow it end to end on a fresh project, with the exact commands used.

How the pieces fit together

Three things have to exist before the workflow can run successfully:

  1. <strong>A GitHub Actions workflow file</strong> that builds the JAR and uploads it as an artifact, then downloads that artifact and deploys it to Azure.
  2. <strong>An Azure identity</strong> that GitHub can authenticate as, without storing a password anywhere. This is done with <strong>OpenID Connect (OIDC)</strong> — GitHub proves who it is using a short-lived token, and Azure trusts that token because of a federated credential you set up in advance.
  3. <strong>GitHub repository secrets</strong> that tell the workflow which identity, tenant, and subscription to use.

&gt; Why this is more involved than a simple <code>az login</code> + password: storing a long-lived password in GitHub is a security liability. OIDC avoids that entirely — no long-lived secret is stored on either side.

Prerequisites

  • A GitHub repo containing your Java project, with a working <code>pom.xml</code>.
  • An existing <strong>Azure App Service (Web App)</strong> with a Java runtime already configured.
  • In this setup, the runtime was <strong>Linux + Java 21 SE</strong>.
  • Azure CLI installed and logged in:
  • <code>az login</code>
  • Owner or Contributor access to the resource group containing your Web App.

Step 1: Create the GitHub Actions workflow

Create <code>.github/workflows/deploy.yml</code> in your repo:

yaml
name: Build and deploy JAR app to Azure Web App - journalapp

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Set up Java version
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'microsoft'

      - name: Build with Maven
        run: mvn -f journalApp-backend/pom.xml clean install -DskipTests

      - name: Upload artifact for deployment job
        uses: actions/upload-artifact@v4
        with:
          name: java-app
          path: ${{ github.workspace }}/journalApp-backend/target/*.jar

  deploy:
    runs-on: ubuntu-latest
    needs: build

    # Required for OIDC: GitHub must be allowed to mint an ID token.
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Download artifact from build job
        uses: actions/download-artifact@v4
        with:
          name: java-app

      - name: Login to Azure (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to Azure Web App
        id: deploy-to-webapp
        uses: azure/webapps-deploy@v3
        with:
          app-name: 'journalappapi'
          slot-name: 'Production'
          package: '*.jar'

Notes that commonly break deployments

  • The <strong>build job</strong> runs on its own runner. It produces <code>target/*.jar</code> and uploads it as an artifact (<code>java-app</code>).
  • The <strong>deploy job</strong> runs on a separate runner. It must download the artifact again.
  • <code>permissions: id-token: write</code> is required for OIDC. Without it, <code>azure/login@v2</code> cannot obtain the token.
  • <code>app-name</code> must match your real Azure App Service resource name exactly (it’s not always the same as your repo name).

Step 2: Create an Azure identity for GitHub (OIDC)

If you can register apps in Entra ID, you can use an App Registration. If you can’t (common in student/restricted tenants), use a <strong>User-Assigned Managed Identity</strong>.

2.1 Create the managed identity

bash
az identity create \
  --name journalappapi-github-deploy \
  --resource-group journalApp \
  --location centralindia

This prints details including:

  • <code>clientId</code>
  • <code>principalId</code>
  • <code>tenantId</code>

Save those values.

2.2 Grant the identity permission to deploy

bash
az role assignment create \
  --assignee <principalId> \
  --role Contributor \
  --scope /subscriptions/<subscription-id>/resourceGroups/<resource-group-name>

2.3 Trust GitHub’s OIDC token with a federated credential

bash
az identity federated-credential create \
  --name github-actions-main \
  --identity-name journalappapi-github-deploy \
  --resource-group journalApp \
  --issuer "https://token.actions.githubusercontent.com" \
  --subject "repo:<github-org>/<repo-name>:ref:refs/heads/main" \
  --audiences "api://AzureADTokenExchange"

Key point: the <code>subject</code> ties Azure trust to <strong>only your exact repo + branch</strong>. If you later deploy from <code>workflow_dispatch</code> on other branches/environments, create additional federated credentials for those subjects.

Step 3: Add GitHub repository secrets

Your workflow reads these secrets:

  • <code>AZURE_CLIENT_ID</code>
  • <code>AZURE_TENANT_ID</code>
  • <code>AZURE_SUBSCRIPTION_ID</code>

Using GitHub UI

Repo → <strong>Settings</strong> → <strong>Secrets and variables</strong> → <strong>Actions</strong> → <strong>New repository secret</strong> Add each secret individually.

Using the <code>gh</code> CLI

bash
gh secret set AZURE_CLIENT_ID --body "<clientId from Step 2>"
gh secret set AZURE_TENANT_ID --body "<tenantId from Step 2>"
gh secret set AZURE_SUBSCRIPTION_ID --body "<your subscription id>"

Confirm:

bash
gh secret list

&gt; These identifiers are not “passwords”. The real security is provided by OIDC + the federated credential trust.

Step 4: Push and watch the workflow

Commit the workflow file and push to <code>main</code> (or trigger manually via <strong>workflow_dispatch</strong>):

bash
git add .github/workflows/deploy.yml
git commit -m "ci: build jar and deploy to azure web app (no docker)"
git push

Watch the run:

bash
gh run watch

Or use the GitHub <strong>Actions</strong> tab.

Troubleshooting reference

SymptomLikely cause
<code>Not all values are present. Ensure &#39;client-id&#39; and &#39;tenant-id&#39; are supplied</code>Secrets referenced in the workflow are missing, misspelled, or scoped incorrectly (e.g., environment scoping doesn’t match the job).
<code>Insufficient privileges</code>Your account lacks permissions to create directory objects in Entra ID. Use a Managed Identity instead.
Login succeeds, deploy fails<code>app-name</code> doesn’t match the App Service name, or the identity lacks a role assignment on the resource group.
<code>zsh: no such file or directory: appId</code>You copied a command with placeholder notation like <code>&lt;appId&gt;</code> and it was typed literally into the terminal.
Node.js warnings in logsInformational; not usually the cause unless a specific action fails.

Why this approach has <strong>no Docker</strong> anywhere

<code>azure/webapps-deploy@v3</code> performs a standard <strong>zip-deploy</strong> of the artifact you point it at. Because your <strong>App Service was already configured with a Java runtime stack</strong> (for example, <strong>Java 21 SE on Linux</strong>), Azure can run the deployed JAR directly. There’s no image build, no registry push, and no Dockerfile in the repository. If you were using a container-first App Service (or a generic Linux container environment), you would need a Dockerfile + registry push. This guide avoids that by targeting a Java runtime Web App.