Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Kamal deployment #518

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .kamal/secrets
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.

# Grab the registry password from ENV
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD

# Improve security by using a password manager. Never check config/master.key or config/credentials/*.key into git!
RAILS_MASTER_KEY=$(cat config/credentials/production.key)

# Either use .env or rails credentials to store database password.
# HOSTEDGPT_DATABASE_PASSWORD=$HOSTEDGPT_DATABASE_PASSWORD
credentials=$(bin/rails credentials:show --environment production)
HOSTEDGPT_DATABASE_PASSWORD=$(echo "$credentials" | yq '.database.password // "password"')

# Used by postgres:16 image to set password
POSTGRES_PASSWORD=$HOSTEDGPT_DATABASE_PASSWORD
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ group :development do
gem "rubocop-capybara"
gem "rubocop-minitest"
gem "dockerfile-rails", ">= 1.6"

gem "kamal", "~> 2.0"
end

group :test do
Expand Down
33 changes: 33 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ GEM
aws-eventstream (~> 1, >= 1.0.2)
base64 (0.2.0)
bcrypt (3.1.20)
bcrypt_pbkdf (1.1.1)
bcrypt_pbkdf (1.1.1-arm64-darwin)
bcrypt_pbkdf (1.1.1-x86_64-darwin)
bigdecimal (3.1.7)
bindex (0.8.1)
bootsnap (1.17.0)
Expand All @@ -131,7 +134,9 @@ GEM
reline (>= 0.3.1)
dockerfile-rails (1.6.10)
rails (>= 3.0.0)
dotenv (3.1.4)
drb (2.2.1)
ed25519 (1.3.0)
erubi (1.12.0)
event_stream_parser (1.0.0)
faraday (2.8.1)
Expand All @@ -142,6 +147,10 @@ GEM
multipart-post (~> 2)
faraday-net_http (3.0.2)
ffi (1.17.0)
ffi (1.17.0-aarch64-linux-musl)
ffi (1.17.0-arm64-darwin)
ffi (1.17.0-x86_64-darwin)
ffi (1.17.0-x86_64-linux-gnu)
drnic marked this conversation as resolved.
Show resolved Hide resolved
globalid (1.2.1)
activesupport (>= 6.1)
hashie (5.0.0)
Expand All @@ -162,6 +171,17 @@ GEM
json (2.7.1)
jwt (2.8.1)
base64
kamal (2.2.2)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
concurrent-ruby (~> 1.2)
dotenv (~> 3.1)
ed25519 (~> 1.2)
net-ssh (~> 7.0)
sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3)
zeitwerk (~> 2.5)
language_server-protocol (3.17.0.3)
lint_roller (1.1.0)
logger (1.6.1)
Expand Down Expand Up @@ -196,8 +216,13 @@ GEM
net-protocol
net-protocol (0.2.2)
timeout
net-scp (4.0.0)
net-ssh (>= 2.6.5, < 8.0.0)
net-sftp (4.0.0)
net-ssh (>= 5.0.0, < 8.0.0)
net-smtp (0.4.0.1)
net-protocol
net-ssh (7.3.0)
nio4r (2.7.0)
nokogiri (1.16.3-aarch64-linux)
racc (~> 1.4)
Expand Down Expand Up @@ -229,6 +254,7 @@ GEM
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
ostruct (0.6.0)
parallel (1.24.0)
parser (3.2.2.4)
ast (~> 2.4.1)
Expand Down Expand Up @@ -378,6 +404,12 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sshkit (1.23.1)
base64
net-scp (>= 1.1.2)
net-sftp (>= 2.1.2)
net-ssh (>= 2.8.0)
ostruct
standard (1.32.1)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
Expand Down Expand Up @@ -455,6 +487,7 @@ DEPENDENCIES
dockerfile-rails (>= 1.6)
image_processing (~> 1.13.0)
importmap-rails
kamal (~> 2.0)
minitest-stub_any_instance
name_of_person
omniauth (~> 2.1)
Expand Down
46 changes: 43 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@ This project is led by an experienced rails developer, but I'm actively looking
- [Troubleshooting Render](#troubleshooting-render)
- [Deploy the app on Fly.io](#deploy-the-app-on-flyio)
- [Deploy the app on Heroku](#deploy-the-app-on-heroku)
- [Deploy to own servers with Kamal2](#deploy-to-own-servers-with-kamal2)
- [Deploy on your own server](#deploy-on-your-own-server)
- [Configure optional features](#configure-optional-features)
- [Give assistant access to your Google apps](#configuring-google-tools)
- [Configuring Google Tools](#configuring-google-tools)
- [Authentication](#authentication)
- [Password authentication](#password-authentication)
- [Google OAuth authentication](#google-oauth-authentication)
- [HTTP header authentication](#http-header-authentication)
- [Contribute as a developer](#contribute-as-a-developer)
- [Running locally](#Running-locally)
- [Alternatively, you can skip Docker:](#alternatively-you-can-set-skip-docker)
- [Running locally](#running-locally)
- [Alternatively, you can skip Docker](#alternatively-you-can-skip-docker)
- [Running tests](#running-tests)
- [Understanding the Docker configuration](#understanding-the-docker-configuration)
- [Changelog](#changelog)
Expand Down Expand Up @@ -111,6 +112,45 @@ Eligible students can apply for Heroku platform credits through [Heroku for GitH

You may want to read about [configuring optional features](#configure-optional-features).

## Deploy to own servers with Kamal

[Kamal](https://kamal-deploy.org/) offers zero-downtime deploys, rolling restarts, asset bridging, remote builds, accessory service management, and everything else you need to deploy and manage your web app in production with Docker. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized.

First, create your production credentials file.

```plain
bin/rails credentials:edit --environment production
```

Next, uncomment the `database:` section

```yaml
database:
password: some-long-string
```

Second, create a Docker Hub access token and store it as local env var `KAMAL_REGISTRY_PASSWORD`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know github lets you host containers. I was using that for a different github project. If I were to enable github container registery for HostedGPT would that save people the trouble of needing to create a Docker Hub account? It must be that Kamal needs to pull from a docker container registery? (I need to watch the Kamal tutorial video)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Kamal likes to build a new image and deploy it; whereas docker compose supports running pre-built containers or re-building them.

Specifically:

  • the servers processes are going to run based on the image that is built from the local src project
  • the accessories processes are run based on existing images.

So each person who wants to use Kamal will be building their own image to their own docker registry (docker hub, or wherever).


Next, edit `config/deploy.yml`:

1. Change `my-docker-user` to your Docker Hub username
2. Change `168.192.0.1` to the IP or hostname of your target Linux server
3. If you need to `ssh` into that server as anything other than `root` user, then uncomment `ssh:` section and edit your ssh username.
4. Change `hostedgpt.example.com` to the public CNAME or A record that points to your server IP address.

Next, commit all the changes to git so Kamal picks them up.

```plain
git add .
git commit -m "Add production credentials and Kamal config"
```

Now, run the command to setup the Postgres database, build HostedGPT using docker buildx, and deploy it to your server:

```plain
kamal setup
```

## Deploy on your own server

There are only two services that need to be running for this app to work: the Puma web server and a Postgres database.
Expand Down
21 changes: 12 additions & 9 deletions config/database.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
default: &default
adapter: postgresql
encoding: unicode
host: localhost
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
port: <%= ENV['HOSTEDGPT_DATABASE_PORT'] || 5432 %>
<% if RUBY_PLATFORM =~ /darwin/ %>
<% if ENV["HOSTEDGPT_DATABASE_HOST"] %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rails will already, by default, read DATABASE_URL from the environment and use that as the database configuration instead of what's in database.yml. Does that make it so we don't need to declare all these new ENV variables?

I'm pretty sure other users of HostedGPT have asked for these config so maybe the DATABASE_URL behavior is just too unknown. It probably doesn't hurt to add these regardless.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if DATABASE_URL or PRIMARY_DATABASE_URL env var is provided, then rails ignores the database.yml file. So database.yml is only for users who are not using these env vars.

I thought it'd made sure all the existing behaviour/defaults existed if the new env vars weren't provided. It's definitely difficult to read YAML + ERB :)

host: <%= ENV["HOSTEDGPT_DATABASE_HOST"] %>
<% end %>
<% if ENV["HOSTEDGPT_DATABASE_PORT"] %>
port: <%= ENV["HOSTEDGPT_DATABASE_PORT"] %>
<% end %>
<% if RUBY_PLATFORM =~ /darwin/ %>
gssencmode: disable
<% end %>
<% end %>

development:
<<: *default
database: <%= ENV['HOSTEDGPT_DEV_DB'] || "hostedgpt_development" %>
database: <%= ENV.fetch("HOSTEDGPT_DEV_DB", "hostedgpt_development") %>

test:
<<: *default
database: <%= ENV['HOSTEDGPT_TEST_DB'] || "hostedgpt_test" %>
database: <%= ENV.fetch("HOSTEDGPT_TEST_DB", "hostedgpt_test") %>

production:
<<: *default
database: hostedgpt_production
username: hostedgpt
database: <%= ENV.fetch("HOSTEDGPT_PRODUCTION_DB", "hostedgpt_production") %>
username: <%= ENV.fetch("HOSTEDGPT_DATABASE_USERNAME", "hostedgpt") %>
password: <%= ENV["HOSTEDGPT_DATABASE_PASSWORD"] %>

107 changes: 107 additions & 0 deletions config/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Name of your application. Used to uniquely configure containers.
service: hostedgpt

# Name of the container image.
image: my-docker-user/hostedgpt

# Deploy to these servers.
servers:
web:
- 168.192.0.1
# job:
# hosts:
# - 168.192.0.1
# cmd: bin/rake solid_queue:start

# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!).
proxy:
ssl: true
host: hostedgpt.example.com
app_port: 8080

# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
username: my-docker-user

# Always use an access token rather than real password when possible.
password:
- KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
secret:
- RAILS_MASTER_KEY
- HOSTEDGPT_DATABASE_PASSWORD
clear:
# Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
# When you start using multiple servers, you should split out job processing to a dedicated machine.
RUN_SOLID_QUEUE_IN_PUMA: true

# Set number of processes dedicated to Solid Queue (default: 1)
# JOB_CONCURRENCY: 3

# Set number of cores available to the application on each server (default: 1).
# WEB_CONCURRENCY: 2

HOSTEDGPT_FORCE_SSL: "false"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why disable SSL, was there some complication with Kamal? I thought I remembered hearing that Kamal would auto-configure SSL for you using Let's Authenticate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this either and perhaps it wasn't necessary. I just found I needed to turn off force_ssl for the app running behind kamal proxy. Hopefully we learn more over time about good Rails/Kamal practices.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have this deployed with kamal and the FORCE_SSL issue was related to the health check. I had to uncomment the config.ssl_options in production.rb:

# Skip http-to-https redirect for the default health check endpoint.
config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }


# Match this to any external database server to configure Active Record correctly
HOSTEDGPT_DATABASE_HOST: hostedgpt-db
HOSTEDGPT_DATABASE_USERNAME: postgres
HOSTEDGPT_PRODUCTION_DB: hostedgpt_production

# Log everything from Rails
RAILS_LOG_LEVEL: debug

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
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"

# 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:
- "hostedgpt_storage:/rails/storage"

# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets

# Configure the image builder.
builder:
arch: amd64

# # Build image via remote server (useful for faster amd64 builds on arm64 computers)
# remote: ssh://docker@docker-builder-server
#
# # Pass arguments and secrets to the Docker build process
# args:
# RUBY_VERSION: ruby-3.3.5
# secrets:
# - GITHUB_TOKEN
# - RAILS_MASTER_KEY

# Use a different ssh user than root
# ssh:
# user: deploy

# Use accessory services (secrets come from .kamal/secrets).
accessories:
db:
image: postgres:16
host: 168.192.0.1
# port: 5432
env:
clear:
POSTGRES_DB: hostedgpt_production
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
2 changes: 1 addition & 1 deletion config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
# config.assume_ssl = true

# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
config.force_ssl = ENV["HOSTEDGPT_FORCE_SSL"] != "false"

# Log to STDOUT by default
config.logger = ActiveSupport::Logger.new($stdout)
Expand Down
10 changes: 10 additions & 0 deletions lib/templates/rails/credentials/credentials.yml.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: <%= secret_key_base %>

active_record_encryption:
primary_key: <%= SecureRandom.alphanumeric(32) %>
deterministic_key: <%= SecureRandom.alphanumeric(32) %>
key_derivation_salt: <%= SecureRandom.alphanumeric(32) %>

# database:
# password: <%= SecureRandom.alphanumeric(32) %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that this template is used when following the instructions to execute bin/rails credentials:edit --environment production. But the next instruction is to uncomment these two lines. Should we instead just uncomment them within this template file?

Loading