Deploying Home Cooked Apps with Rails

24 Nov 2024

Shreyas Prakash headshot

Shreyas Prakash

As a Rails enthusiast, I’ve always wanted a better deployment solution to house my hobby projects. It was not that there was no good solution available: We have AWS, Heroku, Hatchbox, Fly, Render.io and various other such PaaS alternatives.

AWS has been too complex personally to build side projects. That, and the +500% markup.

All these PaaS providers were ultimately wrappers sitting on top of the SaaS applications, and I felt that one could do away with this. The opening keynote by DHH on Rails World 2024 seemed messianic, as he was talking about the same issue of wrappers on top of other wrappers, spiking up the cost of deploying even simple apps. What made this keynote exciting was the unravelling of Kamal, and how it abstracts the complexity of deployment out of the picture.

Builders have to build. Builders don’t have to necessarily be DevOps engineers to build. I was tempted to try this sooner, as I was painfully frustrated by my experience in deploying Rails app on Heroku.

In the following blog/tutorial, I’ll take you through my process of deploying a Rails 8 app on Hetzner VPS using Kamal 2, deploying directly to the Hetzner VPS.

  • I chose SQLite as the database for production (as this comes as a defacto standard for Rails 8 applications).
  • I chose Hetzner VPS, as it seemed to be the most cost effective solution (for ~4/month)(ComparedtoHerokuwhichmightevencometo/month) (Compared to Heroku which might even come to 300/month with the database addon costs)

Setting up Hetzner VPS

First step is to configure the Hetzner VPS in the right way. On Hetzner, spin up a basic server. From the Cloud Console, choose the location of a server closer to your residence. For the operating system for your VPS, I chose Ubuntu (as that’s a popular Linux OS, and therefore easier to find help online for debugging). For type, I chose Shared vCPU, as it’s easier and cheaper (I chose the x86 architecture). Regarding networking options, I chose both Public IPv4, and Public IPv6 addresses. I then added an SSH key for me to authenticate into the server from my local environment (more secure than password authentication). I also selected Backups as an option since we’re going to use sqlite3 as a database in production environment. (SQlite are single file databases, and hence are more susceptible to data losses). After all these steps, you give your server a name, and then pay for the nominal fee to get it live.

I also did a gut check to see if I’m able to enter the Hetzner VPS on my local machine with this command:

ssh root@[ip-address]

I also set up private/public SSH keys and added the public key to my Hetzner VPS for authentication while logging into my root server on Hetzner.

Cloudflare for DNS/SSL management

If we already have a website domain, the next step here is to add the relevant nameservers from the place you purchased. In my case, I’d purchased my website on namecheap.com, so I added the nameservers from namecheap, so that Cloudflare is able to handle the DNS itself. After this step, under the SSL/TLS section, I give the option to have Full encryption. Under Edge Certificate, ensure that the Always Use HTTPS and Automatic HTTPS rewrites is checked as active.

I updated the DNS settings on Cloudflare as follows:

A (DNS only | auto TTL) [website-name.com] points to [hetzner-ipv4-address]
A (DNS only | auto TTL) [subdomain.website-name.com] points to [hetzner-ipv4-address]
A (DNS only | auto TTL) [www.website-name.com] points to [hetzner-ipv4-address]
AAAA (DNS only | auto TTL) [website-name.com] points to [hetzner-ipv6-address]
AAAA (DNS only | auto TTL) [subdomain.website-name.com] points to [hetzner-ipv6-address]
AAAA (DNS only | auto TTL) [www.website-name.com]  points to  [hetzner-ipv6-address]

Now that we have setup an active Hetzner VPS, as well as connected the website domain to Cloudflare, we can move on to the next step, which is to setup the Rails app.

Setting up a Rails 8 app

I setup a vanilla Rails 8 installation for this demo purpose.

rails new [app-name] 
cd [app-name]
bundle install

I then generated some controllers and view files for my Rails app, so that I could view a ‘hello world’ when I’m accessing the website homepage.

rails g controller Home index


class HomeController < ApplicationController
  def index
  end
end

home_controller.rb

<h1> Hello world! </h1>

index.html.erb

root "home#index"

routes.rb

While deploying, I was facing errors as the net-pop gem was not compatible with ruby 3.3.3 version. While searching for internet solutions, I found this github issue which helped me resolve the bug. Long story short, I had to update the relevant Gemfile.lock lines:

...
    net-imap (0.5.1)
      date
      net-protocol
    net-pop (0.1.2)
      net-protocol
    net-protocol (0.2.2)
      timeout
...
...
...

Gemfile.lock

After setting this all up, I then did a local deployment just to be sure about everything working together as expected.

Setting up Kamal deployment

The next step now was to setup the deployment to point the Rails 8 app to the Hetzner VPS. This was enabled through Kamal, the new deployment tool from Rails team. Before we proceed with Kamal, we would need an account on DockerHub. After account creation, once we go to the settings, we are provided a Docker Access Token which can then be saved and used later as a KAMAL_REGISTRY_PASSWORD.

