Distributing Your Application

Package, sign, and share your Tauri v2 app with users on Windows, macOS, and Linux using installers, portable builds, website hosting, and GitHub Releases.

Distributing a Tauri application means turning the code you built into something a non‑developer can install or run on their own machine. That involves picking the right package format, signing the binaries so the operating system trusts them, and putting the final files where users can download them.

This section covers the entire flow — from a successful tauri build to a download link that works on Windows, macOS, and Linux. It assumes you already have a Tauri v2 project with a React + Vite frontend and that you can run the build command. If that command still fails, revisit Building a Tauri Application before continuing here. Signing is covered in Code Signing.

What You Actually Ship

After a build, Tauri produces one or more platform‑specific artifacts inside src-tauri/target/release/bundle/. The exact files depend on the operating system you build on and the bundle formats enabled in your configuration.

PlatformTypical bundle formatsFile extension(s)
WindowsNSIS installer, MSI, WiX.exe (installer), .msi
macOSDMG, app bundle.dmg, .app (directory)
LinuxAppImage, Deb, RPM.AppImage, .deb, .rpm

Those files are what you hand to users. A .app bundle on macOS is self‑contained, but most users expect a .dmg. On Linux, AppImage is the easiest single‑file format because it bundles the required WebKitGTK libraries. A plain binary without a package is not feasible for Tauri on Linux — the app needs WebKitGTK at runtime, and packaging it into an AppImage or Flatpak keeps things portable.

Setting the Version Before You Distribute

Every distribution must carry a version number. Tauri resolves it from the version field in tauri.conf.json. If that field is absent, it falls back to the package.version in src-tauri/Cargo.toml.

src-tauri/tauri.conf.json
{
  "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
  "productName": "MyApp",
  "version": "1.2.0",
  "identifier": "com.example.myapp",
  "build": {
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build",
    "devUrl": "http://localhost:1420",
    "frontendDist": "../dist"
  },
  "app": {
    "withGlobalTauri": true,
    "windows": [],
    "security": { "csp": null }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/icon.ico",
      "icons/icon.icns"
    ]
  }
}

Version mismatch across config files:

If you set a version in tauri.conf.json, that version overrides the one in Cargo.toml — but your Rust backend and frontend can still read the Cargo.toml value independently. Always keep them aligned to avoid confusion during debugging. A quick manual check before a release prevents the classic “we tagged 2.0.0 but the app still says 1.0.0” scenario.

Sharing Installers

The most common distribution method is to provide an installer file that users download and run. The build command already produces those installers when the bundle configuration is active. You control which formats get built with the bundle section in tauri.conf.json, and you can even pass command‑line flags to override the config.

For Windows, the two main formats are the NSIS installer and MSI. NSIS is the default and produces a .exe installer with a familiar wizard. MSI is preferred in enterprise environments that use Group Policy deployment.

Run the build on a Windows machine:

npm run tauri build

The output lands in src-tauri/target/release/bundle/. You’ll find both an .exe and, if enabled, an .msi in the msi/ subfolder.

To generate only a specific format (e.g., MSI), you can split build and bundle:

npm run tauri build -- --no-bundle
npm run tauri bundle -- --bundles msi

Windows SmartScreen warnings:

An unsigned installer triggers a SmartScreen warning that says “Windows protected your PC.” Most users won’t click past it. You need an Authenticode certificate to sign the installer and remove that warning. See the code signing chapter for the full procedure.

Portable Applications

Sometimes an installer is overkill — you just want a self‑contained executable that users can copy onto a USB stick or drop into a folder.

  • Windows: The NSIS bundler supports a portable mode. Set bundle > nsis > installMode to "portable" in tauri.conf.json. The build output is a single .exe that writes no registry entries and creates no Start Menu shortcuts. This works well for tools, utilities, and beta releases.
  • macOS: The .app bundle is inherently portable. You can compress it into a zip and share it. Just remember that notarization is still required for users to open it without a security override.
  • Linux: The AppImage format is the closest equivalent. It bundles everything the app needs, including a compatible WebKitGTK. A truly standalone binary without any bundled libraries is not realistic for Tauri on Linux because the system WebKitGTK varies too much across distributions.

Portable mode and updates:

A portable app usually lacks an integrated auto‑update mechanism because it has no consistent install location. If you plan to ship a portable version, consider including a manual “check for updates” button that points to your website or GitHub Releases page.

Hosting Installers on Your Own Website

Once you have the signed installer files, you can put them behind a download button on your product site. The server just needs to serve the files with the correct MIME types. For common bundles:

  • .exe: application/vnd.microsoft.portable-executable
  • .msi: application/x-ole-storage
  • .dmg: application/x-apple-diskimage
  • .AppImage: application/octet-stream
  • .deb: application/vnd.debian.binary-package
  • .rpm: application/x-rpm

Static hosting on services like Netlify, Vercel, or S3 with CloudFront works fine. Pay attention to two details that get missed surprisingly often:

  1. Set proper Cache-Control headers so that when you replace an installer with a new version, returning users don't get a stale cached file.
  2. Avoid renaming the file between releases if you plan to use auto‑updates later. The update plugin relies on a consistent filename or a manifest that maps versions to URLs.

