# How Do I Set Up a Ruby and Rails Dev Environment in a Cloud Sandbox?

> A new sandbox has no Ruby. Install rbenv and ruby-build, then rbenv install 4.0.9, which compiled in 4.4 minutes on small. Bundle a real Rails app, add libvips, run its tests, and save a named snapshot so the next sandbox has Ruby and every gem ready in seconds.

Source: https://sandboxer.com/guides/ruby-dev-environment-in-a-cloud-sandbox. Updated 2026-10-27.

**Short answer:** A new sandbox has no Ruby. Install rbenv and ruby-build, then rbenv install 4.0.9, which compiled in 4.4 minutes on small. Bundle a real Rails app, add libvips, run its tests, and save a named snapshot so the next sandbox has Ruby and every gem ready in seconds.

We ran every command on this page on prod on 2026-10-27 in eu-central, on `small` sandboxes (2 vCPU, 4 GB). The project is [basecamp/once-campfire](https://github.com/basecamp/once-campfire), the open-source Campfire chat app, at commit `ca9b963`: a Rails app with a `Gemfile.lock` of about 120 gems, 17 of them with native extensions, and 649 Minitest tests. For a fresh app we used `rails new` with Rails 8.1.4 (released 2026-09-24).

## Results

| Step | Time on `small` |
| - | - |
| `apt-get install ruby-full` (Ubuntu's Ruby 3.2.3) | 4.4 s |
| Build dependencies from the ruby-build wiki, with apt | 5.3 s |
| `rbenv install 4.0.9` (compiled from source) | 264.5 s |
| `bundle install`, campfire, empty gem directory | 135.2 s |
| `bundle install`, nothing to do | 0.45 s |
| `bin/rails test`, 649 tests on 2 workers (2 runs) | 31.8, 27.3 s |
| `gem install rails -v 8.1.4` | 2.7 s |
| `rails new blog` | 28.3 s |
| `sbx new --from` the snapshot, to ready | 3.9 s |
| From the snapshot: `bundle install` | 0.51 s |
| From the snapshot: `bin/rails test` | 35.7 s |
| From the snapshot: `rails new blog2` | 13.3 s |

The lesson: in Ruby the slow part is compiling, twice. Ruby itself builds from source, and gems like `nio4r`, `puma`, `bcrypt` and `json` compile C extensions during `bundle install`. Together that was about 6.7 minutes before the first test ran. A snapshot skips all of it.

## 1. What the image has, and why apt's Ruby is a trap

```bash
sbx new --name rubydev-1 --type small
sbx exec sb_3voqtmqv2c bash -lc 'ruby --version; apt-cache policy ruby-full | head -3; rustc --version'
```

```text
bash: line 1: ruby: command not found
ruby-full:
  Installed: (none)
  Candidate: 1:3.2~ubuntu1
rustc 1.99.1 (58cf8f9c3 2026-10-13)
```

Ruby is not installed. That was the image on 2026-10-27; it changes over time. The full inventory is in [What's pre-installed in a sandbox](/guides/whats-preinstalled-in-a-sandbox). `build-essential`, `libssl-dev` and Rust are already there.

Ubuntu 24.04's `ruby-full` installs in 4 seconds, but it is Ruby 3.2.3, and its gem directory belongs to root:

```bash
sbx exec sb_3voqtmqv2c bash -lc 'gem install bundler'
```

```text
ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the /var/lib/gems/3.2.0 directory.
```

You could `sudo gem install`, but then gems live outside `/home/user` and mix with apt's files. Rails 8.1.4 needs Ruby 3.2 or newer (its rubygems.org page on 2026-10-27), so 3.2.3 still runs it, but it is several releases behind the current one. Use a version manager instead.

## 2. Install the current Ruby with rbenv and ruby-build

The current stable release on [ruby-lang.org/en/downloads](https://www.ruby-lang.org/en/downloads/) on 2026-10-27 was Ruby 4.0.9, released 2026-10-23. We followed the [rbenv README](https://github.com/rbenv/rbenv#basic-git-checkout) (git checkout) and installed the build packages listed for Ubuntu in the [ruby-build wiki](https://github.com/rbenv/ruby-build/wiki#ubuntudebianmint):

```bash
sbx exec sb_3voqtmqv2c --timeout 300 bash -lc '
  sudo apt-get install -y -qq build-essential autoconf libssl-dev libyaml-dev zlib1g-dev libffi-dev libgmp-dev rustc
  git clone -q https://github.com/rbenv/rbenv.git ~/.rbenv
  git clone -q https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
  ~/.rbenv/bin/rbenv init'
```

```text
writing ~/.bashrc: now configured for rbenv.
```

That line is a catch in a sandbox. Ubuntu's `~/.bashrc` returns early for non-interactive shells, so `sbx exec … bash -lc` never reaches the rbenv line, and `rbenv` stays "not found". Put the same line in `~/.profile`, which login shells read:

```bash
sbx exec sb_3voqtmqv2c bash -lc 'echo '\''eval "$(~/.rbenv/bin/rbenv init - --no-rehash bash)"'\'' >> ~/.profile'
sbx exec sb_3voqtmqv2c bash -lc 'rbenv --version; rbenv install --list | head -3'
```

```text
rbenv 1.3.2-25-g07e9b1e
3.3.13
3.4.12
4.0.9
```

ruby-build compiles from source, which runs past the 30-second default of `sbx exec`. Start it with `--detach` and read the log:

```bash
sbx exec sb_3voqtmqv2c --detach "bash -l b1.sh > b1.log 2>&1"   # b1.sh runs: time rbenv install 4.0.9; rbenv global 4.0.9
sbx exec sb_3voqtmqv2c "cat b1.log"
```

```text
==> Downloading ruby-4.0.9.tar.gz...
==> Installing ruby-4.0.9...
-> ./configure "--prefix=$HOME/.rbenv/versions/4.0.9" --enable-shared --with-ext=openssl,psych,+
-> make -j 2
-> make install
==> Installed ruby-4.0.9 to /home/user/.rbenv/versions/4.0.9
264.516 s
ruby 4.0.9 (2026-10-23 revision 3e93b23afc) +PRISM [x86_64-linux]
true
4.0.20
4.0.20
```

The two `true` lines say YJIT is built in (`RubyVM::YJIT.respond_to?(:enable)`) and turns on with `ruby --yjit`; ruby-build compiled it because `rustc` was present. Ruby 4.0.9 ships RubyGems 4.0.20 and Bundler 4.0.20. The install is 158 MB under `~/.rbenv`, on the sandbox disk.

## 3. Bundle a real Rails app and run its tests

campfire's `.ruby-version` says `4.0.9`, so rbenv picks it up in the repo:

```bash
sbx exec sb_3voqtmqv2c --timeout 120 bash -lc 'git clone -q https://github.com/basecamp/once-campfire campfire && cd campfire && git checkout -q ca9b963961ab'
sbx exec sb_3voqtmqv2c --detach "bash -l b2.sh > b2.log 2>&1"   # time bundle install --jobs 2, then the checks below
```

```text
== bundle install, empty gem dir
135.167 s
exit 0
117
17
bigdecimal 4.1.3 json 2.21.2 prism 1.9.0 date 3.5.1 stringio 3.2.0 psych 5.5.0 erb 6.0.7 io-console 0.9.4 nio4r 2.7.5 bcrypt 3.1.22 ffi 1.17.2 openssl 4.0.2 puma 8.0.2 rbs 4.2.0 websocket-driver 0.8.2 debug 1.11.0 lexxy 0.9.24
603M	/home/user/.rbenv/versions/4.0.9/lib/ruby/gems/4.0.0
== bundle install, nothing to do
0.453 s
```

117 gems installed and 17 of them compiled native extensions; that compile is likely most of the 135 seconds, though we didn't time it separately. campfire pulls Rails, Propshaft and Turbo from GitHub `main`, so Bundler also cloned those repos. The gems land in the rbenv Ruby's own gem directory, 603 MB.

The first `bin/rails db:prepare` failed:

```text
bin/rails aborted!
LoadError: libvips or the ruby-vips gem is not available (LoadError)
/home/user/campfire/config/initializers/vips.rb:5:in '<top (required)>'
```

Gems install fine without system libraries that are only loaded at runtime. Active Storage's image processing needs libvips, which the image doesn't have. One apt package fixed it, and then the database and the test suite worked:

```bash
sbx exec sb_3voqtmqv2c --timeout 120 bash -lc 'sudo apt-get install -y -qq --no-install-recommends libvips42'
sbx exec sb_3voqtmqv2c --detach "bash -l b3.sh > b3.log 2>&1"   # db:prepare, then bin/rails test twice
```

```text
== apt libvips
3.414 s
== db:prepare
3.091 s
exit 0
== rails test, run 1
Running 649 tests in parallel using 2 processes
Finished in 28.764620s, 22.5624 runs/s, 136.3133 assertions/s.
649 runs, 3921 assertions, 0 failures, 0 errors, 2 skips
31.791 s
== rails test, run 2
27.333 s
```

campfire's `test_helper.rb` calls `parallelize(workers: :number_of_processors)`, so on `small` the tests ran in 2 processes. The installed package was `libvips42t64` 8.15.1. Because apt installs to `/usr`, which is also on the sandbox disk, the snapshot in step 5 keeps it.

## 4. rails new, and a dev server on a private URL

```bash
sbx exec sb_3voqtmqv2c --timeout 300 bash -lc 'gem install rails -v 8.1.4 --no-document && rails new blog --skip-git && cd blog && bin/rails generate scaffold Post title:string body:text && bin/rails db:migrate && bin/rails test'
```

```text
Rails 8.1.4
…
         run  bundle install --quiet
         run  bundle lock --add-platform=x86_64-linux
…
7 runs, 11 assertions, 0 failures, 0 errors, 0 skips
```

`gem install rails` took 2.7 seconds and `rails new` 28.3 seconds, including four quiet `bundle install` runs. Some of its gems were already installed from campfire, so on a bare Ruby expect it to take longer.

Start Puma on `127.0.0.1` in the background and host the port with `--localhost`. The sandbox's public IPv6 firewall (`ufw`) is closed except SSH by default, and binding to `127.0.0.1` keeps the port private whatever the firewall does:

```bash
sbx exec sb_3voqtmqv2c --cwd blog --detach "bin/rails server -b 127.0.0.1 -p 3000 > ~/rails.log 2>&1"
sbx host sb_3voqtmqv2c 3000 --localhost --title "Rails dev"
```

```text
{"ok":true,"type":"port.hosted",…,"url":"https://brisk-bright-ibis-3000.box.sandboxer.com?_token=<token>","access":"private","isProtected":true,"localhost":true}
```

Without the token the URL answered `401`. The token link answered `302` and set a cookie. Then Rails itself refused the request with `403`:

```text
<h1>Blocked hosts: brisk-bright-ibis-3000.box.sandboxer.com, brisk-bright-ibis-3000.box.sandboxer.com</h1>
```

In development, Rails only answers `localhost` and IP addresses unless you add hosts to [`config.hosts`](https://guides.rubyonrails.org/configuring.html#config-hosts). A leading dot allows a domain and all its subdomains, so one line in `config/environments/development.rb` covers every sandbox and port:

```ruby
Rails.application.configure do
  # …
  config.hosts << ".box.sandboxer.com"
end
```

Environment files are read at boot, so the request still got `403` until we ran `bin/rails restart`. After the restart, `/posts` answered `200` in about 0.2 seconds (0.47 s for the first request). campfire's own development config sets `config.hosts = []`, which allows any host, so it doesn't hit this.

A `--detach` process does not come back if the sandbox boots from disk. To keep a dev server running across that, use a systemd unit as in [Run a Next.js dev server with a preview URL](/guides/run-a-next-js-dev-server-with-a-preview-url). For VS Code or Cursor over SSH, see [SSH into a cloud sandbox from VS Code or Cursor](/guides/ssh-into-a-cloud-sandbox-from-vs-code-or-cursor); we did not test an editor for this page.

## 5. Save it as a named snapshot and start from it

Stop the server first: a snapshot keeps files, not processes.

```bash
sbx exec sb_3voqtmqv2c bash -lc 'kill $(cat blog/tmp/pids/server.pid)'
sbx snapshot sb_3voqtmqv2c rubydev-cache
sbx new --name rubydev-2 --type small --from rubydev-cache
```

The save took 2.8 seconds and the new sandbox was ready in 3.9 seconds. The snapshot holds the whole disk: rbenv and Ruby 4.0.9, the `~/.profile` line, every gem, libvips, and both apps. In the new sandbox:

```text
ruby 4.0.9 (2026-10-23 revision 3e93b23afc) +PRISM [x86_64-linux]
/home/user/.rbenv/shims/ruby
== bundle install, as saved
0.508 s
== rails test, as saved
649 runs, 3921 assertions, 0 failures, 0 errors, 2 skips
35.722 s
== rails new, from snapshot
13.253 s
```

That is 0.5 seconds to a ready bundle instead of 4.4 minutes of compiling Ruby plus 2.3 minutes of `bundle install`. The tests take the same time either way; they are CPU-bound. To name the snapshot after a hash of `Gemfile.lock` and `.ruby-version` and rebuild it when either changes, follow [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot).

The first sandbox also sat idle long enough to stop on its own. `sbx resume` brought it back in 2.2 seconds with rbenv, the gems and both apps in place.

## FAQ

### Is Ruby installed in a Sandboxer sandbox?

Not on 2026-10-27. `apt-get install ruby-full` gives Ubuntu's Ruby 3.2.3 in about 4 seconds, but its gem directory needs sudo. For a current Ruby, install rbenv and ruby-build and compile it; 4.0.9 took 4.4 minutes on `small`.

### Why does rbenv say "command not found" in sbx exec?

`rbenv init` writes to `~/.bashrc`, and Ubuntu's `~/.bashrc` stops early in non-interactive shells. Add `eval "$(~/.rbenv/bin/rbenv init - --no-rehash bash)"` to `~/.profile` and run commands with `bash -lc`.

### Why does my Rails app show "Blocked hosts" on the hosted URL?

Rails host authorization only allows localhost in development. Add `config.hosts << ".box.sandboxer.com"` to `config/environments/development.rb` and restart the server.

### Where do gems go, and do they survive sbx stop?

With rbenv, in `~/.rbenv/versions/<version>/lib/ruby/gems`, under `/home/user` on the sandbox disk. `sbx stop` keeps the disk, and a named snapshot or a fork copies it.

### Should I use small or default for Ruby?

`small` worked for everything here, including 649 parallel tests. More vCPUs speed up the Ruby compile (`make -j`) and parallel tests; with a snapshot you rarely compile again.

## Related

- [Set up a Node.js dev environment in a cloud sandbox](/guides/node-dev-environment-in-a-cloud-sandbox)
- [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot)
- [What's pre-installed in a sandbox](/guides/whats-preinstalled-in-a-sandbox)
- [Hosting ports](/docs/hosting)
