> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy your first Rails 8 application on Upsun

> Learn how to build and deploy a modern Rails 8 blog application on Upsun's cloud platform. This hands-on tutorial walks you through local development setup, creating a feature-rich blog application, and deploying it to production with proper monitoring and scaling. Discover why Platform-as-a-Service remains relevant even with Rails 8's new deployment features, and learn best practices for cloud deployment. Perfect for developers wanting to quickly ship production-ready Rails applications without managing complex infrastructure.


export const PostMeta = ({data = {}}) => {
  const {author, date, image} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (!image && authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
      {image && <img src={image} alt="" className="post-meta-image" aria-hidden="true" />}
    </div>;
};

<PostMeta data={{ author: ["gmoigneu"], date: "2024-10-23T10:00:00+00:00", image: "/images/posts/tutorials/rails8-upsun-tutorial/rails8-thumbnail.webp" }} />

Rails 8.0 Release Candidate just dropped, and it's packed with exciting features focused on deployment simplicity. While Rails 8 introduces tools like Kamal for "PaaS-free" deployment, there's still immense value in using a modern cloud platform like Upsun - especially when you want to focus on building features rather than managing infrastructure.

Let's build a simple blog application and deploy it to production **in under 10 minutes**!

<img src="https://mintcdn.com/upsun-c9761871/OR9CPqO13fyi9LoK/images/posts/tutorials/rails8-upsun-tutorial/rails8-blog-index.webp?fit=max&auto=format&n=OR9CPqO13fyi9LoK&q=85&s=4b81cda859072b41bc13811bbb8c24c8" alt="Final Blog Application" width="1381" height="1046" data-path="images/posts/tutorials/rails8-upsun-tutorial/rails8-blog-index.webp" />

## Prerequisites

* Basic Ruby knowledge
* macOS (though Windows/Linux instructions available)
* [Upsun CLI](https://docs.upsun.com/administration/cli.html) installed
* [Ruby](https://www.ruby-lang.org/) 3.3+ installed

## Part 1: Local Development

### Setting Up Your Environment

First, let's install Ruby and Rails on your Mac:

```bash {filename="Terminal"} theme={null}
# Install Ruby 3.3 via brew
brew install ruby

# Install Rails 8 RC
gem install rails -v8.0.0.rc1

# Ensure bundler is up to date especially if you just upgraded Ruby
gem uninstall bundler --all --force && gem install bundler
```

If you want to use PostgreSQL locally via the `pg` gem, you need to install `postgresql@15` on your machine to compile the extension:

```bash {filename="Terminal"} theme={null}
brew install postgresql@15
gem install pg
```

If you only want to use `sqlite` locally and `pg` on your production install, you will need to add `bundle config set frozen false` to the Upsun build phase so that `bundle` can still install the gem on Upsun.

### Bootstraping your Rails 8 blog

Let's create a new Rails application:

```bash {filename="Terminal"} theme={null}
rails new rails-blog
cd rails-blog
```

<Note>
  **Pro tip:** Rails 8 uses SQLite by default, which works great for local development but also with Upsun's persistent storage!
</Note>

### Building the Blog Features

We'll follow the standard Rails blog tutorial but with some modern twists. Here are the key models we'll create:

```bash {filename="Terminal"} theme={null}
# Generate the Article model
bin/rails generate model Article title:string body:text
bin/rails generate model Comment commenter:string body:text article:references

# Run migrations
bin/rails db:migrate
```

Add the following to `app/models/article.rb` to map the relationship between `Articles` and `Comments`:

```ruby {filename="app/models/article.rb"} theme={null}
class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
  
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end
```

Head over to `routes.rb` and define the following:

```ruby {filename="routes.rb"} theme={null}
root "articles#index"
resources :articles do
  resources :comments
end
```

We are using a `resources` here to make thing simple but we could also specify only the `index` and `show` routes.

And add the reverse one:

```ruby {filename="app/models/comment.rb"} theme={null}
class Comment < ApplicationRecord
  belongs_to :article
end
```

### To the controllers

Let's create the two controllers we need there to handle our `Articles` and `Comments`.

The `articles_controller.rb` just specifies two actions, `index` and `show`:

```ruby {filename="app/controllers/articles_controller.rb"} theme={null}
class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end
end
```

The `comments_controller.rb` only handles the `Comment` creation process as the display is handled by the `ArticlesController` directly:

```ruby {filename="app/controllers/comments_controller.rb"} theme={null}
class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  private
    def comment_params
      params.require(:comment).permit(:commenter, :body)
    end
end
```

### Let's create the Views

With the magic of [Claude.ai](https://claude.ai), we can generate some boilerplate TailwindCSS templates for our pages. If you are curious, here is the prompt that was used. Don't forget to upload your `.erb` template in the UI first.

<Note>
  **Prompt:** Refactor the following Ruby on Rails template. Using TailwindCSS, create a simple and modern design to list the articles from the blog. Include the main blog name. Each article will be show with its title, publication date and a read more button.
</Note>

Let's start with our index page `app/views/articles/index.html.erb`:

```html {filename="app/views/articles/index.html.erb"} theme={null}
<div class="container mx-auto px-4 py-8">
  <h1 class="text-4xl font-bold text-gray-800 mb-8">Rails 8 on Upsun</h1>

  <h2 class="text-2xl font-semibold text-gray-700 mb-6">Articles</h2>

  <div class="space-y-6">
    <% @articles.each do |article| %>
      <div class="bg-white shadow-md rounded-lg p-6 hover:shadow-lg transition duration-300">
        <h3 class="text-xl font-semibold text-blue-600 hover:text-blue-800">
          <%= link_to article.title, article_path(article), class: "hover:underline" %>
        </h3>
        <% if article.created_at %>
          <p class="text-sm text-gray-500 mt-2">
            Published on: <%= article.created_at.strftime("%B %d, %Y") %>
          </p>
        <% end %>
        <%= link_to "Read more", article_path(article), class: "inline-block mt-4 text-blue-600 hover:text-blue-800 hover:underline" %>
      </div>
    <% end %>
  </div>

  <% if @articles.empty? %>
    <p class="text-lg text-gray-600 mt-8">No articles found.</p>
  <% end %>
</div>
```

And now the single article template `app/views/articles/show.html.erb`:

```html {filename="app/views/articles/show.html.erb"} theme={null}
<div class="container mx-auto px-4 py-8">
  <h1 class="text-4xl font-bold mb-4"><%= @article.title %></h1>

  <div class="prose lg:prose-xl mb-8">
    <%= @article.body %>
  </div>

  <h2 class="text-2xl font-semibold mb-4">Comments</h2>
  <div class="space-y-4 mb-8">
    <% @article.comments.each do |comment| %>
      <div class="bg-gray-100 p-4 rounded-lg">
        <p class="font-semibold"><%= comment.commenter %></p>
        <p class="mt-2"><%= comment.body %></p>
      </div>
    <% end %>
  </div>

  <h2 class="text-2xl font-semibold mb-4">Add a comment:</h2>
  <%= form_with model: [ @article, @article.comments.build ], class: "space-y-4" do |form| %>
    <div>
      <%= form.label :commenter, class: "block text-sm font-medium text-gray-700" %>
      <%= form.text_field :commenter, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" %>
    </div>
    <div>
      <%= form.label :body, class: "block text-sm font-medium text-gray-700" %>
      <%= form.text_area :body, rows: 4, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" %>
    </div>
    <div>
      <%= form.submit class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
    </div>
  <% end %>
</div>
```

### Add the TailwindCSS gem

If you refresh your project, you will notice that the styles are not yet applied. We need to add TailwindCSS to our project:

```bash {filename="Terminal"} theme={null}
bundle add tailwindcss-rails
bin/rails tailwindcss:install
```

The install command will generate the `tailwind.config.js` file. No need to change anything there.

You can now either build once or watch automatically for any change:

```bash {filename="Terminal"} theme={null}
bin/rails tailwindcss:build
# OR
bin/rails tailwindcss:watch
```

Refresh and your styles should now be properly generated and injected into `application.html.erb`:

```ruby {filename="application.html.erb"} theme={null}
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
```

<img src="https://mintcdn.com/upsun-c9761871/OR9CPqO13fyi9LoK/images/posts/tutorials/rails8-upsun-tutorial/rails8-blog-show.webp?fit=max&auto=format&n=OR9CPqO13fyi9LoK&q=85&s=6f52866e66d834553ccc02f58f0b664a" alt="Article" width="1381" height="1046" data-path="images/posts/tutorials/rails8-upsun-tutorial/rails8-blog-show.webp" />

**Great! Our app is now complete!**

## Part 2: Preparing for Upsun deployment

### Creating Your Upsun Project

```bash {filename="Terminal"} theme={null}
# Create a new project
upsun project:create rails-blog

# Follow the prompts!
[...]
```

Our Upsun git remote is now setup on the repository:

```shell {filename="Terminal"} theme={null}
git remote -v
origin	git@github.com:gmoigneu/upsun-rails-8-tutorial.git (fetch)
origin	git@github.com:gmoigneu/upsun-rails-8-tutorial.git (push)
upsun	6hd2pooi7mzay@git.ch-1.platform.sh:6hd2pooi7mzay.git (fetch)
upsun	6hd2pooi7mzay@git.ch-1.platform.sh:6hd2pooi7mzay.git (push)
```

### Configuring for Deployment

The magic happens in `.upsun/config.yaml`.
Let's break down each section of a production-ready Rails configuration:

#### 1. Application Definition

```yaml {filename=".upsun/config.yaml"} theme={null}
applications:
  rails-blog:
    type: "ruby:3.3"
```

This defines your application's runtime environment:

* `rails-blog` is your application name
* `type: "ruby:3.3"` specifies the base image we want to use and the Ruby version

#### 2. Database Relationship

```yaml {filename=".upsun/config.yaml"} theme={null}
    relationships:
      database: "postgresql:15"
```

This creates a connection between your application and services:

* Links your app to a PostgreSQL 15 database
* Makes the database credentials available via environment variables `DATABASE_`.
* Creates a secure internal network between your app and database

#### 3. Web Server Configuration

```yaml {filename=".upsun/config.yaml"} theme={null}
    web:
      commands:
        start: "bundle exec puma -b unix://$SOCKET"
      upstream:
        socket_family: unix
      locations:
        "/":
          root: "public"
          passthru: true
          expires: 1h
```

This configures how your application handles web requests:

* `start` command launches Puma using a Unix socket
* `socket_family: unix` optimizes for performance by using Unix sockets instead of TCP
* `locations` configures the web server:
  * `root: "public"` serves static files from the public directory
  * `passthru: true` forwards requests to Rails when no static file is found
  * `expires: 1h` sets cache headers for better performance

#### 4. Build and Deploy Hooks

```yaml {filename=".upsun/config.yaml"} theme={null}
    hooks:
      build: |
        set -e
        bundle config set frozen false
        bundle install
        bundle exec rails assets:precompile
      deploy: |
        set -e
        bundle exec rails db:migrate
```

These hooks run at specific points in the deployment process:

* `build` runs during the build phase when you are not connected to any other services:
  * Installs Ruby dependencies
  * Compiles assets
* `deploy` runs when deploying the new container on the host:
  * Runs database migrations
  * You could other actions like cache clearing and the likes

#### 5. Persistent Storage

```yaml {filename=".upsun/config.yaml"} theme={null}
    mounts:
      "/log":
        source: storage
        source_path: log
      "/storage":
        source: storage
        source_path: storage
      "/tmp":
        source: storage
        source_path: tmp
```

Configures persistent storage for your application:

* `/log` for application logs
* `/storage` for uploaded files and Active Storage
* `/tmp` for temporary files
* Each mount persists data across deployments and can be shared between containers

#### 6. Environment Variables

```yaml {filename=".upsun/config.yaml"} theme={null}
    variables:
      env:
        PIDFILE: "tmp/server.pid"
        RAILS_ENV: "production"
```

Sets environment variables for your application:

* `PIDFILE` specifies where Puma should store its process ID
* `RAILS_ENV` ensures Rails runs in production mode

#### 7. Services Definition

```yaml {filename=".upsun/config.yaml"} theme={null}
services:
  database:
    type: postgresql:15
```

Defines the services your application needs:

* Creates a PostgreSQL 15 database instance
* Automatically manages backups and updates
* Provides high-availability configuration

#### 8. Routing Configuration

```yaml {filename=".upsun/config.yaml"} theme={null}
routes:
  "https://{default}/":
    type: upstream
    upstream: "rails-blog:http"
  "https://www.{default}":
    type: redirect
    to: "https://{default}/"
```

Configures how requests reach your application:

* Sets up HTTPS automatically
* Routes requests to your Rails application
* Redirects www to non-www (or vice versa)
* `{default}` is replaced with your environment URL

The last step is to make sure we properly generate the `DATABASE_URL` environment variable. Upsun automatically detects any `.environment` file at the root of the repository.

```bash {filename=".environment"} theme={null}
# Set database environment variables
export DATABASE_URL="postgres://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_PATH}"
```

And that's all you need!

<Note>
  You can fetch the [complete configuration on the example repository](https://github.com/gmoigneu/upsun-rails-8-tutorial/blob/main/.upsun/config.yaml).
</Note>

### Database Configuration

Update `config/database.yml`:

```yaml {filename="config/database.yml"} theme={null}
production:
  url: <%= ENV['DATABASE_URL'] %>
```

### Asset Serving

Rails 8 uses Propshaft for asset compilation, which works seamlessly with Upsun's static file serving:

```ruby {filename="config/environments/production.rb"} theme={null}
config.public_file_server.enabled = true
config.public_file_server.headers = {
  'Cache-Control' => 'public, max-age=31536000'
}
```

## Deploying Your Application

```bash {filename="Terminal"} theme={null}
# Commit your changes
git add .
git commit -m "Ready for deployment"

# Deploy to Upsun
upsun push
```

Once the build and deploy processes are complete, your new environment will be ready to serve traffic.

<Note>
  💡 Remember you can always use `upsun ssh` to connect to the container and `upsun sql` to connect to your database.
</Note>

If you have setup some seeds in `seeds.rb`, ssh to the container and run:

```bash {filename="Terminal"} theme={null}
upsun ssh
rake db:seed
```

## Monitoring and Scaling

Upsun provides built-in monitoring and scaling capabilities:

```bash {filename="Terminal"} theme={null}
# View application logs
upsun logs

# Scale your application
upsun scale web:2
```

## Complete Source Code

Find the complete source code for this tutorial on GitHub: [upsun-rails-8-tutorial](https://github.com/gmoigneu/upsun-rails-8-tutorial)

## Conclusion

While Rails 8 makes self-hosting easier, using a Platform-as-a-Service like Upsun lets you focus on what matters most - building great applications. The time saved on infrastructure management, security updates, and scaling concerns easily justifies the platform cost for most teams.

Happy Ruby coding! 🚀

***
