Deploy Rails 7 with Docker and Nginx
Tags: rails, docker, nginx, devops
Published: 2021-10-03 20:36:52

The easiest solution to deploy a Rails application is something like [Heroku](https://heroku.com) or [Hatchbox](https://www.hatchbox.io/) or [DigitalOcean Apps](https://www.digitalocean.com/products/app-platform/). But for some small projects I like to use my existing VPS. 

I assume you have a VPS with Ubuntu 20.04 and Docker and Nginx installed.
If you use [DigitalOcean](https://m.do.co/c/238c76f53b4f) you can select the Docker image in the marketplace and add Nginx to it.

## New Rails 7 app

Lets create a new Rails project with [PostgreSQL](https://www.postgresql.org/), [esbuild](https://esbuild.github.io/) and [Tailwind](https://tailwindcss.com/) on your local machine:
```shell
rails new demo -d postgresql --edge -j esbuild --css tailwind
```

Adjust you config/database.yml with the settings for your database.

Scaffold a simple table:
```shell
bin/rails g scaffold Book name:string
```

Then create your database and tables:
```shell
bin/rails db:create
bin/rails db:migrate
```

You can create a root path in routes.rb:
```ruby
root "books#index"
```

Now you can start the website with:
```shell
bin/dev
```


## Docker

Let's go to the VPS. I transfer my code with Github.  
For large projects you would probably use CI, but this is just a small project.

I prefer to create a small shell script to do the build steps and start rails.
This is the content of `bin/prod`:

```shell
#!/usr/bin/env bash
export RAILS_ENV=production
bundle install
yarn install
yarn build
yarn build:css
bin/rails assets:precompile
bin/rails server -b 0.0.0.0
```

Let's make it executable with: `chmod a+x bin/prod`

Now create a Dockerfile:

```docker
FROM ruby:3
RUN apt-get update -qq && apt-get install -y nodejs npm postgresql-client
RUN npm install -g yarn
RUN gem update --system

# use a global path instead of vendor
ENV GEM_HOME="/usr/local/bundle"
ENV BUNDLE_PATH="$GEM_HOME"
ENV BUNDLE_SILENCE_ROOT_WARNING=1
ENV BUNDLE_APP_CONFIG="$GEM_HOME"
ENV PATH="$GEM_HOME/bin:$BUNDLE_PATH/gems/bin:${PATH}"

# make 'docker logs' work
ENV RAILS_LOG_TO_STDOUT=true

# copy the source
WORKDIR /app
COPY . /app
RUN rm -f tmp/pids/server.pid
RUN bundle install

# build and start
CMD ["bin/prod"]
```

The master.key file is not in git for safety. 
There are several solutions for this, but I just recreate the file on the server:
```shell
echo "30acf9tralalalalala7af75eb7" > config/master.key
```

Now it's time to create the docker image.  
Run this inside the root folder of the demo project:
```shell
docker build -t demo:0.0.1 .
```

You should now see the image with `docker images`.

Lets run it:
```shell
docker run -d -p 3001:3000 --name demo --env RAILS_ENV=production -v ~/demo:/app demo:0.0.1
```

The docker container is exposing port 3000, but I map that to 3001 since I already have an other website running on port 3000.

You should probably have a seperate Postgres server, but I also run that inside a Docker.
To allow access to this container I create a seperate network and add the two containers in it.
In database.yml you can than use postgres_container as host.
```shell
docker network create demo_network
docker network connect demo_network demo
docker network connect demo_network postgres_container
```

To create the database and tables:
```shell
docker exec demo bin/rails db:create
docker exec demo bin/rails db:migrate
```

In case of errors you can use `docker logs demo` to find the error.

## Nginx

I use Nginx as a proxy to the different Rails projects and to load the assets directly.
To create a new configuration:
```shell
sudo vi /etc/nginx/sites-available/demo
```

And this is the content: (you need to change the domain and paths)
```nginx
upstream demo {
  server localhost:3001;
}

server {
  server_name demo.example.org;

  root /home/user/demo/public;
  access_log /home/user/demo/log/nginx.access.log;
  error_log /home/user/demo/log/nginx.error.log info;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @demo;
  location @demo {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header  X-Forwarded-Proto $scheme;
    proxy_set_header  X-Forwarded-Ssl on; # Optional
    proxy_set_header  X-Forwarded-Port $server_port;
    proxy_set_header  X-Forwarded-Host $host;

    proxy_redirect off;

    proxy_pass http://demo;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 100M;
  keepalive_timeout 10;
}
```

Enable it:
```shell
sudo ln -s /etc/nginx/sites-available/demo /etc/nginx/sites-enabled/demo
```

Test the configuration:
```shell
sudo nginx -t
```

And restart Nginx:
```shell
sudo systemctl restart nginx
```

You should now have a working website.
It's a good idea to add Let's Encrypt with the certbot tool.
This is explained [here](https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx).