Snapping a Rails app

Hi all,

new snapcrafter here. I want to snap my Rails-based web application for deployment on Ubuntu Core IoT devices:

  • Ruby on Rails API backend. Requires bundler and rails gems to be available as binaries, as well as installing gems from a Gemfile (package.json for Ruby apps, if you will)
  • PostgreSQL database
  • nginx Webserver
  • two separate JavaScript SPA bundles
  • Required config files to set up the database, web server etc.
Show snapcraft.yaml
name: my-snap
version: "0.1.0"
summary: My snap app
description: Testing snapcraft for my app.
grade: devel
confinement: devmode

apps:
  my-app:
    command: sh $SNAP/opt/scripts/bin/start-stack.sh
    daemon: simple
    environment:
      RUBYLIB: "$SNAP/usr/local/lib/site_ruby/2.5.0:$SNAP/usr/local/lib/x86_64-linux-gnu/site_ruby:$SNAP/usr/local/lib/site_ruby:$SNAP/usr/lib/ruby/vendor_ruby/2.5.0:$SNAP/usr/lib/x86_64-linux-gnu/ruby/vendor_ruby/2.5.0:$SNAP/usr/lib/ruby/vendor_ruby:$SNAP/usr/lib/ruby/2.5.0:$SNAP/usr/lib/x86_64-linux-gnu/ruby/2.5.0"

parts:
  # Scripts and wrappers
  start-stack:
    source: ./src
    plugin: dump
    organize:
      start-stack.sh: opt/scripts/bin/start-stack.sh

  my-app:
    source: ./components/my-app
    build-packages: [libpq-dev]
    stage-packages: [postgresql, libc6, libpq-dev]
    plugin: ruby
    ruby-version: "2.5.1"
    gems: [bundler, rails]
    override-build: |
      snapcraftctl build
      # $SNAPCRAFT_PART_INSTALL/bin/bundle install --path vendor/bundle
    organize:
      usr/lib/postgresql/9.5/bin/postgres: usr/bin/postgres

As for my questions:

  1. Would experienced snapcrafters advise to bundle all dependencies (e. g. define postgresql, nginx as separate parts), or just pull them in with stage-packages? I don’t require custom builds or special flags.

  2. How would I go about the problem that Postgres service cannot run as root? When I start the Postgres with a command, it aborts as Postgres refuses to run as root. Would sudo -u postgres /bin/dir/postgres -D $SNAP_DATA/postgres work on a fresh Ubuntu Core device?

Any advice or hints appreciated, thanks in advance!

Update on September 11, 2018 8:28 AM:
See post #6 in this thread for my learnings so far.

You shouldn’t need to bundle libc, as that will be provided by the base snap (called core).

If you are OK with the versions of dependencies provided in the Ubuntu 16.04 repository then it’s fine to use those as-is. I’d suggest that postgres and nginx are probably fine to include from Ubuntu repositories.

Postgres running as root is a difficult problem unless postgres provides a flag like --root-is-really-ok-i-promise-pinky-swear, or a configuration option to allow it to run as root.

With snaps even though a service sees itself as running as root it will be in an isolated environment so the protections afforded by running as an unprivileged user are accounted for by apparmor and namespaces. The problem is a lot of applications refuse to run as root, as you’ve discovered with postgres, so we might need to figure out a source-code patch which would require we build it from scratch.

We currently don’t have a mechanism, that I’m aware of, for strong privilege separation such as that used by qmail where the system is comprised of multiple daemons that all run under their own unique user account.

There was work a while back on providing an ability within snapd to allow a snap to create user accounts on installation, but I don’t think it’s finished yet, or at least it’s not possible to run a daemon under that new account yet. I might be wrong here though.

I’m just gonna ping the IRC channel about this thread to get someone who knows snapd well to pop in…

Thanks to @chipaca… There’s a post here about developing the functionality for user accounts being supported. As I thought it’s not ready yet, but work is ongoing…

You might also find the nextcloud snapcraft.yaml a useful reference, it is the most comprehensive web application snap I’m aware of:

1 Like

Daniel, thanks for your quick and comprehensive reply!

libc6 was required at some point in my build attempts with ruby, otherwise Snapcraft would abort with the message that I need to include libc6 as a stage-package. However, I can’t reproduce the config which produces said error. So nvm :slight_smile:

Thanks for explaining the problem and current status around users/groups for services. As @wimpress recommended, I consulted @kyrofa 's Nextcloud snap for guidance – which led me to the question if building dependencies from source is the better way to go since he adds a lot of customizations (as explained in his blog series. He uses MySQL’s escape hatch user=root to address the “service only runs as root” problem, but PostgreSQL doesn’t offer anything like this. I currently don’t have the time to properly patch those checks out of the Postgres sources – I’ll test if my app performs adequately on SQLite (it’s a single-user IoT device), which is a lot easier to set up in confinement.

