Getting ready for production (Apache + uWSGI + Mojo)

raigaurav

Gaurav Rai

Posted on May 9, 2021

Getting ready for production (Apache + uWSGI + Mojo)

In previous article we have created few REST API's.
Now lets try to deploy it to production. It will be a long article so brace your self.
For production deployment we will be using few things -

  1. Apache2 Server
  2. uWSGI
  3. Docker

Mojolicious already comes with hypnotoad which is a production grade server. You can use it and you will be ready in no time. This way is already mentioned in Deploying a Mojolicious Application using Hypnotoad and Apache. If you are interested you can read it. When I first deployed my mojo app to production I followed that approach (thank you perlmaven šŸ˜ƒ).

There are several other ways which we will see. But before that I want to thank a unknown person. While exploring more about this deployment process and what to choose I stumble upon this link - https://codeday.me/es/qa/20190709/1032129.html
This link is not working as of today(Don't try to run it on 'http' as it is redirecting to some malicious site). I am not sure who is the owner of this website. But this website contains a lot of useful information regarding Perl. Even though it was in Japanese language (AFAIK), which you can translate easily, I found a great amount of knowledge here. Its a shame its not working anymore. This is also a motivation for me to write this article as I don't want that knowledge to get lost on the internet. I hope someday it will come up and people can see lot of interesting article on it. The thing which I am going to write next is inspired from one of the article mentioned there(the uWSGI part). Right now even I am not sure whether that is the correct link or not, but that is the only one I have. So a big thanks to that unknown person.

I encourage you to look at this stackoverflow answer to understand the basic concepts. It talks about Nginx, PSGI, Plack, Starman and Dancer. It has one of the best explanation and it should be added in hall of fame if possible. šŸ˜

So lets get started.

Why uWSGI ?

According to uWSGI project -

The uWSGI project aims at developing a full stack for building hosting services.
Versatility, performance, low-resource usage and reliability are the strengths of the project.
The "WSGI" part in the name is a tribute to the namesake Python standard, as it has been the first developed plugin for the project.

Please note that uWSGI(highly-performant WSGI server implementation) is not any language specific. I am taking an example for Perl language, but it is almost similar to other language (Python, Ruby,PHP etc.). I will provide the option in between for other languages too.

Sample Web App Architecture :

Perl

Code <-> Web framwork (Mojolicious, Dancer, Catalyst etc.) <-> PSGI <-> uWSGI <-> Web Server(apache, nginx) <-> Clients
Enter fullscreen mode Exit fullscreen mode

Python

Code <-> Web framwork (Django, Flask etc.) <-> WSGI <-> uWSGI <-> Web Server(apache, nginx) <-> Clients
Enter fullscreen mode Exit fullscreen mode

Ruby

Code <-> Web framwork (Rails etc.) <-> RACK <-> uWSGI <-> Web Server(apache, nginx) <-> Clients
Enter fullscreen mode Exit fullscreen mode

Please keep in mind that PSGI/WSGI/RACK is not Yet Another web application framework. PSGI/WSGI/RACK is a specification to decouple web server environments from web application framework code. Nor is PSGI a web application API. Web application developers (end users) will not run their web applications directly using the PSGI interface, but instead are encouraged to use frameworks that support PSGI.

uWSGI is toolkit that contains PSGI/WSGI/RACK middleware, helpers and adapters to web servers. In other words, they are the implementation of PSGI/WSGI/RACK specification.

There are several ways to setup the particular architecture mentioned above.

  1. Using Gunicorn or 'Green Unicorn' (inspired from Ruby 'Unicorn') for Python
    OR
    Using Plack(inspired from Ruby 'Rack') and Starman / Starlet / Gazelle for Perl.

  2. Using (WSGI + uWSGI) for Python
    OR
    (PSGI + uWSGI) for Perl
    OR
    (RACK + uWSGI) for Ruby.

All these WSGI/PSGI/RACK are plugin provided by uWSGI which extend across almost all languages.

So the question is which option is best or which has more advantage over other - I will try to explain with help of Perl but I hope it is true across other language as everyone is inspired from each other.

  1. The PSGI 'protocol' (like WSGI) is essentially a calling convention for a subroutine. A request enters the application as a subroutine call with a hash as an argument. The application responds through the return value of the subroutine: an arrayref that contains an HTTP status code, HTTP headers and body. There is more than that, but those are the essential elements.

  2. What this means is that a process can only implement PSGI if the process contains a Perl interpreter. To achieve this, the process can be implemented in Perl or implemented in a language like C that can be loaded by the libperl.so shared library. Similarly, a process can only implement WSGI if it contains a Python interpreter.

  3. In reality the PSGI application is within the Starman process. So there are really only two parts (although both parts are multi-process containers).

  4. When we say that "nginx has uWSGI directly integrated", this does not mean that a WGSI application runs within the Nginx process. It means that the WSGI application runs in a separate uwsgi process and Nginx communicates with that process through a TCP socket using the uWSGI protocol. This is essentially the same model as Nginx with Starman behind, but with the distinction that the socket connection to Starman will use the HTTP protocol:

    .----------------------.          .-----------.
    |       Starman        |          |   Nginx   |
    |                      |   HTTP   |      /    |   HTTP
    | .------------------. |<---------|   Apache  |<-------(internet)
    | | PSGI Application | |          |           |
    | '------------------' |          |           |
    '----------------------'          '-----------'
Enter fullscreen mode Exit fullscreen mode

The HTTP protocol has higher overhead than the uWSGI protocol(remember OSI Model - HTTP at Application Layer(7) while TCP at Transport Layer(4)), so you can get better performance by running an application server that speaks the uWSGI socket protocol and can load libperl.so to implement the PSGI interface. uWSGI can do that :

    .----------------------.           .----------.
    |        uWSGI         |           |  Nginx   |
    |                      |   uWSGI   |     /    |   HTTP
    | .------------------. |<----------|  Apache  |<-------(internet)
    | | PSGI Application | |           |          |
    | '------------------' |           |          |
    '----------------------'           '----------'
Enter fullscreen mode Exit fullscreen mode

Hence it is encouraged to use uWSGI over any language specific implementation.

All few thing to note here is that-

  1. uWSGI implementation is available in almost all language (no more mod_perl or mod_python (language specific))
  2. It can be implemented across CGI script also even mason too.
  3. Applicable across different Web server. So if tomorrow you want nginx instead of Apache, its 5 min of work. Even some has out of box support for it (e.g. Nginx).
  4. Scalability
  5. Speed

How to use uWSGI ?

  • First install 'uWSGI'. It is available as package in several OS/distributions. So at most you have to do
sudo apt-get install uwsgi
Enter fullscreen mode Exit fullscreen mode
  • Install the plugin specific to language you are using. Each language has a plugin associated with it -
    uwsgi-plugin-psgi    -> Perl
    uwsgi-plugin-python3 -> Python3
    uwsgi-plugin-python  -> Python2.7
    uwsgi-plugin-ruby    -> Ruby
Enter fullscreen mode Exit fullscreen mode

You can install these plugin using apt-get(which I prefer).
OR

curl http://uwsgi.it/install | bash -s psgi /tmp/uwsgi
Enter fullscreen mode Exit fullscreen mode
  • Web server(Apache, Nginx) specific changes needed

For Apache - mod_proxy_uwsgi. More info here

sudo apt-get install libapache2-mod-proxy-uwsgi
Enter fullscreen mode Exit fullscreen mode

Nginx includes uwsgi protocol support out of the box. More info here

How to Run

Running on terminal (without web server)-

Python

uwsgi --http-socket :8080 --wsgi-file <Application Script>
Enter fullscreen mode Exit fullscreen mode

Perl

uwsgi_psgi --http-socket :8080 --psgi script/my_app
Enter fullscreen mode Exit fullscreen mode

OR

uwsgi --plugins http,psgi --http :8080 --http-modifier1 5 --psgi script/my_app
Enter fullscreen mode Exit fullscreen mode

Please note that 'http-modifier' tag in option.

  1. uWSGI supports various languages and platform. When the server receives a request it has to know where to ā€˜routeā€™ it.
  2. Each uWSGI plugin has an assigned number (the modifier), the perl/psgi one has the 5. So ā€“http-modifier1 5 means ā€œroute to the psgi pluginā€.
  3. ruby/rack has 7.
  4. lua has 6.

If using socket (through Web Server)-

Python

uwsgi --socket 127.0.0.1:8080 -w wsgi
Enter fullscreen mode Exit fullscreen mode

Perl

uwsgi_psgi --socket :8080 --protocol=http --psgi script/my_app
Enter fullscreen mode Exit fullscreen mode

There are various command line parameters. Have a look at Options to understand them and use them as per your need.
Since it support a large range of options, we will be using the config file instead of command line params as it will be easy to read and maintain.

Whatever we have read till now is generic introductions of uWSGI.
Now lets try to use it for our mojo app.

Creating Apache configuration

You can use any web server you like. I am just using Apache(I have a soft corner for it šŸ˜…).
Create a file in etc\apache2.conf

<VirtualHost *:80>
    ServerAdmin grai@gmail.com
    ServerName  mojo-react-app.com

    RewriteEngine on
    # This checks to make sure the connection is not already HTTPS
    RewriteCond %{HTTPS} off [OR]
    RewriteCond %{HTTP:X-Forwarded-Proto} !https

    # Redirect http (port 80) to https (port 443)
    RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [NC,R=301,L]

</VirtualHost>

<VirtualHost *:443>
    ServerAdmin grai@gmail.com
    ServerName  mojo-react-app.com

    SSLEngine on
    SSLProxyEngine on
    SSLProxyVerify none 
    SSLProxyCheckPeerCN off
    SSLProxyCheckPeerName off
    SSLProxyCheckPeerExpire off
    SSLCertificateFile /etc/ssl/certs/server.crt
    SSLCertificateKeyFile /etc/ssl/private/server.key

    DocumentRoot /var/www/html
    <Proxy *>
        Order deny,allow
        Allow from all
    </Proxy>
    ProxyRequests Off
    ProxyPreserveHost On
    ProxyPass / uwsgi://127.0.0.1:6363/ keepalive=On
    ProxyPassReverse / uwsgi://127.0.0.1:6363/
    RequestHeader set X-Forwarded-Ssl on
    RequestHeader set X-Forwarded-Proto "https"

    ErrorLog ${APACHE_LOG_DIR}/mojo-react-app-error.log
    LogLevel warn
    CustomLog ${APACHE_LOG_DIR}/mojo-react-app-access.log combined
</VirtualHost>
Enter fullscreen mode Exit fullscreen mode

We have two host listening on 80(http) and 443(https). While the first one is doing nothing except redirecting to 443. It means if we try to access out website from browser as plain http it will redirect the connection to https and will force us to go through that path. We will look into it in real time. But its a good addition to a website. With just few lines of code we are supporting both http and https.
I really love that when a web site owner does this (redirect).

The configuration is pretty standard. You can find more details here and here about what each one of these means.
One thing I would like to point out is ProxyPass and ProxyPassReverse. Here we just doing the forwarding. 6363 is the port where our uWSGI app is listening. We are forwarding the incoming request to it which will forward it to back-end server. More details - Reverse Proxy.
SSLCertificateFile and SSLCertificateKeyFile contains the path to CA signed certificate file on your system.
Also, we have created a separate error and access log for our application(which you should do in case you have multiple application running). All the incoming request hitting Apache and error if any will be logged to those logs.

Creating uWSGI configuration

As I mentioned before we will be using config file for uWSGI instead of command line, so lets create that file. I am creating a ini format file but other formats are also acceptable(xml, json, yaml)

[uwsgi]
project = MojoReactApp
chdir = /home/mojo_react_app
# spawn the specified number of workers/processes
workers = 4
# enable master process
master = true

[uwsgi_psgi]
ini = :uwsgi
# Currently the module lacks the ability to set modifiers, though this will be fixed soon.
# An alternative is to set the plugin you want to use as the first one (0)
plugins = 0:psgi
psgi = script/mojo_react_app
# set uwsgi protocol modifier1 (perl/psgi is 5)
http-socket-modifier1 = 5
# do not catch $SIG{__DIE__}
perl-no-die-catch = true

[development]
# This will load the uwsgi section below
ini = :uwsgi_psgi
# set environment variable
env = PLACK_ENV=development
# bind to the specified UNIX/TCP socket using uwsgi protocol
socket = 127.0.0.1:6363
logto = log/uwsgi_development.log

[staging]
# This will load the uwsgi section below
ini = :uwsgi_psgi
# set environment variable
# staging is similar to production, there is no separate DB for staging
env = PLACK_ENV=production
# bind to the specified UNIX/TCP socket using uwsgi protocol
socket = 127.0.0.1:6363
logto = log/uwsgi_staging.log

[production]
# This will load the uwsgi section below
ini = :uwsgi_psgi
# set environment variable
env = PLACK_ENV=production
# bind to the specified UNIX/TCP socket using uwsgi protocol
socket = 127.0.0.1:6363
logto = log/uwsgi_production.log
Enter fullscreen mode Exit fullscreen mode

Please have a look at Configuration for more details about each items. Check the ini-files section for creating the ini config.

By default, uWSGI uses the [uwsgi] section, but you can specify another section name while loading the INI file with the syntax filename:section, that is:

uwsgi --ini myconf.ini:app1
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can load another section from the same file by omitting the filename and specifying just the section name.

  • In our [uwsgi] section we have created the config which we want globally available.
  • [uwsgi_psgi] section contain items specific to psgi plugin. If you are using some other plugin (e.g. ruby) you can update it for that. I have added the comment on each line to understand it better.
  • I have created 3 more section - [development], [staging] and [production]. This is something which I generally do for all my project. In all 3 I have loaded the [uwsgi_psgi] section and in [uwsgi_psgi] I have loaded [uwsgi] meaning all those items are available in these 3 section.
  • You can add the params which you think is specific to particular environment(e.g. PLACK_ENV).
  • socket contains the address where the request coming from Apache will be forwarded (remember ProxyPass). This is the address where your mojolicious app will be running.
  • logto contains the filename where the logs will be generated.

The reason I have created 3 sections - dev, stag and prod because it make everything so smooth. In just few min you have your specific environment ready. We will look into it more and what I meant by this statement.

Creating the signed certificate

For now I am creating a self-signed certificate
but for actual production use you should get a CA signed certificate. If your website is going to be in a public space accessible across world, better to get a proper certificate.
A self-signed certificate is ok till your usage is limited (intranet site etc.).

There are plenty of place where you can learn how to create it. One of them is - How To Create a Self-Signed SSL Certificate
Also, you should not be committing this certificate in your repo(git, svn etc). I have just committed it for demo.
This is private to you, use it with utmost care.

Creating the startup script

Lets create a wrapper script which will start our app.
Inside script\start_mojo_react_app.sh

#!/bin/bash
# This script can be run by docker or directly by provide the parameter.
# Move it outside and run -
# ./start_mojo_react_app.sh -m "development" or
# ./start_mojo_react_app.sh -m "production"

# Restart Web server (here Apache)
service apache2 restart

# start Mojo React App
while getopts "m:" opt; do
    case ${opt} in
        m)
            mode="${OPTARG}"
            echo "Running in '$mode' mode"
            if [ "$mode" == "development" ]; then
                # Since we are using uwsgi, we have commented the morbo
                # exec morbo -l "https://*:6363" script/mojo_react_app
                exec uwsgi --ini etc/uwsgi.conf:$mode
            elif [[ "$mode" = "production" || "$mode" = "staging" ]]; then
                # exec hypnotoad -f script/mojo_react_app
                exec uwsgi --ini etc/uwsgi.conf:$mode
            else 
                echo "Wrong mode provided. Accepted value - development, staging or production" 1>&2
            fi
            ;;
        : )
            echo "Invalid option: $OPTARG requires an argument" 1>&2
    esac
