HOWTO: Web microservice in Alpine Linux Docker lazarus / 38-Networking and Web Programming ---------------------------------------------------------------------- PierceNg · 2020-08-15 02:29 Hi all, Here's a quick HOWTO on containerizing Pascal web micro services in Alpine Linux Docker. Terminology-wise, a Docker image is a bunch of stuff on disk that packages everything required to run as a container, and a Docker container is a running image. As for Alpine Linux, unlike the vast majority of Linux distros, Alpine is build around the 'musl' libc, which makes it smaller and more resource efficient. The Alpine Linux Docker image is 5MB in size. For fcl-web and BrookFreePascal (the pure Pascal version) that come with pure Pascal HTTP servers, with the FPC-built executable named 'helloworld', here's the Dockerfile with comments: Code: [Select]# Start with the Alpine 3.12 Docker image. FROM alpine:3.12 # Install libc6-compat, because executables generated by FPC are linked with libc. RUN apk --no-cache --update add libc6-compat # Set working directory in the container image. WORKDIR /app # Copy the executable from host into container image. COPY helloworld /app/helloworld # Add uid/gid to run the binary. Don't want to run as root. RUN addgroup -g 1099 apprunner \   && adduser -D -u 1099 -G apprunner -h /home/apprunner apprunner # Set uid/gid for the running program. USER apprunner:apprunner # Make the TCP port 8080 available to outside the container. EXPOSE 8080 # Run the program. CMD ["/app/helloworld"]   Build Docker image: Code: [Select]%  sudo docker build -t helloworld:fpc . Sending build context to Docker daemon  1.331MB Step 1/8 : FROM alpine:3.12  ---> a24bb4013296 Step 2/8 : RUN apk --no-cache --update add libc6-compat  ---> Using cache  ---> 8bd9ae66e9fe Step 3/8 : WORKDIR /app  ---> Using cache  ---> 1c69cb1c14e3 Step 4/8 : COPY helloworld /app/helloworld  ---> de5ffddf76bb Step 5/8 : RUN addgroup -g 1099 apprunner   && adduser -D -u 1099 -G apprunner -h /home/apprunner apprunner  ---> Running in 54199f0a8f4f Removing intermediate container 54199f0a8f4f  ---> c7d18145bc02 Step 6/8 : USER apprunner:apprunner  ---> Running in 0a544c5fc8f6 Removing intermediate container 0a544c5fc8f6  ---> a3868d1f2033 Step 7/8 : EXPOSE 8080  ---> Running in 829a1b804c0d Removing intermediate container 829a1b804c0d  ---> 7389810c7acf Step 8/8 : CMD ["/app/helloworld"]  ---> Running in 882e002e08b0 Removing intermediate container 882e002e08b0  ---> 399269970984 Successfully built 399269970984 Successfully tagged helloworld:fpc Check its size: Code: [Select]% sudo docker images REPOSITORY                TAG               IMAGE ID            CREATED             SIZE helloworld                fpc               399269970984        2 minutes ago       7.51MB Run it like this: Code: [Select]%  sudo docker run --rm -p 8080:8080 helloworld:fpc Talk to it: Code: [Select]%  curl http://127.0.0.1:8080 Hello world! I haven't had success with the newer Brook Framework, which requires its own shared library written in C, and I haven't tried the Fano framework, which has external dependency on libmicrohttpd and whole bunch of other stuff. Pierce ---------------------------------------------------------------------- PierceNg · 2020-08-15 02:51 Brook Framework requires the C library libsagui. I've built it on my Ubuntu host. Code: [Select]% ls -l total 1968 -rw-r--r-- 1 pierce pierce     307 Aug 15 08:33 Dockerfile -rwxrwxr-x 1 pierce pierce 1104056 Aug 15 08:53 hellohttpsrv* -rwxr-xr-x 1 pierce pierce  901816 Aug 15 00:32 libsagui.so.3* Dockerfile, mostly same as the previous one, with the addition of copying in the libsagui file: Code: [Select]FROM alpine:3.12 RUN apk --no-cache --update add libc6-compat WORKDIR /app COPY hellohttpsrv /app/helloworld COPY libsagui.so.3 /lib/libsagui.so.3 RUN addgroup -g 1099 apprunner \   && adduser -D -u 1099 -G apprunner -h /home/apprunner apprunner USER apprunner:apprunner EXPOSE 8080 CMD ["/app/helloworld"] I've named the Docker image helloworld:brook4. The Docker container exits immediately though: Code: [Select]% sudo docker run --rm -p 8080:8080 helloworld:brook4 Server running at http://localhost:8080 % But running it interactively works: Code: [Select]% sudo docker run -it --rm -p 8080:8080 helloworld:brook4 sh /app $ ls -l total 1080 -rwxrwxr-x    1 root     root       1104056 Aug 15 00:53 helloworld /app $ ./helloworld Server running at http://localhost:8080 In another terminal window: Code: [Select]% curl http://127.0.0.1:8080            Hello worldHello world% As for Fano, it wants a whole bunch of shared libraries: Code: [Select]% ldd app.cgi linux-vdso.so.1 (0x00007ffd734da000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f34ccbcc000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f34ccbc6000) libmicrohttpd.so.12 => /lib/x86_64-linux- gnu/libmicrohttpd.so.12 (0x00007f34ccba1000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f34cc9af000) /lib64/ld-linux-x86-64.so.2 (0x00007f34ccc08000) libgnutls.so.30 => /lib/x86_64-linux-gnu/libgnutls.so.30 (0x00007f34cc7d9000) libp11-kit.so.0 => /lib/x86_64-linux-gnu/libp11-kit.so.0 (0x00007f34cc6a3000) libidn2.so.0 => /lib/x86_64-linux-gnu/libidn2.so.0 (0x00007f34cc680000) libunistring.so.2 => /lib/x86_64-linux-gnu/libunistring.so.2 (0x00007f34cc4fe000) libtasn1.so.6 => /lib/x86_64-linux-gnu/libtasn1.so.6 (0x00007f34cc4e8000) libnettle.so.7 => /lib/x86_64-linux-gnu/libnettle.so.7 (0x00007f34cc4ae000) libhogweed.so.5 => /lib/x86_64-linux-gnu/libhogweed.so.5 (0x00007f34cc476000) libgmp.so.10 => /lib/x86_64-linux-gnu/libgmp.so.10 (0x00007f34cc3f2000) libffi.so.7 => /lib/x86_64-linux-gnu/libffi.so.7 (0x00007f34cc3e4000) % Well, that's it for this weekend. Some other time maybe... ---------------------------------------------------------------------- PierceNg · 2020-08-15 04:33 Alright I couldn't resist trying just one more thing. Running the Brook Framework Docker image with '-it' works: Code: [Select]% sudo docker run -it --rm -p 8080:8080 helloworld:brook4 15-8-20 12:30:19: run -it --rm -p 8080:8080 helloworld:brook4 Server running at http://localhost:8080 In another terminal window: Code: [Select]% curl http://127.0.0.1:8080 Hello worldHello world% ---------------------------------------------------------------------- PierceNg · 2020-08-22 07:20 It's weekend again and here's my update. Using fcl-web's new-style routing, the program is now in one source file. It uses DumpRequest to echo the request to the client. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i program HelloHeadersPure; {$mode objfpc}{$H+} uses  cthreads, httpdefs, httproute, webutil,  fphttpapp; procedure doEchoRequest(aReq: TRequest; aResp: TResponse);begin  DumpRequest(aReq, aResp.contents, true);end; begin  HTTPRouter.registerRoute('*', @doEchoRequest);  Application.Port:=8080;  Application.Threaded:=True;  Application.Initialize;  Application.Run;end.  fcl-web also supports libmicrohttpd. Here's the equivalent program. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i program HelloHeadersMu; {$mode objfpc}{$H+} uses  cthreads, httpdefs, httproute, webutil,  microhttpapp, custmicrohttpapp; procedure doEchoRequest(aReq: TRequest; aResp: TResponse);begin  DumpRequest(aReq, aResp.contents, true);end; begin  HTTPRouter.registerRoute('*', @doEchoRequest);  Application.Port:=8080;  Application.Options:=[mcoThreadPerConnection, mcoSelectInternally];  Application.Initialize;  Application.Run;end.  I've named the pure Pascal version hhpure and the libmicrohttpd version hhmu. Below are some figures using hey, a tool similar to ab, to get a feel of the relative performances. This is hhpure running in Docker container on my laptop, 5000 connections. Code: [Select]Summary:   Total: 4.4411 secs   Slowest: 3.0444 secs   Fastest: 0.0006 secs   Average: 0.0233 secs   Requests/sec: 1125.8421     Total data: 14365000 bytes   Size/request: 2873 bytes Latency distribution:   10% in 0.0011 secs   25% in 0.0014 secs   50% in 0.0019 secs   75% in 0.0028 secs   90% in 0.0048 secs   95% in 0.0083 secs   99% in 1.0273 secs Now hhmu running in Docker container on my laptop, 5000 connections. Code: [Select]Summary:   Total: 0.7462 secs   Slowest: 0.1419 secs   Fastest: 0.0003 secs   Average: 0.0071 secs   Requests/sec: 6700.4482     Total data: 14300000 bytes   Size/request: 2860 bytes Latency distribution:   10% in 0.0005 secs   25% in 0.0020 secs   50% in 0.0041 secs   75% in 0.0088 secs   90% in 0.0173 secs   95% in 0.0230 secs   99% in 0.0457 secs Now for a slightly more 'real world' test. I run the timings from my laptop to a VPS running each Docker image in turn, with Caddy as the Internet-facing HTTPS reverse proxy in front. The VPS is 1 CPU with 2GB RAM. At any one time there are 7-10 Docker containers doing real world stuff, but the overall VPS system load is generally very low. I made no attempt to manage the load of the VPS while running these tests. This is hhpure: Code: [Select]Summary:   Total: 25.3875 secs   Slowest: 0.9708 secs   Fastest: 0.1706 secs   Average: 0.2407 secs   Requests/sec: 196.9471     Total data: 15315000 bytes   Size/request: 3063 bytes Latency distribution:   10% in 0.1780 secs   25% in 0.1836 secs   50% in 0.2009 secs   75% in 0.2881 secs   90% in 0.3221 secs   95% in 0.3560 secs   99% in 0.7131 secs This is hhmu: Code: [Select]Summary:   Total: 23.1742 secs   Slowest: 1.2708 secs   Fastest: 0.1685 secs   Average: 0.2233 secs   Requests/sec: 215.7569     Total data: 15250000 bytes   Size/request: 3050 bytes Latency distribution:   10% in 0.1732 secs   25% in 0.1754 secs   50% in 0.1818 secs   75% in 0.2381 secs   90% in 0.3088 secs   95% in 0.3210 secs   99% in 1.1814 secs The two are now comparable. Going by the in-laptop local results, these runs' outcomes were dominated by the performance of the Caddy HTTPS reverse proxy and the network between my laptop and the VPS. ---------------------------------------------------------------------- PierceNg · 2020-08-29 07:25 Reading these forums and mailing lists, seems FCGI is a preferred mode of deploying webapps. So here's the FCGI version. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i program HelloHeaderFCGI; {$mode objfpc}{$H+} uses  cthreads, httpdefs, httproute, webutil,  fpfcgi; procedure doEchoRequest(aReq: TRequest; aResp: TResponse);begin  DumpRequest(aReq, aResp.contents, true);end; begin  HTTPRouter.registerRoute('*', @doEchoRequest);  Application.Port:=8080;  Application.Initialize;  Application.Run;end.  I tested it locally, Caddy in front, HTTP only, Caddy not in Docker, the Pascal program in Docker. Code: [Select]Summary:   Total: 3.1658 secs   Slowest: 3.0522 secs   Fastest: 0.0006 secs   Average: 0.0150 secs   Requests/sec: 1579.3659     Total data: 14975000 bytes   Size/request: 2995 bytes Latency distribution:   10% in 0.0014 secs   25% in 0.0018 secs   50% in 0.0025 secs   75% in 0.0030 secs   90% in 0.0034 secs   95% in 0.0039 secs   99% in 0.0124 secs The results show that it is faster than the pure Pascal version and much slower than the libmicrohttpd version.  Since the FCGI numbers are comfortably better than the "real world" HTTPS tests I did last week, I'll not test this FCGI version that way. To be clear, these tests have not been done in any rigorous scientific manner. They do give me a sense of the relative performance of the different approaches. Specifically, I am now prepared to deploy a microservice running the pure Pascal HTTP server behind my current reverse proxy setup given the expected workload. ---------------------------------------------------------------------- mr-highball · 2020-08-29 20:11 Cool, thanks for posting the results and examples ---------------------------------------------------------------------- ionsem · 2023-01-01 18:57 Hello, thanks for the examples. Unfortunately containers from examples above cannot be stopped correctly: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i podman stop nostalgic_tharp WARN[0010] StopSignal SIGTERM failed to stop container nostalgic_tharp in 10 seconds, resorting to SIGKILL nostalgic_tharp  do you have any ideas how to improve this? ---------------------------------------------------------------------- PierceNg · 2023-01-02 04:47 Quote from: ionsem on January 01, 2023, 07:57:28 pmHello, thanks for the examples. Unfortunately containers from examples above cannot be stopped correctly: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i podman stop nostalgic_tharp WARN[0010] StopSignal SIGTERM failed to stop container nostalgic_tharp in 10 seconds, resorting to SIGKILL nostalgic_tharp  do you have any ideas how to improve this? Yes the examples don't deal with signals.  Here's a new version that handles SIGTERM. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i program HelloHeaderPure; {$mode objfpc}{$H+} uses  cthreads, baseunix, httpdefs, httproute, webutil,  fphttpapp, fphtml; procedure doEchoRequest(aReq: TRequest; aResp: TResponse);begin  DumpRequest(aReq, aResp.contents, true);end; procedure mySigHandler(signal: cint); cdecl;begin  writeln('Signaled. Exiting...');  halt(0);end; var  sigaction: PSigActionRec; begin  // Set up Unix signal handler  new(sigaction);  sigaction^.sa_handler := sigActionHandler(@mySigHandler);  fillchar(sigaction^.sa_mask, sizeof(sigaction^.sa_mask), #0);  sigaction^.sa_flags := 0;  fpSigAction(SigTerm, sigaction, nil);  dispose(sigaction);   // App stuff  HTTPRouter.registerRoute('*', @doEchoRequest);  Application.Port:=8080;  Application.Threaded:=True;  Application.Initialize;  Application.Run;end. Run container in one shell: Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i # docker run --rm --name hhpure hhpure Kill the container from another shell: Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i # docker kill --signal=HUP hhpurehhpure# docker kill --signal=TERM hhpurehhpure Back in other shell: Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i # docker run --rm --name hhpure hhpureSignaled. Exiting... Tested on Ubuntu 20.04 host, hhpure running in Alpine Linux 3.16 container. Please try on Podman. Something curious though. I modified the program to also deal with SIGHUP and SIGUSR1, but nothing happened when I sent those signals from the other shell. May have to do with hhpure running in the container as PID 1 but not UID 0. Something to look into. ---------------------------------------------------------------------- ionsem · 2023-01-02 07:29 Thanks a lot ! It works OK now. Also I've added signal SIGINT which should be treated on ctrl+c. It's strange why these signals are not treated in the core of lazarus/freepascal as other languages do.