To document this for other snapcrafters that come across issues while snapping Ruby/Rails applications, here are the steps I took to get my Rails stack running in a snap. Thanks again to @lucyllewy for providing insight about current limitations of users/groups in snaps, and to @zyga-snapd for helping me understand the layouts feature!

Prelude: I require a strictly confined snap, because the app will be deployed on Ubuntu Core devices where snaps with classic confinement are not available.

Task: Getting the database to work

As explained by @lucyllewy in this thread, services in a snap can only run as root user at the moment. Programs such as Postgres straight-out refuse to start as root. This means that if your Rails app uses a PostgreSQL database, you need to either:

  1. Build Postgres from source with custom patches that disable hardcoded “am I running as root” checks
  2. Switch to another database – in my case, SQLite3 is enough for the use case. MySQL has an escape hatch which allows you to run it as root anyway, see @kyrofa’s Nextcloud snap for details.
  3. Use the Postgresql snaps by commandprompt to manually start and manage Postgres – I didn’t try that yet.

Task: bundling Ruby and gem dependencies

If your app relies on other versions than what’s available in the Ubuntu 16.04 repos (currently 2.3.0), use the ruby plugin provided by snapcraft. It lets you select any Ruby version published on ruby-lang.org and also provides convenient keywords to specify global gems that should be installed. See snapcraft help ruby for details.

Here’s the stanza from my snapcraft.yaml that specifies the Rails app as a part:

my-rails-app:
    source: path-to-some-repo
    build-packages: [libsqlite3-dev] # Libraries required during gem native extension builds
    stage-packages: [sqlite3] # Bundling sqlite3 drivers
    plugin: ruby
    ruby-version: "2.5.1"
    gems: [bundler, rails] # Installing bundler and rails in $SNAP/bin to run bundle and rails commands in scripts
    override-build: |
      mkdir $SNAPCRAFT_PART_INSTALL/my-rails-app 
      cp -a * $SNAPCRAFT_PART_INSTALL/my-rails-app
      snapcraftctl build
      cd $SNAPCRAFT_PART_INSTALL/my-rails-app
      bin/bundle install --path vendor/bundle --without development test
      rm -rf $SNAPCRAFT_PART_INSTALL/api/tmp $SNAPCRAFT_PART_INSTALL/api/log

The override-build section shows how you can move the app to a subdirectory in the final snap. This is useful e. g. if you’re bundling a separate frontend app or other things that might conflict with your Rails app’s files, or if you just want a clean top-level directory. If you don’t care where bundler installs your app’s gem dependencies or if dev/test are installed as well, you can just use the ruby plugin keyword use-bundler: true. The last line ensures that tmp and log directories are removed because we are replacing those with bind mounts in the next section. The default .gitignore of a fresh Rails app includes those directories with .gitkeep files, so they are present in a fresh clone of your repository if you don’t untrack them.

Task: Providing Rails with write access to tmp/log directories

Rails apps currently require write access to the directories tmp and log in their application root. However, those are read-only in a snap environment. To work around this, you need to use the (currently-experimental-but-soon-to-be-stable) snapd feature: layouts.

passthrough: # TODO: Remove once in stable snapd
  layout:
    $SNAP/my-rails-app/tmp: # my-rails-app == subdirectory from override-build
      bind: $SNAP_DATA/rails_tmp # or any other name that suits you
    $SNAP/my-rails-app/log:
      bind: $SNAP_DATA/rails_log

This creates bind-mounts that are writable by Rails even though the directories are inside the snap’s read-only filesystem.

Task: Running Ruby/Rails commands in snap hooks

To set up your Rails app, run migrations etc., you can use snap hooks. However, hooks are not automatically wrapped by snapd to ensure that the snap’s binaries and libraries are in the PATH (I have opened a PR for snapcraft documentation to explain this for first-time snap creators like me). To ensure that everything bundled by the Ruby plugin is available during hook runs, add the following to your snapcraft.yaml:

# top-level section in snapcraft.yaml
environment:
  PATH: "$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH"
  LD_LIBRARY_PATH: "$LD_LIBRARY_PATH:$SNAP/lib:$SNAP/usr/lib:$SNAP/usr/lib/x86_64-linux-gnu"
  GEM_PATH: "$SNAP/lib/ruby/gems/2.5.0"
  RUBYLIB: "$SNAP/lib/ruby/2.5.0:$SNAP/lib/ruby/2.5.0/x86_64-linux"
  GEM_HOME: "$SNAP/lib/ruby/gems/2.5.0"
  LD_LIBRARY_PATH: "$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH"

I just copied over all environment exports that snapd created for a command in my snap. So if you’re unsure what to put here, create a dummy app with a simple command, run snapcraft prime my-dummy-command and inspect the file snap-project/prime/my-dummy-command.wrapper for guidance.

1 Like