((λ (x) (create x)) '(knowledge))

It actually is a snap?

For some reason we're doing snap packaging now ยท September 14th, 2025

This one might come as a bit of an oddity for any long time readers. I'm not a massive fan of Ubuntu, and yet, here I am packaging snaps. We all know about the flatpak vs snap debate, so how I've ended up on this side of the divide seemingly at random is, well it is what it is. I should probably also mention that I'm packaging things for chocolatey now too, and that's this ridiculous jumble of powershell nonsense. Now, I'm being unkind, both of these packaging systems actually have merit. They're both pretty easy to get acquainted with and contribute to, but they feel wildly different from the type of work I've done packaging for Alpine and in a former role maintainer private Debian packages. I'll do an overview on Chocolatey later though, for now, lets talk a little bit about snaps.

What's a Snap exactly?

On the surface it's a bunch of yaml like every other DevOps inspired tool out there, but beneath the veneer of slight transposed json is a build ecosystem that curates a custom suqashfs container just for your application and all of its dependencies to live in. And beyond that even is a daemon service and apparmor configurations that help mount and expose that content in a secure fashion to the host os. All with the result being that snap install firefox essentially gets you a firefox command you can run, even if it's technically doing a whole lot more than that and might not be as performant as the apt package was.

I won't debate the merits of that specific package, but I do find the idea of snaps as a bleeding edge distribution mechanism on top of what is ostensibly slow Debian Sid with 6 month "stable" snapshots an interesting idea. I find snaps even more interesting when you take into consideration the fact that this will work on boring stable Debian LTS releases as well. No need to futz with the apt package spec, or go through the rigors of contributing to Debian or even Ubuntu to be able to meaningfully package and distribute up to date packages across a plethora of systemd based distributions!

Now that, I find really cool. There's several ways to affect private configuration of systems, but very few to make it easy to distribute your own software to systems you may occasionally deal with. That scenario happens often enough that I personally think investing the time learning a new toolset it worth it.

All of that said, lets snap up fServ as it's a favorite of mine and a daily use tool. For anyone outside my immediate circle, fServ is a file transfer server, with a semi-complete implementation of a 9p2000 file server backed into it. If that sounds like a weird combination of things for a tool, then you'd be right, but my use case is typically enabling development work from Linux onto a fleet of Windows and Linux systems that vary wildly. Having a single tool that is also a single static binary that I can quickly throw up and pull down is pretty critical to my personal workflow. Plus, it lets me edit config files and write powershell scripts in Emacs on the host without installing Emacs or using tramp, it's cool.

This is the snapcraft I've come up with for fServ, it's a little different from other golang packages I've seen but only because I want it to be as small as possible.

name: fserv
base: bare          # This is the squashfs that gets distributed, which should only container our static binary
build-base: core24  # This is an ubuntu 24.04 base, where our package is initially compiled
version: "1.0.0"
summary: A tiny popup file server
description: |
   fServ provides a way to quickly serve files from, or transfer files to a system.
   fServ generates its own self signed HTTPS certificate to encrypt in transit traffic sent between it and the client.

   Additional fServ can be used as an ad hoc 9pfs file server, though it's entirely experimental.

grade: stable        # This apparently does something, but Popey says to just set it stable and ignore it, so I did.
confinement: strict  # This is the apparmor confinement level, if you set it to classic the snap needs manual approval
                     # with strict confinement fServ's snap will only work on specified directories on the host.
parts:
  fserv:
    plugin: go
    source: "https://krei.lambdacreate.com/durrendal/fServ.git"
    source-tag: $SNAPCRAFT_PROJECT_VERSION
    source-type: git
    override-pull: |    # craftctl couldn't resolve the tags on gitea, so I had to add some small custom logic for that
      craftctl default
      git fetch --tags
      git checkout $SNAPCRAFT_PROJECT_VERSION
    build-environment:
      - CGO_ENABLED: "0"  # This indicates we're compiling statically
    build-snaps:
      - go/latest/stable  # This pulls in the tooling to compile golang packages in the 24.04 base
    build-packages:
      - gcc
    override-build: | # and obviously this compiles fServ and sticks it into the squashfs
       cd src/fServ
       go build -o $SNAPCRAFT_PART_INSTALL/bin/fServ


apps:
  fserv:  # this key actually becomes the cli invocation of the snap
    command: bin/fServ
    plugs: # and each of these plugs allow a set of predefined permissions
      - network       # These two allows fServ to run on 0.0.0.0
      - network-bind
      - home             # And these two allow fServ to be run from /home
      - removable-media  # and /mnt and /media respectively

Really it's not too much of a lift to get the configuration down. No worse than any other package spec, and we're hand waving a ton of complexity by statically compiling and using golang. The only minor annoyance is that fServ is invalid so the snap is named fserv and if I try to set apps.fServ the cli invocation of the snap becomes fserv.fServ which is obtuse and a bad user experience. Maybe I'm holding it wrong though! I'll find out as I continue down the rabbit hole.

Once you've got your little yaml manifest together you need a couple of things.

  • An Ubuntu system, or honestly probably just something running snapd
  • the snapcraft command, which is a snap package
# Install snapcraft
sudo snap install snapcraft

# cd into the repo with the manifest & initialize
sudo snapcraft init

# package the squashfs
snapcraft pack

# install and confirm functionality
sudo snap install fserv_*.snap --devmode

Anyways, this process will actually take a bit. Snapcraft uses LXD as its build backend, so it'll install that and put together a clean room environment to build the package in. I think it takes about 8gb of space to handle a single snap? I use 8gb volumes by default for my incus VMs and ended up needed to expand my snapcraft building VM in the process of packaging this and a couple other things.

Now, at this point you could stick the .snap file on a web server somewhere and just curl it down and install it with snap anywhere else that shares the same carch as the build system. But that's probably not what you want. You should release the package by uploading it to snapcraft.io, if for no other reason than you can then hook launchpad up to your github repo and build across all of the carches that Ubuntu currently supports.

It's enough to push the snap for the carch I built it on by just uploading it though. So maybe that's enough.

snap upload --release=stable fserv_*.snap

I'm too much of a completionist to stop here. On my gitea instance I created a snap organization and a repo for fServ, then created a push mirror on github so that I could connect the push mirror to Launchpad. That was totally worth it, because now whenever I release a new version of the snap package it gets rebuilt by Canonical's CI system. That's less for me to build and maintain, and a more trustworthy build source than my personal systems. This is a really nice feature resource for them to curate honestly.

This is what the build console looks like for all of the carches fServ is built on. Sadly the i386 build issue looks to be a compatibility issue with core24, I might be able to fix it by using core 20 though. The Snapcraft CI system

And this is what the build logging looks like. Very typical CI logging, but the key call out is that it's the exact same process you'd get if you run snapcraft through a manual build process which makes it really easy to reason about build failures. If anyone is curious here's the full build log from this screenshot. An arm64 build of fserv on snapcraft's ci

Where'd you even learn this nonsense?

Well it all sort of started with the desire to have the Nebula mesh VPN on Debian systems. Version 1.9.3 of Nebula is actually packaged on Debian Trixie and in Sid, so there was something within reach, but version 1.9.5 of Nebula introduces a fix that allows you to issue v2 Nebula certs to clients, and then 1.9.6 has fixes for Windows system locking up. If I want Nebula on my Alpine boxes it's as easy as apk add nebula and I get an up to date version, for Windows there's (now) a Chocolatey package. But Debian? I would need to hand roll it, or dig back out my dust Debian packaging hat, and I have so little interest in doing that.

But I found that there's a snap package for nebula!, but it was unfortunately stuck on version 1.8.2 until very recently.

Instead of diving into packaging my own thing off the bat I took the opportunity to update the existing Nebula snap package. Which I was able to do with the current maintainers help and kindness! And I even have some additional changes pending that will enable the nebula-cert command to be exposed as nebula.nebula-cert and that will allow the snap to be used to manage the nebula CA, or at very least to print details about existing certificates for debugging/automation purposes. I've been talking with the current maintainer of this snap about potentially taking over maintenance of the package long term.

And of course none of my understanding of snaps came from a vacuum, Popey's blog has some great posts about debugging and maintaining snap packages. And I'm a huge 2.5 admins fan, so there's some appeal to seeing the technology demonstrated and learning it as well.

In the end, the quick foray into a new package ecosystem has netted me several things I need/want, and the maintenance burden on the contributions are pretty minimal given the amount of automation Canonical has provided for snapcraft. And the pragmatist in my appreciates all of this. Obviously this is quite literally just distributing something as a static binary in a different shaped box, but the real benefit is that I don't have to do anything other than change a version number in a yaml file. That level of simplistic maintenance is actually quite nice, and worth the effort.