A while back, my friend who runs a wedding website came to me with an idea. He wanted to create a virtual try-on app so people could try wedding gowns, something not unlike Google’s Doppl. I had no experience working with the OpenAI API, but out of curiosity, I agreed. (Note: this was a paid gig.)
He gave me access to his Trusty old Ubuntu 14.04 box. It hosts about 10 old Ruby on Rails apps running on Nginx + Passenger. Each app is deployed to a subdirectory, and the server itself is proxy_pass
’d from a main server that provides the domain name, SSL certs, and so on. It was probably a good stack in 2015.
Trusty Tahr
Since the server was so old, I needed a way to install a modern Ruby. Fortunately, RVM was already installed. But Ruby 3.4 wouldn’t compile. Ruby 3.3 wouldn’t compile either. Ruby 3.2 was the same story, because OpenSSL 1.0.1f was too old. So I compiled OpenSSL myself and used it to compile Ruby 3.4. It worked.
Then I moved on to the usual stuff like CRUD, authentication, permission, etc. But every form submission caused Rails to complain about a CSRF token mismatch. I checked the session cookie name and saw ["_app_session
, with the weird ["
. It turned out to be an issue with Passenger not working with Rack 3. So I downgraded Rack to 2.2 and everything was fine again.
Next, I worked on image upload for users taking selfies and for businesses uploading gown photos. I installed Active Storage and it complained there was no libvips. I apt-get
-installed it, but it was too old. So I uninstalled it and installed ImageMagick instead. Thank goodness Active Storage still works with ImageMagick.
I then implemented the selfie-taking feature, which was easy to google. But it uses JavaScript, and the modern way to run JavaScript in Rails is via importmap, which I couldn’t figure out. Eventually, I downloaded the Writebook app from 37signals and copied what they do.
Modern Rails also comes with Turbo and Hotwire, which are supposedly good for your app and shouldn’t be disabled. But they make writing JavaScript harder. I tried some hacky workarounds and wasn’t happy. In the end, I restructured my scripts into Stimulus controllers so Turbo and Hotwire could be happy.
Solid Queue
After the user submitted their selfie, I needed to run a background job to send the images to the OpenAI API for processing. In the past, you’d set up Resque or Sidekiq with Redis, which is a pain. I’d been hearing about this new Solid Queue thing in Rails 8, so I gave it a try.
I followed the setup instructions and ran into a bug with db:migrate:reset
where the queue database was created empty, so db:prepare
didn’t work. I had to delete the queue database manually before proceeding. After the queue database was recreated, I wrote an Upstart (not systemd!) script to start Solid Queue. Nobody uses Upstart in 2025, so that was somewhat tricky.
I started the Solid Queue service, but the background jobs didn’t run. The log showed errors about FOR UPDATE SKIP LOCKED
. It turns out PostgreSQL 9.3 was too old to support that syntax. I didn’t have the guts to upgrade Postgres, so I switched databases, with the main app staying on Postgres, and Solid Queue running on SQLite.
Action Cable
Next, I wanted to update the waiting page when OpenAI returned a result image. That’s done via WebSocket, and Rails’ solution is Action Cable. Despite reading the documentation repeatedly, Action Cable never clicked for me.
I installed Action Cable and generated a channel, but it didn’t work. The browser console showed WebSocket connection to 'wss://site.com/app/cable' failed
. I tried modifying the Nginx configuration, mounting Action Cable at different locations, and like 5 different ways to let Rails know it was running in a subdirectory.
Eventually, I got it right. The browser console no longer showed the error, but Action Cable still didn’t work. The server log then showed WebSocket error occurred: wrong number of arguments (given 2, expected 1)
.
It turned out Passenger didn’t work correctly with Action Cable. I was on version 6.0.2, but the version that supports Action Cable is 6.0.17. I thought it was just a minor version difference, but that’s how Phusion labels their updates, major or minor.
So I tried to upgrade Passenger. First, I installed the latest passenger
gem, but when it came time to recompile Nginx, I paused. It’s too risky, what if it broke the other apps? Then I tried to build a .deb
package. I briefly explored something called pbuild
, which got me nowhere. In the end, I couldn’t build a .deb
package because of dependency hell and I had no energy left.
So I thought, forget Passenger. I’d switch this app to Puma while all other apps stayed on Passenger. I wrote another Upstart script for Puma, updated the Nginx config, tweaked and tweaked, and finally got my app running again. But now the browser console showed WebSocket connection to 'wss://site.com/app/cable' failed
again.
Okay, great. So I retraced all my Action Cable configuration steps and retried those 5 subdirectory tricks. In the end, the correct solution was to add a map
block in config.ru
. Now Action Cable finally works.
OpenAI
The next step was to actually call the OpenAI API. I installed the official openai
gem. There was a hiccup about a missing OpenSSL certificate, but I fixed it quickly.
I tried making an images/edits
request with the gem. I passed images as IO
objects, StringIO
objects, Pathname
s, OpenAI::FilePart
s, but the API kept complaining about the image
parameter being missing. OpenAI’s docs only have Python and JavaScript examples, and the gem’s docs don’t mention making an images/edits
request with multiple files. And apparently nobody on the internet does, either.
In the end, I base64-encoded the images and passed the strings and got a different error. Progress! The API complained that image
was an array and I should use image[]
instead. I thought that was handled automatically.
Although it wasn’t mentioned in the docs, I tried changing the key in the hash to image[]
. And I got told to use image%5B%5D[]
instead. At that point, I was convinced the gem was buggy and decided to write my own wrapper. It’s just a multipart HTTP POST request, after all.
I wrote the wrapper and successfully sent the data to the API endpoint. But no images were generated. I kept getting errors. It turned out some wedding gowns are “sexy”, like showing bare shoulders, and OpenAI flags them as NSFW. Okay, I guess, but that severely limits the app’s viability.
I then tried a photo of a safe gown design and finally got a result image back. Unfortunately, the image was terrible. The woman in the image didn’t look anything like the woman in the selfie. I tried more selfies and discovered OpenAI can’t reproduce Asian faces accurately. I kept tweaking the prompt, but after many tries, the results were still garbage. At that point, I gave up.