Docker Tutorial Part 3: Kamal with Alternative Proxy ==================================================== In this document we discuss using Kamal orchestration with an external reverse proxy, such as `nginx `_ or `haproxy `_. We also discuss opening additional ports on docker containers orchestrated with Kamal. This document assumes you have a working knowledge of using Kamal for orchestration of Docker containers, as described in :doc:`kamal`. Using an Alternative Proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, Kamal uses ``kamal-proxy`` during deployments to perform health checks on startup, open ports 80 and 443 on your server(s), and allow for a seamless transition from old containers to new. But sometimes we need to run a different reverse proxy + load balancer like `nginx `_ or `haproxy `_, which already do the same thing, so we have no need for kamal-proxy. We can disable ``kamal-proxy`` using a ``proxy:`` directive in our *deploy.yml* file, like so: .. code-block:: servers: web: hosts: - 10.10.10.1 - 10.10.10.2 proxy: false .. Caution:: When doing above, be sure to comment out or remove any other ``proxy:`` directives in your *deploy.yml*. Opening Ports +++++++++++++ Since ``kamal-proxy`` is disabled, we will need to use an ``options:`` stanza to ``publish:`` the ports we want to open. The following would open port 80 on the public interface proxying to port 80 on the container, same for ports 443, 8080 and 4430: .. code-block:: servers: web: hosts: - 10.10.10.1 - 10.10.10.2 proxy: false options: publish: - 80:80 - 443:443 - 8080:8080 - 4430:4430 Resolving port conflicts ++++++++++++++++++++++++ With the above config we see that the initial ``kamal setup`` is fine, but subsequent deploys will fail due to "port already allocated" errors. This is because Kamal will try to start the new container before shutting down the old one, which ``kamal-proxy`` used to handle for "seamless deployments", but is no longer running. The solution is to run a *hook* script which will stop containers before Kamal starts the new ones: https://kamal-deploy.org/docs/hooks/overview/ The following ``pre-app-boot`` hook script issues ``docker container stop`` on the containers first. *.kamal/hooks/pre-app-boot*: .. code-block:: #!/bin/python3 # # Kamal hook script 'pre-app-boot' to try to stop all containers prior to # kamal starting the new ones. This is for when you need to listen on multiple # ports using options: publish: and start getting "port already allocated" errors. # import os, subprocess, datetime # Open a logfile. f = open('/var/log/pre-app-boot.log', "a+") # Try to stop containers before kamal boots the app. hosts = os.environ['KAMAL_HOSTS'].split(',') for host in hosts: x = str(datetime.datetime.now()) f.write(x + ': stopping containers on ' + host + ' ... ') result = subprocess.run(['ssh', host, 'docker container stop $(docker container ls -q)'], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: f.write('error: docker container stop failed\n') else: f.write('success\n') f.close() Applying Group Sizes ++++++++++++++++++++ An issue with above is deploys will by default block until *all* containers have stopped, causing a downtime which grows exponentially with the number of hosts. The solution for this is to use the ``boot:`` option to set group sizes. For example, say you have 4 hosts and want to deply them in "sets" of 2 at a time, with a 10 second pause between. You can add a ``boot:`` stanza to your config, like so: .. code-block:: servers: web: hosts: - 10.10.10.1 - 10.10.10.2 proxy: false options: publish: - 80:80 - 443:443 - 8080:8080 - 4430:4430 boot: limit: 2 wait: 10 This tells Kamal to deploy 2 at a time, so the script above also runs/stops 2 at a time. This way you always have (the other) 2 hosts in production to pick up traffic and deploy seamlessly. More details in the official docs here: https://kamal-deploy.org/docs/configuration/booting/ Setting Environment Variables per-server ++++++++++++++++++++++++++++++++++++++++ You might also have to pass custom ``--env`` variables to the docker run command. This can be done with tags. For example say you need to pass the IP address as an environment variable. You add tags by adding them to the server IP addresses separated by a colon. To modify our existing *deploy.yml* it looks like this: .. code-block:: servers: web: hosts: - 10.10.10.1: 10.10.10.1 - 10.10.10.2: 10.10.10.2 .. Note:: Although we are using the IP addresses in this example, tags can have any value. We then add an ``env:`` stanza which uses the tags to fill in the variables for those specific hosts. Note too, we also add a ``clear:`` tag for a custom environment variable that will be applied *globally* to all hosts. .. code-block:: env: clear: MY_GLOBAL_HOST_VAR: true tags: 10.10.10.1: MY_HOST_IP: 10.10.10.1 10.10.10.2: MY_HOST_IP: 10.10.10.2 Putting it All Together +++++++++++++++++++++++ The full config.yml: .. code-block:: # Deploy to these servers. servers: web: hosts: # Tags are done in the form of IP: , we use the tag here as also the IP address, # so we can pass custom ENV vars containing specific IPs to each docker run command # (see below) - 10.10.10.1: 10.10.10.1 - 10.10.10.2: 10.10.10.2 proxy: false options: publish: - 80:80 - 443:443 - 8080:8080 - 4430:4430 boot: limit: 2 wait: 10 env: clear: MY_GLOBAL_HOST_VAR: true tags: 10.10.10.1: MY_HOST_IP: 10.10.10.1 10.10.10.2: MY_HOST_IP: 10.10.10.2