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:
- Build Postgres from source with custom patches that disable hardcoded âam I running as rootâ checks
- 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.
- 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.