Lately I've started to put my turntable back into working order. 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.
From Capistrano to Kamal
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 al. 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 Dockerfile and direct support for Kamal: the Docker-based deployment 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 Oracle Cloud Always Free instance, using its Arm-based Ampere A1 Compute offering. On this server and connection, a fresh deploy that transfers the 130MB+ compressed Docker image over the SSH tunnel has stayed below 300KB/s. That is a small price to pay in my case, but it is an observation from this setup rather than a universal Kamal limit.
A smaller Docker image
This is probably a good mid-point to share my optimized dockerfile :
# syntax = docker/dockerfile:1
ARG RUBY_VERSION=4.0.6
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, is 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 stylus and its housing. This is straightforward as it doesn't require me to go crazy trying to adjust a new cartridge from scratch.
Replacing Docker Desktop with Colima
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; this 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.
Before removing Docker Desktop, preserve or migrate any local images, volumes, and Docker contexts you still need; uninstalling it can otherwise discard local data. 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, Colima adds a container runtime without breaking Kamal whilst removing the icky Docker Desktop behemoth.