done
Enter fullscreen mode Exit fullscreen mode
  • In my case I have merged the staging and production environment. But you can segregate them based on your need.
  • I have commented the morbo(development server) and hypnotoad(HTTP and WebSocket production server) line. The reason being we are using uWSGI. In case you don't want to use uWSGI you can uncomment those and remove uWSGI line. Also don't forget to add the hypnotoad specific config and update the ProxyPass and ProxyPassReverse in apache2.conf šŸ˜
    ProxyPass / http://127.0.0.1:6363/ keepalive=On
    ProxyPassReverse / http://127.0.0.1:6363/
Enter fullscreen mode Exit fullscreen mode

Creating the Dockerfile

Now lets wrap our application with all the configuration file created above.
Create a Dockerfile in mojo_react_app\Dockerfile

# build environment
FROM ubuntu:18.04

# By default will run in 'dev' mode
ENV mode=development

# Needed dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    gnupg \
    curl \
    vim \
    less \
    openssl=1.1.1-1ubuntu2.1~18.04.9 \
    libssl-dev \
    zlib1g-dev \
    apache2 \
    uwsgi=2.0.15-10.2ubuntu2.1 \
    uwsgi-plugin-psgi=2.0.15-10.2ubuntu2.1 \
    libapache2-mod-proxy-uwsgi=2.0.15-10.2ubuntu2.1

