Applications Azure

Ruby 3.4 with Rails 8 on Ubuntu 24.04 on Azure User Guide

| Product: Ruby 3.4 with Rails 8 on Ubuntu 24.04 LTS on Azure

Overview

Ruby on Rails is the convention-over-configuration web framework that pioneered modern MVC web development — ActiveRecord ORM, ActionView templates, ActionCable websockets, ActiveJob background queues, scaffolded CRUD generators. The cloudimg 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/cloudimg/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 cloudimg starter project at /opt/rails/cloudimg includes a scaffolded Article model with full CRUD UI at /articles and an Admin model seeded at first boot.

What is included:

  • 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/cloudimg/vendor/bundle
  • Puma 6 binding to /run/puma/cloudimg.sock (2 workers, 2-8 threads by default)
  • nginx reverse proxy on TCP :80 (proxy_pass to the puma socket, serves /assets/ from public/assets/)
  • PostgreSQL 16 from Ubuntu noble main, with a per-VM cloudimg role + cloudimg_production database
  • cloudimg starter project at /opt/rails/cloudimg with a scaffolded Article resource (full CRUD at /articles)
  • Admin model with has_secure_password (bcrypt) seeded with email: cloudimg
  • Per-VM SECRET_KEY_BASE (64 hex), DB password (32 hex), Admin password (32 hex) rotated at first boot
  • systemd units: puma.service, rails-firstboot.service, plus nginx.service and postgresql.service
  • 24/7 cloudimg support

Prerequisites

Active Azure subscription, SSH key, VNet + subnet. Standard_B2s (4 GB RAM) is comfortable for a 2-worker Puma + Postgres. NSG inbound: 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>

Step 4: Service Status + Versions

sudo systemctl is-active puma.service nginx.service postgresql.service
/opt/ruby/bin/ruby --version
cd /opt/rails/cloudimg && sudo -u rails /opt/ruby/bin/bundle exec bin/rails --version

All four services active and Ruby 3.4.1 + Rails 8.0.5 reported

Step 5: Read Per-VM Credentials

sudo cat /stage/scripts/rails-credentials.log

Pick up RAILS_URL, RAILS_ARTICLES_URL, RAILS_ADMIN_USER, RAILS_ADMIN_EMAIL, RAILS_ADMIN_PASSWORD. 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.

Step 6: 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/cloudimg/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")"; fi

The first two calls hit Puma through nginx. The third proves nginx is serving precompiled assets directly from public/assets/ (faster than going through Rails).

Rails root and /articles return HTTP 200; static asset served by nginx

Step 7: 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.

Rails Articles index — empty list, scaffold UI

Step 8: Create an Article

Click New article, fill in the title and body fields, and submit.

Rails Articles New form with title and body fields

Step 9: View the Created Article

After submission the scaffold redirects to the article detail page, where you can edit or destroy.

Rails Article show page after successful create

Step 10: Run Rails Console / Management Commands

set -a; . /etc/rails/cloudimg.env; set +a
cd /opt/rails/cloudimg
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 — already-applied migrations are no-ops.

Step 11: Add a Rails Generator (model/scaffold)

Replace <Resource> and the field list with your own:

cd /opt/rails/cloudimg
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.service

The scaffold emits routes, a controller, views, and a migration. After db:migrate and assets:precompile, restart Puma so the new code is loaded.

Step 12: Tune Puma Workers

Edit /etc/rails/cloudimg.env to raise PUMA_WORKERS, then restart:

sudo systemctl restart puma.service

Defaults on Standard_B2s:

  • PUMA_WORKERS: 2
  • PUMA_THREADS_MIN: 2
  • PUMA_THREADS_MAX: 8
  • RAILS_ENV: production
  • RAILS_LOG_TO_STDOUT: true
  • RAILS_SERVE_STATIC_FILES: true
  • DB: PostgreSQL 16, role cloudimg, db cloudimg_production

For production raise workers to 2 × CPU + 1 (e.g. 9 on a D4s_v5).

Step 13: Add Your Domain to Allowed Hosts

Rails 8 ships with Rails.application.config.hosts empty by default in production, which means 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.service

For most setups behind nginx no change is needed — the proxy passes the original Host header through and Rails accepts it.

Step 14: Install Let's Encrypt SSL

Point your domain DNS A record at this VM, then:

sudo apt-get install -y python3-certbot-nginx
sudo certbot --nginx -d apps.example.com -m you@example.com --agree-tos --non-interactive --redirect

Certbot edits /etc/nginx/sites-available/rails-cloudimg to add the SSL server block and a :80 → :443 redirect, then reloads nginx. Renewal is via the certbot.timer systemd unit (runs twice daily).

Step 15: Backups

Take a consistent backup with the app running by pg_dump-ing the database, plus 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 cloudimg cloudimg_production | gzip > /var/backups/cloudimg-db-$(date +%F).sql.gz
sudo tar czf /var/backups/cloudimg-storage-$(date +%F).tgz -C /opt/rails/cloudimg storage

Periodically copy /var/backups to Azure Blob Storage (az storage blob upload-batch) for off-VM retention.

Step 16: 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.log

RAILS_LOG_TO_STDOUT=true sends Rails logs to the systemd journal via Puma's stdout. For more verbose output during debugging set RAILS_ENV=development in /etc/rails/cloudimg.env and restart Puma — but never in production, as it disables view caching, exposes stack traces, and reloads classes on every request.

Security

  • RAILS_ENV=production by default — production-safe out of the box (no debug stack traces, view caching on)
  • Per-VM SECRET_KEY_BASE (64 hex chars from openssl rand)
  • Puma binds to a Unix socket (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 via has_secure_password
  • Credentials log at /stage/scripts/rails-credentials.log is mode 0600 (root-only)
  • Restrict NSG inbound on :80 to the IP ranges that need the Rails app
  • Install python3-certbot-nginx for Let's Encrypt automation (Step 14)

Support

cloudimg provides 24/7/365 expert technical support. Guaranteed response within 24 hours, one hour average for critical issues. Contact support@cloudimg.co.uk.