Skip to content
Draft
6 changes: 6 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ export default defineConfig({
{ text: 'shadcn/ui', link: '/cookbook/integrating-shadcn-ui' },
],
},
{
text: 'Deployment',
items: [
{ text: 'Deploy with Kamal', link: '/cookbook/deploy-with-kamal' },
],
},
],
},
],
Expand Down
233 changes: 233 additions & 0 deletions docs/cookbook/deploy-with-kamal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# Deploy with `Kamal`

Rails 8 will ship with [Kamal](https://kamal-deploy.org/) preconfigured as the default deployment tool.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true when you generate a new app with rails new -j esbuild or other builders. By default (with importmap), you'll be missing Node.js-related dependencies.

Copy link

@mpressen mpressen Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment related to this line though ?
'Cause I'm pretty sure that Kamal is the default deployment tool for rails 8, even without -j esbuild flag.

My guess is that your comment relates to the following lines (l.4-5) :bowtie: :
If your application does not require [SSR](/guide/server-side-rendering.md), you simply just need to [update your asset_path](#update-asset-path-inconfig-deploy-yml), and deployment should work seamlessly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I ment to comment the next line 😅

Copy link
Contributor Author

@brodienguyen brodienguyen Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skryukov I did update my PR to highlight the difference between the new Dockerfile compared to the fresh default Rails 8 one. The preview of the new docs have been updated in the PR description as well. Could you help me to re-review it, please?

If your application does not require [SSR](/guide/server-side-rendering.md), you simply just need to
[update your asset_path](#update-asset-path-inconfig-deploy-yml), and deployment should work seamlessly.

However, if you plan to configure your Inertia Rails application with [SSR](/guide/server-side-rendering.md) enabled,
a few additional tweaks may be required. This guide will walk you through the steps to quickly configure
[Kamal](https://kamal-deploy.org/) for deploying your next Inertia Rails application with
[SSR](/guide/server-side-rendering.md) support.

> Note: This guide is based on Rails 8.0 and Kamal 2.3.0 at the time of writing.


## Update your Dockerfile

It is crucial to ensure that the **_Install JavaScript dependencies_** step is executed in the **_base_** image. This
guarantees that the Node.js runtime is available for both the **_build_** stage and the **_runtime_** stage.

```dockerfile
# syntax=docker/dockerfile:1
# check=error=true

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t fresh_rails .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name fresh_rails fresh_rails

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.3.6
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install JavaScript dependencies // [!code ++]
ARG NODE_VERSION=22.11.0 // [!code ++]
ARG YARN_VERSION=1.22.22 // [!code ++]
ENV PATH=/usr/local/node/bin:$PATH // [!code ++]
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ // [!code ++]
/tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ // [!code ++]
npm install -g yarn@$YARN_VERSION && \ // [!code ++]
rm -rf /tmp/node-build-master // [!code ++]

# Set production environment
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems and node modules // [!code ++]
# Install packages needed to build gems // [!code --]
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git libpq-dev node-gyp pkg-config python-is-python3 && \ // [!code ++]
apt-get install --no-install-recommends -y build-essential git pkg-config && \ // [!code --]
rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY .ruby-version Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile

# Install node modules // [!code ++]
COPY package.json yarn.lock ./ // [!code ++]
RUN yarn install --frozen-lockfile // [!code ++]

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

RUN rm -rf node_modules // [!code ++]


# Final stage for app image
FROM base

# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER 1000:1000

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
```


## Setup server role to run SSR server in `config/deploy.yml`

The Node-based Inertia SSR server is used to pre-render pages on the server before sending them to the client.
The `vite_ssr` role ensures that the SSR server runs separately from the main Rails app server.

```yml
# Deploy to these servers.
servers:
web:
- 192.168.0.1
vite_ssr: // [!code ++]
hosts: // [!code ++]
- 192.168.0.1 // [!code ++]
cmd: bundle exec vite ssr // [!code ++]
options: // [!code ++]
network-alias: vite_ssr // [!code ++]
# job:
# hosts:
# - 192.168.0.1
# cmd: bin/jobs
```


## Specify the Vite server in `config/deploy.yml`

The Rails app needs to know where to send SSR requests. Add the `VITE_RUBY_HOST` environment variable
to ensure your Rails application can connect to the correct SSR server. The value **_VITE_RUBY_HOST: "vite_ssr"_**
must match the **_network-alias_** defined in the `vite_ssr` role above.

```yml
# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
secret:
- RAILS_MASTER_KEY
clear:
# Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
# When you start using multiple servers, you should split out job processing to a dedicated machine.
SOLID_QUEUE_IN_PUMA: true

VITE_RUBY_HOST: "vite_ssr" // [!code ++]

# Set number of processes dedicated to Solid Queue (default: 1)
# JOB_CONCURRENCY: 3

# Set number of cores available to the application on each server (default: 1).
# WEB_CONCURRENCY: 2

# Match this to any external database server to configure Active Record correctly
# Use inertia_rails_svelte5_ssr-db for a db accessory server on same machine via local kamal docker network.
# DB_HOST: 192.168.0.2

# Log everything from Rails
# RAILS_LOG_LEVEL: debug

```


## Update asset_path in`config/deploy.yml`

Update the asset_path to `/rails/public/vite` if you haven't.

```yml
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets // [!code --]
asset_path: /rails/public/vite // [!code ++]
```


## Ensure that your `vite.config.ts` is configured to support SSR

Configure Vite with an `ssr` block in your `vite.config.ts` file to ensures all dependencies are bundled for SSR.

```js
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { defineConfig } from 'vite'
import ViteRails from "vite-plugin-rails"

export default defineConfig({
ssr: {// [!code ++]
noExternal: true,// [!code ++]
},// [!code ++]
plugins: [
svelte(),
ViteRails({
envVars: { RAILS_ENV: "development" },
envOptions: { defineOn: "import.meta.env" },
fullReload: {
additionalPaths: [],
},
}),
],
})
```

## Configure SSR URL in the Inertia's Rails adapter

To enable Server-Side Rendering (SSR) in your Inertia Rails application, you need to specify
the correct SSR server URL in the adapter. By default, the adapter points to `http://localhost:13714`,
but this must align with the **_VITE_RUBY_HOST_** value defined in your `deploy.yml` when we deploy it to production.

Update the configuration in `config/initializers/inertia_rails.rb` to dynamically construct the
SSR URL using Vite Ruby's protocol, host, and port settings:

```ruby
InertiaRails.configure do |config|
config.ssr_enabled = ViteRuby.config.ssr_build_enabled
config.ssr_url = "#{ViteRuby.config.protocol}://#{ViteRuby.config.host}:13714" # [!code ++]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ElMassimo I saw you had defined default value for ssr_url option at lib/inertia_rails/configuration.rb as http://localhost:13714.

Should we dynamically generate it using this approach please?

ssr_url: "#{ViteRuby.config.protocol}://#{ViteRuby.config.host}:13714"

Given it results the same value but works out of the box for all environments.

BrodieMFL 2024-11-29 at 22 20 58

Copy link
Contributor

@ElMassimo ElMassimo Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, the default value of ssr_url was added in:


inertia_rails does not depend on vite_ruby, nor is vite_ruby so commonly used that it would make sense to auto-detect it and use it as a default.

In addition, I would suggest to avoid using ViteRuby.config, as it refers to the configuration for the Vite Dev Server, and not the SSR server.

Even the protocol might differ in many apps:

  • In many cases it's desirable to run the Vite dev server over SSL so that HTTP2 can be used and connections are multiplexed (loading unbundled files faster in development).
  • In contrast, a common setup for running the Inertia SSR server is for that to be in the same box using plain HTTP (without SSL).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guess this part is a leftover from when I tried to understand what bin/vite ssr does (which is basically just a shortcut) 😅

I think it's better to use something like this instead: ssr_url: ENV.fetch('INERTIA_SSR_URL', 'http://localhost:13714') and set INERTIA_SSR_URL in the Kamal deployment config (any suggestions for a better name?).

Copy link
Contributor Author

@brodienguyen brodienguyen Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ElMassimo, @skryukov for your detailed discussions and suggestions. I agree with all those valid points and prepare another PR to allow to set Inertia SSR URL via the ENV variable:

config.version = ViteRuby.digest
end
```


## Deploy and enjoy 🎉

Once everything is set up, you can deploy your application by running:

* `kamal setup` (if you haven’t provisioned the server yet).
* `kamal deploy` (to deploy your application).

In just a few minutes, your application will be live and ready, complete with SSR support! 🎉
Good luck, and happy deploying! 🚀