Tony Messias

March 26, 2023

Kamal and Laravel (previously MRSK)

Update 09.24.2023: MRSK was renamed to Kamal.

I've recorded a video version of this article, which you can find on my YouTube channel.

---

The 37signals folks released a new tool for deploying containerized web apps called Kamal. When we think about deploying containerized apps, we might think of Kubernetes, Docker Swarm, Hashicorp's Nomad, or any container service our cloud provider may have. But Kamal is much, much simpler than that. It's minimalistic by design.

Kamal is inspired by another deployment tool called Capistrano. While most container deployment tools (aka. Orchestrators) work in a declarative approach, Kamal is designed around imperative commands. You can think of it as a thin layer on top of Docker CLI commands.

If you have Ruby installed, you may install Kamal as a gem:

gem install kamal

But if you don't have Ruby and don't want to bother installing it, you don't have to 'cause we can use Kamal's Docker image to run it locally. We may do so by adding an alias to our `~/.bashrc` file or equivalent:

# If you're on Linux...
alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "${SSH_AUTH_SOCK}:/ssh-agent" -v /var/run/docker.sock:/var/run/docker.sock -e "SSH_AUTH_SOCK=/ssh-agent" ghcr.io/basecamp/kamal:latest'

# Or, if you're on macOS...
alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock" -e SSH_AUTH_SOCK="/run/host-services/ssh-auth.sock" -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/basecamp/kamal:latest'

The alias will work pretty much the same, but it has some drawbacks:

  • The `redeploy` command might not work because it skips registry auth to speed up the process. The alternative is either using a `--skip-push` flag or using the `deploy` command instead;
  • The `init` command will create a `config/deploy.yml` file, but since this container runs as root, the file will be created as root too, so if you're on Linux, change the ownership of the file, and you'll be good

Besides that, everything works pretty much the same.

Designed for VMs

Kamal was designed to run on any infrastructure setup you may have. You can use Kamal to run your app in the cloud or on-prem.

All you have to do is run `kamal init` at the root of your project, configure the `config/deploy.yml` file generated by the `init` command, and deploy your app to your servers.

You can configure VMs for your apps and use Kamal to package the app in a container image, SSH into the servers, pull the new version, and swap the running containers in a zero-downtime way. Your app will run behind a Traefik container that will forward the requests to the new container after it passes the health check.

Since resource management (CPU, memory, etc.) can be configured at the VM level, the idea is that you'll provision your VMs based on the resources your app needs. If you need more resources, you either scale up your VM, provision a bigger one, or more VMs if you can horizontally scale the work.

However, Kamal does have a way to pass container options, which will be passed along to the `docker run ...` command. We can probably use it to specify container runtime resource constraints if necessary. This may be needed to run multiple containers in the same VM. Say your app runs in different roles: web, worker, and cron. You'd have to set up a VM for each role because we cannot run multiple instances of the same container in the same host. But that will change very soon as the work has been merged already. Currently, the role names and destinations are considered when deploying your apps! You could even run multiple environments on the same server, and everything would work.

Destination Environments

We could configure everything in our `config/deploy.yml` file, but I prefer a destination config file instead. We still need the base `config/deploy.yml` file, which we can use for the basic configuration of our app, and we could create a "production" config file by creating a `config/deploy.production.yml` file where we could only have the server's configuration and other things specific to production.

Using a destination environment also lets us keep using the base `.env` for local development and have a specific `.env.production` file that will be used for our production destination. This way, we can keep developing locally and still have the production config environment variables around when necessary.

Managing Environment Secrets

Since Kamal uses the dotenv approach, managing secrets is a matter of ensuring our `.env.production` file is never committed in plain text to our repository and adding it to our `.dockerignore` so we don't accidentally push it inside our image either. Here's the `.dockerignore` I use:

/vendor/
/tests/
/node_modules/
/stubs/
/.env*

We can use Laravel's encrypted environment files to keep the configs in the repository. This way, we could use something like 1Password to share the encryption key with our teammates, and they would be able to decrypt the env file for deployment.

# Creates a .env.production.encrypted
php artisan env:encrypt --env=production --key={MY_SECRET_KEY}

# Recreates the .env.production from the encrypted file
php artisan key:decrypt --env=production --key={MY_SECRET_KEY}

Conclusion

That's it. Really. Kamal is so simple. I'll be using it more and more for my projects.

About Tony Messias