November 21, 2024

Crafting a Snap package from a NodeJS application

Snaps are purposefully designed to be easy to use and maintain, offering added benefits of security and scalability. Encapsulating your NestJS app into a snap is a straightforward and rewarding process, and you will find an easy to start template right here in this article! Let’s discover this process together and ensure creating a snap package is not a “snappy” process!

Necessary tools

I will assume you have the snapcraft command installed and running with either LXD or Multipass. If not, please refer to the official Canonical documentation about how to set it up:

As for the build architecture, I am using an AMD64 machine hence the produced package will have this architecture. If you are on ARM, the steps should be the same. Just be mindful that cross-compilation will not produce a valid architecture package for the target, in case you feel adventurous 😆.

This article also assumes you have a running NodeJS installed (I use version 20.15).

Initializing the project

For the purpose of this article, I will initialize an empty repository with npm init then add a sample ExpressJS application code. You can find the full source code hosted on Github.

Now let’s see how our server app looks like. There is nothing fancy, just an endpoint which returns a different message depending on how the app is being run: either normal Node commands or from snap package.

const express = require('express');
const app = express();
const port = 3000;

// Define a simple API endpoint
app.get('/api/info', (req, res) => {
  // If the process is run in snap, it'll automatically receive some Snap specific variables
  const runningInSnap = !!process.env.SNAP;
  res.json({ 
    runningInSnap,
    message: runningInSnap ? `Hello from Snap package: ${process.env.SNAP_NAME}` : 'Hello from ordinary Node process',
  });
});

// Start the server
app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});

We determine whether we are in snap by checking the existence of the SNAP environment variable and we send a different message accordingly. We also leverage the SNAP_NAME variable to return the name of the package. You can find the full list of Snap variables here.

You should see an output from the endpoint like the following when running with the normal npm start:

Crafting the snapcraft.yml file

Inside the app folder, we will create a snap folder which contains the Yaml configuration file. It’s not super fancy actually, the idea is that I tried to build it several times and it was more laborious but it didn’t have to. Keep it simple and you will get there always!


name: snappy-snapcraft
base: core22
version: '1.0.0'
summary: Showcase how to create snaps with snapcraft in various scenarios. Please check my blog at afivan.com for info
description: |
  Demonstration of how to embed regular NodeJS apps into snap packages which can be further published to the Snap store.

grade: devel # must be 'stable' to release into candidate/stable channels
confinement: strict

apps:
  snappy:
    command: bin/snappy
    plugs:
      - network-bind
      - network

parts:
  snappy:
    plugin: npm
    source: .
    npm-node-version: 20.14.0
    npm-include-node: true

Let’s explain the specific pieces of this file a bit:

  1. base: core22 – this directive specifies the base image upon the snap package will be based. Possible values are core20, core22 or core24. For instance, core20 is based on Ubuntu 20.04 LTS and core22 is based on based on Ubuntu 22.04 LTS. More details here
  2. grade: devel – specifies the “stability” of the app and the channel it’s being published to. You can change to stable once you thoroughly test your snap
  3. confinement: strict – since the snaps run in their own confined space, this value has to be strict in order to publish your snap. It can be switched to devmode if you need to figure out the plugs which the app needs
  4. apps:snappy… – this is where we define the commands we expose externally when the snap is installed. Also we link the command which we run from inside the snap with its corresponding plugs that it requires. In our case we require the network and network-bind plugs which do make sure we are able to define a server which can access the network (internet included)
  5. parts:snappy… – our juiciest part where we control how the snap is actually created. Here we make use of the npm plugin which is included in snapcraft. This is in charge of taking the necessary steps (like npm install) so that the app is having all that it needs in order to run. In our example, we specify the version 20.14.0 of NodeJS to be used and we set the npm-include-node to true so that the node binary will be available in the snap because we need it.
    For a list of supported plugins please check this official documentation

Building and testing the actual snap

Now we have reached the stage when we need to try our luck, keep our fingers crossed and hit that build command and see what happens 🙂

Hooray! After hitting the snapcraft command in the repo folder we can see our first snap was successfully built! It’s now time to test it by issuing the following command:

snap install ./snappy-snapcraft_1.0.0_amd64.snap --devmode --dangerous

You may need to provide root credentials or you can run with sudo directly.

After the snap is installed, time to run it with the snappy-snapcraft.snappy command:

We observe that the server started successfully on port 3000. Let’s check out our endpoint and see what result it produces:

Notice that we are able to call it and moreover it sees that it runs in snap mode and returns the snap name in the message.

Conclusion

Although it may not seem to complicated, there is a lot of research, trial and error on this one. I remember I wrapped my head around this snap topic 1 year ago to no avail. But this time with lots of perseverance I managed to get my snap working as it should! Also, I decided to write on this topic because I see a lot of potential in snaps, documentation is fine but doesn’t go too far in some advanced topics and code samples are quite few. So hopefully, you find this useful and don’t hesitate to write a comment or contact me with any question, much appreciated!

Thanks for reading, I hope you found this article useful and interesting. If you have any suggestions don’t hesitate to contact me. If you found my content useful please consider a small donation. Any support is greatly appreciated! Cheers  😉

afivan

Enthusiast adventurer, software developer with a high sense of creativity, discipline and achievement. I like to travel, I like music and outdoor sports. Because I have a broken ligament, I prefer safer activities like running or biking. In a couple of years, my ambition is to become a good technical lead with entrepreneurial mindset. From a personal point of view, I’d like to establish my own family, so I’ll have lots of things to do, there’s never time to get bored 😂

View all posts by afivan →