skip to content

On an inherited server set up with RVM gemsets, a cron job running `cd /srv/billing && bundle exec rake invoices:send` uses the wrong Ruby, though it works over SSH; why, and how do you fix it?

level: seniorimportance: should knowfreq 20%

answer

  1. cron shells never source RVM
  2. rvm use needs a sourced function
  3. RVM is not a function
  4. ~/.rvm/wrappers/ruby-4.0.7@billing
  5. rvm cron setup snapshots env

basics

~20 s

RVM's selection is environment state set by a shell function your SSH login shell sources; cron's minimal shell never loads it, so no cd hook and no gemset. Use RVM wrappers, rvm in DIR do, or rvm cron setup.

solid answer

~50 s

Over SSH, a login shell sources `~/.rvm/scripts/rvm`, which defines the `rvm` function and the `cd` hook, loads the default alias and, on `cd /srv/billing`, reads `.ruby-version` and `.ruby-gemset`. Cron starts a minimal non-login `/bin/sh` that does none of that, so `bundle` is the system one or missing, and the `@billing` gems are not on the gem path. Adding `rvm use` to the job fails too: without the sourced function, RVM prints `RVM is not a function, selecting rubies with 'rvm use ...' will not work.`, because a child process cannot change its parent's environment. Fixes that need no shell magic: call the wrapper `~/.rvm/wrappers/ruby-4.0.7@billing/bundle`, use `rvm in /srv/billing do bundle exec rake invoices:send` (what `rvm cron command` writes), or run `rvm cron setup` to copy the environment into the crontab, re-running it after any Ruby change.

code

bash · 6 lines
bash
# crontab - pick one
# 1) wrapper: environment baked into the executable
15 2 * * * cd /srv/billing && /home/deploy/.rvm/wrappers/ruby-4.0.7@billing/bundle exec rake invoices:send

# 2) let RVM read /srv/billing's .ruby-version and .ruby-gemset
15 2 * * * /home/deploy/.rvm/bin/rvm in /srv/billing do bundle exec rake invoices:send

go deeper

for a junior

Recall that rvm use only works in a shell that sourced RVM, and that cron does not.

for a middle

Explain that a child process cannot change its parent's environment, which is why rvm must be a sourced function to switch Rubies.

for a senior

Reproduce cron's bare environment, confirm with which ruby and RbConfig.ruby, and pin every non-interactive caller with a wrapper or rvm in DIR do.

for a principal

Decide whether to keep patching RVM callers on inherited servers or schedule a move to a manager whose selection works without shell hooks.

