Deploying a Ruby on Rails app

Hi community!

I did a search but couldn’t find any similar topic.

I am trying to deploy a Ruby on Rails (RoR) app to my VPS (Ubuntu 24.04, Oracle Cloud).

My VPS is running the latest version of Hestia and I am hosting several domains there. I would like to deploy a RoR app to one of those domains (actually to a subdomain like app.test.com).

What’s the best way to do that without breaking everything?

Will this work?

Thank you in advance!

Chris

if you give me an example, I can try to set one up on my homelan.
I know a TINY bit of ruby. Not really, I’m a hack- but I can do anything I need these days.

I can’t gamble with my main VPS with new projects like that right now.

Thank you!

I researched about it and here is what could work. Didn’t test it though.

1. Create the subdomain in HestiaCP

In Hestia:

  1. Add web domain: app.test.com
  2. Enable SSL/Let’s Encrypt.
  3. Confirm it serves a normal placeholder page.
  4. Do not deploy Rails into public_html.

Good structure:

/home/YOUR_HESTIA_USER/web/app.test.com/public_html/
  temporary Hestia placeholder only

/var/www/rails/appname/
  Rails app lives here

2. Create a dedicated Linux user for Rails

sudo adduser --disabled-password --gecos "" deploy
sudo usermod -aG www-data deploy

Do not run Rails as root.

3. Install Ruby with rbenv under the deploy user

The Rails deployment guide found externally recommends rbenv or RVM because system Ruby packages are often outdated.

sudo apt update
sudo apt install -y git curl build-essential libssl-dev libreadline-dev zlib1g-dev \
  libyaml-dev libffi-dev libgdbm-dev libpq-dev pkg-config

sudo su - deploy

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init - bash)"' >> ~/.bashrc
source ~/.bashrc

git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

# choose the Ruby version your app requires
rbenv install 3.3.6
rbenv global 3.3.6

gem install bundler --no-document
ruby -v
bundle -v

exit

Use the Ruby version required by your Gemfile/.ruby-version.

4. Deploy Rails into a release-style directory

The Rails/Puma/Nginx guide recommends a Capistrano-style layout with current, releases, and shared directories for safer deployments and rollback.

Example:

sudo mkdir -p /var/www/rails/appname/{releases,shared}
sudo mkdir -p /var/www/rails/appname/shared/{config,log,tmp/sockets,tmp/pids}
sudo chown -R deploy:deploy /var/www/rails/appname

Deploy one release:

sudo su - deploy

RELEASE=$(date +%Y%m%d%H%M%S)
git clone YOUR_GIT_REPO_URL /var/www/rails/appname/releases/$RELEASE

cd /var/www/rails/appname/releases/$RELEASE

bundle config set --local deployment true
bundle config set --local without 'development test'
bundle install

ln -sfn /var/www/rails/appname/shared/config/database.yml config/database.yml
ln -sfn /var/www/rails/appname/shared/log log
ln -sfn /var/www/rails/appname/releases/$RELEASE /var/www/rails/appname/current

exit

Create your production database.yml and .env in shared/config / shared.

5. Configure Rails production settings carefully

At minimum:

RAILS_ENV=production
SECRET_KEY_BASE=...
RAILS_SERVE_STATIC_FILES=true
RAILS_LOG_TO_STDOUT=true

In config/environments/production.rb, ensure:

config.hosts << "app.test.com"
config.force_ssl = true

If Rails blocks the host, you will see host authorization errors.

6. Configure Puma privately

Safer simple option: bind Puma to localhost port

Use a unique local-only port, for example 127.0.0.1:3101.

/var/www/rails/appname/shared/config/puma.rb:

directory "/var/www/rails/appname/current"
environment ENV.fetch("RAILS_ENV", "production")

workers ENV.fetch("WEB_CONCURRENCY", 2).to_i
threads_count = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
threads threads_count, threads_count

bind "tcp://127.0.0.1:3101"

pidfile "/var/www/rails/appname/shared/tmp/pids/puma.pid"
state_path "/var/www/rails/appname/shared/tmp/pids/puma.state"

stdout_redirect "/var/www/rails/appname/shared/log/puma.stdout.log",
                "/var/www/rails/appname/shared/log/puma.stderr.log",
                true

plugin :tmp_restart

A Unix socket is also good and is commonly recommended for Nginx/Puma on the same machine. But a localhost TCP port is simpler with Hestia templates and avoids socket permission problems. The key is: bind only to 127.0.0.1, never 0.0.0.0.

7. Create a systemd service for Puma

/etc/systemd/system/rails-appname.service:

[Unit]
Description=Rails appname Puma service
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/www/rails/appname/current

Environment=RAILS_ENV=production
EnvironmentFile=/var/www/rails/appname/shared/.env

ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C /var/www/rails/appname/shared/config/puma.rb
ExecReload=/bin/kill -s SIGUSR1 $MAINPID

Restart=on-failure
RestartSec=5
TimeoutStopSec=70
KillMode=mixed
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Enable it:

