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:
- Add web domain:
app.test.com
- Enable SSL/Let’s Encrypt.
- Confirm it serves a normal placeholder page.
- 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