Ruby on Rails remains a productive choice for full-stack web applications, particularly for teams that value convention-over-configuration and rapid development velocity. Deployed on a Hong Kong VPS with CN2 GIA routing, a Rails application with Puma, Nginx, and Sidekiq provides a proven production stack that scales from startup to thousands of concurrent users.
Step 1: Install Ruby via rbenv
<code">apt update && apt upgrade -y apt install -y git curl libssl-dev libreadline-dev zlib1g-dev \ autoconf bison build-essential libyaml-dev libncurses5-dev \ libffi-dev libgdbm-dev libjemalloc-dev nginx postgresql \ postgresql-contrib redis-server certbot python3-certbot-nginx ufw # Create deploy user useradd -m -s /bin/bash deploy # Install rbenv as deploy user su - deploy git clone https://github.com/rbenv/rbenv.git ~/.rbenv git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc echo 'eval "$(rbenv init -)"' >> ~/.bashrc source ~/.bashrc # Install Ruby 3.3 (latest stable) rbenv install 3.3.3 rbenv global 3.3.3 # Install Bundler gem install bundler --no-document ruby --version # Should show ruby 3.3.3
Step 2: Database Setup
<code">exit # Back to root sudo -u postgres psql << 'SQL' CREATE DATABASE myapp_production; CREATE USER myapp_user WITH PASSWORD 'StrongRailsDBPass!'; GRANT ALL PRIVILEGES ON DATABASE myapp_production TO myapp_user; ALTER DATABASE myapp_production OWNER TO myapp_user; SQL
Step 3: Deploy Rails Application
<code">su - deploy mkdir -p /var/www/myapp && cd /var/www/myapp git clone https://github.com/your-org/myapp.git . bundle config set --local without 'development test' bundle install # Configure credentials cat > /var/www/myapp/.env << 'EOF' RAILS_ENV=production SECRET_KEY_BASE=GENERATE_WITH_rails_secret DATABASE_URL=postgresql://myapp_user:StrongRailsDBPass!@localhost/myapp_production REDIS_URL=redis://:RedisPass@localhost:6379/0 EOF chmod 600 /var/www/myapp/.env
Generate Secret Key Base
<code">bundle exec rails secret # Copy this value to SECRET_KEY_BASE in .env
Run Database Migrations and Precompile Assets
<code">source .env bundle exec rails db:migrate RAILS_ENV=production bundle exec rails assets:precompile RAILS_ENV=production
Step 4: Configure Puma
<code">cat > /var/www/myapp/config/puma.rb << 'EOF'
workers ENV.fetch("PUMA_WORKERS") { 2 }
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
preload_app!
rackup DefaultRackup
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "production" }
pid "tmp/pids/server.pid"
# Socket for Nginx
bind "unix:///var/www/myapp/tmp/sockets/puma.sock"
on_worker_boot do
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end
before_fork do
ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
end
EOF
mkdir -p /var/www/myapp/tmp/{pids,sockets,cache,log}Systemd Service for Puma
<code">exit # Back to root cat > /etc/systemd/system/puma.service << 'EOF' [Unit] Description=Puma HTTP Server for myapp After=network.target postgresql.service redis.service [Service] Type=simple User=deploy WorkingDirectory=/var/www/myapp EnvironmentFile=/var/www/myapp/.env ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C /var/www/myapp/config/puma.rb ExecReload=/bin/kill -USR1 $MAINPID Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF systemctl enable --now puma systemctl status puma
Step 5: Nginx Configuration
<code">cat > /etc/nginx/sites-available/myapp << 'EOF'
upstream puma {
server unix:///var/www/myapp/tmp/sockets/puma.sock fail_timeout=0;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
root /var/www/myapp/public;
# Serve static assets directly
location ~* \.(ico|css|js|gif|jpe?g|png|svg|woff2|ttf)$ {
expires max;
add_header Cache-Control "public, immutable";
gzip_static on;
try_files $uri =404;
}
# Rails asset pipeline output
location /assets {
gzip_static on;
expires max;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location / {
try_files $uri/index.html $uri @puma;
}
location @puma {
proxy_pass http://puma;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;
proxy_read_timeout 60s;
}
client_max_body_size 50m;
keepalive_timeout 10;
error_page 500 502 503 504 /500.html;
}
EOF
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
certbot --nginx -d yourdomain.com -d www.yourdomain.com
nginx -t && systemctl reload nginxStep 6: Sidekiq Background Jobs
<code">cat > /etc/systemd/system/sidekiq.service << 'EOF' [Unit] Description=Sidekiq Background Worker After=network.target postgresql.service redis.service [Service] Type=simple User=deploy WorkingDirectory=/var/www/myapp EnvironmentFile=/var/www/myapp/.env ExecStart=/home/deploy/.rbenv/shims/bundle exec sidekiq -e production -C config/sidekiq.yml Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF
<code">cat > /var/www/myapp/config/sidekiq.yml << 'EOF' :concurrency: 5 :queues: - [critical, 3] - [default, 2] - [low, 1] :logfile: log/sidekiq.log EOF systemctl enable --now sidekiq
Example Sidekiq Job
<code"># app/jobs/order_confirmation_job.rb
class OrderConfirmationJob < ApplicationJob
queue_as :critical
def perform(order_id)
order = Order.find(order_id)
OrderMailer.confirmation(order).deliver_now
order.update(confirmation_sent_at: Time.current)
end
end
# Enqueue from controller:
OrderConfirmationJob.perform_later(order.id)Step 7: Deployment with Capistrano
<code"># Gemfile — add Capistrano gem 'capistrano', require: false gem 'capistrano-rbenv', require: false gem 'capistrano-rails', require: false gem 'capistrano3-puma', require: false bundle exec cap install STAGES=production
<code"># config/deploy.rb lock "~> 3.19" set :application, "myapp" set :repo_url, "git@github.com:your-org/myapp.git" set :branch, "main" set :deploy_to, "/var/www/myapp" set :linked_files, %w[.env] set :linked_dirs, %w[log tmp/pids tmp/cache tmp/sockets public/uploads] set :keep_releases, 5 # config/deploy/production.rb server "YOUR_VPS_IP", user: "deploy", roles: %w[app db web], port: 22222
<code"># Deploy from local machine: bundle exec cap production deploy # Capistrano: # - Connects to VPS via SSH # - Pulls latest code from Git # - Installs gems (production only) # - Runs db:migrate # - Precompiles assets # - Symlinks linked files (.env) # - Restarts Puma gracefully (zero-downtime)
Rails Performance Optimisations for Asia-Pacific
<code"># config/environments/production.rb
# HTTP caching headers for Nginx proxy cache
config.public_file_server.headers = {
'Cache-Control' => 'public, max-age=31536000'
}
# Fragment caching with Redis
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
connect_timeout: 30,
read_timeout: 0.2,
write_timeout: 0.2,
reconnect_attempts: 1,
}
# Database connection pool optimisation
# In config/database.yml:
production:
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
checkout_timeout: 5
prepared_statements: true # Significant performance boost with PgBouncer disabledMonitoring Rails in Production
<code"># Check Puma status systemctl status puma cat /var/www/myapp/tmp/pids/server.pid | xargs ps -p # Check Sidekiq systemctl status sidekiq tail -f /var/www/myapp/log/sidekiq.log # Rails production log tail -f /var/www/myapp/log/production.log | grep -E "ERROR|WARN|Completed 5" # Database connections sudo -u postgres psql -c "SELECT count(*), state FROM pg_stat_activity GROUP BY state;"
Conclusion
Ruby on Rails with Puma, Sidekiq, and Nginx on a Hong Kong VPS provides a mature, battle-tested production stack. Puma’s multi-threaded architecture handles concurrent requests efficiently, Sidekiq processes background jobs reliably via Redis, and Capistrano automates zero-downtime deployments. CN2 GIA routing delivers your Rails application to mainland Chinese users at 30–60ms response times — a far better experience than Rails applications hosted in the US or Europe.
Deploy your Rails app: Browse Server.HK Hong Kong VPS plans — a 4 GB plan with 4 vCPU handles Puma, Sidekiq, and PostgreSQL for most Rails production workloads.