---
title: "Dockerfile-less-deploys"
description: "Bringing Convention over Configuration to fly.io"
---

# Dockerfile-less-deploys

![custom gift wrapping booth at a mall](https://fly.io/ruby-dispatch/dockerfile-less-deploys/assets/gift-wrapping-cover.webp)

[Annie Ruygt](https://annieruygtillustration.com/)

[Fly.io](https://fly.io/) is a great place to run Rails applications, especially if you plan on running them on multiple servers around the world so your users have a fast, snappy, low-latency experience. [Give us a whirl](https://docs.fly.io/rails/getting-started) and get up and running quickly.

## Introduction

First, let’s talk about why we decided to use [OCI](https://opencontainers.org/) container images in the first place. In short, containers are freaking awesome. They allow us to package up our applications and all of their dependencies into a single, self-contained unit that can be easily extracted, deployed, and run on any machine. This makes it super easy for our users to get up and running with Fly.io, without having to worry about all the little details of setting up their environment.

This naturally leads to a baseline approach where every framework uses a [Dockerfile](https://docs.docker.com/engine/reference/builder/) and a [TOML](https://toml.io/en/) file. This is great for system administrators, polyglots, and Rails developers who are comfortable with Dockerfiles. What that leaves behind is Rails developers who spend most of their time in an IDE on Macs or Windows; which frankly is most of them. Many of which have been spoiled by Heroku for the past 10 years.

A desire to avoid Dockerfiles has lead many to prefer to use buildpacks, nixpacks or other alternatives, and when they have problems with those approaches instead of reporting the problems to the maintainers of these alternatives they report the problem to us.

Below is a proof of concept of an alternative approach where from a Fly.io platform point of view everything is Dockerfiles and TOML files and from a developer point of view everything is Rails and Ruby, giving us the best of both worlds.

This is not a radical change. `flyctl` will already build you an initial Dockerfile that meets many needs. This merely takes that approach further by dynamically generating a custom tailored Dockerfile on *every* Deploy.

What it does mean is that the Dockerfile needs to be correct and complete every time. No more relying on webpages of instructions. Fortunately [Thor](http://whatisthor.com/) and [ERB](https://github.com/ruby/erb#erb) are good at this.

In order to run this make sure you have flyctl version v0.0.433 or later as this is when support was added for dockerignore files to be provided at deploy time.

## You can play with this right now.

It’ll take less than 10 minutes to get your Rails application running globally.

[Try Fly for free →](https://docs.fly.io/rails)

---

## Part one, a simple visitor counter

Start by creating a simple application and scaffold a visitor counter table:

```
rails new welcome --css tailwind
cd welcome
git add .
git commit -a -m 'initial commit'
bin/rails generate scaffold visitor counter:integer
bin/rails db:migrate
```

Modify the index method in the visitor controller to find the counter and increment it.

Edit `app/controllers/visitors_controller.rb`:

```
  # GET /visitors or /visitors.json
  def index
    @visitor = Visitor.find_or_create_by(id: 1)

    @visitor.update!(
      counter: (@visitor.counter || 0) + 1
    )
  end
```

Change the index view to show the fly.io balloon and the counter.

Replace `app/views/visitors/index.html.erb` with:

```
<div class="absolute top-0 left-0 h-screen w-screen mx-auto mb-3 bg-navy px-20 py-14 rounded-[20vh] flex flex-row items-center justify-center" style="background-color:rgb(36 24 91)">
  <img src="https://fly.io/static/images/brand/brandmark-light.svg" class="h-[50vh]" style="margin-top: -15px" alt="The monochrome white Fly.io brandmark on a navy background" srcset="">

  <div class="text-white" style="font-size: 40vh; padding: 10vh" data-controller="counter">
    <%= @visitor.counter.to_i %>
  </div>
</div>
```

Define the root path to be the visitors index page:

Edit `config/routes.rb`:

```
  # Defines the root path route ("/")
  root 'visitors#index'
```

Save our work so we can see what changed later.

```
git add .
git commit -m 'initial application'
```

Now let’s do our first deployment. If desired add `—name` and `—org` options to the `generate` command below, or let them default:

```
bundle add fly-rails
bin/rails generate fly:app
bin/rails fly:deploy
```

Note that a volume is created. That’s to store the [sqlite3](https://www.sqlite.org/index.html) database. Making that work actually takes multiple steps: create a [volume](https://docs.fly.io/volumes/overview), mount the volume, and set an environment variable to cause Rails to put the database on the mounted volume.

All of that is taken care of for you.

To see your app in production, run `fly open`.

---

## Part two: change the database to PostgreSQL

While sqlite3 is more than adequate for this silly example, many applications require something more. Let’s switch to [postgresql](https://www.postgresql.org/).

Edit `config/database.yml`:

```
production:
  adapter: postgresql
```

Deploy your change:

```
bin/rails fly:deploy
```

At this point, a `pg` gem is installed, a `PostgreSQL` database is created, and a [secret](https://docs.fly.io/apps/secrets) is set. Also, there now is a separate [release](https://docs.fly.io/reference/configuration#the-deploy-section) step that will run your database migrations before restarting your server.

Again, all without you having to worry about anything.

---

## Part three: update the counter without requiring a refresh

Sending asynchronous updates requires a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) as well as an ability to process updates in the background. Rails makes this easy.

Start by generating a new [Action Cable](https://guides.rubyonrails.org/action_cable_overview.html) channel:

```
bin/rails generate channel counter
```

Make a partial that puts the counter into a [Turbo Frame.](https://turbo.hotwired.dev/handbook/frames)

Create `app/views/visitors/_counter.html.erb`:

```
<%= turbo_frame_tag(dom_id visitor) do %>
  <%= visitor.counter.to_i %>
<% end %>
```

Update the view to add `turbo_stream_from` and render the partial.

Update `app/views/visitors/index.html.erb`:

```
<%= turbo_stream_from 'counter' %>

<div class="absolute top-0 left-0 h-screen w-screen mx-auto mb-3 bg-navy px-20 py-14 rounded-[20vh] flex flex-row items-center justify-center" style="background-color:rgb(36 24 91)">
  <img src="https://fly.io/static/images/brand/brandmark-light.svg" class="h-[50vh]" style="margin-top: -15px" alt="The monochrome white Fly.io brandmark on a navy background" srcset="">

  <div class="text-white" style="font-size: 40vh; padding: 10vh" data-controller="counter">
    <%= render "counter", visitor: @visitor %>
  </div>
</div>
```

Add `broadcast_replace_later` to the controller.

Edit `app/controllers/visitors_controller.rb`:

```
  # GET /visitors or /visitors.json
  def index
    @visitor = Visitor.find_or_create_by(id: 1) 

    @visitor.update!(
      counter: (@visitor.counter || 0) + 1
    )

    @visitor.broadcast_replace_later_to 'counter',
      partial: 'visitors/counter'
  end
```

Deploy your change:

```
bin/rails fly:deploy
```

At this point, a `redis` gem is installed (if it wasn’t already), an [Upstash Redis](https://docs.fly.io/upstash/redis) cluster is created if your organization didn’t already have one (otherwise that cluster is reused), and a secret is set.

Once again, all without you having to worry about anything.

---

## Part four: change your cable adapter to any_cable

We’ve tried out two different databases. Let’s try out [AnyCable](https://anycable.io/) as an alternate cable implementation.

Modify `config/cable.yml`:

```
production:
  adapter: any_cable
```

Deploy your change:

```
bin/rails fly:deploy
```

Note that this time you are likely to see `502 Bad Gateway`. That’s because [nginx](https://www.nginx.com/) typically starts faster than Rails and at this point this is just a demo. Don’t worry, Rails will start in a few seconds and things will work once it starts. If you check the logs you will often see a similar problem where anycable go starts faster than anycable rpc, but that also corrects itself.

Once again, gems are installed and this time at runtime multiple processes are run, including one additional process (nginx) to transparently route the websocket to anycable. All on a single 256MB fly machine. The details are messy, but you don’t have to worry about them.

---

## Recap

We’ve deployed four different configurations without having to touch anything but Rails files.

Run the following command to see what files were modified:

```
git status
```

In addition to the `config` and `app` files that you modified you should see two files:

- `config/fly.rb`
- `fly.toml`

Both are relatively small, in fact `fly.toml` is only one line. The other file is likely to change dramatically so don’t get too attached to it. What it is meant to describe is the deployment specific information that can’t be gleaned from the configuration files alone, things like machine and volume sizes. The hope is that it will cover replication and geographic placement of machines; conceptually similar to what terraform provides today but expressed at a much higher level and in a familiar Ruby syntax.

If you want to see the configuration files that actually are used, run the following command:

```
bin/rails generate fly:app --eject
```

Note: this demo uses fly machines v2, and requires a script (`rails deploy`) to build a Dockerfile and run the underlying commands and APIs to create machines, set secrets, etc. It is possible to run with nomad (a.k.a. v1) by passing `--nomad` on the `bin/rails generate fly:app` command, and while this will allow you to run vanilla `fly deploy` the trade off is this is accomplished by creating a `Dockerfile` and various other artifacts.

---

## Futures

Some examples of things worth exploring

- Not implemented yet, but it should be possible to modify the size of a volume in `config/fly.rb` and deploy to make a change.
- While the above demo made use of fly’s Postgres offering, you may very well want a managed alternative. Or go the other way and run Debian’s Postgres within the same VM. Or go with a different database entirely. All should be easy as setting some [custom credentials](https://edgeguides.rubyonrails.org/security.html).
- [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html) supports a number of back-ends including Amazon’s S3 and Google’s Cloud. Let’s make that easy too.
- The example above ran AnyCable in the same VM which may not be optimal for scaling reasons. Not to mention that taking AnyCable down every time you deploy a change to your application will drop sessions. We should make it easy to say that I want two of this application running in this region and three of that application running in this other region.
- Currently `fly deploy` can be run as a GitHub action. Extend this work to cover `bin/rails fly:deploy`.
- Consider adding other `bin/rails fly:` tasks that add value. Perhaps one that directly runs `rails console` on the deployed machine. Perhaps another that simply sets the working directory properly on ssh.

The common theme of all of the above is to look for a higher level abstraction than what is currently provided in Dockerfiles and fly.toml, while retaining the ability to drop down and say things like “also install this Debian package” or “also expose this port”.

Feedback is welcome on [community.fly.io](https://community.fly.io/t/dockerfile-less-deploys-rails-demo/9234/1).

Oh, and it probably is worth mentioning that the source to the fly-ruby gem is [on GitHub](https://github.com/superfly/fly-rails) and written in Ruby. Pull requests welcome!
