Recent

Author Topic: HOWTO: Web microservice in Alpine Linux Docker  (Read 3344 times)

PierceNg

  • Sr. Member
  • ****
  • Posts: 369
    • SamadhiWeb
HOWTO: Web microservice in Alpine Linux Docker
« on: August 15, 2020, 04:29:07 am »
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

  • Sr. Member
  • ****
  • Posts: 369
    • SamadhiWeb
Re: HOWTO: Web microservice in Alpine Linux Docker
« Reply #1 on: August 15, 2020, 04:51:46 am »
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           
<html><head><title>Hello world</title></head><body>Hello world</body></html>%

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

  • Sr. Member
  • ****
  • Posts: 369
    • SamadhiWeb
Re: HOWTO: Web microservice in Alpine Linux Docker
« Reply #2 on: August 15, 2020, 06:33:42 am »
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
<html><head><title>Hello world</title></head><body>Hello world</body></html>%                         

PierceNg

  • Sr. Member
  • ****
  • Posts: 369
    • SamadhiWeb
Re: HOWTO: Web microservice in Alpine Linux Docker
« Reply #3 on: August 22, 2020, 09:20:10 am »
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][+][-]
  1. program HelloHeadersPure;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6.   cthreads, httpdefs, httproute, webutil,
  7.   fphttpapp;
  8.  
  9. procedure doEchoRequest(aReq: TRequest; aResp: TResponse);
  10. begin
  11.   DumpRequest(aReq, aResp.contents, true);
  12. end;
  13.  
  14. begin
  15.   HTTPRouter.registerRoute('*', @doEchoRequest);
  16.   Application.Port:=8080;
  17.   Application.Threaded:=True;
  18.   Application.Initialize;
  19.   Application.Run;
  20. end.
  21.  

fcl-web also supports libmicrohttpd. Here's the equivalent program.

Code: Pascal  [Select][+][-]
  1. program HelloHeadersMu;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6.   cthreads, httpdefs, httproute, webutil,
  7.   microhttpapp, custmicrohttpapp;
  8.  
  9. procedure doEchoRequest(aReq: TRequest; aResp: TResponse);
  10. begin
  11.   DumpRequest(aReq, aResp.contents, true);
  12. end;
  13.  
  14. begin
  15.   HTTPRouter.registerRoute('*', @doEchoRequest);
  16.   Application.Port:=8080;
  17.   Application.Options:=[mcoThreadPerConnection, mcoSelectInternally];
  18.   Application.Initialize;
  19.   Application.Run;
  20. end.
  21.  

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.

« Last Edit: September 08, 2020, 04:45:54 pm by PierceNg »

PierceNg

  • Sr. Member
  • ****
  • Posts: 369
    • SamadhiWeb
Re: HOWTO: Web microservice in Alpine Linux Docker
« Reply #4 on: August 29, 2020, 09:25:10 am »
Reading these forums and mailing lists, seems FCGI is a preferred mode of deploying webapps. So here's the FCGI version.

Code: Pascal  [Select][+][-]
  1. program HelloHeaderFCGI;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6.   cthreads, httpdefs, httproute, webutil,
  7.   fpfcgi;
  8.  
  9. procedure doEchoRequest(aReq: TRequest; aResp: TResponse);
  10. begin
  11.   DumpRequest(aReq, aResp.contents, true);
  12. end;
  13.  
  14. begin
  15.   HTTPRouter.registerRoute('*', @doEchoRequest);
  16.   Application.Port:=8080;
  17.   Application.Initialize;
  18.   Application.Run;
  19. end.
  20.  

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

  • Full Member
  • ***
  • Posts: 233
    • Highball Github
Re: HOWTO: Web microservice in Alpine Linux Docker
« Reply #5 on: August 29, 2020, 10:11:19 pm »
Cool, thanks for posting the results and examples

ionsem

  • Newbie
  • Posts: 5
Re: HOWTO: Web microservice in Alpine Linux Docker
« Reply #6 on: January 01, 2023, 07:57:28 pm »
Hello, thanks for the examples.
Unfortunately containers from examples above cannot be stopped correctly:
Code: Pascal  [Select][+][-]
  1. podman stop nostalgic_tharp
  2. WARN[0010] StopSignal SIGTERM failed to stop container nostalgic_tharp in 10 seconds, resorting to SIGKILL
  3. nostalgic_tharp
  4.  

do you have any ideas how to improve this?

PierceNg

  • Sr. Member
  • ****
  • Posts: 369
    • SamadhiWeb
Re: HOWTO: Web microservice in Alpine Linux Docker
« Reply #7 on: January 02, 2023, 05:47:55 am »
Hello, thanks for the examples.
Unfortunately containers from examples above cannot be stopped correctly:
Code: Pascal  [Select][+][-]
  1. podman stop nostalgic_tharp
  2. WARN[0010] StopSignal SIGTERM failed to stop container nostalgic_tharp in 10 seconds, resorting to SIGKILL
  3. nostalgic_tharp
  4.  

do you have any ideas how to improve this?

Yes the examples don't deal with signals.  :P

Here's a new version that handles SIGTERM.

Code: Pascal  [Select][+][-]
  1. program HelloHeaderPure;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6.   cthreads, baseunix, httpdefs, httproute, webutil,
  7.   fphttpapp, fphtml;
  8.  
  9. procedure doEchoRequest(aReq: TRequest; aResp: TResponse);
  10. begin
  11.   DumpRequest(aReq, aResp.contents, true);
  12. end;
  13.  
  14. procedure mySigHandler(signal: cint); cdecl;
  15. begin
  16.   writeln('Signaled. Exiting...');
  17.   halt(0);
  18. end;
  19.  
  20. var
  21.   sigaction: PSigActionRec;
  22.  
  23. begin
  24.   // Set up Unix signal handler
  25.   new(sigaction);
  26.   sigaction^.sa_handler := sigActionHandler(@mySigHandler);
  27.   fillchar(sigaction^.sa_mask, sizeof(sigaction^.sa_mask), #0);
  28.   sigaction^.sa_flags := 0;
  29.   fpSigAction(SigTerm, sigaction, nil);
  30.   dispose(sigaction);
  31.  
  32.   // App stuff
  33.   HTTPRouter.registerRoute('*', @doEchoRequest);
  34.   Application.Port:=8080;
  35.   Application.Threaded:=True;
  36.   Application.Initialize;
  37.   Application.Run;
  38. end.

Run container in one shell:

Code: Text  [Select][+][-]
  1. # docker run --rm --name hhpure hhpure

Kill the container from another shell:

Code: Text  [Select][+][-]
  1. # docker kill --signal=HUP hhpure
  2. hhpure
  3. <nothing happened>
  4. # docker kill --signal=TERM hhpure
  5. hhpure

Back in other shell:

Code: Text  [Select][+][-]
  1. # docker run --rm --name hhpure hhpure
  2. Signaled. 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

  • Newbie
  • Posts: 5
Re: HOWTO: Web microservice in Alpine Linux Docker
« Reply #8 on: January 02, 2023, 08:29:44 am »
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.

 

TinyPortal © 2005-2018