Kamal, Rails deployments, and Rega turntables

Lately I've started to put back into a working state my turntable. It's been disconnected from my main audio chain for about five years. The TT is a Rega RP3 with a bog-standard Rega Carbon cartridge. Why now one might ask? It wasn't my top priority and streaming Spotify to the WiiM Mini then via a DAC directly into my NAD power amplifier was good enough.

Yet, I started missing my records. I mean I still have them, I can still enjoy their tactility, art and smell, but not their sound. I missed the limits imposed by listening to vinyl: you have to carefully pick your album and then commit to listening to the whole thing, more or less.

Anyway - after some cabling around, I managed to make it work and even better: improve its specs. It went from 33 1/3 rpm +0,57% to just +0,07%. The records sound great and I can now enjoy them in their full glory whilst following the rite of listening just like in Murakami's books .

Now, what's the connection with Kamal and Rails ? I've been using my blog to test out various Ruby/Rails changes, practically there's no need to have a full CMS for something which you can write using HTML and a sprinkle of CSS and JavaScript.

The crux of my blog is to experiment and enjoy programming in a restricted way. It's a blog at the end of the day so it does need a CMS that means CRUD resources, feeds, SEO, partials, caching, public routing, etc. all within a dynamic language like Ruby and aided by Rails.

As any Rails app it does need to be deployed somewhere and somehow. Back in the day I was using Capistrano to deploy to VMs from Linode, Hetzner et el. Then there was a large portion where I deployed to the Heroku free tier behind Cloudflare, but once that was gone I went back to VMs.

Capistrano is considered "old school", non-idempotent, etc. Nowadays, it still works but Rails now comes with a Docker file and direct support for Kamal: the Docker based deploy of choice for Rails apps (if you want to use it).

The Rails app is quite small and the initial version of Kamal required a docker registry to be able to deploy to a VM. This wasn't a big deal as you can create a private docker registry for free in many places including docker itself (this is what I used). Yet for a small app this was annoying for me since I now require an account which is permanently linked to my deploys.

Thankfully as Kamal evolved the support for a local registry was added and this fixed the issue. What happens now is that Kamal starts a local registry container on your machine and forwards the remote server to it over SSH.

This works pretty great in practice: my deploys are not dependent on an account anymore, however there is a small caveat: tunnelling via SSH can be slow. I have my app in an always free Oracle instance which means that on a fresh deploy when it has to download the 130MB+ of compressed docker image it is quite slow as it doesn't go over 300KB/s. Small price to pay in my case, but keep this in mind if you want to bootstrap a start-up.

This is probably a good mid-point to share my optimized dockerfile :

# syntax = docker/dockerfile:1
ARG RUBY_VERSION=4.0.1
FROM ruby:$RUBY_VERSION-slim AS base
WORKDIR /rails
ENV RAILS_ENV="production" BUNDLE_WITHOUT="development:test" BUNDLE_DEPLOYMENT="1"
RUN gem update --system --no-document && gem install -N bundler

FROM base AS build

RUN apt-get update -qq && apt-get install --no-install-recommends -y build-essential pkg-config libyaml-dev libsqlite3-dev && rm -rf /var/lib/apt/lists /var/cache/apt/archives

COPY --link Gemfile Gemfile.lock ./

ENV BUNDLE_PATH="/usr/local/bundle" PATH="/usr/local/bundle/bin:$PATH" BUNDLE_JOBS=4 BUNDLE_RETRY=3
RUN bundle install && rm -rf ~/.bundle/ $BUNDLE_PATH/ruby/*/cache $BUNDLE_PATH/ruby/*/bundler/gems/*/.git

COPY --link . .

RUN chmod +x bin/*
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

FROM base AS runtime

RUN apt-get update -qq && apt-get install --no-install-recommends -y libsqlite3-0 && rm -rf /var/lib/apt/lists /var/cache/apt/archives

ENV BUNDLE_PATH="/usr/local/bundle" PATH="/usr/local/bundle/bin:$PATH"

COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails

RUN useradd rails --create-home --shell /bin/bash && mkdir -p /data /rails/db /rails/tmp /rails/log /rails/public && chown -R rails:rails /rails/db /rails/tmp /rails/log /rails/public /data
USER rails:rails

ENV DATABASE_URL="sqlite3:///data/production.sqlite3" RUBY_YJIT_ENABLE="1"

ENTRYPOINT ["/rails/bin/docker-entrypoint"]

VOLUME /data
EXPOSE 80
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

I have tried to reduce its size so that initial hit will be smaller plus I like to optimize things i.e. I still think the whole image at over 130MB is a bit too much.

Going back to my Rega turntable, its cartridge the Rega Carbon it's actually an Audio-Technica AT3600L , a moving magnet one - which is decent but also the minimum when it comes to sound quality. I do want to upgrade it to something like an Ortofon or a Nagaoka MP-150/300 and whilst they are pricier, the issue is that I have to adjust them properly after installing them.

Rega recently created the Carbon Pro which is a direct upgrade to the Carbon but without removing the whole cartridge just the needle itself and its housing. This is straightforward as it doesn't require for me to go crazy and try to adjust a new cartridge from scratch.

Similarly, Kamal requires docker and the simplest way on macOS is via Docker Desktop which is fine but also kind of an annoyance i.e. I want to get rid of it without dropping Kamal.

Just as a refresher this is how Kamal works more or less:

Local machine
  kamal CLI
    |-- uses Docker/Buildx to build images
    |-- may build locally, multi-arch, or with a remote builder depending on config
    |-- uses SSH to run Docker commands on servers
    |-- can run/use a local registry at localhost:5555

Remote servers
  Docker Engine
    |-- pulls image from registry
    |-- runs app/proxy/accessory containers

I can replace most of Docker Desktop by running:

brew install docker docker-buildx

But this does not give me a running Docker daemon here is where Colima jumps in, as it provides:
- a Linux VM with Docker Engine (dockerd)
- a Docker context/socket (docker context use colima)
- runtime for Buildx builder containers
- runtime for local registry container (localhost:5555 flow)

A nuance to note:

On some Colima setups, buildx may need to be installed or exposed as a Docker CLI plugin before docker buildx works properly. Also, if your local-registry setup needs special Docker daemon settings, such as insecure-registries or registry mirrors, Colima supports that too through its Docker configuration. In my case the simple setup was enough, but it is worth checking the Colima FAQ if your machine behaves differently.

After I fully removed Docker Desktop from its own UI (this felt quite nice), all I had to do was:

colima start --arch aarch64 --vm-type vz --mount-type virtiofs --cpus 1 --memory 1
docker context use colima

Then verify that everything works correctly:



colima status
docker context ls
docker context show
docker info
docker run --rm hello-world
docker buildx ls

And finally just ran kamal deploy and it worked: fast deploys without Docker Desktop. This is just awesome and it finally feels like good old Capistrano without the drawbacks.

Just like the Carbon Pro the Colima adds a container runtime without breaking Kamal whilst removing the icky initial Docker Desktop mammoth.

Credits

Tagged under: