Snap
Packaging — snapcraft.yaml in detail
Every snap is built from a single recipe file, snap/snapcraft.yaml,
that the snapcraft tool reads to fetch sources, build them,
and assemble the result into the .snap squashfs image
described in Fundamentals.
This page goes through that file field by field.
- What it is
- The single YAML recipe (
snapcraft.yaml) that defines every snap's metadata, apps, hooks, and build steps. - Mental model
- Part manifest, part build script — identity fields say what it is,
parts:says how to build it,apps:says what runs. - Key numbers
- 4-step build lifecycle per part: pull → build → stage → prime.
- Reach for it when
- You're packaging any application as a snap, from a single binary to a multi-part build.
Identity: name, version, title, summary, description
- name
- The snap's globally unique identifier on the store — lowercase letters, digits, and hyphens only, can't start or end with a hyphen, max 40 characters. This is what
snap install <name>uses, and it has to be registered (snapcraft register) before you can push a build to the store under it. - version
- A short version string, max 32 characters. Can be hardcoded, or computed at build time from a part (via
adopt-infopointing at a part that sets it withcraftctl set version=...) — common in CI so the snap version tracksgit describeautomatically. - title
- A human-friendly display name for store listings, separate from the machine-oriented
name. - summary
- One line, 78 characters max — shown in search results and
snap info. - description
- The full multi-line description shown on the snap's store page.
Grade
grade: stable or grade: devel. This is a
publishing gate, not a runtime setting: a snap built with
devel grade can be pushed to any channel except
stable, which is exactly the point — it stops an
in-progress build from accidentally landing in front of users who
track the stable channel. Flip it to stable once the
build is actually meant to be released there.
Confinement and base
confinement: strict | classic | devmode — the same three
levels covered in Fundamentals,
declared here at build time rather than chosen by the installer.
base: names the base snap (e.g. core22,
core24) that provides the minimal root filesystem and
core libraries every strictly-confined app in this snap runs against —
see Snap types & Ubuntu Core
for what a base snap actually is under the hood.
Apps: command, daemon, plugs, environment
The apps: map defines every executable the snap exposes,
keyed by app name (exposed as snapname.appname, or bare
snapname if the app name matches the snap name).
- command
- The executable or script actually run, as a path relative to the snap's own root (e.g.
bin/my-tool) — the field that answers "what runs when this app is invoked." - command-chain
- A list of extra executables run before
command, in order — the usual place for wrapper scripts that set up environment variables or paths before handing off to the real binary. - daemon
simple | forking | oneshot | notify | dbus— declares the app as a background service managed by systemd instead of something a user runs directly. Paired withdaemon-scope(system/user),restart-condition,install-mode(whether it starts automatically after install).- plugs
- Which interfaces (see below) this specific app needs — narrower than declaring them snap-wide when only one app actually needs, say,
camera. - environment
- Key/value environment variables set for this app's process.
- passthrough
- An escape hatch: fields written here are copied verbatim into the built
meta/snap.yamleven ifsnapcraftdoesn't have first-class support for them yet.
Hooks
Hooks are scripts snapd runs automatically at specific lifecycle
moments, placed at snap/hooks/<hookname> in the source
tree (ending up at meta/hooks/<hookname> in the built
snap). They run just as confined as the snap's own apps.
| Hook | Fires when |
|---|---|
| install | Right after the snap is first installed, before it starts running. |
| configure | After install, after every refresh, and whenever snap set <name> key=value changes a configuration option. |
| pre-refresh / post-refresh | Immediately before / after an update is applied. |
| remove | Right before the snap is removed — the place to clean up external state the snap created. |
| connect-plug-<iface> / disconnect-plug-<iface> | When a plug this snap declares is connected to / disconnected from a slot — e.g. restart a service so it picks up newly-granted access. |
| connect-slot-<iface> / disconnect-slot-<iface> | The same, from the slot-providing side. |
Parts: how the snap actually gets built
parts: is the actual build recipe — one or more named
components, each fetched and built independently, then combined into
the final snap. Snapcraft runs each part through four lifecycle steps:
pull (fetch source) → build (compile) →
stage (collect build output) → prime
(final trim before packing into the snap).
- plugin
- How to build this part —
nil(nothing, just copy),dump(copy a pre-built tree as-is), or language/build-system plugins likecmake,autotools,python,go,meson. - source
- Where the code comes from — a local path, a git URL (with optional
source-branch/source-tag), or a tarball URL. - build-packages
- Host packages needed only to build this part (compilers, headers) — not shipped in the final snap.
- stage-packages
- Packages whose contents must ship inside the snap (runtime shared libraries the app links against, for instance).
- override-pull / override-build / override-stage / override-prime
- Custom shell commands replacing (or, with
craftctl default, supplementing) the plugin's default behavior at each lifecycle step. - organize / stage / prime
- Filters controlling which files move where, and which ones actually make it into the final snap — used to trim build artifacts that shouldn't ship.
Plugs and slots
Interfaces are how a strictly-confined snap reaches anything outside
its own sandbox. A plug is a snap declaring "I need
this kind of access" (network, home,
camera, gpio, i2c); a
slot is what provides it — usually the system itself,
but sometimes another snap. snap connect <snap>:<plug> <provider>:<slot>
wires them together (some interfaces auto-connect, others need an
explicit connection or store review).
One interface worth knowing by name: content — lets one snap expose a directory of files (a shared library, a data set, a trained ML model) that another snap reads from its own confined filesystem view, without either snap needing broad filesystem access. This is the standard way to let a model update independently of the engine that consumes it — see the inference-snap pattern in Snap types & Ubuntu Core.
Other fields worth knowing
- architectures
- Which CPU architectures to build for, as
build-on/run-onpairs — matters immediately once you're targetingarm64orarmhfboards rather than justamd64. - layout
- Remaps paths at runtime for software that hardcodes absolute paths (e.g.
/etc/foo) outside the snap's own directory, without needing to patch the upstream source. - epoch
- Marks breaking changes in on-disk data format across versions, so snapd won't auto-refresh a device straight across an incompatible boundary.
- license
- An SPDX license expression for the store listing.
- icon, contact, website, source-code, issues, donation
- Store-listing metadata — cosmetic, but expected by the store's review process for published snaps.
A full annotated example
name: sensor-bridge
title: Sensor Bridge
version: '1.4.2'
summary: Reads I2C sensors and republishes over MQTT
description: |
A small daemon that polls I2C sensors on the board and republishes
readings over MQTT for the fleet dashboard to consume.
grade: stable
confinement: strict
base: core22
apps:
sensor-bridge:
command: bin/sensor-bridge
daemon: simple
restart-condition: on-failure
plugs: [i2c, network]
environment:
LOG_LEVEL: info
parts:
sensor-bridge:
plugin: go
source: .
build-packages: [gcc]
stage-packages: [libc6]
plugs:
i2c:
interface: i2c
path: /dev/i2c-1
This ties directly back to I2C
and Linux drivers — the
i2c plug here is exactly what grants access to
/dev/i2c-1 that a strictly-confined snap wouldn't have by
default.
Real-world applications
- Publishing a CLI tool or daemon — the identity + apps + parts pattern shown above is exactly what most snapcraft.yaml files in the store look like.
- IoT sensor bridges — the i2c-plug example above reflects a real, common pattern for packaging edge devices that need hardware access under strict confinement.
- CI/CD pipelines — the
adopt-info+craftctl set versionpattern is standard for automatically versioning snaps from git tags in build pipelines. - Multi-language builds — the
parts:system's per-part plugins let one snap combine, say, a Go backend and a Python tool in a single package.
Gotchas
namemust be registered on the store (snapcraft register) before the first push under that name succeeds — a mismatch between the localsnapcraft.yamlname and the registered name is a common first-publish failure.- A version bump alone does not change
gradeor push a build to a channel —snapcraft push/snapcraft releasetargets a channel explicitly, independent of whatgradesays (grade only blocks stable). - Hooks run under the same confinement as the snap's apps — a
configurehook that needs an interface not yet connected fails the same way an app would, often silently from the user's point of view. - Missing
stage-packagesfor a runtime shared library is a very common "works when built, crashes when installed" bug — the build environment has libraries the confined runtime snap doesn't.