Building REST APIs with Qt, Docker and Caddy

Petar Koretić
9 min readDec 16, 2022

--

Minimal REST API built with Qt

Qt is a widely known framework for build cross platform applications, as we have shown in previous examples. It is written in ever so popular C++ language and has been utilized in a wide range of industries, including automotive, aviation, and finance due to it’s ease of use and performance.
One of the key features of Qt is its modular structure, which allows developers to select only the components necessary for their specific project, thereby reducing the overall codebase size and simplifying maintenance.
In this article, we will be focusing on the HTTP(S) server functionality of Qt, which has been recently released as a technical preview. We will use this to implement a basic REST API, which can be extended with more complex routes as needed.
To provide better security, we will place it behind the Caddy HTTPS server, an open-source enterprise-grade solution. The final application will be packaged using Docker Compose for easy deployment.

Note: it is expected to have knowledge of mentioned technologies but relevant links are provided throughout the article to learn about any components.

QtHttpServer

Currently Qt is at version 6.4.2 and it comes with a technical preview of a HTTP(S) server which was originally introduced in 2019.

While Qt has many modules including support for TCP servers, MQTT client, Websocket server and client and more, there was not a straitghforward way to build an HTTP server in Qt without resorting to external libraries.
And while that might not be the biggest requirement it was always a drawback as it would lead to reinvetion of the wheel or using questionably mantainable code or even using other technology stacks even if Qt could be a suitable fit.

Having a technical preview of the module finally gives us an opportunity to have this functionality added easily to existing services that might need it and give feedback to Qt project which can be used for further improvements.

Limitations

Going back to introduction, this module is still in technical preview which means it might change and it’s primariliy intended for developers usage, so not something that is expected to be already put into production. However, as shown in comments of official blog post that doesn’t mean it can’t be done.

Nevertheless, as listed in official docs it’s not intended to be used directly on the public internet, even with builtin HTTPS support, so in this post we are going to put it behind Caddy — enterprise ready HTTPS server.

Additionally there are other limitations that you might want to take into account as described in https://bugreports.qt.io/browse/QTBUG-60105.

Warning: using a proxy doesn’t solve all potential attack surfaces so this article doesn’t recommend putting it into production, at least not until module is officially released. Take it at your own risk.

Installation & Building

As a technical preview it is also not available in every distribution just yet. One of the distributions that do have it by default is Alpine and Arch linux. This makes it perfect for making a Docker Alpine image for production deploy while developing with it on an Arch linux machine.

For others, it can be easily installed using official installer.

Qt Installer with Qt HTTP Server Technical Preview library

Once it’s installed it can be added to QMake projects .pro file with

QT += httpserver

while Cmake examples are equally available in the documentation.

Building a REST API

As always Qt docs on QtHttpServer have us covered with recent blog post showing this exact usecase so make sure to check those out.

For our purpose we are going to make a simple HTTP REST API which returns “Hello World” on our HTTP GET / request.

#include <QtCore>
#include <QtHttpServer>

int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QHttpServer httpServer;

const auto port = httpServer.listen(QHostAddress::Any, 8282);

if (!port)
return 0;

httpServer.route("/", []() {
return "Hello world";
});


qDebug() << QCoreApplication::translate("qtdockercaddy",
"Running on http://127.0.0.1:%1/").arg(port);
return app.exec();
}

This runs our server at port 8282 and opening the URL in the browser gives us the expected result.

Our REST API working over HTTP in the browser

This barely touches REST API definition but extending it is quite straightforward using many examples in documentation and is left as an exercise for the reader. We are also going to expand it in the future articles so feel free to stick around.

Packaging it with a Dockerfile

For easing deploy and management of backend applications, docker containers are very popular and good choice. This is also a good fit for us so we can package Qt libraries that are only needed for running our REST API.

As a base we are going to use a very popular official Docker Alpine linux image which is only 5MB in size.

FROM alpine:3.17 AS build

RUN apk add --no-cache qt6-qtbase-dev qt6-qthttpserver-dev make g++

RUN mkdir -p /app
COPY qtdockercaddy.pro /app
COPY main.cpp /app

WORKDIR /app/
RUN qmake6 && make -j$(nproc)

FROM alpine:3.17
COPY --from=build /app/qtdockercaddy /app/qtdockercaddy
RUN apk add --no-cache qt6-qthttpserver

EXPOSE 8282
ENTRYPOINT ["/app/qtdockercaddy"]

Docker file is fairly straightfoward following best practices.

Multi stage image enables us to package only minimum that is needed for our running application. We copy the source files and build the image to get resulting binary application. After that we copy the binary to a new Alpine image layer where we install only runtime QtHttpServer files needed to run it as an actual service.

Still, this results in a 153MB image as QtHttpServer has quite a bit of dependencies. Using a static build is a good choice for backend development with containers to lower the size and improve performance as shown with Go images.
Making static Qt builds on the other hand is not as straightfoward and it has some drawbacks so it’s outside of the scope of this article. Feel free to check static Qt docker builds here.

