I have been shiping most of my Go apps with Kamal for a while now. I am sharing the main config files I am using, with two different apps: this blog and a object storage service.
Quoting the definition from the Kamal website: “Kamal offers zero-downtime deploys, rolling restarts, asset bridging, remote builds, accessory service management, and everything else you need to deploy and manage your web app in production with Docker. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized.”
Practically, it allows for setting up the service on one or multiple bare servers and shipping the app there. And because Go builds into a binary, I am only shipping that to the server, embedded into a very minimal Docker image.
For details of the setup, Kamal commands, and general documentation, please read the documentation on the Kamal website.
Kamal can use an elaborate setup, but I have kept things simple. Here’s the
deploy.yml
:
service: blog
image: edimoldovan/blog
servers:
web:
- 37.27.83.82
proxy:
ssl: true
host: eduardmoldovan.com
app_port: 8000
registry:
username: edimoldovan
password:
- KAMAL_REGISTRY_PASSWORD
builder:
arch: arm64
Simple and concise — it only contains a few lines, configuring the IP to ship to, name of the service, port for the proxy to link to, the domain, and the Docker registry access password.
This last bit, the
KAMAL_REGISTRY_PASSWORD
is configured in the
secrets
file:
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
There is only one additional file I am using, the
Dockerfile
:
# The build stage
FROM golang:1.23 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo server.go
# The run stage
FROM scratch
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8000
CMD ["./server", "-port", "8000"]
As you can see, I have configured a builder with the latest version of Go. This builds the binary, which is then copied into the image that runs it. Otherwise, the file contains only standard Docker config.
This is a service that works like an S3 bucket and performs all operations through an API. Most Kamal and Docker settings are the same, with the addition of mounted folders for the SQLite database and the files folder where objects are stored.
Here’s the
deploy.yml
:
service: object-storage
image: edimoldovan/object-storage
servers:
web:
- 37.27.83.82
proxy:
ssl: true
host: objects.makros.app
app_port: 8080
registry:
username: edimoldovan
password:
- KAMAL_REGISTRY_PASSWORD
builder:
arch: arm64
env:
clear:
SOME_OTHER_VAR: 123
secret:
- MAKROS_OBJECT_STORAGE_TOKEN
volumes:
- "/root/storage/sqlite:/app/sqlite"
- "/root/storage/files:/app/files"
And here’s the
Dockerfile
:
# The build stage
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY . .
RUN apk add --no-cache gcc libc-dev
RUN CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo main.go
# The run stage
FROM alpine:latest
WORKDIR /app
RUN apk add --no-cache sqlite-libs
COPY --from=builder /app/main .
RUN ls -l /app/main
EXPOSE 8080
CMD ["./main", "-port", "8080"]
That's it. Each time I run
kamal deploy
these get shipped from my laptop directly to the Hetzner machines.