# Needed dependencies specific to project
RUN curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n Mojolicious@9.17
RUN curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n Mojolicious::Plugin::OpenAPI@4.03
RUN curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n Mojolicious::Plugin::SwaggerUI@0.0.4
RUN curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n IO::Socket::SSL@2.070
RUN curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n JSON::XS@4.03

# Apache specific configuration
RUN \
    a2enmod headers && \
    a2enmod proxy && \
    a2enmod proxy_http && \
    a2enmod proxy_uwsgi && \
    a2enmod rewrite && \
    a2enmod ssl

# Disable the default apache home page on port 80
RUN a2dissite 000-default.conf

# Copy your codebase from local to inside container
COPY . /home/mojo_react_app
RUN mkdir -p /home/mojo_react_app/log/

# These will be used while setting virtual host in apache.conf
ENV APACHE_LOG_DIR /var/log/apache2
ENV APACHE_LOCK_DIR /var/lock/apache2
ENV APACHE_PID_FILE /var/run/apache2.pid

# Copy Apache config file
COPY etc/apache2.conf /etc/apache2/sites-available/mojo_react_app.conf
RUN ln -s /etc/apache2/sites-available/mojo_react_app.conf /etc/apache2/sites-enabled/mojo_react_app.conf

WORKDIR /home/mojo_react_app

