Kamal Proxy – A minimal HTTP proxy for zero-downtime deployments
github.com
github.com
>To route traffic through the proxy to a web application, you *deploy* instances of the application to the proxy. *Deploying* an instance makes it available to the proxy, and replaces the instance it was using before (if any).
>e.g. `kamal-proxy deploy service1 --target web-1:3000`
'Deploy' is a fairly overloaded term already. Fun conversations ahead. Is the app deployed? Yes? No I mean is it deployed to the proxy? Hmm our Kamal proxy script is gonna need some changes and a redeployment so that it deploys the deployed apps to the proxy correctly.
Unsure why they couldn't have picked something like 'bind', or 'intercept', or even just 'proxy'... why 'deploy'..
That does not address OP's point. Any plain old reverse proxy handles this usecase, particularly those already used extensively as ingress controllers for container orchestration systems. Some even support specialized routing strategies.
Other than NIH, what exactly does kamal-proxy offers that any other reverse proxy doesn't already provide?
There’s some more explanation on why they decided to switch away from Traefik:
Unfortunately no. I posted my comment after reading that PR. It says nothing of substance, and presents no justification. The case to roll out their own is summarized in this claim:
> No need for cordfiles, which were a hack used to stop Traefik from sending request to a container to make it unhealthy. Kamal-proxy just sends traffic where we tell it to.
The thing is, Traefik also "just sends traffic where we tell it to." It's one of it's most basic feature. In fact, that's the whole point of a reverse proxy. Why roll out their own?
And even if for any reason at all Traefik didn't cut it, why not pick one of the many many production-grade reverse proxies around? Why roll their own?
Those questions have no answer.
I don't know why Kamal chose Swarm over k8s or k3s (simplicity perhaps?) but then, complexity needs a home, you can push it around but cannot hide it, hence a home grown proxy.
I have not tried Kamal proxy to know, but I am highly skeptical of something like this, because I am pretty sure I will be chasing it for support for anything from WebSockets to SSE, to HTTP/3 to various types of compression and encryption.
Your list of things a reverse proxy might do is a good example to me of how I expect this to go: what starts out as an ambition to be simple inevitably has to grow & grow more of complexity it sought to avoid.
Part of me strongly thinks we need competition & need other things trying to create broad ideally extensible ways or running systems. But a huge part of me sees Kamal & thinks, man, this is a lot of work being done only to have to keep walking backwards into the complexity they were trying to avoid. Usually second system syndrome is the first system being simple the second being overly complicated, and on the tin the case is inverse, but man, the competency of Kube & it's flexibility/adaptability as being a framework for Desired State Management really shows through for me.
Fully agree. I get resume-driven development vibes out of this project.
In k8s for example, you can use Docker and won't need a proxy for ZDD (while you might want one for Ingress and other uses)
If that's the reason then I feel someone failed to do even the most cursory research on the topic.
I mean, a staple of any container orchestration service is the use of an ingress controller to handle dialups and blue/green deployments.
More to the point, ingress controllers is the name given to the role assigned to any of the myriad reverse proxies readily available for years, if not decades, to handle this very same usecase.
I guess both versions of the app must be running simultaneously, with new traffic being routed to the new version of the app.
But what about DB migrations? Assuming the app uses a single database, and the new version of the app introduces changes to the DB schema, the new app version would modify the schema during startup via a migration script. However, the previous version of the app still expects the old schema. How is that handled?
The load balancer starts accepting connections on Server2 and stops accepting new connections on Server1. Then, Server1 disconnects when all of its connections are closed.
It could be different Servers or multiple Workers on one server.
During that window, as the other comments said, migrations have to be backwards compatible.
Secondly, each code version has to work with the current schema and the schema after a future migration, making all code effectively backwards compatible.
Your deploys end up being something like:
- Deploy new code that works with current and future schema
- Verify everything still works
- Run migrations
- Verify everything still works
- Clean up the acquired technical debt (the code that worked with the schema that no longer exists) at some point, or run out of runway and it won't be an issue
Though I'm still surprised that some people run DB alteration on application start up. Never saw one in real life.
I think I've seen it more commonly in the Golang ecosystem, for some reason. Also not sure how common it is nowadays, but seen lots of deployments (contained in Ansible scripts, Makefiles, Bash scripts or whatever) where the migration+deploy is run directly in sequence automatically for each deploy, rather than as discrete steps.
Edit: Maybe it's more of an educational problem than something else, where learning resources either don't specify when to actually run migrations or straight up recommend people to run migrations on application startup (one example: https://articles.wesionary.team/integrating-migration-tool-i...)
There's a couple of fundamental rules to follow. First, don't put something that will have insane impact into the application deploy changes. 99% of the DB changes are very cheap, and very minor. If the deploy is going to be very expensive, then just don't do it, we'll do it out of band. This has not been a problem in practice with our 20ish person team.
Second, it was kind of like double entry accounting. Once you committed the change, you can not go back and "fix it". If you did something really wrong (i.e. see above), then sure, but if not, you commit a correcting entry instead. Because you don't know who has recently downloaded your commit, and run it against their database.
The changes are a list of incremental steps that the system applies in order, if they had not been applied before. So, they are treated as, essentially, append only.
And it has worked really well for us, keeping the diverse developers who deploy again local databases in sync with little drama.
I've incorporated the same concept in my GUI programs that stand up their own DB. It's a very simple system.
We had no real need to address that aspect, and I would do something more formal with those if I had to, such as having a separate file with the store proc, and simply a note that it has changed in the change file. I mean, that's a bit of a trick.
Personally I found the idea of having multiple instances running the same schema update job at the same time (even if locks would keep it from running in practice) to be concerning so I always had it disabled for deploys.
So maybe there's something I'm missing about running DB migrations after the new code has been deployed - could you explain?
It kinda doesn't matter which you do first. And if you squint a little, it's effectively the same thing, because the migration will likely only become available via a deployment too
So yeah, the only things that's important is that the DB migration can't cause an incompatibility with any currently deployed version of the code - and if it would, you'll have to split the change so it doesn't. It'll force another deploy for the change you want to do, but it's what you're forced to do if maintenance windows aren't an option. Which is kinda a given for most b2c products
There's a good post about it here: https://rtpg.co/2021/06/07/changes-checklist.html
Besides, once you've run a service for a while that has acquired enough data for migrations to take a while, you realize that there are in fact two different types of migrations. "Schema migrations" which are generally fast and "Data migrations" that depending on the amount of data can take seconds or days. Or you can do the "data migrations" when needed (on the fly) instead of processing all the data. Can get gnarly quickly though.
Splitting those also allows you to reduce maintenance downtime if you don't have zero-downtime deployments already.
Schema migrations are versioned in git with the app, with up/down (or forward/reverse) migration scripts and are applied automatically during deployment of the associated code change to a given environment.
SQL Data migrations are stored in git so we have a record but are never applied automatically, always manually.
The other thing we've used along these lines, is having one or more low priority job(s) added to a queue, to apply some kind of change to records. These are essentially still data migrations, but they're written as part of the application code base (as a Job) rather than in SQL.
Holding requests until an upstream is available is also something Caddy does well, just configure the reverse_proxy with try_duration and try_interval, it will keep trying to choose a healthy upstream (determined via active health checks done in a separate goroutine) for that request until it times out.
Their proxy headers handling doesn't consider trusted IPs so if enabled, someone could spoof their IP by setting X-Forwarded-For. At least it's off by default, but they don't warn about this.
This looks pretty undercooked. I get that it's simple and that's the point, but I would never use this for anything in its current state. There's just so many better options out there.
I wouldn't run my Web apps without something like Cloudflare in front; it's great for DDoS, WAF, Rate Limiting, so on.
Assuming that, I just use the Cloudflare-signed certificate and configure that in my Nginx. It's really easy to do and it expires in 20 years or something.
Following the example, it starts 4 replicas of a 'web' service. You can create a service by running a deploy to one of the replicas, let's say example-web-1. What does the other 3 replicas do?
Now, let's say I update 'web'. Let's assume I want to do a zero-downtime deployment. That means I should be able to run a build command on the 'web' service, start this service somehow (maybe by adding an extra replica), and then run a deploy against the new target?
If I run a `docker compose up --build --force-recreate web` this will bring down the old replica, turning everything moot.
Instructions unclear, can anyone chime in and help me understand?
Or just switch my DNS or router between two backends?
"Why not just run Capistrano, Kubernetes or Docker Swarm?
...
Docker Swarm is much simpler than Kubernetes, but it’s still built on the same declarative model that uses state reconciliation. Kamal is intentionally designed around imperative commands, like Capistrano.
Ultimately, there are a myriad of ways to deploy web apps, but this is the toolkit we’ve used at 37signals to bring HEY and all our other formerly cloud-hosted applications home to our own hardware."
For the second part about updating the service, I'm a little less clear. I guess the expectation would be to bring up a differently-named service within the same network, and then `kamal-proxy deploy` it? So maybe the expectation is for service names to include a version number? Keeping the old version hot makes sense if you want to quickly be able to route back to it.
[0]: https://docs.docker.com/reference/compose-file/deploy/#endpo...
That’s where you have a proxy which effectively pauses traffic for a few seconds - incoming requests appear to take a couple of seconds longer than usual, but are still completed after that short delay.
During those couple of seconds you can run a blocking infrastructure change - could be a small database migration, or could be something a little more complex as long as you can get it finished in less than about 5 seconds.
I feel that when you've grown to the size where zero downtime deploys are so important you'd do custom code just to enable them, it would be best to just invest in industry best practices and do backwards compatible deploys.
There are some context about why they switched and decided to roll their own, from the PR.
Man, I've been there where you wish third party software had some feature but writing your own is WORST thing you can do for a company 9/10 times. My current company is dealing with massive tech debt because of all this homegrown software.
It looks to me that they were already using Traefik, and Traefik does indeed support the feature they wished to have.
Apparently they also got rid of support for healthchecks, which all by itself would be expected to require an extensive justification. But no.
I would love to hear why the people behind this decision felt that Traefik's support for dynamic route configuration wasn't suited to configure routes dynamically.
I would also love to hear the rationale behind the decision to invest their time rolling their own reverse proxy instead of just doing docker run on one of the many production-grade reverse proxies readily available.
I don't know. This has vibes of spending months in a development project to avoid spending hours reading a manual.
The did not remove support for healthchecks. They're directly mentioned in the README.
> It will immediately begin running HTTP health checks to ensure it's reachable and working and, as soon as those health checks succeed, will start routing traffic to it.
I don't know. This has vibes of doing little to no research and then shit-talking a product.
But I don't think they ever published comparisons to anything else than Traefik
But this looks like somehow a re-invention of what Kubernetes provides.
Kubernetes has come a long way in terms of ease of deployment on bare metal.
Obviously, rebooting the machine the proxy is running on is trickier though. I don't feel confident they've done enough to properly support having multiple proxy instances running side by side (no shared storage mechanism for TLS certs at least), which would allow upgrading one at a time and using a router/firewall/DNS in front of it to route to both normally, then switch it to one at a time while doing maintenance to reboot them, and back to both during normal operations.
I think the PR that pushes this kamal-proxy project explicitly removes supports for healthchecks? So it's unclear.
In theory, a reverse proxy like Traefik supports this feature.
In practice it does too.
So I don't know. It seems there's some rationale that's definitely missing from the whole story. I doubt people haphazardly decide to roll out a custom reverse proxy developed in-house. The reasons laid out in the doc definitely don't seem to be it.
As I said elsewhere in this topic, this is all too basic IMO, (disclaimer: I'm a Caddy maintainer) Caddy does this all in a more robust way.
Uber, for example, moved to the cloud. I feel like in the span between them there are far more companies for which Kamal is not enough.
I hope I'm wrong, though. It'll be nice for many companies to be have the choice of exiting the cloud.
I can get weeks advance notice before we have a load increase through new users
I feel like this is a bias in the HN bubble: In the real world, 99% of companies with any sort of web servers (cloud or otherwise) are running very boring, constant, non-Uber workloads.
I am pretty sure Uber belongs to the 1% of the internet companies in terms of scale. 37Signals isn't exactly small either. They spend $3M a year on infrastructure in 2019. Likely a lot higher now.
The whole Tech cycle needs to stop having a top down approach where everyone are doing what Big tech are using. Instead we should try to push the simplest tool from low end all the way to 95% mark.
Cloud is a scam for the vast majority of companies.
The other thing is that cloud hardware is generally very very slow and many engineers don't seem to appreciate how bad it is. Slow single thread performance because of using the most parallel CPUs possible (which are the cheapest per W for the hyperscalers), very poor IO speeds, etc.
So often a lot of this devops/infra work is solved by just using much faster hardware. If you have a fairly IO heavy workload then switching from slow storage to PCIe4 7gbyte/sec NVMe drives is going to solve so many problems. If your app can't do much work in parallel then CPUs with much faster single threading performance can have huge gains.
Most businesses have fairly predictable interactive workload patterns, and their batch jobs are not high priority and can be managed as such (with the usual scheduling and bin packing orchestration). Wikipedia is one of the top 10 visited sites on the internet, and they run in their own datacenter, for example. The FedNow instant payment system the Federal Reserve recently went live with still runs on a mainframe. Bank of America was saving $2B a year running their own internal cloud (although I have heard they are making an attempt to try to move to a public cloud).
My hot take is public cloud was an artifact of ZIRP and cheap money, where speed and scale were paramount, cost being an afterthought (Russ Hanneman pre-revenue bit here, "get big fast and sell"; great fit for cloud). With that macro over, and profitability over growth being the go forward MO, the equation might change. Too early to tell imho. Public cloud margins are compute customer opportunities.
To a vast majority of Wikipedia users who are not logged in, all it needs to do is show (potentially pre-rendered) article pages with no dynamic, per-user content. Those pages are easy to cache or even offload to a CDN. FOr all the users care, it could be a giant key-value store, mapping article slugs to HTML pages.
This simplicity allows them to keep costs down, and the low costs mean that they don't have to be a business and care about time-on-page, personalized article recommendations or advertising.
Other kinds of apps (like social media or messaging) have very different usage patterns and can't use this kind of structure.
Reddit can’t turn a profit, Signal is in financial peril. Meta runs their own data centers. WhatsApp could handle ~3M open TCP connections per server, running the operation with under 300 servers [1] and serving ~200M users. StackOverflow was running their Q&A platform off of 9 on prem servers as of 2022 [2]. Can you make a profitable business out of the expensive complex machine? That is rare, based on the evidence. If you’re not a business, you’re better off on Hetzner (or some other dedicated server provider) boxes with backups. If you’re down you’re down, you’ll be back up shortly. Downtime is cheaper than five 9s or whatever.
I’m not saying “cloud bad,” I’m saying cloud where it makes sense. And those use cases are the exception, not the rule. If you're not scaling to an event where you can dump these cloud costs on someone else (acquisition event), or pay for them yourself (either donations, profitability, or wealthy benefactor), then it's pointless. It's techno performance art or fancy make work, depending on your perspective.
[1] https://news.ycombinator.com/item?id=33710911
[2] https://www.datacenterdynamics.com/en/news/stack-overflow-st...
If you're running an ecommerce store for example, you could buy some extra capacity from AWS for Christmas and Black Friday, and rely on your own servers exclusively for the rest of the year.
This. Mostly disk latency, for me. People who have only ever known DBaaS have no idea how absurdly fast they can be when you don’t have compute and disk split by network hops, and your disks are NVMe.
Of course, it doesn’t matter, because the 10x latency hit is overshadowed by the miasma of everything else in a modern stack. My favorite is introducing a caching layer because you can’t write performant SQL, and your DB would struggle to deliver it anyway.
This. Those complaining about performance seem to come from people who are not be aware of latency numbers.
Sure, the latency from reading data from a local drive can be lower than 1ms, whereas in block storage services like AWS EBS it can take more than 10ms. An order of magnitude slower. Gosh, that's a lot.
But whatever your disk access needs, your response will be sent over the wire to clients. That takes between 100-250ms.
Will your users even notice a difference if your response times are 110ms instead of 100ms? Come on.
I was referring more to how it's extremely rare to have a stack as simple as request --> LB --> app --> DB. Instead, the app almost always a micro service, even when it wasn't warranted, and each service is still making calls to DBs. Many of the services depend on other services, so there's no parallelization there. Then there's the caching layer stuck between service --> DB, because by and large RDBMS isn't understood or managed well, so the fix is to just throw Redis between them.
I don't think this is a good argument. Even though disk latencies can add up, unless you're doing IO-heavy operations that should really be async calls, they are always a few orders of magnitude smaller than the whole response times.
The hypothetical gains you get from getting rid of 100% of your IO latencies tops off at a couple of dozen milliseconds. In platform-as-a-service offerings such as AWS' DynamoDB or Azure's CosmosDB, which involve a few network calls, an index query normally takes between 10 and 20ms. You barely get above single-digit performance gains if you lower risk latencies down to zero.
In relative terms, if you are operating an app where single-millisecond deltas in latencies are relevant, you get far greater decreases in response times by doing regional and edge deployments than switching to bare metal. Forget about doing regional deployments by running your hardware in-house.
There are many reason why talks about performance needs to start by getting performance numbers and figuring out bottlenecks.
I’ve seen API calls that result in hundreds of DB calls. While yes, of course refactoring should be done to drop that, the fact remains that if even a small number of those calls have to read from disk, the latency starts adding up.
It’s also not uncommon to have horrendously suboptimal schema, with UUIDv4 as PK, JSON blobs, etc. Querying those often results in lots of disk reads simply due to RDBMS design. The only way those result in anything resembling acceptable UX is with local NVMe drives for the DB, because EBS just isn’t going to cut it.
I wasn't expecting blasting speed on the lowest tear, but 10x slower is bonkers.
You don't need to roll out your own reverse proxy project to run services in-house.
Any container orchestration service was designed for that scenario. It's why they exist.
Under the hood, applications include a reverse proxy to handle deployment scenarios, like blue/green, onebox, canary, etc.
You definitely do not need to roll your own project to do that.
And due to the insane markup of many cloud services it can make sense to just use beefier servers 24/7 to deal with the peaks. From my experience crazy traffic outliers that need sophisticated auto-scaling rarely happens outside of VC-fueled growth trajectories.
https://basecamp.com/cloud-exit
Of course, it's fair to say that rebuilding the components that the industry uses for hosting on bare metal is NIH syndrome.