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:
- <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.
- <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.
- <strong>GitHub repository secrets</strong> that tell the workflow which identity, tenant, and subscription to use.
> 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:
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
az identity create \
--name journalappapi-github-deploy \
--resource-group journalApp \
--location centralindiaThis prints details including:
- <code>clientId</code>
- <code>principalId</code>
- <code>tenantId</code>
Save those values.
2.2 Grant the identity permission to deploy
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
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
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:
gh secret list> 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>):
git add .github/workflows/deploy.yml
git commit -m "ci: build jar and deploy to azure web app (no docker)"
git pushWatch the run:
gh run watchOr use the GitHub <strong>Actions</strong> tab.
Troubleshooting reference
| Symptom | Likely cause |
|---|---|
| <code>Not all values are present. Ensure 'client-id' and 'tenant-id' 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><appId></code> and it was typed literally into the terminal. |
| Node.js warnings in logs | Informational; 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.