Update (2024-10-25): This post was updated to clarify that temporary registry credentials are needed for initial setup. These will be replaced with your private registry credentials later.
DHH commented in his 2024 RailsWorld Keynote that "you shouldn't have to use DockerHub or any other registry service to run [Kamal 2]."
Until Kamal 2 has first-class support for running your own private Docker registry, we can use a minor trick with kamal-proxy
to make it work today.
This technique also works well for hosting other accessories that require a web interface. Hans Schnedlitz recently tweeted about using the same technique to deploy Puny Monitor as an accessory on the same server as his app.
We will use several new features of Kamal 2 to make this happen:
kamal-proxy
: Simplifies routing and SSL terminationkamal network
: Allows containers to communicate with each other privatelyAliases
: Enables more flexible configuration and maintains an "Infrastructure as Code" workflow even with the few tricks we need to perform
We will also need to bypass the default kamal setup
steps and manually bootstrap our server because docker login
will not work until the registry is up and running.
Step-by-Step Guide to Host Your Own Docker Registry with Kamal 2
Prerequisites
This guide assumes that you have:
- An app you want to deploy with Kamal 2
- A server you want to deploy it to that is ready for Kamal 2 but does not have anything set up yet
- A domain name that you want to use for the registry
- Kamal 2 installed on your local machine
- You've run
kamal init
locally for your app
See the Kamal 2 installation guide for help getting these set up if you haven't done so already.
1. Create a password file for the Docker Registry
We will use basic HTTP auth to secure the private registry. The Docker Registry image already supports including a password file generated with htpasswd
, so we just need to create that file.
We can generate that file locally at config/registry.htpasswd
with this command:
docker run --entrypoint htpasswd httpd:2 -Bbn testuser testpassword > config/registry.htpasswd
Remember to replace testuser
and testpassword
with your own username and password.
htpasswd stores these credentials hashed, so we can safely store the file in our repository. But remember to store the plaintext password securely in your secrets management tool of choice.
2. Update your `deploy.yml` file for setup
For setup, we need to add/update two components in the app's deploy.yml
file:
Temporarily, use credentials for any Docker registry that you can access (you will need this to run the setup steps, but we will replace later with our private registry). For an alternative, see [1].
Add the
registry
accessory using the officialregistry
Docker imageAdd an alias to configure
kamal-proxy
to route traffic to theregistry
accessory on the registry's domain name
Here's an example deploy.yml
that adds these components:
registry:
# Temporary, use credentials for any Docker registry that you can access
server: docker.io
username: your-dockerhub-username
password: your-dockerhub-password
# Later, we will replace these with credentials for our private registry
#server: registry.myhost.com
#username: testuser
#password:
#- MY_REGISTRY_PASSWORD
aliases:
# Replace "SERVICE" with the service name configured for this app
# Set "--host" to the domain name you want to use for the registry
add-registry-to-proxy: |
server exec docker exec kamal-proxy kamal-proxy deploy registry
--target "SERVICE-registry:5000"
--host "registry.myhost.com"
--tls
--deploy-timeout "30s"
--drain-timeout "30s"
--health-check-path "/"
--buffer-requests
--buffer-responses
--log-request-header "Cache-Control"
--log-request-header "Last-Modified"
--log-request-header "User-Agent"
accessories:
registry:
image: registry:latest
host: HOST # replace with server ip address
port: "127.0.0.1:5000:5000" # exposes port 5000 only to the kamal network
env:
clear:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
files:
- config/registry.htpasswd:/auth/htpasswd # mount the password file
directories:
- data:/var/lib/registry # data directory for the registry
3. Bootstrap the server
As previously mentioned, we cannot use the default kamal setup
command because docker login
will not work until the registry is up and running.
Instead, run a couple of commands to set up kamal
on the server, install docker
, and start kamal-proxy
:
kamal server bootstrap
kamal proxy boot
4. Boot the accessory
We can now manually boot the registry
accessory:
kamal accessory boot registry
5. Add the registry to the proxy
Finally, we will configure the proxy to route traffic to the registry. We already set up an alias to make this step easy:
kamal add-registry-to-proxy
The alias uses kamal-proxy deploy
to connect the registry
container to the public internet on the desired domain name with --tls.
Adjust the alias as needed to fit your specific use case.
6. Replace the temporary registry credentials
Now that the registry is up and running, we can replace the temporary registry credentials with our private registry credentials.
In the deploy.yml
file, update the registry:
settings to use the server and credentials of your private registry:
registry:
server: registry.myhost.com
username: testuser
password:
-MY_REGISTRY_PASSWORD
Test the registry and deploy
We can now test the registry by running docker login
on our local machine to make sure we can connect to it:
docker login registry.myhost.com -u testuser
If everything is working, you should see output that looks like this:
Login Succeeded
And we are ready to deploy!
Now we can run kamal deploy
and everything will work as expected with your images built by kamal
pushed to your new private registry:
kamal deploy
Conclusion
With Kamal 2 and a few techniques to self-host a private Docker registry, we can now fully self-host our app on a single server with no external dependencies!
--
[1] If you don't have a registry available, as a hack you can specify any hostname that will respond 200 to the path /v2/
. For example, if you specify server: github.com
, Docker login will succeed because github.com/v2/
always responds 200. Docker login doesn't do anything fancier than check for a 200.