## Why the SSH session works RVM is mostly a large bash library that has to be **sourced** into a shell. The installer adds a line to your shell profile that sources `~/.rvm/scripts/rvm` (or `/usr/local/rvm/scripts/rvm` for a multi-user install). When an interactive **login** shell starts, that line: - defines `rvm` as a **shell function**, so `rvm use` can rewrite the calling shell's `PATH`, `GEM_HOME` and `GEM_PATH`; - installs the directory-change hook (functions replacing `cd`, `pushd` and `popd` in bash, a `chpwd` hook in zsh); - sources `~/.rvm/environments/default` when no RVM Ruby is on `PATH` yet; - on `cd /srv/billing`, finds `.ruby-version` and `.ruby-gemset` and selects `ruby-4.0.7@billing`. So in your SSH session `bundle` resolves to the gemset's executable and the `@billing` gems are on the gem path. ## Why cron does not Cron runs each line with a minimal environment and a non-login `/bin/sh -c`. It reads no profile, so none of the four steps above happen: `cd` is the builtin, no project file is read, `PATH` is the short system default, and `bundle` is the operating system's copy or not found at all. The same applies to systemd units, some CI runners, `sudo` commands and scripts started with `sh`. ## Why adding `rvm use` does not help A process can change its own environment and pass it to its children, never to its parent. When RVM was not sourced, the `rvm` that runs is the `~/.rvm/bin/rvm` **executable**, a child process. It detects this and prints: `RVM is not a function, selecting rubies with 'rvm use ...' will not work.` followed by advice to allow login shells in the terminal (or run `/bin/bash --login`), or, when `SUDO_USER` is set, to stop putting `sudo` in front of `rvm`. Sourcing `~/.rvm/scripts/rvm` at the top of a bash script makes the function available, but it is fragile in cron. ## Robust fixes | Fix | How it selects the Ruby | Watch out for | |---|---|---| | wrapper path | `~/.rvm/wrappers/ruby-4.0.7@billing/bundle` sets the environment, then runs the real `bundle` | path changes when Ruby is upgraded; the `default` alias also has a wrappers directory | | `rvm in DIR do CMD` | the `rvm` executable reads DIR's project files, then runs CMD there | needs the full path to `rvm` in cron | | `rvm cron command "@daily" ...` | writes a crontab line using `rvm in "$PWD" do` | run it from the project directory | | `rvm cron setup` | copies the current environment's variables into the crontab between `#sm start rvm` and `#sm end rvm` | a snapshot; re-run after changing Ruby or gemset | | `bash -lc '...'` | forces a login shell that sources the profile | depends on the profile staying correct and quiet | Wrappers are the usual choice for systemd `ExecStart` lines, because a unit file cannot source anything. `rvm wrapper show` lists them and `rvm wrapper regenerate` rebuilds them. ## Diagnosing on the box 1. Run the job's command through `env -i /bin/sh -c '...'` to reproduce cron's bare environment. 2. Log `which ruby`, `ruby -v` and `ruby -e 'puts RbConfig.ruby'` from the job itself. 3. Compare with `rvm current` in your SSH session; `ruby-4.0.7@billing` there versus `/usr/bin/ruby` in the job confirms the cause. 4. Check whether the repository still has a `.rvmrc`: with no terminal, RVM will not prompt for trust, so an untrusted `.rvmrc` is simply not loaded. Pin the Ruby and gemset explicitly in every non-interactive caller; do not rely on `cd` or on the default alias silently matching the application. ## Common mistakes on inherited servers - **Pointing jobs at `~/.rvm/wrappers/default`.** It works until someone runs `rvm use ... --default` for an unrelated reason and every job silently follows. - **Fixing one crontab and missing the rest.** Search the deploy user's crontab, systemd units, logrotate hooks and deploy scripts for `rvm`, `.rvm/` and hard-coded gem paths before changing any Ruby. - **Removing an old Ruby after an upgrade.** Wrapper paths and `rvm cron setup` snapshots name the old Ruby; they break (wrappers) or keep stale paths (snapshots) the moment it is removed. - **Running `sudo rvm ...` to get past permission errors.** The `sudo` process never sourced RVM, which leads straight back to the not-a-function warning.

  • Why is `rvm cron setup` riskier than a wrapper path after a Ruby upgrade?
    `rvm cron setup` copies the variables of the environment active when you ran it (`PATH`, `GEM_HOME`, `GEM_PATH` and friends) into the crontab as a snapshot. Upgrading to a new Ruby or gemset leaves those old paths in place until someone re-runs it. A wrapper path names its Ruby too, but it fails loudly when that Ruby is removed rather than silently mixing old paths.
  • What should a systemd unit use to start a Puma or Rake process under an RVM gemset?
    Point `ExecStart` at the wrapper for that environment, such as `/home/deploy/.rvm/wrappers/ruby-4.0.7@billing/bundle exec ...`, and set `WorkingDirectory`. A unit file cannot source RVM, so `rvm use` there hits the not-a-function failure; the wrapper sets the environment itself before running the real executable.
  • What does it mean when RVM's not-a-function warning also tells you to avoid `sudo`?
    RVM saw `SUDO_USER` set: `sudo` started a fresh process that never sourced the user's RVM, so `rvm` ran as the executable. RVM's advice is not to put `sudo` in front of `rvm`, because RVM calls `sudo` itself for steps such as installing system packages.

saying these in an interview costs you the question

  • Adds rvm use to the cron line and expects it to switch Ruby
  • Fixes it by running the job with sudo rvm
  • Assumes cron reads ~/.bash_profile like an SSH login
  • Believes cd in a script triggers RVM's project-file switch
  • Treats rvm cron setup as updating itself after a Ruby upgrade