sudo systemctl daemon-reload
sudo systemctl enable rails-appname
sudo systemctl start rails-appname
sudo systemctl status rails-appname --no-pager

Test locally:

curl -I http://127.0.0.1:3101

Do not continue to Hestia/Nginx until this works.

8. Create a custom Hestia Nginx template for this subdomain

This avoids the duplicate location / risk.

First identify whether your Hestia stack uses:

ls /usr/local/hestia/data/templates/web/nginx/
ls /usr/local/hestia/data/templates/web/nginx/php-fpm/

If you are using Nginx as proxy in front of Apache, templates are usually under:

/usr/local/hestia/data/templates/web/nginx/

Create copied templates:

cd /usr/local/hestia/data/templates/web/nginx

sudo cp default.tpl rails-proxy.tpl
sudo cp default.stpl rails-proxy.stpl

Now edit rails-proxy.stpl so the SSL server block proxies to Puma. A minimal conceptual version is:

server {
    listen      %ip%:%web_ssl_port% ssl;
    server_name %domain_idn% %alias_idn%;

    ssl_certificate     %ssl_pem%;
    ssl_certificate_key %ssl_key%;

    root %home%/%user%/web/%domain%/public_html;

    access_log /var/log/nginx/domains/%domain%.log combined;
    error_log  /var/log/nginx/domains/%domain%.error.log error;

    client_max_body_size 50M;

    location / {
        proxy_pass http://127.0.0.1:3101;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_redirect off;
    }

    location /assets/ {
        proxy_pass http://127.0.0.1:3101;
        proxy_set_header Host $host;
        expires max;
        add_header Cache-Control public;
    }

    location /error/ {
        alias %home%/%user%/web/%domain%/document_errors/;
    }

    include %home%/%user%/conf/web/%domain%/nginx.ssl.conf_*;
}

For HTTP rails-proxy.tpl, either proxy to Puma or redirect to HTTPS. Since Hestia/Let’s Encrypt may need HTTP validation, be careful not to break /.well-known/acme-challenge/. A conservative HTTP template is:

server {
    listen      %ip%:%web_port%;
    server_name %domain_idn% %alias_idn%;

    root %home%/%user%/web/%domain%/public_html;

    access_log /var/log/nginx/domains/%domain%.log combined;
    error_log  /var/log/nginx/domains/%domain%.error.log error;

    location ^~ /.well-known/acme-challenge/ {
        root %home%/%user%/web/%domain%/public_html;
        default_type "text/plain";
    }

    location / {
        return 301 https://$host$request_uri;
    }

    include %home%/%user%/conf/web/%domain%/nginx.conf_*;
}

The include line is important because the template guide states it is the hook that allows conf_* files to be loaded for the domain.

Apply the template only to the subdomain:

sudo /usr/local/hestia/bin/v-change-web-domain-proxy-tpl YOUR_HESTIA_USER app.test.com rails-proxy
sudo /usr/local/hestia/bin/v-rebuild-web-domain YOUR_HESTIA_USER app.test.com

sudo nginx -t
sudo systemctl reload nginx

If nginx -t fails, do not reload. Revert the template assignment:

sudo /usr/local/hestia/bin/v-change-web-domain-proxy-tpl YOUR_HESTIA_USER app.test.com default
sudo /usr/local/hestia/bin/v-rebuild-web-domain YOUR_HESTIA_USER app.test.com
sudo nginx -t && sudo systemctl reload nginx

9. Oracle Cloud firewall/network rules

Because Puma listens on 127.0.0.1:3101, you do not need to open port 3101 in Oracle Cloud.

Only these should be public:

22    SSH, ideally restricted to your IP
80    HTTP
443   HTTPS
8083  Hestia panel, ideally restricted if possible

Oracle’s guidance states that OCI traffic depends on both network rules and the Ubuntu host firewall, and that ports must be opened in both places when external access is required. For this Rails deployment, the safest design is to avoid external access to Puma entirely and expose only Hestia/Nginx on ports 80/443.

10. Deployment/update workflow

For future app updates:

sudo su - deploy

RELEASE=$(date +%Y%m%d%H%M%S)
git clone YOUR_GIT_REPO_URL /var/www/rails/appname/releases/$RELEASE
cd /var/www/rails/appname/releases/$RELEASE

ln -sfn /var/www/rails/appname/shared/config/database.yml config/database.yml
ln -sfn /var/www/rails/appname/shared/log log

bundle config set --local deployment true
bundle config set --local without 'development test'
bundle install

RAILS_ENV=production bundle exec rails db:migrate
RAILS_ENV=production bundle exec rails assets:precompile

ln -sfn /var/www/rails/appname/releases/$RELEASE /var/www/rails/appname/current

exit

sudo systemctl restart rails-appname

Rollback:

sudo su - deploy
ln -sfn /var/www/rails/appname/releases/PREVIOUS_RELEASE /var/www/rails/appname/current
exit

sudo systemctl restart rails-appname