Rails API-only Applications
Rails API-only Applications are composed of two parts: a streamlined Rails server typically serving JSON responses, and a JavaScript client, typically using a framework such as React, Vue, or Angular.
Minimal Dockerfile
Below is a Dockerfile that deploys a Create React App client with a Rails API server:
# syntax = docker/dockerfile:1
FROM node:slim AS react
RUN npx create-react-app client
RUN cd client; npm run build
FROM ruby:slim AS build
RUN apt-get update &&\
apt-get install --yes build-essential git
RUN gem install rails
RUN rails new demo --minimal --skip-active-record --api
FROM ruby:slim
COPY --from=build /demo /demo
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=react /client/build /demo/public
WORKDIR demo
ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
EXPOSE 3000
CMD bin/rails server
Differences from a minimal Rails app:
- A
react
build stage based on thenode:slim
image and consisting of twoRUN
steps: one that creates a react app, and one that builds it. - An
--api
flag is added to therails new
command. - A
COPY
statement copies the build artifacts to the demo application’s public directory. RAILS_SERVE_STATIC_FILES
is set to true. This is necessary as[[statics]]
won’t find index.html files at the root. Even so, adding a statics section to thetoml
file is useful for the remainder of the bundled assets:[[statics]] guest_path = "/demo/public" url_prefix = "/"
If the above is deployed you will see a spinning react logo.
Example
Following is a example that demonstrates a React client working with a Rails API server. First the React client:
# syntax = docker/dockerfile:1
FROM node:slim AS react
RUN npx create-react-app client
RUN node --version > client/.node-version
COPY <<-"EOF" client/src/App.js
import logo from './logo.svg';
import './App.css';
import React, { useState, useEffect } from 'react';
function App() {
let [versions, setVersions] = useState('loading...');
useEffect(() => {
fetch('api/versions')
.then(response => response.json())
.then(versions => {
setVersions(Object.entries(versions)
.map(([name, version]) => `${name}: ${version}`).join(', ')
)
});
});
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>{ versions }</p>
</header>
</div>
);
}
export default App;
EOF
RUN cd client; npm run build
FROM ruby:slim AS build
RUN apt-get update &&\
apt-get install --yes build-essential git
RUN gem install rails
RUN rails new demo --minimal --skip-active-record --api
FROM ruby:slim
COPY --from=build /demo /demo
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=react /client/build /demo/public
COPY --from=react /client/.node-version /demo
WORKDIR demo
RUN bin/rails generate controller Api versions
COPY <<-"EOF" app/controllers/api_controller.rb
class ApiController < ApplicationController
def versions
render json: {
ruby: RUBY_VERSION,
rails: Rails::VERSION::STRING,
node: IO.read('.node-version').strip.sub(/^v/, '')
}
end
end
EOF
ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
EXPOSE 3000
CMD bin/rails server
The above captures the node version into a file, and defines a React hook that fetches version information from the API server and renders the rotating React logo followed by this version information.
Now the server implementation:
# syntax = docker/dockerfile:1
FROM node:slim AS react
RUN npx create-react-app client
RUN node --version > client/.node-version
COPY <<-"EOF" client/src/App.js
import logo from './logo.svg';
import './App.css';
import React, { useState, useEffect } from 'react';
function App() {
let [versions, setVersions] = useState('loading...');
useEffect(() => {
fetch('api/versions')
.then(response => response.json())
.then(versions => {
setVersions(Object.entries(versions)
.map(([name, version]) => `${name}: ${version}`).join(', ')
)
});
});
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>{ versions }</p>
</header>
</div>
);
}
export default App;
EOF
RUN cd client; npm run build
FROM ruby:slim AS build
RUN apt-get update &&\
apt-get install --yes build-essential git
RUN gem install rails
RUN rails new demo --minimal --skip-active-record --api
FROM ruby:slim
COPY --from=build /demo /demo
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=react /client/build /demo/public
COPY --from=react /client/.node-version /demo
WORKDIR demo
RUN bin/rails generate controller Api versions
COPY <<-"EOF" app/controllers/api_controller.rb
class ApiController < ApplicationController
def versions
render json: {
ruby: RUBY_VERSION,
rails: Rails::VERSION::STRING,
node: IO.read('.node-version').strip.sub(/^v/, '')
}
end
end
EOF
ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
EXPOSE 3000
CMD bin/rails server
This consists of an additional COPY
statement to add the .node-version
information to the image, and a controller that returns various
version strings as a JSON object.
Recap
From a Fly.io perspective what you have is a standard Rails application with an additional build step. That build step bundles the client into static assets (HTML, CSS, and JavaScript).