export KAMAL_REGISTRY_PASSWORD=[enter-your-password-here]

shell

This stores this in a secure fashion for the Kamal wrapper to make use of.

service: [app-name]

image: [dockerhub-username]/[app-name]

servers:
  web:
    - [hetzner-ipv4-address]

proxy:
  ssl: true
  host: [app-name].[website]
  app_port: 3000

# Credentials for your image host.
registry:
  username: [dockerhub-username]
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - RAILS_MASTER_KEY
  clear:
    DB_HOST: 192.168.0.2


aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole"

volumes:
  - "[app-name]_storage:/storage/database.sqlite3"

# Configure the image builder.
builder:
  arch: amd64

config/deploy.yml

default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3


test:
  <<: *default
  database: storage/test.sqlite3

production:
  primary:
    <<: *default
    database: storage/database.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate
  queue:
    <<: *default
    database: storage/production_queue.sqlite3
    migrations_paths: db/queue_migrate
  cable:
    <<: *default
    database: storage/production_cable.sqlite3
    migrations_paths: db/cable_migrate

config/database.yml

After this step, we update the Dockerfile ensuring that the /storage folder is also added as a directory for the database.

ARG RUBY_VERSION=3.3.3
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    bundle exec bootsnap precompile --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

# Final stage for app image
FROM base

# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails

# Run and own only the runtime files as a non-root user for security
#
RUN mkdir /storage

RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
    chown -R rails:rails db log storage tmp storage
USER 1000:1000

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 3000
CMD ["./bin/thrust", "./bin/rails", "server"]

Dockerfile

After all these changes, we save the files and add git version control to it: git init, git add . and then git commit -m "new" .

Also ensure that you docker login to autheticate, and make sure that Docker Desktop app is running so that you could dockerize the application. After this,

kamal init
docker login
kamal setup
kamal deploy

And you’re done!

After running these commands, your Rails 8 application should be successfully deployed on your Hetzner VPS, accessible via your configured domain name. The entire setup process shows how modern deployment tools like Kamal can simplify complex DevOps tasks.

Some key takeaways:

  1. Cost-effectiveness: At roughly 4/monthonHetznerVPS,thissolutionissignificantlymoreeconomicalthantraditionalPaaSproviderslikeHeroku,whichcanrunupto4/month on Hetzner VPS, this solution is significantly more economical than traditional PaaS providers like Heroku, which can run up to 300/month with database add-ons.
  2. Simplified DevOps: Kamal abstracts away much of the complexity involved in containerization and deployment, making it accessible even for developers with limited DevOps experience.
  3. Production-Ready: With Cloudflare handling SSL and DNS management, and Docker ensuring consistent environments, this setup is robust for production.

Subscribe to get future posts via email (or grab the RSS feed). 2-3 ideas every month across design and tech

Read more

  1. Stop arguing, and start drawing circles togethermathematics
  2. The whole world is just the snake eating its own tailmental-models
  3. Life lessons and hot takes from my 30slifestyle
  4. Building a skill for coherent science illustrationsscience
  5. My agentic engineering workflow (step by step)agentic-coding
  6. Every darn thing is a kekulean loop if you notice itdesign-thinking
  7. Hammock driven developmentagentic-coding
  8. Peculiar ways number three fits into our funny little brainsmental-models
  9. AI sandwich as a defacto principle for anything agentic engineering relatedagentic-coding
  10. How I write essays in 2026writing
  11. Authority in the guise of evidencecritical-rationalism
  12. Map is not the territoryphilosophy
  13. Self hypnosis as a manifestation ritualmeditation
  14. Hegelian dialectic for structured reasoning with AI agentsphilosophy
  15. How I prepare for tough negotiations nowadaysnegotiation
  16. When should we steelthread somethingproduct-development
  17. Breadboarding, shaping, slicing, and steelthreading solutions with AI agentsproduct
  18. Healthy conflict in teams have a tipping pointteam-building
  19. How I deslopify AI writingwriting
  20. How I started building softwares with AI agents being non technicalagentic-coding
  21. Read raw transcriptswriting
  22. Legible and illegible tasks in organisationsproduct
  23. L2 Fat marker sketchesdesign
  24. Writing as moats for humanswriting
  25. Beauty of second degree probesdecision-making
  26. Boundary objects as the new prototypesprototyping
  27. One way door decisionsproduct
  28. Finished softwares should existproduct
  29. How I periodically rank my rough draftsobsidian
  30. Flipping questions on its headinterviewing
  31. Vibe writing maximswriting
  32. How I blog with Obsidian, Cloudflare, AstroJS, Githubwriting
  33. How I build greenfield apps with AI-assisted codingagentic-coding
  34. We have been scammed by the Gaussian distribution clubmathematics
  35. Classify incentive problems into stag hunts, and prisoners dilemmasgame-theory
  36. I was wrong about optimal stoppingmathematics
  37. Thinking like a shipmental-models
  38. Hyperpersonalised N=1 learningeducation
  39. New mediums for humans to complement superintelligenceagentic-coding
  40. Maxims for AI assisted codingagentic-coding
  41. Virtual bookshelvesaesthetics
  42. It's computational everythingtrends
  43. Public gardens, secret routesdigital-garden
  44. Git way of learning to codeagentic-coding
  45. Style Transfer in AI writingagentic-coding
  46. Understanding codebases without using codeagentic-coding
  47. Vibe coding with Cursoragentic-coding
  48. Virtuoso Guide for Personal Memory Systemsmemory
  49. Writing in Future Pastwriting
  50. Publish Originally, Syndicate Elsewhereblogging
  51. Poetic License of Designdesign
  52. Idea in the shower, testing before breakfastsoftware
  53. Technology and regulation have a dance of ice and firetechnology
  54. How I ship "stuff"software
  55. Writing is thinkingwriting
  56. Song of Shapes, Words and Pathscreativity
  57. How do we absorb ideas better?knowledge
  58. Read writers who operatewriting
  59. Brew your ideas lazilyideas
  60. Trees, Branches, Twigs and Leaves — Mental Models for Writingwriting
  61. Compound Interest of Private Noteswriting
  62. Conceptual Compression for LLMsagentic-coding
  63. Meta-analysis for contradictory research findingsdigital-health
  64. Proof of workproduct
  65. Gauging previous work of new joinees to the teamleadership
  66. Task management for product managersproduct
  67. Beauty of Zettelswriting
  68. Stitching React and Rails togetheragentic-coding
  69. Exploring "smart connections" for note takingwriting
  70. Deploying Home Cooked Apps with Railssoftware
  71. Repetitive Copypromptingwriting
  72. Questions to ask every decadejournalling
  73. Balancing work, time and focusproductivity
  74. Hyperlinks are like cashew nutswriting
  75. Brand treatments, Design Systems, Vibesdesign
  76. How to spot human writing on the internetwriting
  77. Can a thought be an algorithm?product
  78. Opportunity Harvestingcareers
  79. How does AI affect UI?design
  80. Everything is a prioritisation problemproduct
  81. How I do product roastsproduct
  82. The Modern Startup Stacksoftware
  83. In-person vision transmissionproduct
  84. How might we help children invent for social good?social-design
  85. The meeting before the meetingmeetings
  86. Design that's so bad it's actually gooddesign
  87. Lessons learnt interview prepping for product rolesinterviewing
  88. Obsessing over personal websitessoftware
  89. English is the hot new programming languagesoftware
  90. Better way to think about conflictsconflict-management
  91. The role of taste in building productsdesign
  92. Dear enterprises, we're tired of your subscriptionssoftware
  93. Products need not be user centereddesign
  94. World's most ancient public health problemsoftware
  95. Pluginisation of Modern Softwaredesign
  96. Let's make every work 'strategic'consulting
  97. Making Nielsen's heuristics more digestibledesign
  98. Startups are a fertile ground for risk takingentrepreneurship
  99. Insights are not just a salad of factsdesign
  100. Minimum Lovable Productproduct
  101. Methods are lifejackets not straight jacketsmethodology
  102. How to arrive at on-brand colours?design
  103. Minto principle for writing memoswriting
  104. Importance of Whytask-management
  105. Quality Ideas Trump Executionsoftware
  106. Why I prefer indie softwareslifestyle
  107. Use code only if no code failscode
  108. Self Marketing
  109. Personal Observation Techniquesdesign
  110. Design is a confusing worddesign
  111. A Primer to Service Design Blueprintsdesign
  112. Rapid Journey Prototypingdesign
  113. Visualise detailed file structures on CLIcli
  114. Do's and Don'ts of User Researchdesign
  115. Design Manifestodesign
  116. Complex project management for productproducts
  117. How might we enable patients and caregivers to overcome preventable health conditions?digital-health
  118. Pedagogy of the Uncharted — What for, and Where to?education
  119. Future of Equity with Ludovick Petersinterviewing
  120. Future of Ageing with Mehdi Yacoubiinterviewing
  121. Future of Tacit knowledge with Celeste Volpiinterviewing
  122. Future of Mental Health with Kavya Raointerviewing
  123. Future of unschooling with Che Vanniinterviewing
  124. Future of Rural Innovation with Thabiso Blak Mashabainterviewing
  125. Future of work with Laetitia Vitaudinterviewing
  126. How might we prevent acquired infections in hospitals?digital-health
  127. The why to endure any howentrepreneurship
  128. Design education amidst social tribulationsdesign
  129. How might we assist deafblind runners to navigate?social-design