HOSTING FOR AI-BUILT APPS
Deploy a Rails blog API with PostgreSQL on Yeeted
A Yeeted sample: Rails 8 API-only on managed postgres. The tree is what
rails new gives you:
rails new blog-rails --api --database=postgresql \
--skip-solid --skip-docker --skip-kamal --skip-ci --skip-git \
--skip-jbuilder --skip-brakeman --skip-rubocop --skip-test
Plus one model, two controllers, a migration, one pinned gem (below) and the
three files every example here carries (yeeted.yaml, smoke.sh, this
README). Configuration largely follows the generated project. That is the point: it deploys close to
as generated.
What it demonstrates
- The
ruby railsbuild branch:bundle installonruby:3.3-slim, a per-buildSECRET_KEY_BASE, anddb:migrateas the RELEASE step, run at boot againstDATABASE_URLunder Rails' own migration advisory lock. DATABASE_URLwins overconfig/database.yml. The generated production section hardcodesdatabase: blog_rails_productionand aBLOG_RAILS_DATABASE_PASSWORDthat is never set here. Rails still connects to the attached database, becauseDATABASE_URLtakes precedence. Nothing needs editing.force_sslis fine.config.force_ssl = trueandconfig.assume_ssl = trueare both on in the generatedproduction.rb. The gateway terminates TLS and forwardsX-Forwarded-Proto, so requests are not redirect-looped and health checks answer 200.
The one edit rails new needed
gem "json", "~> 2.7"
json 3.0 dropped the positional options argument from JSON.parse, and
ActiveSupport 8.1 still calls it that way:
activesupport-8.1.3.1/lib/active_support/json/decoding.rb:25
data = ::JSON.parse(json, options)
so every JSON request body raises ArgumentError: wrong number of arguments
(given 2, expected 1), which Rails reports as
ActionDispatch::Http::Parameters::ParseError and a 400 with an empty
body. Form-encoded bodies are unaffected, which is what makes it look like
the client is at fault rather than the stack.
This is not platform-specific: The dependency combination described above resolves json 3.x and behaves the same anywhere. Pinning to 2.x is the fix until ActiveSupport catches up.
Two things worth copying
Give an API app a root route. The root JSON index gives visitors a useful
starting point. This example declares /healthz as its health-check path.
Rescue ParameterMissing. Production Rails turns an unhandled one into a
400 with an empty body, which reads as the platform rejecting the request
rather than the app. PostsController rescues it into a JSON message.
Endpoints
| Route | What it is |
|---|---|
GET / |
a JSON index: app name, Rails version, endpoints |
GET /healthz |
ok |
GET /api/posts |
the 50 newest posts |
POST /api/posts |
{"post":{"title":"…","body":"…"}}, 201 or 400 |
Known cosmetic noise
The generated Gemfile carries image_processing, so every boot logs
Using vips to process variants requires the libvips library. This app has no
ActiveStorage variants; the gem is left in because the tree is meant to be what
rails new produces, and the warning is harmless.
No asset pipeline
--api means there is no assets:precompile task at all, and the build says
so rather than failing:
yeeted: no assets:precompile task (API-only, or no asset pipeline) - skipping
A full-stack Rails app is a different story. jsbundling-rails and
cssbundling-rails need node and yarn, which ruby:3.3-slim does not carry,
and that precompile FAILS the build by design. It used to be swallowed, and
the image shipped and 500'd on its first asset.
Tests: ./smoke.sh https://... (smoke). --skip-test was passed: Rails model
tests want a database, and every example here runs its unit tests with no
services at all.
Run locally
Use Ruby 3.3 or newer, Bundler, and PostgreSQL with a disposable database.
From ruby/blog-rails inside the examples tree, set DATABASE_URL to that
local database using your environment, then run:
bundle install
bin/rails db:migrate
bin/rails server -b 0.0.0.0 -p 3000
Open http://localhost:3000 for the JSON index. This example has no in-memory
or SQLite fallback and no standalone unit tests. The checked-in Gemfile and
lockfile are the source of truth for the dependency versions described above.
Deploy and verify
Follow the deployment walkthrough. Upload this
folder, including its lockfile, migrations, and yeeted.yaml. The manifest
requests PostgreSQL; the platform supplies DATABASE_URL and runs Rails
migrations during the release step. This is an API-only app, so there is no
asset compilation step.
sh smoke.sh "$PREVIEW_URL"
curl -i "$PREVIEW_URL/api/posts" -H 'Content-Type: application/json' \
-d '{"post":{"title":"My first Yeeted post","body":"Hello from Rails"}}'
curl -fsS "$PREVIEW_URL/api/posts"
A valid write returns 201. The list response contains the post and
"store":"postgres". A blank title returns 400. The smoke script checks
health, the root index, creation, reading, and validation. Wrap request fields
in post; this is different from the Next.js example's JSON shape.
What to change before real use
The API has no author authentication or per-user ownership. Add those before collecting real users' content. Keep a database backup before experimenting with schema changes; rolling back an application version does not reverse Rails migrations. A full-stack Rails application with JavaScript or CSS bundling has different build requirements from this API-only example.