Deploying a Rails PWA with Kamal 2 on Hetzner

Kamal 2: Built-In with Rails 8

Rails 8 includes Kamal 2 out of the box, meaning:

Dockerfile for Rails 8 PWA (with SQLite)

#syntax=docker/dockerfile:1
#check=error=true

#Base Ruby image (Rails 8 compatible)
ARG RUBY_VERSION=3.3.6
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

#Set working directory
WORKDIR /rails

#Install required system dependencies
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y \
      curl libjemalloc2 libvips sqlite3 \
      nodejs npm yarn && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

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

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

#Install dependencies
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 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 source code
COPY . .

#Install JavaScript dependencies
RUN yarn install --frozen-lockfile

#Precompile assets and bootsnap cache
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
RUN bundle exec bootsnap precompile app/ lib/

#Final runtime image
FROM base

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

#Set ownership and permissions
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 node_modules public/assets
USER 1000:1000

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

#Expose only port 80 (handled by Kamal-proxy)
EXPOSE 80

#Start server using Puma
CMD ["./bin/thrust", "./bin/rails", "server"]

1. Initialize Kamal in Your Rails 8 App

If you're starting a new project:

rails new my_pwa
cd my_pwa

For existing projects, initialize Kamal:

kamal init

2. Updating Your kamal deploy.yml Configuration

Modify config/deploy.yml with your server and registry details:

#Name of your application. Used to uniquely configure containers.
service: my_pwa

#Name of the container image.
image: your-username/my_pwa

#Deploy to these servers.
servers:
  web:
    - 192.168.0.1

#Enable SSL auto certification via Let's Encrypt.
proxy:
  ssl: true
  host: app.YOUR-DOMAIN-NAME.com

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

#Inject ENV variables into containers.
env:
  secret:
    - RAILS_MASTER_KEY
  clear:
    SOLID_QUEUE_IN_PUMA: true

#Use a persistent storage volume for sqlite database files and local Active Storage files.
#Recommended to change this to a mounted volume path that is backed up off server.
volumes:
  - "my_pwa_storage:/rails/storage"

#Bridge fingerprinted assets (CSS, JS) between versions.
asset_path: /rails/public/assets

#Configure the image builder.
builder:
  arch: amd64

#Aliases for common tasks.
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"

3. Running Kamal Setup on the Hetzner VPS

Kamal automates everything, including installing Docker and dependencies. Run:

kamal setup

This will:


4. Deploying Your Rails 8 PWA with Kamal 2

Run the following command to build and deploy:

kamal deploy

Kamal will: