Deployment: Rails API and React SPA on Digital Ocean with nanobox.io

Mendel Hornbacher
Jan 26, 2018 · 5 min read

Yes, this is another deployment guide.

In this post, we will take a sample rails API with a react SPA nested under a client folder created with create-react-app and deploying it as docker containers using the nanobox platform.

The simple starting point

If you do not have a code base set up, there is a handy guide here that will help you out with that. In general I am assuming that the repository is a simple rails server that has a react app nested under a folder called client .

Getting set up

Nanobox.io relies on a file called boxfile.yml to define the structure of your application. So lets create one.

boxfile.ymlrun.config:
engine: ruby
engine.config:
runtime: ruby-2.3
extra_packages:
- nodejs
- nginx
extra_steps:
- npm install

In plain English, this .yml file states the following

boxfile.yml -> in englishWhen I call run or deploy:
use ruby
use ruby version 2.3
apart from that use the following as well:
1. nodejs (for the react spa)
2. nginx (for the actual server routing)
when the app is run, run the following commands:
1. npm install (more on this later)

At the end of this run config we run npm install on the root directory. Fair warning, this will fail if we do not have a package.json file in the root directory. So lets create one that calls npm install in the client directory after npm install is called.

package.json{
"name": "<app-name>",
"version": "<app-version>",
"scripts": {
"postinstall": "npm install --prefix client"
}
}

Setting up the containers

Now lets add the containers to our boxfile.yml :

...
web.client:
routes:
- /
cwd:
client: client
start:
nginx: nginx -c /app/nginx/client.conf
client: npm start
web.api:
routes:
- api:/
start:
nginx: nginx -c /app/nginx/server.conf
api: bundle exec puma -C config/puma.rb
writeable_dirs:
- tmp
- log
log_watch:
rails: 'log/production.log'
data.db:
image: nanobox/postgresql:9.5

This creates the following containers:

  1. web.client: runs npm start in the client directory an serves the website on web.site.
  2. web.api: runs the rails server with puma on port 3000 under api.web.site.
  3. data.db: a simple postgresql database.

Note: here is the basic template for the nginx config that is used for the server and the client.

nginx/server.conf, nginx/client.confworker_processes 1;
daemon off;
events {
worker_connections 1024;
}
http {
include /data/etc/nginx/mime.types;
sendfile on;
gzip on;
gzip_http_version 1.0;
gzip_proxied any;
gzip_min_length 500;
gzip_disable "MSIE [1-6]\.";
gzip_types text/plain text/xml text/css
text/comma-separated-values
text/javascript
application/x-javascript
application/atom+xml;
# Proxy upstream to the puma/nodejs process
upstream rails {
server 127.0.0.1:3000;
}
# Configuration for Nginx server {
# Listen on port 8080
listen 8080;
root /app/public;
try_files $uri/index.html $uri @rails;
# Proxy connections to rails
location @rails {
proxy_pass http://rails;
proxy_redirect off;
proxy_set_header Host $host;
}
}
}

Performance problems

Running nanobox deploy at this point should spin up a perfectly functional deployment. However, you will notice in your dashboard that your app is using nearly 800 MB of ram with 0–5 users! (And if you only have 512 mb the react app will not even be able to launch.)

Building React SPA and serving as static files

In order to improve performance I went on a journey to serve the react app as static files from nginx. First things first we need to build the app.

First I tried adding a before_live hook to do it on the server. However, the performance problems I mentioned before just got worse! On a digital ocean droplet with 1 GB of ram, the build would still be killed from lack of ram! So I realized that local is the way to go.

After asking around on slack and browsing the docs, it appears that extra_steps runs locally on your machine (and file system is not yet set to read-only) before it is sent to the servers to be deployed.

The steps I found work can be broken down as follows:

  1. Move into the client directory and build the react app before deploying the application (output goes into client/build)
  2. Use an express server to write the environment variables to /conf/env.js so that they can be used in the react app
  3. Set up an express server to return the build directory as static files (and return index.html if the path is not found

Note: express needs to be added to client/package.json.

Here is what needs to be added to the boxfile.yml file:

boxfile.yml...
deploy.config:
extra_steps:
- cd client && npm run build && cd ..
...
web.client
...
writeable_dirs:
- client/config
...

The server.js file (creates a env.js file with specified environment variables):

client/server.jsconst express = require('express'); // Load express to serve the website
const path = require('path'); // Load path to find the files to serve
const fs = require('fs'); // Load fs to write the enviorment variables into a file
const port = process.env.PORT || 3000; // set the default port back to 3000 (8080 causes conflicts in nanobox, do not use)
// set up the express application
const app = express();
// write out the relevent client side enviroment variables to env.js
fs.writeFileSync(
__dirname + '/config/env.js',
'var config = {REACT_APP_API_URL: "' + process.env.REACT_APP_API_URL
+ '"};'
);
// serve the files generated by `react-scripts build`
app.use(express.static(__dirname + '/build'));
// serve the env.js file generated above
app.use(express.static(__dirname + '/config'));
// for all other paths return the index.html file from the build (react application handles all other routes)
app.get('*', function (request, response) {
response.sendFile(path.resolve(__dirname, 'build/index.html'));
});
// set the app to start on port 3000 (or PORT)
app.listen(port);

The index.html file (load the env variables):

client/public/index.html...
<script type="text/javascript" src="/env.js"></script>
</body>
...

And finally an example on how to load an environment variable defined on the server into your react app:

let API_URL = process.env.REACT_APP_API_URL; // use the environment variable in your test environment// in production get the property from the window.config
if (process.env.NODE_ENV === "production"){
API_URL = window.config.REACT_APP_API_URL
}

Note: since it is being set to an internal variable if someone modifies the config variable after the page is loaded it will not change the behavior of the application.

Conclusion

Setting up a rails app set up this way to a live production server that is able to scale on the fly was much harder then anticipated (especially since Heroku does not support multiple HTTP endpoints, yet). I hope that this post makes the process just a little bit simpler.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade