Snapping a Rails app

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