Skip to content
Back to journal
CapacitorAndroidCI

How to Build a Signed APK from a Capacitor Project Without Android Studio (2026)

CCode2Native EngineeringEngineering team
Published
Updated

Want to see your own site as an app? Paste its address: the free preview shows it in a phone frame. No account needed.

If you have a Capacitor project that already contains an android/ folder, you do not need Android Studio on your own machine to ship it — but something still has to run the Gradle build with a full Android SDK. Build your web assets and run npx cap sync android (both need only Node.js), then let a CI runner such as GitHub Actions run Gradle, sign the output with your release keystore, and hand back an APK for sideloading plus an AAB for Google Play.

A note on our own scope before you read further: Code2Native's source builds do not compile Capacitor projects yet — Capacitor is on our waitlist, and a Capacitor upload stops with an explanation. If your app also works as a deployed website, our URL wrapper is the no-toolchain route today. This guide covers the Capacitor path itself: which half of the build stays on your machine, how the Gradle half moves to CI, how signing works, and the honest alternatives.

The real problem: every Capacitor app is two projects stapled together

A Capacitor app is a web project (Vite, Angular CLI, whatever) plus a completely ordinary Android Gradle project living in android/. The two halves have wildly different toolchain demands, and that asymmetry is the whole story.

The web half is easy. npm run build compiles your app into dist/, and npx cap sync android copies those assets into android/app/src/main/assets/public and wires each installed plugin into the Gradle files. Any machine with Node.js can do this.

# The half that only needs Node.js — runs anywhere
$ npm ci && npm run build
dist/ built in 14.2s
$ npx cap sync android
Copying web assets from dist to android/app/src/main/assets/public
Updating Android plugins
[info] Sync finished in 0.8s

Then you hit the other half, and the machine that just built your web app perfectly greets you with this:

# The half that needs the Android toolchain
$ cd android && ./gradlew assembleRelease
FAILURE: Build failed with an exception.
* What went wrong:
SDK location not found. Define a valid SDK location with an
ANDROID_HOME environment variable or by setting the sdk.dir
path in your project's local.properties file.

Fixing that means installing the Android SDK, a matching JDK, and accepting a pile of licenses — in practice, installing Android Studio. And the versions are a moving target. As of mid-2026, a Capacitor 7 android/ folder expects Java 21 with compile/target SDK 35, while Capacitor 8 raises the floor again to SDK 36, AGP 8.13 and Gradle wrapper 8.14.x — effectively a current Android Studio install. Meanwhile a project scaffolded in the Capacitor 5 era assumes JDK 17 and an older AGP. Set up your machine for one and the other breaks. This treadmill is exactly why npx cap migrate exists.

