Packaging & Distribution
How to package a Linux desktop app: Flatpak, deb, RPM, Snap, and AppImage compared — with the files, commands, and release practices for installation and updates.
On Linux, packaging is not a final step. It is part of the architecture. A platform-native build and a cross-target build solve different problems, and the same source tree should not be forced into every channel.
You do not have to ship every format. You do have to choose deliberately, because each one changes how the app is built, sandboxed, updated, and supported.
Two packaging decisions
Section titled “Two packaging decisions”Packaging answers two different questions:
| Decision | Question | Examples |
|---|---|---|
| Platform-native | What does this target’s platform support and expect? | Ubuntu: Snap, deb. Fedora: Flatpak, RPM. Debian: deb, Flatpak. Arch: Flatpak, pacman. |
| Cross-target | What single artifact can carry the app across the reference targets? | Flatpak/Flathub. |
Choose the scope before the channel. A platform-native default is not a universal default; a cross-target channel is not the Ubuntu native default.
Choose the channel
Section titled “Choose the channel”Ubuntu documents two supported package formats: deb and Snap. Flatpak is not installed by default and is not Canonical’s supported path, but it is a mature, open-source, cross-distribution channel. Start with the scope: one target or several.
Ubuntu-supported paths
Section titled “Ubuntu-supported paths”| Channel | Best for | Updates | Sandbox | Effort |
|---|---|---|---|---|
| deb / PPA | Ubuntu/Debian users, archive integration | apt, unattended-upgrades | No (unless hardened) | Medium–high |
| Snap | Ubuntu App Center, confined apps | snapd, automatic refresh | Yes, AppArmor | Medium |
| Distro archive | System components, long-lived packages | Distro updates | Varies | High |
Ubuntu’s App Center recommends snaps for third-party apps because they are sandboxed and update independently of the release. It also manages debs from the Ubuntu archive and can install third-party debs, but third-party debs are discouraged: they have full access to the user’s resources and may add their own repositories.
Cross-distro and alternative paths
Section titled “Cross-distro and alternative paths”| Channel | Best for | Updates | Sandbox | Effort |
|---|---|---|---|---|
| RPM | Fedora/RHEL/openSUSE users, archive integration | dnf, dnf-automatic | No (unless hardened) | High |
| Flatpak / Flathub | Upstream desktop apps, cross-distro; not shipped by Ubuntu | OSTree deltas | Yes, portals | Medium |
| AppImage | Portable demos, closed beta, legacy | Manual or app-specific | No | Low |
| Source tarball / self-hosted repo | Developers, packagers, special audiences | User/distro or self-hosted | Varies | Low–high |
A pragmatic release matrix:
- If you support one target, start with its native path. A deb in a PPA or the archive for apt integration; a Snap for the App Center and confined auto-updates; an RPM for Fedora archive integration; Flatpak/Flathub on Fedora.
- If you support both reference targets, use Flatpak/Flathub as the common upstream build. It gives you one artifact and one store across Ubuntu and Fedora. Document the extra setup: Ubuntu does not ship Flatpak or Flathub by default.
- Add Snap or RPM only when the audience or a required integration justifies the work. Do not add a native channel just to mirror the cross-target build.
- Add AppImage only for portable demos or users who cannot install packages.
- Keep a source tarball for packagers and developers.
- Do not ship every channel by default. Every channel is a maintenance, testing, and support commitment.
Flatpak
Section titled “Flatpak”Flatpak is covered in depth in Flatpak & Portals. It is the cross-target default when one app supports both reference targets. It is not the Ubuntu native default: Snap and deb are the target-native paths there, and Ubuntu does not install Flatpak or Flathub by default. If you choose this channel, set it up explicitly:
sudo apt install flatpak flatpak-builderflatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepoThe App Center does not manage Flatpak apps. Users who want Flatpak install it themselves or use a distribution that ships it.
For local development, type: dir sources are fine. For a Flathub submission, replace them with a git source pinned to a tag/commit or an archive source with a checksum; Flathub does not accept the app source code inside the submission PR.
The packaging-specific workflow is:
flatpak-builder --user --install --force-clean build-dir io.github.yourname.Notes.ymlflatpak-builder --repo=/tmp/notes-repo --force-clean build-dir io.github.yourname.Notes.ymlflatpak build-update-repo /tmp/notes-repoFor a self-hosted repository, publish the OSTree repo and a .flatpakref:
[Flatpak Ref]Title=NotesName=io.github.yourname.NotesBranch=stableUrl=https://example.org/flatpak/repoGPGKey=...IsRuntime=falseFor public distribution, use Flathub instead of running your own repository. It gives you a store listing, delta updates, verification, and a build service.
Flatpak runtimes have their own support windows. Track the runtime-version branch, rebase before the runtime reaches end-of-life, run flatpak update, and remove unused runtimes with flatpak uninstall --unused. Do not assume that an Ubuntu LTS support window applies to a GNOME or KDE runtime.
deb packages
Section titled “deb packages”A Debian package is a directory tree plus metadata. The Ubuntu packaging guide and the new-package guide cover the archive process; the example below is the minimal local layout.
The minimum source package layout is:
notes/├── debian/│ ├── changelog│ ├── control│ ├── copyright│ ├── install│ └── rules├── meson.build└── src/debian/control
Section titled “debian/control”Source: notesSection: utilsPriority: optionalMaintainer: Your Name <you@example.com>Build-Depends: debhelper-compat (= 13), meson, ninja-build, python3, libgtk-4-dev, libadwaita-1-devStandards-Version: 4.7.4.0Rules-Requires-Root: noHomepage: https://example.org/notes
Package: notesArchitecture: allDepends: ${misc:Depends}, python3, python3-gi, gir1.2-gtk-4.0, gir1.2-adw-1Description: small note-taking app for the Linux desktop Notes is a local-first note-taking application with a native GTK 4 and libadwaita interface.Use Architecture: any if you ship compiled code. For a Python app, all is correct, but the dependencies must be available in every supported release. If they are not, build a Flatpak instead of bundling a private Python runtime in /opt.
debian/rules
Section titled “debian/rules”#!/usr/bin/make -f
%: dh $@Make it executable: chmod +x debian/rules.
Because this is an Architecture: all Python package, the build produces notes_0.1.0-1_all.changes. Use the matching file for lintian, or use ../*.changes so the command works for either architecture.
debian/changelog
Section titled “debian/changelog”notes (0.1.0-1) resolute; urgency=medium
* Initial release.
-- Your Name <you@example.com> Tue, 10 Nov 2026 12:00:00 +0100The distribution (resolute for Ubuntu 26.04) and the version must match the target archive.
Build and validate
Section titled “Build and validate”sudo apt install debhelper devscripts lintiandpkg-buildpackage -b -us -uclintian ../notes_0.1.0-1_all.changesFor a clean build that matches the archive, use sbuild or pbuilder rather than your desktop environment. A package that only builds on your machine is not a package.
debuild -S -sadput ppa:yourname/notes ../notes_0.1.0-1_source.changesA PPA is a good stepping stone. It is not a substitute for the Ubuntu archive, and users still need to add it manually. Follow Build packages in a PPA for the upload and signing flow.
What goes in the package
Section titled “What goes in the package”| Asset | Path |
|---|---|
| Binary/script | /usr/bin/notes |
| Desktop entry | /usr/share/applications/io.github.yourname.Notes.desktop |
| Metainfo | /usr/share/metainfo/io.github.yourname.Notes.metainfo.xml |
| Icon | /usr/share/icons/hicolor/scalable/apps/io.github.yourname.Notes.svg |
| GSettings schema | /usr/share/glib-2.0/schemas/ |
| Translations | /usr/share/locale/ |
| License | /usr/share/doc/notes/copyright |
Do not install into /opt unless the app is genuinely self-contained and you understand the trade-offs with themes, icons, and desktop integration.
RPM packages
Section titled “RPM packages”RPM is the native package format on Fedora, RHEL, and openSUSE. On the Fedora KDE target it is the archive-native secondary channel: Flatpak/Flathub is the target default because it carries the current Qt/KDE runtime, while RPM is for apps that need archive integration, a system service, or a Fedora-only audience. State the support scope; do not present both as equal defaults.
The Fedora Packaging Guidelines are the contract. A spec file describes the source, build, install, files, and changelog; it is not a shell script. The skeleton below is illustrative — replace the URL and app ID, and build it in mock before claiming Fedora support:
Name: notesVersion: 0.1.0Release: 1%{?dist}Summary: Small note-taking app for the Linux desktopLicense: MITURL: https://example.org/notesSource0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gzBuildRequires: cmakeBuildRequires: ninja-buildBuildRequires: extra-cmake-modulesBuildRequires: qt6-qtbase-develBuildRequires: qt6-qtdeclarative-develBuildRequires: kf6-kirigami-develBuildRequires: kf6-ki18n-develBuildRequires: kf6-kconfig-develBuildRequires: kf6-kcoreaddons-develBuildRequires: kf6-kdbusaddons-develBuildRequires: desktop-file-utilsBuildRequires: appstreamRequires: hicolor-icon-theme
%descriptionNotes is a local-first note-taking application with a native KDE interface.
%prep%autosetup
%build%cmake -G Ninja%cmake_build
%install%cmake_install
%checkdesktop-file-validate %{buildroot}%{_datadir}/applications/io.github.yourname.Notes.desktopappstreamcli validate --no-net %{buildroot}%{_datadir}/metainfo/io.github.yourname.Notes.metainfo.xml
%files%license LICENSE%{_bindir}/notes%{_datadir}/applications/io.github.yourname.Notes.desktop%{_datadir}/metainfo/io.github.yourname.Notes.metainfo.xml%{_datadir}/icons/hicolor/scalable/apps/io.github.yourname.Notes.svg
%changelog* Wed Nov 11 2026 Your Name <you@example.org> - 0.1.0-1- Initial packageThe %license LICENSE line requires a license file in the source tree; add one that matches the metainfo’s project_license. Every find_package component in the Qt example needs a matching BuildRequires; check the list when you add KIO, KCrash, or Notifications.
The commands separate a local build from a clean build:
sudo dnf5 install rpmdevtools mock rpmlint fedpkgrpmdev-setuptreecp notes.spec ~/rpmbuild/SPECS/spectool -g -R ~/rpmbuild/SPECS/notes.specrpmbuild -ba ~/rpmbuild/SPECS/notes.specmock -r fedora-45-x86_64 --rebuild ~/rpmbuild/SRPMS/notes-0.1.0-1.fc45.src.rpmrpmlint ~/rpmbuild/SPECS/notes.spec ~/rpmbuild/RPMS/x86_64/notes-0.1.0-1.fc45.x86_64.rpmrpmbuild proves the spec parses on your machine; mock builds in a clean Fedora chroot and is the check that matters. rpmlint catches metadata and file-layout problems. For a package destined for Fedora, use fedpkg lint in the dist-git checkout and follow the review process. For a package that is not in Fedora, COPR provides a build service and repository; it is not a substitute for Fedora review.
Do not install into /opt and do not bundle a private Qt runtime. Use the distribution’s Qt/KF6 packages for an RPM, and the Flatpak runtime for a Flatpak; mixing the two produces an app that is neither reproducible nor supportable.
A Snap is a squashfs image with an AppArmor confinement profile and a snapcraft.yaml recipe. core26 is the recommended base for Ubuntu 26.04.
name: notesbase: core26version: '0.1.0'summary: Small note-taking app for the Linux desktopdescription: | Notes is a local-first note-taking application with a native GTK 4 and libadwaita interface.grade: stableconfinement: strict
apps: notes: command: usr/bin/notes extensions: [gnome] # Add `plugs: [gsettings]` only if the app reads or writes host GSettings.
parts: python-runtime: plugin: nil stage-packages: - libpython3.14-minimal - libpython3.14-stdlib - python3-minimal - python3.14-minimal - python3.14-venv
notes: after: [python-runtime] plugin: meson source: . meson-parameters: - --prefix=/usr build-packages: - python3 - python3-gi - gir1.2-gtk-4.0 - gir1.2-adw-1 stage-packages: - python3-gi - gir1.2-gtk-4.0 - gir1.2-adw-1snapcraft packsudo snap install --dangerous notes_0.1.0_amd64.snapsnap run notessnapcraft lintcommandis relative to the snap root. With--prefix=/usr, the binary isusr/bin/notes, notbin/notes.extensions: [gnome]pulls in the GNOME content snaps and desktop helpers.- Keep
plugsminimal. Prefer portals and specific interfaces;home,network, and the legacyunity7interface are broad and should not be defaults. confinement: strictis the right default.classicis legitimate for tools that genuinely need broad host access, but it is reviewed manually and should not be the first choice.- The Snap Store review checks metadata, confinement, and policy. Expect iteration.
Snap’s automatic refresh is a feature, not a bug. Do not add a self-updater; test the app across a refresh, including with unsaved work.
For the full build and review flow, use the Snapcraft documentation and the Craft a snap tutorial. Read the current interfaces reference and XDG desktop portals guidance. For automation, use the snapd REST API rather than shelling out to snap.
OCI images with Rockcraft
Section titled “OCI images with Rockcraft”If the deliverable is a service or cloud workload rather than a desktop app, build it as an OCI image with Rockcraft. Rocks are minimal, Ubuntu-based images with a supported build path. Do not use them as a desktop packaging format.
AppImage
Section titled “AppImage”AppImage is a single executable file that mounts a squashfs image. It is useful for demos, portable tools, and users who cannot install packages, but it has no sandbox, no repository, and no standard update mechanism.
# Example with linuxdeploy and the AppImage pluginlinuxdeploy --appdir AppDir --executable build/notes \ --desktop-file data/io.github.yourname.Notes.desktop \ --icon-file data/io.github.yourname.Notes.svglinuxdeploy-plugin-appimage --appdir AppDirIf you ship an AppImage:
- Include an
AppRunthat setsLD_LIBRARY_PATHcorrectly. - Provide a
.desktopfile and icon inside the image. - Publish a checksum and a GPG signature.
- Document that updates are manual.
- Do not treat it as your only distribution channel.
Versioning and release notes
Section titled “Versioning and release notes”Use semantic versioning for the app (MAJOR.MINOR.PATCH) and a distribution-specific revision suffix (0.1.0-1) for deb packages. The AppStream <releases> section should match the version users see.
<releases> <release version="0.1.0" date="2026-11-10"> <description> <p>First public release.</p> <ul> <li>Local-first notes with adaptive sidebar.</li> <li>Keyboard-first navigation and search.</li> </ul> </description> </release></releases>Release notes are user-facing. Write them for users, not for the commit log. Mention breaking changes, migration steps, and known issues.
CI: build every channel from one tag
Section titled “CI: build every channel from one tag”A release pipeline should produce:
- a Flatpak build that passes
flatpak-builder-lint; - a deb source and binary package that passes
lintian; - an RPM built in
mockon Fedora 45 that passesrpmlint; - a Snap that installs and launches on a clean VM;
- an AppImage (optional) with a checksum;
- a source tarball with a signed tag.
name: releaseon: push: tags: ['v*']jobs: flatpak: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: bundle: notes.flatpak manifest-path: io.github.yourname.Notes.yml cache-key: flatpak-builder-${{ hashFiles('**/*.yml', '**/*.json', '**/Cargo.lock', '**/Cargo.toml') }} deb: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 - run: sudo apt-get update - run: sudo apt-get install -y devscripts equivs lintian - run: sudo mk-build-deps -i -y debian/control - run: dpkg-buildpackage -b -us -uc - run: lintian ../*.changes snap: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 - uses: snapcore/action-build@v1 id: build - run: sudo snap install --dangerous ${{ steps.build.outputs.snap }} rpm: runs-on: ubuntu-26.04 container: fedora:45 steps: - uses: actions/checkout@v4 - run: dnf5 install -y rpmdevtools rpmlint - run: rpmdev-setuptree - run: cp notes.spec ~/rpmbuild/SPECS/ - run: spectool -g -R ~/rpmbuild/SPECS/notes.spec - run: rpmbuild -ba ~/rpmbuild/SPECS/notes.spec - run: rpmlint ~/rpmbuild/SPECS/notes.spec ~/rpmbuild/RPMS/*/*.rpmmock is the stronger clean-chroot check. Run it on a Fedora 45 host or VM; the container job is a fast spec/build check, not a substitute for a clean chroot.
Pin the runner image and the runtime. A release that depends on “whatever was latest” is not reproducible.
Updates
Section titled “Updates”| Channel | Update mechanism | What the app should do |
|---|---|---|
| Flatpak | OSTree deltas via Flathub; runtime branch has its own EOL | Rebase before runtime EOL; do not self-update |
| Snap | snapd automatic refresh | Nothing; handle restart gracefully |
| deb | apt / unattended-upgrades | Nothing; do not self-update |
| RPM | dnf / dnf-automatic | Nothing; do not self-update |
| AppImage | Manual | Check for updates only with consent; link to download |
Never replace your own binary from inside the app. It breaks package ownership, signatures, and sandboxing. If the app needs a newer version of itself, tell the user and link to the store.
Packaging checklist
Section titled “Packaging checklist”- ☐ One application ID across desktop file, metainfo, icon, D-Bus, and Flatpak.
- ☐ The app builds from a clean checkout with no network access.
- ☐ The desktop file passes
desktop-file-validate. - ☐ The metainfo passes
appstreamcli validate --no-net. - ☐ The Flatpak passes
flatpak-builder-lint. - ☐ The deb passes
lintian. - ☐ The RPM builds in
mockon Fedora 45 and passesrpmlint. - ☐ The Snap uses
core26, installs and launches on a clean VM withstrictconfinement, and passessnapcraft lint. - ☐ The app launches with no home directory access (Flatpak) and degrades gracefully.
- ☐ The release states whether it is platform-native or cross-target, and does not present a cross-target channel as the target-native default.
- ☐ The channel choice passes the quality signals check; Flatpak setup is documented separately from stock Ubuntu.
- ☐ The upgrade path from the previous version is tested.
- ☐ Release notes and screenshots are current.
- ☐ The license and copyright are installed.
- ☐ The uninstall path removes app data only with consent.
Sources
Section titled “Sources”- Snapcraft bases
- Core24 to core26
- Fedora Packaging Guidelines
- RPM Packaging Guide
- Fedora Developer: RPM packaging
- COPR user documentation
- GNOME extension
- Flathub requirements
- Debian Policy
- AppImage documentation
- snapd REST API
Next: Security & Privacy — the threat model behind the sandbox, secrets, and permissions.