Finally only two things to be done. docker build -t qtdockercaddy . will make us an image, and docker run -it -p 8282:8282 qtdockercaddy will run it as before, but this time from a docker container !

Putting it behind Caddy reverse proxy

As it stands, QtHttpServer is not intended for public usage since as a new implementation doesn’t have complete resilience against public attacks.
Equally, a single HTTP(S) service is not commonly exposed alone on the public internet. It would be commonly behind API gateway or for purpose of additional security, resilience and performance behind a load balancer.

A known service for that purpose is Nginx and is highly recommended as an HTTP(S) server due to many posibilities and benefits.
However here we are going to look at a bit newer alternative, Caddy.
Simplicity of configuration, deploying, high security and being very performant, all make it a very good solution.
It has a builtin in support for automatic HTTPS and very good default configuration that is easy to extend when needed.
Make sure to check intro and documentation pages. For those that like performance benchmarks do check out 35 Million Hot Dogs: Benchmarking Caddy vs. Nginx.

So anyway, let’s make our Caddy reverse proxy. For that we are going to use a Caddyfile for easy configuration.

localhost:8181
reverse_proxy qtdockercaddy:8282

Yeah. That is it.

Note: qtdockercaddy used above is a hostname that is created as part of Docker compose file as shown below and explained in Docker compose networking documentation.

First line will make our Caddy HTTPS server (signed with self certificate since we are running this on a localhost) which runs at port 8181 and will be used as reverse proxy for our Qt REST API at port 8282 we made earlier. Actually this is so simple we could run this without configuration file as a one liner, however, things will never stay that simple so it’s good to start with a Caddyfile that can be extended.

caddy run in the location where Caddyfile is will make for a full blown reverse proxy HTTPS server.

Dockerfile? We don’t have to create a custom one, instead we will just use an official image prepared by the Docker and Caddy team. In case you want to use Nginx, do check their official Docker image.

Note: Caddy has automatic builtin HTTP to HTTPS redirection. Since we are using a custom port this would need additional configuration not presented here.

Deploying it all using Docker compose

Now we have all components by themselves. We made a REST API with Qt, prepared a Docker container for it and configured a Caddyfile for our reverse proxy. Now is the time to have this all deployed as one stack and this is where Docker compose comes into play.

Note: In real world scenarios it is expected to separate a proxy or load balancer running on machines with public access, from our services that should be running in a private network.

version: "3.8"

services:
qtdockercaddy:
image: qtdockercaddy
restart: unless-stopped

caddy: # https://hub.docker.com/_/caddy
image: caddy:latest
restart: unless-stopped
ports:
- "${HTTPS_PORT:-8181}:8181"
- "${HTTPS_PORT:-8181}:8181/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config

volumes:
caddy_data:
external: true
caddy_config:

Given that we have only two services, the Docker compose file is fairly simple. First service is qtdockercaddy. This is where we specify our qtdockercaddy image that we built earlier.

We don’t expose any ports because we don’t want our HTTP REST API with Qt to be accessible from outside. Regardless of that, as part of Docker networking we are having Caddy and our service in the same network so they can communicate in between.

Second is our Caddy reverse proxy. For that we are just using official instructions where we put our own Caddyfile and make it possible to use enviroment variables for port configuration.

Note: our service can be pushed on a private Docker registry for deployments that are not supposed to be public.

All that is left is running docker compose up which will run everything we made as one stack and make our REST API accessible at HTTPS port 8181.

Our REST API on localhost over HTTPS in a browser

As expected we are using self signed HTTPS certificate on localhost that Caddy produced so there is an error in a browser that the certificate is invalid. We could manually provide an certificate we trust to Caddy or install one Caddy just created in our browser, but this is not a valid strategy for public usage. For that we need a valid domain for which Caddy will fetch a certificate as explained in the docs.

Running on public domain

This is what Caddy makes very simple out of the box.

Once we get a domain and point it to the server where REST API with Docker compose is going to be running, we have to change Caddyfile configuration to replace localhost with actual domain name and have it listen on default 80 and 443 ports for automatic HTTPS to be working.

example.com
reverse_proxy qtdockercaddy:8282

We also have to change compose file back to stock Caddy docker image configuration, so that Caddy can listen on port 80 and 443 which are both required for ACME challenge.

ports:
- "80:80"
- "443:443"
- "443:443/udp"

After restarting Caddy server with Docker (compose), Caddy will fetch a valid certificate for our domain and we will finally have our REST API running in a production.

Our REST API on public domain over HTTPS in a browser

Summary

There is a lot of things presented in the article but in reality it only scratched the surface. This should be considered more of an intro so make sure to check out provided links to the documentation of each mentioned component in the article, especially if you are new to this.

What article shows is how Qt could be used for HTTP API development, once HTTP module is released as stable. Nevertheless, Qt can still be, and is used for many non UI scenarios and this could be one way to deploy those.

The whole localhost setup can be found at https://github.com/pkoretic/qt-docker-caddy.

Happy holidays!

--

--

Petar Koretić
Petar Koretić

Responses (1)