# Expose both http and https port
EXPOSE 80 443

COPY script/start_mojo_react_app.sh /start_mojo_react_app.sh
RUN chmod a+x /start_mojo_react_app.sh

ENTRYPOINT /start_mojo_react_app.sh -m "$mode"
Enter fullscreen mode Exit fullscreen mode

I have added comment for better understanding.

  • We are using the ubuntu(18.04) image. You can use the recent one (20.04) but still some plugins are missing for this version hence I have used 18.04. You can also use the official Perl docker image instead if you want (e.g. perl:5.32).

  • By default we will running in development mode. You can override it by providing different value at runtime (e.g. staging or production). This is the one which will cause the the different section to pick in uwsgi.conf.

  • Next we are installing some tools(e.g. vim). In case you don't want it you can remove those lines. Few notable plugins/tools are - openssl(for https), apache2, uwsgi, uwsgi-plugin-psgi(for perl), libapache2-mod-proxy-uwsgi(for apache2).
    I have used some version which are specific to ubuntu(18.04), you can update that as per your image or not use at all.

  • Next I have installed some dependencies specific to our Mojolicious app. I am using cpanminus for this. I have added the version number also to get that specific version. There are again multiple way to achieve this. One of them is using cpanm(with cpanfile) from command line as mentioned here and here. Another is using Carton which is dependency manager for Perl(similar to Bundler in Ruby).
    Since our scope is limited I have used it like that. In big project you may want to follow one of above mentioned approach.

  • Next is some Apache specific configuration. We are enabling some modules in Apache. More info at a2enmod.
    We are enabling the ability of handling HTTP proxy requests, handling the uWSGI protocol, ssl etc. Also we disable the default Apache homepage which is available on port 80.

  • After that we are copying our mojo_react_app code base and placing it at /home/mojo_react_app inside container.

  • We are setting some environment variables which we are using in apache.conf (remember ${APACHE_LOG_DIR}).

  • Next we copied our virtual host config file and copied it inside container(site-available). Also we created a symbolic link for it in /etc/apache2/sites-enabled/.

  • Now we make the change our working dir to /home/mojo_react_app inside container where all our code base is.

  • We have exposed the 80 and 443 port to outside world.

  • We copied the startup script to our current working dir(which is /home/mojo_react_app).

  • Finally we are running that startup script with mode as param (by default - development).

