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.