Scalling app deployed to Dokku

I am trying various open source PaaS based on Docker and so far the easiest one is Dokku.

The problem with Dokku is that it does not manage scaling of your app by launching more containers with yours application. You can install third-party plugins, which will start multiple processes inside single docker container, but this doesn't work well for web apps listening on one port. And I could not find how to leverage SO_REUSEPORT in Express.js app.

So my solution to this - create multiple Dokku apps with identical environment setup and manually write nginx config pointing to all these apps. So my naive approach was:

server {
        listen 80;
        server_name example.com;
        resolver 127.0.0.1;
 
        upstream dokku {
                server web1.dokku.localdomain:80;
                server web2.dokku.localdomain:80;
                server web3.dokku.localdomain:80;
                server web4.dokku.localdomain:80;
        }
 
        location / {
                proxy_pass http://dokku;
        }
}

My assumption was that if I will NOT set "Host" header in my "location" block, it will pass hostname of the specific upstream server to upstream. It will NOT! Looks like Nginx will resolve all domains to IP addresses once and from there it will use name of the upstream group (i.e. "dokku" in example above) inside "Host:" header. Which will not work for Dokku container since it need to know correct hostname...

The funny part is that when you use "proxy_pass http://web1.dokku.localdomain" instead of using upstream group name, it WILL pass hostname to upstream. So until I find proper solution, I fixed it with a little bit of Lua code. For people who have no clue what it doing - I define an array of available hosts and select random one on each request.

server {
        listen 80;
        server_name example.com;
        resolver 127.0.0.1;
 
        location / {
                set $upstream "";
                rewrite_by_lua '
                        local upstreams = {
                                "http://web1.dokku.localdomain",
                                "http://web2.dokku.localdomain",
                                "http://web3.dokku.localdomain",
                                "http://web4.dokku.localdomain"
                        }
                        ngx.var.upstream = upstreams[ math.random( #upstreams ) ] 
                ';
                proxy_pass $upstream;
        }
}

This potentially could be expanded to query Redis instead of hardcoding backends in config, but code above works for now.

UPDATE: snippet above is not quite performant actually, since it looks like it resolves host each time request is proxied. Much better solution would be something along these lines:

server {
        listen 80;
        server_name example.com;
        resolver 127.0.0.1;
 
        upstream dokku {
                server dokku.localdomain:80;
        }
 
        location / {
                set $upstream "";
                rewrite_by_lua '
                        local upstreams = {
                                "web1.dokku.localdomain",
                                "web2.dokku.localdomain",
                                "web3.dokku.localdomain",
                                "web4.dokku.localdomain"
                        }
                        ngx.var.upstream = upstreams[ math.random( #upstreams ) ] 
                ';
                proxy_set_header Host $upstream;
                proxy_pass dokku;
        }
}

Comments

Thanks for the article!
Can you please advice how do you push to all these repos?

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

To submit this form you need to use Internet Explorer v10+ or recent version of Chrome, Firefox or Safari.