Build a Desktop App with Wails #6: Build and Distribution — Packaging per Platform
The app is finished, so one job remains: turning it into something that runs on other people’s computers. This post covers the structure of what wails build produces, packaging per platform, and the constraint you will run into most often when distributing Wails apps — cross-compilation — together with its solution, a CI matrix build. This is the final part of the series.
It runs in eight parts (six main plus two deep-dives).
- #1 What Wails is — lightweight desktop apps in Go
- #2 Project structure and the dev loop — wails dev and bindings
- #3 Connecting Go and the frontend — method bindings and events
- #4 System integration — dialogs, menus, and window control
- #5 Real-world features — persisting settings and error handling
- #6 Build and distribution — packaging per platform ← this post
wails build — a single binary #
A production build is one command.
wails build
# output: under build/bin/During this step the frontend is built in production mode and the result is embedded inside the Go binary. HTML, JS, and CSS all live in the one executable, so there is no asset folder to ship alongside it. The hot-reload server of wails dev is development-only; the built app runs on its embedded assets alone.
What each platform produces #
The shape of the output depends on the OS you build on.
| Platform | Default output | Distribution form |
|---|---|---|
| Windows | appname.exe | Ship as-is, or an NSIS installer |
| macOS | appname.app bundle | Wrap in a dmg or ship as a zip |
| Linux | executable binary | Ship as-is; distro packaging is separate |
Windows can be distributed as a single exe, but to get Start Menu registration and uninstall support you build an installer. Wails has built-in support for generating an NSIS installer.
wails build -nsisThe macOS output is a .app bundle. The practical hurdle here is code signing and notarization. An unsigned app triggers the Gatekeeper warning, and only a notarized app runs without warnings for users who downloaded it. Knowing that this requires an Apple Developer Program account (99 dollars a year) plus the codesign and notarytool steps is as far as this series goes — for personal distribution, many people start with a zip plus a right-click-to-open note.
Linux gives you a plain binary, which needs the WebKitGTK libraries present on the system to run. Turning it into distro packages (deb, AppImage, and so on) is the territory of separate tools.
Icons and app metadata #
The project’s build/ directory is where packaging resources live. Replace build/appicon.png and the build converts it into each platform’s format (icns, ico); adjust platform metadata through build/darwin/Info.plist and the files under build/windows/. The app name and output file name are managed in wails.json at the project root.
{
"name": "wails-todo",
"outputfilename": "wails-todo",
"frontend:install": "npm install",
"frontend:build": "npm run build"
}The cross-compilation constraint and CI matrix builds #
Go is famously free with cross-compilation, but a Wails app must in principle be built on the target OS, because it depends on each platform’s WebView and native code. A few workarounds exist for building Windows output from macOS, but the official recommendation is per-platform builds.
You do not need to own all three OSes. A GitHub Actions matrix build compiles on all three platform runners for you.
jobs:
build:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
- run: wails build
- uses: actions/upload-artifact@v4
with:
name: app-${{ matrix.os }}
path: build/bin/The Linux runner needs an extra step that installs the WebKitGTK development packages. Extend this so a pushed tag attaches all three platforms’ outputs to a release, and you can sustain three-platform distribution single-handedly.
A sense of size and performance #
This is where the reason for choosing Wails shows up in numbers. For a project the size of our todo app, the outputs land roughly in these ranges.
- Wails — a binary in the single-digit MB range, frontend assets included. It borrows the OS WebView at runtime, so it carries no browser engine.
- Electron — the same app starts in the hundreds of MB because it bundles Chromium and Node.js.
Memory usage is lighter for the same structural reason. Using the OS WebView also means the rendering engine varies with the OS version, so treat very new CSS features with care and check the WebView version of your target OSes first.
Series recap #
One line for each of the six parts.
- #1 Wails ties a Go backend and a web frontend together over the OS WebView — a desktop framework lighter than Electron.
- #2 wails dev provides the hot-reload development loop, and the project splits into the Go root and the frontend directory.
- #3 Go methods become frontend functions through bindings, and notifications in the other direction flow as events.
- #4 OS integration — dialogs, menus, window control, the clipboard — is the job of the runtime package.
- #5 Data is stored under UserConfigDir, and a Go error becomes a rejected Promise in the frontend.
- #6 Builds produce a single binary, and the per-platform build constraint is solved with a CI matrix.
Closing — and the deep-dives #
Building the screen with web technology and handling the system with Go is one of the gentlest paths into desktop apps for anyone with web development experience. If you followed along this far, you have completed a full cycle of turning an idea into an executable and handing it over.
The main series ends here, and two deep-dive posts follow: #7 Adding a frontend framework, which swaps the vanilla front half for a framework, and #8 Debugging, which finds the cause when things break. Building a single app all the way to shipping and polish is covered in the Wails in Practice course.