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. It's 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 it runs a local registry and on deploy Kamal creates a SSH tunnel to my local machine which acts as a registry.
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
# Set production environment
ENV RAILS_ENV="production" \
BUNDLE_WITHOUT="development:test" \
BUNDLE_DEPLOYMENT="1"
RUN gem update --system --no-document && \
gem install -N bundler
# Build stage
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 gem specs
COPY --link Gemfile Gemfile.lock ./
# Install gems including executables (puma)
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 app code
COPY --link . .
# Make bin/* executable
RUN chmod +x bin/*
# Precompile assets (dummy secret to allow skipping credentials)
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Runtime stage
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
# Set up non-root user and permissions
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
# Runtime ENV
ENV DATABASE_URL="sqlite3:///data/production.sqlite3" \
RUBY_YJIT_ENABLE="1"
# Entrypoint prepares the DB
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
VOLUME /data
EXPOSE 80
# Start Puma with config file
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 locally to build image
|-- uses SSH to run docker commands on servers
|-- runs/uses 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)
After I fully removed Docker Desktop from its own UI (this felt quite nice), all I had to do was:
colima start --arch aarch64
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 add a container runtime without breaking Kamal but removing the icky initial Docker Desktop mammoth.