All the docker keywords and there meaning is already available at - Dockerfile reference. Check it out for more info.

With that we are ready to start our application.

Running the application

Build the dockerfile

docker build --pull=false --no-cache=false -t mojo_react_app:development .
Enter fullscreen mode Exit fullscreen mode

Create the container

docker create --name mojo_react_app_development -t \
       -p 0.0.0.0:80:80 -p 0.0.0.0:443:443 \
       --env mode=development docker create --name 
Enter fullscreen mode Exit fullscreen mode

Copy the signed certificates

We will copy the self signed certificate inside the created container. This certificate will be available somewhere on your prod machine(private). For now I am copying it from the project dir.

docker cp mojo_react_app/apache-certificate/apache_certificate_development.crt mojo_react_app_development:/etc/ssl/certs/server.crt

docker cp mojo_react_app/mojo_react_app/apache-certificate/apache_certificate_development.key mojo_react_app_development:/etc/ssl/private/server.key
Enter fullscreen mode Exit fullscreen mode

Remember the destination path is the one which we used in apache2.conf for virtual host configuration.

Start the container

docker start mojo_react_app_development
Enter fullscreen mode Exit fullscreen mode

Show running containers

Just to check whether our container is running or not

docker ps | grep mojo_react_app_development
Enter fullscreen mode Exit fullscreen mode

