Fly Launch - Rails 8 - Default issues

We just launched a new rails 8 app.

The launcher did not specify a process for app and the port was set to 8080.

One thing that is confusing is the Dockerfile using thruster - are we under the impression that we can completely remove all the thruster and kamal gems when deploying on fly?

This might be related to: Rails 8 I always get a bad gateway 502 when the machine starts

Thanks!

First question, did you use the command line (‘fly launch’) or the Fly UI launcher?

No, you don’t need Kamal or thruster with fly.io. Yes, you can remove them. But if you have a Dockerfile, we will use it, though we will override portions of it in the fly.toml.

  • If your CMD starts with thrust, we will override the command in the fly.toml by creating a process. This is because you don’t need thruster with fly.io, and because there is a problem after auto_start which is a combination of our problem and thruster’s problem. Once we have a fix, we will remove this override.
  • If the EXPOSE line says 80, we will replace that with 8080 as ports less than 1024 are reserved for root, and rails applications don’t run as root.

Did you use the Dockerfile-rails gem to create your Dockerfile?

It does use thruster by default to serve static file, but you can override this. See the readme of the gem soure repo at GitHub - fly-apps/dockerfile-rails: Provides a Rails generator to produce Dockerfiles and related files.

We used the fly launch cli command, opened the WebUI to change some configs.

The new rails app had the stock Dockerfile prior to fly launch and its contents were as so:

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

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t lead_funnels .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name lead_funnels lead_funnels

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.3.4
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 postgresql-client && \
    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 libpq-dev 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 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 prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

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

We also stripped out all the kamal and thruster gems from the Gemfile after initializing the rails 8 app.

I can see from the logs during the first launch the fly cli running the following command:

bin/rails generate dockerfile --label=fly_launch_runtime:rails --skip --postgresql --no-prepare

Being that the Dockerfile already existed, I am not sure that it correctly generated that file to avoid anything related to thruster or kamal by default.

The fly.toml file was also still trying to use the 8080 port.

In order to get things cleaned up, I deleted the Dockerfile and then ran the original bin/rails generate dockerfile --label=fly_launch_runtime:rails --skip --postgresql --no-prepare

This then created a more appropriate looking Dockerfile:

# syntax = docker/dockerfile:1

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.3.4
FROM ruby:$RUBY_VERSION-slim AS base

LABEL fly_launch_runtime="rails"

# Rails app lives here
WORKDIR /rails

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

# Update gems and bundler
RUN gem update --system --no-document && \
    gem install -N bundler


# 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 libpq-dev

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

# 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

# Install packages needed for deployment
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl postgresql-client && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# 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 groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
    chown -R 1000:1000 db log storage tmp
USER 1000:1000

# Entrypoint sets up the container.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

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

I then updated the fly.toml to the following:

primary_region = 'iad'
console_command = '/rails/bin/rails console'

[build]

[deploy]
  release_command = "./bin/rails db:prepare"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = 'suspend'
  auto_start_machines = true
  min_machines_running = 1
  processes = ['app']

  [[http_service.checks]]
    port = 3000
    interval = '10s'
    timeout = '2s'
    grace_period = '5s'
    method = 'GET'
    path = '/up'
    protocol = 'http'
    tls_skip_verify = false

    [http_service.checks.headers]
      X-Forwarded-Proto = 'https'

[[vm]]
  memory = '512mb'
  cpu_kind = 'shared'
  cpus = 1

[[statics]]
  guest_path = '/rails/public'
  url_prefix = '/'

We are now up and running. It just seems the initial fly launch command is not properly setting up a stock rails 8 app when there is kamal conflicts.

Let’s talk through this. I think that should have worked, but may not have been what you expected.

The key in the excerpt above is --skip. This tells the dockerfile generator not to overwrite your existing files. This is because you may have hand edited them (I’m gathering you didn’t, but others do).

When you launched your app, we read the Dockerfile and took note of the EXPOSE instruction. It says port 80, which is fine if you are running as root or in a docker container, but if you are running as a non-root user on a VM, that won’t work, so we adjust that for you to 8080.

Next you should have seen:

? Do you want to tweak these settings before proceeding?

If you said yes, you could have changed the port. If you said no, it will remain as 8080.

In the fly.toml that was produced you should have gotten:

[env]
  PORT = '8080'

[processes]
  app = './bin/rails server'

This would have told the rails server to use port you selected (or in this case, didn’t override), and to run ./bin/rails server instead of the CMD that is present in your Dockerfile.

Yes I think you are correct, with the new rails 8 app including a Dockerfile, I wonder if there should possibly be a prompt to override the standard generated Dockerfile by rails when deploying on fly. I would almost guess this is the case for 90%+ of developers.

That being said, we did change the settings, which I also would think is a very high use case as you will typically always want to choose the org, change initial size, append addons like PG, etc.

When this UI comes up - yes it might be defaulted to port 8080 - but to be completely honest as a rails developer - I saw that and instantly changed it to 3000 (I believe in the past fly would default this to 3000, but I understand why you have changed it from 80 to 8080 due to kamal thruster defaults in rails 8)

As for the fly.toml additions, from memory, I am almost positive, that the initial fly launch did not add the env PORT = ‘8080’ as I remember in git history, I saw this in a second commit. The second commit was done after we manually ran the docker generate command that fly runs (bin/rails generate dockerfile --label=fly_launch_runtime:rails --skip --postgresql --no-prepare). So there might be an issue with the initial launch command or it was because the WebUI changed the port from 8080 to 3000?

As for the [processes] block, this was never added, and it seems to be working at the moment without it as you saw in the latest supplied fly.toml file above. This feels off to me, so we will probably manually add it back in, as I prefer things to be explicitly defined.

Either way, something does feel off here, as prior to rails 8 the fly launch “just worked” without any confusion to thruster vs no thruster, port 80 → 8080 or 3000 as well as the Dockerfile not being generated while launching the fly app to avoid kamal defaults with thruster. I just feel like there can be an improvement to launching rails 8 apps on fly to get back to that “it just works” feeling.

What are your thoughts @rubys?

I claim that if you had removed the thruster gem AND removed the Dockerfile before you ran fly launch, everything would have worked as you expected.

If you have a reproducible test case where PORT and/or [processes] was not added to fly.toml, I’ll gladly look into it, but I just tried it again, and I see both present.