In this article
Android APK files can be built on any computer, but an iOS IPA normally needs a Mac and Xcode. You can get around that by letting a cloud macOS machine do the build. This guide shows how to build and sign an IPA from Windows or Linux with GitHub Actions and Fastlane, then install it through TestFlight or an ad hoc link.
Key takeaways
- Building an iOS app needs Apple's tools, but a cloud macOS runner such as GitHub Actions can run them for you.
- Fastlane match with an App Store Connect API key is the easiest way to manage signing without a Mac.
- PhoneGap Build is gone, and Ionic Appflow is being retired, so choose a CI service that is still maintained.
- TestFlight is the simplest way to install test builds, and ad hoc links work for registered devices.
What is an IPA, and why does iOS normally need Xcode?
An IPA (iOS App Store Package) is the installable file for an iPhone or iPad app, the same way an APK is for Android. Creating one means compiling native code and signing it with a certificate issued by Apple. Both steps need Apple's software (Xcode and its command-line tools), which runs only on a Mac.
If you develop on Windows or Linux you have two options: own a Mac, or use a cloud-based one. This tutorial takes the second route, using a CI service that provides a macOS machine on demand.
We use Ionic and Capacitor in the examples, but the CI process applies to any project that has an iOS project directory.
Important: PhoneGap Build no longer exists
Older tutorials, including the previous version of this one, relied on PhoneGap Build, where you uploaded a zip of your www folder. Adobe announced the shutdown of that service in August 2020 and it closed on October 1, 2020.
The usual replacement, Ionic Appflow, is being retired as well. One third-party migration guide gives a shutdown date of December 31, 2027, so check Ionic's support documentation for the current date before you depend on it.
Your options for building an IPA without a Mac
| Option | Best for | Notes |
|---|---|---|
| GitHub Actions + Fastlane (this guide) | Developers who want control and low cost | Free minutes for public repos, paid minutes for private ones. macOS minutes cost more than Linux minutes. |
| Codemagic | Teams wanting a simpler UI | Cloud CI built for mobile apps, with iOS signing support built in. |
| Bitrise | Larger teams, many workflows | Mobile-focused CI/CD, usually the more expensive option. |
| Capawesome Cloud / Capgo | Capacitor apps with live updates | Capacitor-specific build and OTA services. |
| Expo EAS Build | React Native / Expo apps | Cloud builds for Expo projects. |
| Rented cloud Mac (remote desktop) | Occasional manual builds | You use a real macOS desktop and Xcode remotely. Simple, but billed by time. |
| Borrow or buy a Mac | Frequent, heavy iOS work | Still the smoothest option if you ship iOS apps regularly. |
Prerequisites
- Apple Developer Program membership. It is paid and renewed annually (check the fee on Apple's website). Without it you cannot sign installable apps.
- An App ID (bundle identifier), such as com.yourcompany.myapp. Create it in the Apple Developer Portal under Certificates, Identifiers & Profiles, then Identifiers.
- An app record in App Store Connect. You need it for TestFlight.
- An App Store Connect API key. Go to App Store Connect, then Users & Access, Integrations, App Store Connect API, and create a key. Download the .p8 file (Apple allows only one download) and save the Key ID and Issuer ID.
- A GitHub account and a private repository for your project.
- An iPhone or iPad for testing.
- Node.js and your Ionic/Capacitor app. Any operating system works.
Step 1: Prepare your Capacitor iOS project
In your project folder (Windows, Linux or macOS), run:
npm install @capacitor/core @capacitor/ios
npx cap add ios
npm run build
npx cap sync ios
This creates the native app project in the ios directory, usually ios/App. Commit the ios directory to Git. Set the bundle ID and app name in capacitor.config.ts and make sure the bundle ID matches the App ID you registered with Apple.
Whether you have an .xcworkspace file in ios/App depends on your Capacitor version and on whether you use CocoaPods or Swift Package Manager, so adjust the paths in the Fastfile to match.
Step 2: Set up code signing
Apple needs two things to sign an app: a certificate (who you are) and a provisioning profile (the app, the certificate and, for ad hoc builds, the devices). You can create both without a Mac in two ways.
Option A (recommended): Fastlane match with an API key
fastlane match generates your certificates and provisioning profiles, stores them in an encrypted private Git repository and talks to Apple using your App Store Connect API key. The CI machine fetches those files on every build, so nothing is created by hand.
- Create a separate private Git repository for the signing files, for example ios-certificates.
- Choose a strong passphrase to encrypt them (MATCH_PASSWORD).
- Let your first CI build generate the certificate and profiles (see the Fastfile below), or run fastlane match once from any computer with Ruby and Fastlane installed.
Option B: Create the certificate manually with OpenSSL
This is how earlier versions of this guide did it, and it works on Windows, Linux or macOS. You need OpenSSL 3.x. On Windows, install a current OpenSSL, or use Git Bash or WSL rather than an old 32-bit build.
1. Generate a private key and a certificate signing request (CSR):
openssl genrsa -out ios_distribution.key 2048
openssl req -new -key ios_distribution.key -out CertificateSigningRequest.certSigningRequest -subj '/emailAddress=you@example.com/CN=Your Name/C=IN'
2. Create the certificate in the Apple Developer Portal. Open Certificates, Identifiers & Profiles, then Certificates, and click the + button. Choose Apple Distribution for TestFlight, App Store and ad hoc builds, or Apple Development for development builds. Upload the .certSigningRequest file, then download the .cer file and name it ios_distribution.cer.
3. Create the .p12 file:
openssl x509 -in ios_distribution.cer -inform DER -out ios_distribution.pem -outform PEM
openssl pkcs12 -export -inkey ios_distribution.key -in ios_distribution.pem -out ios_distribution.p12
OpenSSL will ask for an export password. Type it at the prompt rather than putting it in the command, because command-line passwords are saved in shell history.
If a .p12 made with OpenSSL 3 fails to import on macOS or CI (for example with "MAC verify failure"), re-export it with the -legacy option or weaker encryption. See the OpenSSL documentation for the options available.
4. Create the provisioning profile. Under Profiles, click +. Choose Ad Hoc to install on registered devices, or App Store Connect for TestFlight and the App Store. Pick your App ID and certificate (and the devices, for ad hoc), then download the .mobileprovision file.
With Option B your CI job imports the .p12 and .mobileprovision files instead of calling match. Store both as encrypted CI secrets. Option A is easier to maintain, so the rest of this guide uses it.
Step 3: Add Fastlane to your project
In the root of your repository, create a Gemfile:
source 'https://rubygems.org'
gem 'fastlane'
Create fastlane/Matchfile:
git_url(ENV['MATCH_GIT_URL'])
storage_mode('git')
Create fastlane/Fastfile. Change the app identifier, workspace path and scheme to match your project:
default_platform(:ios)
platform :ios do
def asc_key
app_store_connect_api_key(
key_id: ENV['ASC_KEY_ID'],
issuer_id: ENV['ASC_ISSUER_ID'],
key_content: ENV['ASC_KEY_CONTENT'],
is_key_content_base64: true
)
end
desc 'Build a signed ad hoc IPA'
lane :adhoc do
api_key = asc_key
match(type: 'adhoc', app_identifier: ENV['APP_IDENTIFIER'], api_key: api_key, readonly: false)
build_app(
workspace: 'ios/App/App.xcworkspace',
scheme: 'App',
export_method: 'ad-hoc',
output_directory: 'build',
output_name: 'App.ipa'
)
end
desc 'Build and upload to TestFlight'
lane :beta do
api_key = asc_key
match(type: 'appstore', app_identifier: ENV['APP_IDENTIFIER'], api_key: api_key, readonly: false)
build_app(
workspace: 'ios/App/App.xcworkspace',
scheme: 'App',
export_method: 'app-store',
output_directory: 'build',
output_name: 'App.ipa'
)
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)
end
end
Two practical notes:
- Build numbers. TestFlight rejects a build number that was already used. Increase the build number before each upload. Fastlane has increment_build_number and latest_testflight_build_number actions for this.
- Signing style. If Xcode's "Automatically manage signing" is on in your project, the build can conflict with match. Fastlane's update_code_signing_settings action can switch the project to manual signing during CI.
Step 4: Create the GitHub Actions workflow
First add these as repository secrets (Settings, then Secrets and variables, then Actions):
| Secret | What it holds |
|---|---|
| ASC_KEY_ID | Key ID of your App Store Connect API key |
| ASC_ISSUER_ID | Issuer ID shown on the same page |
| ASC_KEY_CONTENT | The .p8 file contents, Base64-encoded |
| APP_IDENTIFIER | Your bundle ID, for example com.yourcompany.myapp |
| MATCH_GIT_URL | URL of your private certificates repository |
| MATCH_PASSWORD | The passphrase that encrypts the certificates |
| MATCH_GIT_BASIC_AUTHORIZATION | Base64 of username:personal-access-token, so CI can read the certificates repo |
Never commit .p8, .p12, .mobileprovision or key files to your repository.
Then create .github/workflows/ios.yml:
name: iOS build
on:
workflow_dispatch:
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- run: npx cap sync ios
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Build IPA
run: bundle exec fastlane adhoc
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
APP_IDENTIFIER: ${{ secrets.APP_IDENTIFIER }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
- uses: actions/upload-artifact@v4
with:
name: ios-ipa
path: build/*.ipa
To build for TestFlight instead, change fastlane adhoc to fastlane beta. Check that the Node version and the action versions match the latest GitHub releases.
To run it, open the Actions tab, select the iOS build workflow and click Run workflow. When the run finishes, download the ios-ipa artifact. That .ipa file is your signed app.
GitHub's runner images ship with specific Xcode versions that change over time. If TestFlight or the App Store rejects your build, check that the runner uses an Xcode version Apple currently accepts.
Step 5: Install and test the app
Option 1 (recommended): TestFlight
- Run the beta lane so the build uploads to App Store Connect.
- Wait for Apple to finish processing the build.
- In App Store Connect, open your app, then TestFlight, and add internal testers (members of your team).
- On the iPhone, install the TestFlight app, accept the invite and install your build.
TestFlight does not need device UDIDs, which makes it the simplest option for most teams. The first build for external testers goes through Apple's beta review.
Option 2: Ad hoc install with a link (Diawi or similar)
Ad hoc builds install only on devices whose UDIDs are registered in your Apple Developer account and included in the provisioning profile.
- Register each test device under Certificates, Identifiers & Profiles, then Devices. Without a Mac you can find a UDID with the Apple Devices app on Windows, or ask the tester to follow Apple's instructions.
- Rebuild the profile so it includes the new devices, then rebuild the IPA.
- Upload the .ipa to a distribution service such as Diawi or Firebase App Distribution.
- Open the generated link in Safari on the iPhone and tap Install. Every iOS browser uses Apple's WebKit engine, but the install prompt works reliably only from Safari.
Free link-sharing plans have limits such as link expiry and file size, so check the service's current terms. Treat uploaded IPA files as private, because anyone with the link can open it.
Development-signed builds on iOS 16 and later also need Developer Mode turned on (Settings, then Privacy & Security, then Developer Mode). Ad hoc and TestFlight builds do not normally need it.
Troubleshooting common errors
| Problem | Likely cause | Fix |
|---|---|---|
| No signing certificate found | Certificate not created or not imported | Run match with write access once, and check MATCH_GIT_URL, MATCH_PASSWORD and the repo access token |
| Provisioning profile doesn't include the device | New device not registered or profile not regenerated | Register the UDID, then re-run match so the profile is refreshed |
| No profile matching bundle ID | Bundle ID mismatch | Make capacitor.config, Xcode and APP_IDENTIFIER match the App ID exactly |
| MAC verification failed on a .p12 | OpenSSL 3 default encryption | Re-export the .p12 with the legacy option |
| Build number already exists in TestFlight | Duplicate build number | Increment the build number before each upload |
| Upload rejected for Xcode or SDK version | Runner image has an older Xcode | Use a newer runner image or select a newer Xcode in the workflow |
| Unable to install on the phone | Device not in the ad hoc profile, or wrong browser | Check the UDID is registered and open the link in Safari |
Need help shipping your iOS app?
Signing and App Store submission are easy to get wrong on the first attempt. Ucodice builds and releases hybrid and native mobile apps for startups and companies. We can set up your build environment, prepare App Store releases, or build the app from scratch.
Talk to our mobile app team | Explore our mobile app development services
Frequently asked questions
Can I develop an iOS app on Windows without a Mac?
Yes, by using a cloud-based Mac. Windows cannot run Apple's build tools, so your CI service (GitHub Actions here) runs them on a macOS machine for you. You write code on Windows or Linux and commit it to Git.
Is it free?
The tools are mostly free or low cost, but not entirely. The Apple Developer Program has an annual fee. GitHub Actions gives free minutes for public repositories and charges for private ones, and macOS minutes cost more than Linux minutes.
Do I need Xcode?
You do not install it yourself, but the cloud runner uses it. Apple's toolchain compiles and signs the iOS app.
Can I release to the App Store this way?
Yes. The beta lane uploads to App Store Connect, and from there you can submit the app for review.
Does this work for Flutter or React Native?
Yes, with changes. Replace the Capacitor and npm commands with your framework's build command (flutter build ios --no-codesign for Flutter). The signing and Fastlane steps stay the same. Expo apps can use EAS Build.
Is PhoneGap Build still available?
No. Adobe shut down PhoneGap Build on October 1, 2020.
Is Ionic Appflow still a good alternative?
Ionic has said Appflow is being retired, with one source giving a shutdown date of December 31, 2027. Check Ionic's documentation for the current date. For the long term, consider GitHub Actions, Codemagic, Bitrise or a Capacitor-specific service.