You can change the development in name and mode to staging or production for different environment.

Lets login to container and see what is going on there.

docker exec -it mojo_react_app_development bash
Enter fullscreen mode Exit fullscreen mode

Lets go to apache log dir to see the logs there
apache_logs
We can see our application access and error are getting generated here. Whatever the request apache received that will be logged here.

Lets go to our project dir and see the uWSGI logs
uwsgi_log
All the mojolicious app log will be logged here.

Lets tail this log and see whats inside
tail_uwsgi
Hmm, all the 4 works which we have configured in [uwsgi] section inside uwsgi.conf is ready to rock.

Just open the browser and hit the http://localhost
redirect_301

  • Just look at the Network tab in browser. Even though you hit http it got redirected to https. The status code 301 says so. The url itself is now https://localhost at the top.
  • Also you can see the server is Apache(2.4.29) running on Ubuntu and not Mojolicious(Perl) since we are using the reverse proxy architecture.

Lets go ahead and click to open the API page.
api
Voila!!!. So far so good. It is similar to what we have seen previously except on https.

Since we are already tailing the uWSGi log, lets see whats happens there -
api_logs
We got a GET request on /api and we render the template based on our internal logic. All the debug message will go away when you run it in production mode.

Go ahead and try to do the GET request on the endpoints. You will be able to do it without any issue.

A high level diagram of architecture we followed is -
architecture

What next

Well even though we are using docker, we have to run several command manually. We have to take care of different environment while creating and starting the image. Not to mention the copy of Apache certificate. What if we can automate it more.
What if we say - make dev and all the thing got taken cared of.
Similarly for make stag and make prod.
This thing will come handy when you will do the automated deployment using Jenkins or some other ways.
I can see the sparkle in your eyes.
One piece
We will look into that in our next section.

Bonus

Our good folks at Mojolicious already thought of containers and clouds. Have a look at Containers for more info.
You can generate Makefile and Dockerfile for your app using just 2 simple command. Inside your project dir -

mojo generate makefile
./script/mojo_react_app generate dockerfile
Enter fullscreen mode Exit fullscreen mode

Honorable Mention -

https://stackoverflow.com/questions/12127566/an-explanation-of-the-nginx-starman-dancer-web-stack/12134555#12134555
https://uwsgi-docs.readthedocs.io/en/latest/index.html
https://codeday.me/es/qa/20190709/1032129.html
https://metacpan.org/pod/PSGI
https://perlmaven.com/deploying-a-mojolicious-application

And a lot of different articles read over the years, the source of which I don't remember. šŸ˜­

The above code is also available at github

Perl onion logo taken from here
Mojolicious logo taken from here
OpenAPI logo taken from here
uWSGI logo taken from here
Docker logo taken from here
Apache logo taken from here
Ubuntu logo taken from here

šŸ’– šŸ’Ŗ šŸ™… šŸš©
raigaurav
Gaurav Rai

Posted on May 9, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related