If your app uses the Tauri updater plugin, you also need to upload a JSON manifest alongside the binaries. That manifest describes the available version, the download URL for each platform, and the signature that proves the update came from you. The updater chapter covers this in detail, but the distribution point is that the manifest must live at a public URL your app can reach.

Distributing via GitHub Releases

GitHub Releases is the most common distribution pipeline for open‑source Tauri apps. Each release can hold binaries for all three platforms, and you get a permanent URL that the updater plugin can query.

Creating a Release Manually

1

Build the application on each target platform

Run the build on Windows, macOS, and Linux machines — or use a CI runner. The artifacts land in src-tauri/target/release/bundle/.

2

Sign the binaries

On macOS: notarize the DMG and app bundle. On Windows: sign the installer with an Authenticode certificate. On Linux: no mandatory signature, but you should still sign your AppImage with the Tauri updater private key if you plan to ship updates.



Generate a signing key specifically for the updater:

npx @tauri-apps/cli signer generate -w ~/myapp-updater.key

This key pair is separate from the OS code‑signing certificate. The private key signs update bundles; the public key lives in tauri.conf.json.

3

Create a new GitHub Release

Go to your repository’s Releases page and click “Draft a new release”. Tag it with the version number (e.g., v1.2.0). Fill in the release notes.

4

Upload the installer files

Attach the built bundles: the Windows installer, the macOS DMG, the AppImage, etc. The release page handles serving them with the right Content‑Type.

5

Upload the update manifest (if using auto‑update)

Drop the JSON update file into the release assets as well. The updater plugin will fetch the release metadata from GitHub’s API and compare versions.

Automating the Pipeline with GitHub Actions

Manual steps work for one‑off releases, but they don’t scale. The tauri-action GitHub Action builds, signs, and publishes your app directly to a GitHub Release — triggered by pushing a version tag.

Below is a production‑ready workflow that builds for Windows (x86_64), macOS (both Apple Silicon and Intel), and Linux. It expects code‑signing secrets for macOS and Windows, plus the Tauri updater private key.

.github/workflows/release.yml
name: Release
on:
  push:
    tags:
      - "v*"
concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: true
jobs:
  release:
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: ubuntu-22.04
            args: ""
            rust_target: x86_64-unknown-linux-gnu
          - platform: macos-latest
            args: "--target aarch64-apple-darwin"
            rust_target: aarch64-apple-darwin
          - platform: macos-latest
            args: "--target x86_64-apple-darwin"
            rust_target: x86_64-apple-darwin
          - platform: windows-latest
            args: ""
            rust_target: x86_64-pc-windows-msvc
    runs-on: ${{ matrix.platform }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "lts/*"
          cache: "npm"
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.rust_target }}
      - name: Rust cache
        uses: swatinem/rust-cache@v2
        with:
          workspaces: src-tauri
      - name: Install frontend dependencies
        run: npm ci
      - name: Install Linux system deps
        if: runner.os == 'Linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y libwebkit2gtk-4.1-dev
      - name: Import Apple signing certificate
        if: runner.os == 'macOS'
        env:
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
        run: |
          CERT_PATH=$RUNNER_TEMP/certificate.p12
          KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain-db
          KEYCHAIN_PASSWORD=$(openssl rand -base64 24)
          echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
          security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security default-keychain -s "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
          security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security list-keychains -d user -s "$KEYCHAIN_PATH"
      - name: Build and release
        uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
        with:
          tagName: v__VERSION__
          releaseName: "MyApp v__VERSION__"
          releaseBody: "See [CHANGELOG](https://github.com/your-org/your-repo/blob/main/CHANGELOG.md) for details."
          releaseDraft: true
          prerelease: false
          args: ${{ matrix.args }}

This workflow does several things that would be tedious and error‑prone to do by hand. It creates a temporary keychain on the macOS runner so that codesign can access the Apple signing certificate without a password prompt (the partition list step is what prevents a hanging GUI dialog). It caches Rust compilation so that subsequent runs complete in minutes instead of a quarter hour. It builds separate .dmg files for Intel and Apple Silicon Macs instead of a universal binary, which keeps downloads smaller and lets users grab only the one they need.

The build results appear as a draft GitHub Release, so you can review the assets and release notes before making it public. After publishing, anyone can download the installers from the repository’s Releases page.

Your pipeline is production‑ready when:

All four matrix jobs complete without errors and the draft release shows a full set of installer files. On the next push of a version tag (e.g., v1.3.0), the workflow runs again and the old concurrency group is cancelled if you push a fix quickly. This means you can tag, review, and publish entirely from GitHub without touching your local machine.

Common Release Pipeline Mistakes

  • Using npm install instead of npm ci in CI. npm ci respects the lockfile exactly and fails if there are mismatches — that’s what you want for reproducible builds.
  • Forgetting to add WebKitGTK dependencies on the Linux runner. Without libwebkit2gtk-4.1-dev, the Rust compilation fails with linker errors about missing libraries. The workflow above includes that step only for the Linux matrix entry.
  • Storing the Tauri updater private key in plain text inside the repository. That key is as sensitive as a code‑signing certificate. Always inject it as a GitHub secret and never echo it in logs.

Summary

This chapter set up the machinery that places your app in front of users. With signed installers, a release pipeline, and a download page, you have the foundation.