gem install kamal
gem install kamal
gem install kamal
kamal version
kamal version
kamal version
rails generate dockerfile
rails generate dockerfile
rails generate dockerfile
# syntax=docker/dockerfile:1
FROM ruby:3.3-slim AS base WORKDIR /rails ENV RAILS_ENV="production" \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" FROM base AS build RUN apt-get update -qq && \ apt-get install --no-install-recommends -y build-essential git libpq-dev COPY Gemfile Gemfile.lock ./
RUN bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache COPY . .
RUN bundle exec rails assets:precompile FROM base RUN apt-get update -qq && \ apt-get install --no-install-recommends -y libpq5 curl && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails 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 USER 1000:1000 ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 3000
CMD ["./bin/rails", "server"]
# syntax=docker/dockerfile:1
FROM ruby:3.3-slim AS base WORKDIR /rails ENV RAILS_ENV="production" \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" FROM base AS build RUN apt-get update -qq && \ apt-get install --no-install-recommends -y build-essential git libpq-dev COPY Gemfile Gemfile.lock ./
RUN bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache COPY . .
RUN bundle exec rails assets:precompile FROM base RUN apt-get update -qq && \ apt-get install --no-install-recommends -y libpq5 curl && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails 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 USER 1000:1000 ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 3000
CMD ["./bin/rails", "server"]
# syntax=docker/dockerfile:1
FROM ruby:3.3-slim AS base WORKDIR /rails ENV RAILS_ENV="production" \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" FROM base AS build RUN apt-get update -qq && \ apt-get install --no-install-recommends -y build-essential git libpq-dev COPY Gemfile Gemfile.lock ./
RUN bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache COPY . .
RUN bundle exec rails assets:precompile FROM base RUN apt-get update -qq && \ apt-get install --no-install-recommends -y libpq5 curl && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails 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 USER 1000:1000 ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 3000
CMD ["./bin/rails", "server"]
service: myaiapp image: yourdockerhub/myaiapp servers: web: hosts: - 203.0.113.10 labels: traefik.http.routers.myaiapp.rule: Host(`myaiapp.com`) traefik.http.routers.myaiapp.tls.certresolver: letsencrypt proxy: ssl: true host: myaiapp.com registry: username: yourdockerhub password: - KAMAL_REGISTRY_PASSWORD env: clear: RAILS_ENV: production RAILS_LOG_TO_STDOUT: "1" RAILS_SERVE_STATIC_FILES: "true" secret: - RAILS_MASTER_KEY - DATABASE_URL - REDIS_URL - OPENAI_API_KEY accessories: db: image: postgres:16 host: 203.0.113.10 port: "127.0.0.1:5432:5432" env: clear: POSTGRES_DB: myaiapp_production secret: - POSTGRES_PASSWORD directories: - data:/var/lib/postgresql/data redis: image: redis:7 host: 203.0.113.10 port: "127.0.0.1:6379:6379" directories: - data:/data
service: myaiapp image: yourdockerhub/myaiapp servers: web: hosts: - 203.0.113.10 labels: traefik.http.routers.myaiapp.rule: Host(`myaiapp.com`) traefik.http.routers.myaiapp.tls.certresolver: letsencrypt proxy: ssl: true host: myaiapp.com registry: username: yourdockerhub password: - KAMAL_REGISTRY_PASSWORD env: clear: RAILS_ENV: production RAILS_LOG_TO_STDOUT: "1" RAILS_SERVE_STATIC_FILES: "true" secret: - RAILS_MASTER_KEY - DATABASE_URL - REDIS_URL - OPENAI_API_KEY accessories: db: image: postgres:16 host: 203.0.113.10 port: "127.0.0.1:5432:5432" env: clear: POSTGRES_DB: myaiapp_production secret: - POSTGRES_PASSWORD directories: - data:/var/lib/postgresql/data redis: image: redis:7 host: 203.0.113.10 port: "127.0.0.1:6379:6379" directories: - data:/data
service: myaiapp image: yourdockerhub/myaiapp servers: web: hosts: - 203.0.113.10 labels: traefik.http.routers.myaiapp.rule: Host(`myaiapp.com`) traefik.http.routers.myaiapp.tls.certresolver: letsencrypt proxy: ssl: true host: myaiapp.com registry: username: yourdockerhub password: - KAMAL_REGISTRY_PASSWORD env: clear: RAILS_ENV: production RAILS_LOG_TO_STDOUT: "1" RAILS_SERVE_STATIC_FILES: "true" secret: - RAILS_MASTER_KEY - DATABASE_URL - REDIS_URL - OPENAI_API_KEY accessories: db: image: postgres:16 host: 203.0.113.10 port: "127.0.0.1:5432:5432" env: clear: POSTGRES_DB: myaiapp_production secret: - POSTGRES_PASSWORD directories: - data:/var/lib/postgresql/data redis: image: redis:7 host: 203.0.113.10 port: "127.0.0.1:6379:6379" directories: - data:/data
KAMAL_REGISTRY_PASSWORD=your_docker_hub_token
RAILS_MASTER_KEY=your_master_key_from_config/master.key
DATABASE_URL=postgresql://postgres:yourpassword@myaiapp-db:5432/myaiapp_production
REDIS_URL=redis://myaiapp-redis:6379/0
OPENAI_API_KEY=sk-your-openai-key
POSTGRES_PASSWORD=yourpassword
KAMAL_REGISTRY_PASSWORD=your_docker_hub_token
RAILS_MASTER_KEY=your_master_key_from_config/master.key
DATABASE_URL=postgresql://postgres:yourpassword@myaiapp-db:5432/myaiapp_production
REDIS_URL=redis://myaiapp-redis:6379/0
OPENAI_API_KEY=sk-your-openai-key
POSTGRES_PASSWORD=yourpassword
KAMAL_REGISTRY_PASSWORD=your_docker_hub_token
RAILS_MASTER_KEY=your_master_key_from_config/master.key
DATABASE_URL=postgresql://postgres:yourpassword@myaiapp-db:5432/myaiapp_production
REDIS_URL=redis://myaiapp-redis:6379/0
OPENAI_API_KEY=sk-your-openai-key
POSTGRES_PASSWORD=yourpassword
kamal setup
kamal setup
kamal setup
kamal deploy
kamal deploy
kamal deploy
# Check what's running
kamal details # View logs
kamal app logs # Open a Rails console on the server
kamal app exec -i "bin/rails console" # Run database migrations
kamal app exec "bin/rails db:migrate" # Rollback to previous version
kamal rollback # Start/stop accessories
kamal accessory start db
kamal accessory stop redis
# Check what's running
kamal details # View logs
kamal app logs # Open a Rails console on the server
kamal app exec -i "bin/rails console" # Run database migrations
kamal app exec "bin/rails db:migrate" # Rollback to previous version
kamal rollback # Start/stop accessories
kamal accessory start db
kamal accessory stop redis
# Check what's running
kamal details # View logs
kamal app logs # Open a Rails console on the server
kamal app exec -i "bin/rails console" # Run database migrations
kamal app exec "bin/rails db:migrate" # Rollback to previous version
kamal rollback # Start/stop accessories
kamal accessory start db
kamal accessory stop redis
#!/bin/bash
echo "Running database migrations..."
kamal app exec "bin/rails db:migrate"
#!/bin/bash
echo "Running database migrations..."
kamal app exec "bin/rails db:migrate"
#!/bin/bash
echo "Running database migrations..."
kamal app exec "bin/rails db:migrate"
chmod +x .kamal/hooks/pre-deploy
chmod +x .kamal/hooks/pre-deploy
chmod +x .kamal/hooks/pre-deploy
# config/routes.rb
get "up" => "rails/health#show", as: :rails_health_check
# config/routes.rb
get "up" => "rails/health#show", as: :rails_health_check
# config/routes.rb
get "up" => "rails/health#show", as: :rails_health_check
# In your Rails app, access them normally:
OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
# In your Rails app, access them normally:
OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
# In your Rails app, access them normally:
OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"]) - A VPS (Ubuntu 22.04+ or Debian 12+) with SSH access
- Docker installed locally
- Ruby 3.2+ locally
- A Docker Hub account (or any container registry) - SSHs into your server
- Installs Docker if needed
- Pushes your image to the registry
- Starts your accessories (Postgres, Redis)
- Runs your app container
- Configures Kamal's built-in proxy (kamal-proxy) with automatic SSL - Your Rails AI app runs in a Docker container on a VPS you control
- Kamal handles zero-downtime deploys via SSH
- Postgres and Redis run as Docker accessories on the same server
- SSL is handled automatically via kamal-proxy and Let's Encrypt
- Secrets are injected at runtime, never in the image
- Health checks prevent bad deploys from taking down your app