Ruby 3
Overview
Ruby on Rails is the convention-over-configuration web framework that pioneered modern MVC web development. It provides the ActiveRecord ORM, ActionView templates, ActionCable websockets, ActiveJob background queues and scaffolded CRUD generators.
This SecureAnyCloud image installs Ruby 3.4.1 (source-compiled via ruby-build into /opt/ruby) and Rails 8.0.5, with gems vendored under /opt/rails/sac/vendor/bundle. Puma 6 fronts the app on a Unix socket, nginx reverse-proxies on TCP :80 and serves compiled assets directly, and PostgreSQL 16 is the production database. A SecureAnyCloud starter project at /opt/rails/sac includes a scaffolded Article model with a full CRUD UI at /articles and an Admin model seeded at first boot.
What you get:
- Ruby 3.4.1 (BSD 2-Clause + custom Ruby License) source-compiled at
/opt/ruby, with/usr/local/bin/{ruby,gem,bundle}symlinks - Rails 8.0.5 (MIT) with gems vendored under
/opt/rails/sac/vendor/bundle - Puma 6 binding to
/run/puma/sac.sock(2 workers, 2-8 threads by default) - nginx reverse proxy on TCP
:80(proxy_pass to the puma socket, serves/assets/frompublic/assets/) - PostgreSQL 16 from Ubuntu noble main, with a per-VM SecureAnyCloud role and a sac_production database
- SecureAnyCloud starter project at /opt/rails/sac with a scaffolded Article resource (full CRUD at /articles)
- Admin model with has_secure_password (bcrypt) seeded with email: SecureAnyCloud
- Per-VM
SECRET_KEY_BASE(64 hex), DB password (32 hex) and Admin password (32 hex), rotated at first boot - systemd units
puma.service,rails-firstboot.service,nginx.serviceandpostgresql.service - 24/7 SecureAnyCloud support
Prerequisites
You need an active Azure subscription, an SSH key, and a VNet with a subnet.
Standard_B2s (4 GB RAM) is comfortable for a 2-worker Puma and PostgreSQL setup.
Configure the NSG inbound rules to allow 22/tcp from your management CIDR and 80/tcp from any client CIDR that needs the Rails UI.
Step 1-3: Deploy + SSH (standard pattern)
ssh azureuser@<vm-ip>Service Status + Versions
sudo systemctl is-active puma.service nginx.service postgresql.service /opt/ruby/bin/ruby --version cd /opt/rails/sac && sudo -u rails /opt/ruby/bin/bundle exec bin/rails --versionRead Per-VM Credentials
sudo cat /stage/scripts/rails-credentials.logThe file lists RAILS_URL, RAILS_ARTICLES_URL, RAILS_ADMIN_USER, RAILS_ADMIN_EMAIL and RAILS_ADMIN_PASSWORD. It also includes the per-VM RAILS_DB_NAME, RAILS_DB_USER and RAILS_DB_PASSWORD. The file is mode 0600 and readable only by root.
HTTP Health + Static Files
curl -sf -o /dev/null -w 'root: HTTP %{http_code}\n' http://127.0.0.1/
curl -sf -o /dev/null -w '/articles: HTTP %{http_code}\n' http://127.0.0.1/articles
ASSET=$(sudo find /opt/rails/sac/public/assets -type f \( -name '*.css' -o -name '*.js' \) 2>/dev/null | head -1) if [ -n "$ASSET" ]; then
curl -sf -o /dev/null -w "asset: HTTP %{http_code}\n" "http://127.0.0.1/assets/$(basename "$ASSET")"; fiThe first two calls reach Puma through nginx. The third call confirms that nginx serves precompiled assets directly from public/assets/, which is faster than going through Rails.
Browse the Rails Articles Index
Browse to http://<vm-ip>/. The root path routes to articles#index. On a fresh boot, the list is empty.
Create an Article
Select New article, fill in the title and body fields, and submit the form.
View the Created Article
After you submit the form, the scaffold redirects to the article detail page, where you can edit or destroy the article.
Run Rails Console / Management Commands
set -a; . /etc/rails/sac.env; set +a cd /opt/rails/sac
sudo -E -u rails /opt/ruby/bin/bundle exec bin/rails runner 'puts "article-count:" + Article.count.to_s'
sudo -E -u rails /opt/ruby/bin/bundle exec bin/rails runner 'puts "admin-count:" + Admin.count.to_s'For an interactive console, use sudo -E -u rails /opt/ruby/bin/bundle exec bin/rails console. bin/rails db:migrate is idempotent, so already-applied migrations are no-ops.
Add a Rails Generator (model/scaffold)
Replace <Resource> and the field list with your own:
cd /opt/rails/sac
sudo -u rails /opt/ruby/bin/bundle exec bin/rails generate scaffold Comment body:text article:references
sudo -u rails /opt/ruby/bin/bundle exec bin/rails db:migrate
sudo -u rails /opt/ruby/bin/bundle exec bin/rails assets:precompile
sudo systemctl restart puma.serviceThe scaffold generates routes, a controller, views and a migration. After db:migrate and assets:precompile, restart Puma so that it loads the new code.
Tune Puma Workers
Edit /etc/rails/sac.env to raise PUMA_WORKERS, then restart:
sudo systemctl restart puma.serviceThe defaults on Standard_B2s are:
- PUMA_WORKERS: 2
PUMA_THREADS_MIN:2PUMA_THREADS_MAX:8- RAILS_ENV: production
RAILS_LOG_TO_STDOUT:trueRAILS_SERVE_STATIC_FILES:true- DB: PostgreSQL 16, role SecureAnyCloud, db sac_production
For production, raise the worker count to 2 × CPU + 1 (for example, 9 on a D4s_v5).
Add Your Domain to Allowed Hosts
Rails 8 ships with Rails.application.config.hosts empty by default in production, so host authorization is not enforced (unlike Django's strict ALLOWED_HOSTS). If you enable host authorization, add your domain in config/environments/production.rb:
Rails.application.config.hosts << "apps.example.com"Then restart Puma:
sudo systemctl restart puma.serviceMost setups behind nginx need no change, because the proxy passes the original Host header through and Rails accepts it.
Install Let's Encrypt SSL
Point your domain's DNS A record at this VM, then run Certbot:
sudo apt-get install -y python3-certbot-nginx
sudo certbot --nginx -d apps.example.com -m you@example.com --agree-tos --non-interactive --redirectCertbot edits /etc/nginx/sites-available/rails-sac to add the SSL server block and a :80 → :443 redirect, then reloads nginx. The certbot.timer systemd unit handles renewal and runs twice daily.
Backups
To take a consistent backup while the app is running, dump the database with pg_dump and archive the ActiveStorage storage/ directory:
PASS=$(sudo grep '^RAILS_DB_PASSWORD=' /stage/scripts/rails-credentials.log | cut -d= -f2-) PGPASSWORD="$PASS" pg_dump -h 127.0.0.1 -U sac sac_production | gzip > /var/backups/sac-db-$(date +%F).sql.gz
sudo tar czf /var/backups/sac-storage-$(date +%F).tgz -C /opt/rails/sac storageCopy /var/backups to Azure Blob Storage (az storage blob upload-batch) on a regular schedule for off-VM retention.
Logs and Troubleshooting
sudo journalctl -u puma.service --no-pager -n 80
sudo journalctl -u nginx.service --no-pager -n 30
sudo journalctl -u postgresql.service --no-pager -n 30
sudo tail -50 /var/log/nginx/access.log
sudo tail -50 /var/log/nginx/error.logRAILS_LOG_TO_STDOUT=true sends Rails logs to the systemd journal through Puma's stdout. For more verbose output while debugging, set RAILS_ENV=development in /etc/rails/sac.env and restart Puma. Never do this in production, because it disables view caching, exposes stack traces and reloads classes on every request.
Security
RAILS_ENV=productionby default, so the image is production-safe out of the box (no debug stack traces, view caching on).- Per-VM
SECRET_KEY_BASE(64 hex chars fromopenssl rand). - Puma binds to a Unix socket and is not exposed to the network.
- PostgreSQL listens on
127.0.0.1:5432(loopback only). - Per-VM DB password (32 hex chars).
- Per-VM Admin password (32 hex chars), bcrypt-hashed through
has_secure_password. - The credentials log at
/stage/scripts/rails-credentials.logis mode0600(root-only). - Restrict NSG inbound on
:80to the IP ranges that need the Rails app. - Install
python3-certbot-nginxfor Let's Encrypt automation (Step 14).
Support
SecureAnyCloud provides 24/7/365 expert technical support, with a guaranteed response within 24 hours and a one hour average for critical issues. Contact sales@secureanycloud.com.
Deploy on Azure
Launch Ruby 3.4 with Rails 8 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com