Capacitor does ship a CLI escape hatch — npx cap build android with --keystorepath / --androidreleasetype flags — but it still requires the full local SDK underneath, its signing defaults to the older jarsigner tool, and the issue tracker has long-running reports (ionic-team/capacitor #7366, #7054) of the apksigner path producing an unsigned APK while claiming success. It automates the last step of a working local setup; it does not replace the setup.

The vendor CI option is closing: Ionic announced in February 2025 that Appflow — the official cloud build service for Capacitor — is no longer sold to new customers, with existing access ending December 31, 2027. If you searched for this article because Appflow is off the table, that instinct is correct.

Moving the Gradle half to a CI runner

The honest division of labor: the Node half runs anywhere, the Gradle half needs a machine with the Android SDK. GitHub's hosted Ubuntu runners ship with the Android SDK preinstalled, so a single workflow can do both halves: install dependencies, build the web assets, sync them into android/, set up the JDK your Capacitor major expects, restore your keystore from repository secrets, and run Gradle.

# .github/workflows/android-release.yml
name: android-release
on: workflow_dispatch
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci && npm run build
      - run: npx cap sync android
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21   # 17 for a Capacitor 5/6-era project
      - name: Restore the release keystore
        run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/release.jks
      - name: Build a signed APK and AAB
        working-directory: android
        env:
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: ./gradlew assembleRelease bundleRelease
      - uses: actions/upload-artifact@v4
        with:
          name: android-release
          path: |
            android/app/build/outputs/apk/release/*.apk
            android/app/build/outputs/bundle/release/*.aab

Gradle only signs a release build if android/app/build.gradle has a release signing config. Read the secrets from the environment so nothing sensitive is committed:

android {
    signingConfigs {
        release {
            storeFile file("release.jks")
            storePassword System.getenv("KEYSTORE_PASSWORD")
            keyAlias System.getenv("KEY_ALIAS")
            keyPassword System.getenv("KEY_PASSWORD")
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

Step-by-step: Capacitor project to signed APK/AAB

1

Check the web build and webDir

npm ci && npm run build must succeed, and its output folder must match webDir in your Capacitor config (usually dist or www).

2

Commit the android/ platform

If you've never added it, run npx cap add android once and commit the folder. Keep android/app/build/ and android/.gradle/ out of Git.

3

Store the keystore as secrets

Base64-encode your .jks (base64 -w0 release.jks) and add it with the alias and both passwords as repository secrets. If the app is already on Google Play, this must be the key Play expects — updates signed with a different key are rejected.

4

Add the workflow and the signing config

The two snippets above. Match java-version to your Capacitor major.

5

Run it, then verify the signature

Download the artifact, then check the APK really is signed with your key: apksigner verify --print-certs app-release.apk. Sideload the APK to test; upload the AAB to Google Play.

CI vs the honest alternatives

Competitor details below are accurate to the best of our knowledge as of mid-2026 — verify current terms before committing.

PathWhat you set up & maintainCostThe catch
Local Android StudioStudio + SDK + JDK matched to your Capacitor major; re-matched on every upgradeFree (plus disk, plus time)Best for daily native-plugin development; overkill if you just need release binaries
GitHub ActionsThe workflow above, keystore as base64 secrets, a signing config in build.gradleFree for public repos; minutes metered on privateYou own the YAML; the first working signing step usually takes an afternoon
Ionic AppflowVendor dashboard, polished Capacitor-native pipeline, live updatesNot sold to new customersWinding down — existing customers keep access until Dec 31, 2027; no new signups
Code2Native URL wrapperNothing local; paste the deployed URL of the web buildPreview free; App license $69 one-timeLoads your live site in a WebView — no bundled assets or Capacitor plugins. Capacitor source builds are on our waitlist

One concession worth spelling out: if your team already has a working CI pipeline with signing configured, keep it. And if the only reason you reached for Capacitor was to get an installable app around a web build, compare it with wrapping the deployed site — our Capacitor vs native comparison walks through that fork.

When this approach is the wrong tool

  • Your app is React Native or Expo. Different toolchain entirely — Expo's EAS Build is the intended cloud path for Expo code.
  • Your Ionic project has no android/ folder. An Ionic app on legacy Cordova needs Cordova's own build. If it is Capacitor underneath, you're one npx cap add android away from the workflow above.
  • You're actively writing a custom native plugin. Iterating on Kotlin/Java code needs a local emulator, breakpoints, and instant rebuilds. Install Android Studio for development; use CI for releases.
  • You don't have a Capacitor project at all. If what you actually have is a deployed website, wrapping it is a simpler, cheaper path than adopting Capacitor first.

FAQ

Do I need to run npx cap sync before Gradle runs?

Yes. Gradle compiles your android/ project as-is, so the web assets must already be synced into it. The good news: sync needs only Node.js, which is why the workflow runs it right before the Java setup.

My project fails locally with “SDK location not found” — will it build in CI?

Usually, because that error is about your machine, not your code: Gradle can't find an Android SDK via ANDROID_HOME or local.properties, and GitHub's hosted Ubuntu runners set ANDROID_HOME for you. Real code errors — a plugin that doesn't compile, a bad Gradle script — still fail, with the full log to show you why.

Why not just use npx cap build android?

If you have a fully working local SDK, you can — it drives Gradle and signs in one command. But it requires that local setup to exist, defaults to legacy jarsigner signing, and has open issues where the apksigner mode reports success while emitting an unsigned APK. It automates the last mile of a configured machine; CI is how you get a configured machine you don't maintain.

Do I need an APK or an AAB for Google Play?

Google Play has required the AAB format for new apps since August 2021; the APK is what you sideload onto devices for testing and direct distribution. The workflow above builds both from the same run, signed with the same key, so you don't have to choose in advance.

Is Ionic Appflow still an option for Capacitor cloud builds?

Only if you're already a customer. Ionic announced in February 2025 that new sales of Appflow are discontinued, and existing customers keep access through December 31, 2027 while the product winds down. As of mid-2026 you cannot sign up, which is why Capacitor teams are re-evaluating their build setup now rather than at the deadline.

Can Code2Native build my Capacitor project?

Not yet. Our source builds compile Flutter and native Android (Gradle) projects; Capacitor, React Native, Ionic, and Expo are on the waitlist, and a Capacitor upload stops with an explanation rather than a half-working build. If your app runs as a deployed website, wrapping that URL works today.

Does your app also run as a website?

Then you can skip the native project entirely: paste the deployed URL and get a Preview APK on your phone in minutes, free. The release APK and AAB come with a one-time App license.

See your website as an app

The preview loads your live site in a phone frame. When it looks right, open an account and build it. Build one Preview APK with a Code2Native watermark. No card required.

Open an account and build →
C

Code2Native Engineering

Engineering team

Written by the Code2Native engineering team — the people who build and operate the cloud build pipeline.