How to quickly create a reverse proxy that supports HTTPS with Docker

This article is the 23rd day entry of Docker Advent Calendar 2020. By the way, I made an entry on December 24th, and I slipped in because only the 23rd day of the calendar was open.

Nowadays, HTTPS is the norm for websites, isn't it? However, it takes a lot of time and effort to support HTTPS. Money too. I've been wondering if it couldn't be done easily.

** Problems with HTTPS support for Web services **

--You have to get an SSL certificate issued and renew it regularly (it's annoying at this point) --You have to set HTTPS in the web application framework (I'm not familiar with it because I don't usually do it) ――I want to concentrate more on building applications (that's my main business) --If the server load increases, you want to distribute the load by using multiple application servers (a big dream). ――But no one is familiar with it (I don't know much about infrastructure)

I created a Docker container type service to deal with these issues.

What is EzGate?

EzGate makes it easy to build a reverse proxy that supports HTTPS. It's a so-called SSL accelerator.

** Features **

--Providing reverse proxy function with nginx --Use free Let's Encrypt for SSL certificate for HTTPS --Automatically renewed when the certificate renewal deadline is approaching --Supports HTTP/2 --If you already have an SSL certificate, or you can specify a certificate for your development environment individually. --Can be used even when there are multiple relay destination servers (load distribution configuration) --Can handle multiple domains (multi-tenant configuration)

Simple example

First, let's take a look at the procedure for setting up a reverse proxy with the simplest configuration with only one web server.

Diagram スクリーンショット 2020-12-24 23.24.10.png

Let's say your web server has an IP address of 172.17.0.4 and is listening on port 80.

In this case, simply specify the environment variables in the EzGate container to complete the reverse proxy configuration.

From here, I've actually tried it using the free tier of Google Compute Engine, so I'll show you the procedure. Note that docker must be installed on the virtual machine in advance. The OS is Ubuntu. I think you can try the same procedure on Cent OS.

#Start the wordpress container as a web server for communication confirmation
$ sudo docker run -d --rm --name server1 wordpress:5.6.0-apache

#Confirmation of container startup
$ sudo docker ps
CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS              PORTS     NAMES
c0f0d145d86d        wordpress:5.6.0-apache    "docker-entrypoint..."   18 seconds ago      Up 16 seconds       80/tcp    server1

#Check the IP address of the container and set it in a variable
$ SERVER1_IP=`sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' server1|tee /dev/stderr`
172.17.0.4

#Remaining setting information
$ DOMAIN=test.35.197.56.116.sslip.io  #The domain of this server. 35.197.56.The 116 part specifies the actual global ID.
$ [email protected]                 #The email address used to obtain the SSL certificate. Please change it to your own.

#Launch reverse proxy
$ sudo docker run -ti -d --name ez-gate -p80:80 -p443:443 \
    -e PROXY_TO=$DOMAIN,$SERVER1_IP:80 \
    -e CERT_EMAIL=$MAIL \
    neogenia/ez-gate:latest

#Check the container log
$ sudo docker logs -f ez-gate

If the container log comes out in a row and it looks like the following, the startup is successful.

----- finish all setups successfully ----- 
--- START MONIT -----
 * Starting daemon monitor monit                                         [ OK ] 
==> /var/log/monit.log <==
[UTC Dec 24 09:56:08] info     :  New Monit id: f439d27cdf8d377e03482d877b043d2e
 Stored in '/var/lib/monit/id'
[UTC Dec 24 09:56:08] info     : Starting Monit 5.25.1 daemon
[UTC Dec 24 09:56:08] info     : 'c91b02ea822e' Monit 5.25.1 started
[UTC Dec 24 09:56:08] error    : 'crond' process is not running
[UTC Dec 24 09:56:08] info     : 'crond' trying to restart
[UTC Dec 24 09:56:08] info     : 'crond' start: '/etc/init.d/cron start'

==> /var/log/nginx/error.log <==

==> /var/log/nginx/error_test_35_197_56_116_sslip_io.log <==

Open a web browser and access the domain set with $ DOMAIN. It is successful when the initial setting screen of Wordpress is displayed. スクリーンショット 2020-12-24 21.01.28.png

The SSL certificate is also set correctly. スクリーンショット 2020-12-24 21.02.08.png

In this way, all you have to do is set the domain and relay destination ** and it will automatically obtain an SSL certificate ** and operate as a reverse proxy. The relay destination does not have to be a Docker container, it can be another server, and it can be accessed by TCP.

Description of environment variables

Let's take a look at the container start command again.

#Launch reverse proxy
$ sudo docker run -ti -d --name ez-gate -p80:80 -p443:443 \
    -e PROXY_TO=$DOMAIN,$SERVER1_IP:80 \
    -e CERT_EMAIL=$MAIL \
    neogenia/ez-gate:latest

In PROXY_TO, set the relay domain and the relay destination server separated by commas. You can also specify a port number such as : 80 for the relay destination (default is 80).

For CERT_EMAIL, set the email address used to obtain the SSL certificate. An incorrect email address can result in an error.

This is all you need for configuration information.

If an error occurs

1: If you get the following error

Traceback (most recent call last):
        7: from /var/scripts/reload_config.rb:227:in `<main>'
        6: from /var/scripts/reload_config.rb:216:in `backup_dir'
        5: from /var/scripts/reload_config.rb:236:in `block in <main>'
        4: from /var/scripts/reload_config.rb:236:in `each'
        3: from /var/scripts/reload_config.rb:238:in `block (2 levels) in <main>'
        2: from /var/scripts/reload_config.rb:96:in `setup_ssl'
        1: from /var/scripts/reload_config.rb:28:in `setup'
/var/scripts/reload_config.rb:19:in `shell_exec': ## ERROR ## exit status: 1 command_line: 'APP_DOMAIN=test.35.197.56.116.sslip.io [email protected] /var/scripts/setup_letsencrypt.sh' (RuntimeError)

There is an error in the settings. For example, the CERT_EMAIL environment variable may not have the correct email address.

2: If you get the following error

Failed authorization procedure. test.35.197.56.116.sslip.io (http-01): urn:ietf:params:acme:error:dns :: No valid IP addresses found for test.35.197.56.116.sslip.io

Name resolution may have failed on the server side of Let's Encrypt. sslip.io may be down, so please try again later or try another free domain. Also, if you can assign a domain yourself, it's best to assign some suitable subdomain.

3: If you get the following error

An unexpected error occurred:
There were too many requests of a given type :: Error creating new order :: too many certificates already issued for: nip.io: see https://letsencrypt.org/docs/rate-limits/

Let's Encrypt error. The number of SSL certificate issuances for that domain has been reached. With popular wildcard DNS such as nip.io, many people are already trying to issue SSL certificates, so this error is common [^ 1].

[^ 1]: At the time of writing this article, sslip.io did not give an error, but as more people try this article, the possibility of getting an error increases.

If you can get a free domain such as Freenom and try it out, or assign a domain yourself, it's best to assign some suitable subdomain.

Please note that if you repeat issuing certificates too many times in a short period of time, you may get caught in the limit of the number of Let's Encrypt and an error may occur [^ 2].

[^ 2]: As of the end of 2020, SSL certificates can only be renewed up to 5 times per hour per account and host.

Also, if you want to restart the ez-gate container, you must first stop the container and do rm.

$ sudo docker kill ez-gate
$ sudo docker rm ez-gate

Use certificates for your development environment

In a local development environment (environment that cannot be accessed from the outside with a global IP), Let's Encrypt certificate cannot be issued. You can use mkcert to use the SSL certificate for your local development environment.

You don't have to use a "self-signed certificate" (commonly known as an oleore certificate) anymore!

Install mkcert

mkcert registers a certification authority in your local environment and is very easy to install. You can install it on macOS using Homebrew.

brew install mkcert

#Firefox users also need:
brew install nss 

See the mkcert Official README for more information.

Create a certificate with mkcert

This is explained when the URL of the development environment is https: // localhost /. If you want to access with another IP address, replace localhost with the IP address.

#Create a folder for storing certificates
$ mkdir certs

#Generate a certificate file for localhost using mkcert
$ mkcert -install  #First time only
$ mkcert -key-file certs/key.pem -cert-file certs/cert.pem localhost

Start by specifying the certificate for the reverse proxy

#Volume mount the certificate storage folder and specify those files with environment variables
$ sudo docker run -ti -d --name ez-gate -p80:80 -p443:443 \
    -e PROXY_TO=localhost,$SERVER1_IP:80 \
    -e CERT_FILE=/mnt/cert.pem \
    -e KEY_FILE=/mnt/key.pem \
    -v `pwd`/certs:/mnt \
    neogenia/ez-gate:latest

#Check the container log
$ sudo docker logs -f ezgat

If it starts normally, open a web browser and access https: // localhost /. If there is no SSL certificate error, it is successful.

Description of environment variables

For CERT_FILE, specify the path of the cert-file generated by mkcert (the path inside the container). For KEY_FILE, specify the path of the key-file generated by mkcert (path in the container).

By specifying the above environment variable, Let's Encrypt SSL certificate acquisition will not be performed, and the HTTPS reverse proxy will run using the specified certificate.

In case of multiple server configuration (load distribution)

If there are multiple relay destination servers, there is no way to specify them with environment variables, so you need to write a configuration file.

Diagram スクリーンショット 2020-12-25 0.37.11.png

Now again, here's an example I've actually tried with GCE.

#Start two wordpress containers as a web server(Actually, it is necessary to share sessions and DB to make a cluster configuration)
$ sudo docker run -d --rm --name server1 wordpress:5.6.0-apache
$ sudo docker run -d --rm --name server2 wordpress:5.6.0-apache

#Confirmation of container startup
$ sudo docker ps
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS        PORTS     NAMES
a8c41d95a57d        wordpress:5.6.0-apache   "docker-entrypoint..."   2 minutes ago       Up 2 minutes  80/tcp    server2
c0f0d145d86d        wordpress:5.6.0-apache   "docker-entrypoint..."   3 hours ago         Up 3 hours    80/tcp    server1

#Check the IP address of each container
$ sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' server1
172.17.0.4
$ sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' server2
172.17.0.5

#Create a new configuration file and open it with an editor
$ mkdir mnt
$ vi mnt/config 

#Write the following content
domain('test2.35.197.56.116.sslip.io') {
  proxy_to "172.17.0.4", "172.17.0.5"    #Relay destination server. Multiple can be specified separated by commas
  cert_email '[email protected]'            #The email address used to obtain the SSL certificate. Please change it to your own.
}

#Exit the editor when you finish writing

#Start the reverse proxy by specifying the config file
$ sudo docker run -ti -d --name ez-gate -p80:80 -p443:443 \
    -v `pwd`/mnt:/mnt \
    -e CONFIG_PATH=/mnt/config \
    neogenia/ez-gate:latest

#Check the container log
$ sudo docker logs -f ez-gate

If it starts normally, open a web browser and try to access it. Let's open the log on the Web server side to confirm that the load is distributed.

#Monitor server1 logs
$ sudo docker logs -f server1

#Open another terminal and monitor server2 logs
$ sudo docker logs -f server2

If you reload the browser in this state, you can confirm that access is coming to both servers.

Multi-tenant type in multiple domains

As a more complicated configuration, you can assign multiple domains and set relay destinations for each.

For example, if it is accessed by www.example.com, it will be relayed to the server called www. When accessed by redmine.example.com, relay to the server called redmine, It is an image like that. It is a so-called multi-tenant configuration.

Diagram スクリーンショット 2020-12-25 0.34.20.png

Configuration file example

Just describe the relay destination for each domain as shown below.

domain('www.example.com') {
  proxy_to "www"                #Relay destination server list. It doesn't have to be IP if the name can be resolved
  cert_email '[email protected]'   #Email address used to obtain SSL certificate
}

domain('redmine.example.com') {
  proxy_to "redmine"            #Relay destination server list. It doesn't have to be IP if the name can be resolved
  cert_email '[email protected]'   #Email address used to obtain SSL certificate
}

How to write a configuration file

The following is a quote from the Official README.

The basic syntax of the configuration file is as follows.

domain('www.example.com') {
  proxy_to "webapp1", "webapp2", ...
}

It is possible to write multiple domain ().

In addition, you can specify the option of cert_email`` nginx_config as follows.

domain('www2.example.com') {
  proxy_to "apache1", "apache2"
  cert_email '[email protected]'

  nginx_config <<~_CONFIG_
    # change upload size max
    client_max_body_size 100M;
  _CONFIG_
}

cert_email takes precedence if the container environment variable CERT_EMAIL is specified. If there is any content you want to add to the nginx configuration file, specify it in nginx_config.

This configuration file is Ruby's internal DSL and is interpreted as a program, so you can read another file or refer to environment variables.

Reload config file

If you change the configuration file, you can reflect the contents of the configuration file without stopping the reverse proxy by executing the reload command as shown below.

docker exec -ti ez-gate /var/scripts/reload_config.rb

If there is an error in the config file, you can rest assured that the reload will fail and the nginx config will not be rewritten and will continue to work.

Use with docker-compose

In the actual development site, there are many cases where configuration management tools such as docker-compose are used instead of docker alone, so by defining environment variables in the configuration configuration file, you can set the same settings as the previous samples. .. A sample YAML file for docker-compose can be found in the Official README (https://github.com/neogenia-jp/EzGate/blob/master/README.ja.md#%E4%BE%8B2).

There is a problem that the certificate is obtained every time the EzGate container is raised or lowered, so there is a way to assign the directory where the certificate file is saved to Docker volume and make it persistent. Allocate / etc/letsencrypt to volume and you're good to go.

Summary

This time, we introduced a service that can be said to be an infrastructure support service created from troubles at our own development site.

In fact, at our company, we have started the company website, Redmine, Mattermost, etc. in a container with such a multi-tenant configuration. Each domain is assigned and hosted on one server.

In addition, at our development site, highly versatile items are cut out in functional units to improve diversion. We are incorporating it so that it can be used as a microservice.

EzGate is being developed as open source. It is a very good product with a track record of operation in several projects, but There are few documents, and there are still many things that need to be maintained in order to spread to the general public, but If you are interested, please use it. Issues and pull requests are also welcome.

That's all from the field.

Recommended Posts

How to quickly create a reverse proxy that supports HTTPS with Docker
How to create a class that inherits class information
How to create a method
I tried to create a padrino development environment with Docker
How to set up a proxy with authentication in Feign
Create a Vue3 environment with Docker!
[Java] How to create a folder
How to start Camunda with Docker
How to run a job with docker login in AWS batch
[For those who create portfolios] How to use binding.pry with Docker
Let's create a Docker container that can connect to CentOS 8 with the minimum configuration by SSH
Create a web environment quickly using Docker
How to share files with Docker Toolbox
[Rails] How to use rails console with docker
How to create a Maven repository for 2020
Create a MySQL environment with Docker from 0-> 1
[Swift5] How to create a splash screen
[rails] How to create a partial template
How to run Blazor (C #) with Docker
How to build Rails 6 environment with Docker
How to interact with a server that does not crash the app
[Docker] How to create a virtual environment for Rails and Nuxt.js apps
How to build a Ruby on Rails development environment with Docker (Rails 6.x)
[Java] Use jsoup to access HTTPS via a proxy that requires authentication
How to create a validator that allows only input to any one field
How to create a server executable JAR and WAR with Spring gradle
Try to build a reverse proxy type configuration with Keycloak (Security Proxy edition)
How to build a Ruby on Rails development environment with Docker (Rails 5.x)
How to create a convenient method that utilizes generics and functional interfaces
How to start a Docker container with a volume mounted in a batch file
I tried to create a portfolio with AWS, Docker, CircleCI, Laravel [with reference link]
How to create a database for H2 Database anywhere
How to create a small docker image of openjdk 11 (ea) application (1GB → 85MB)
[Memo] Create a CentOS 8 environment easily with Docker
[Rails] rails new to create a database with PostgreSQL
[Rails] How to create a graph using lazy_high_charts
How to get a heapdump from a Docker container
Build a WordPress development environment quickly with Docker
How to create pagination for a "kaminari" array
How to create multiple pull-down menus with ActiveHash
How to create a theme in Liferay 7 / DXP
How to give your image to someone with docker
[1st] How to create a Spring-MVC framework project
How to easily create a pull-down in Rails
[Rails] How to create a Twitter share button
How to create an Excel form using a template file with Spring MVC
Build a development environment to create Ruby on Jets + React apps with Docker
How to use docker compose with NVIDIA Jetson
How to create member variables with JPA Model
How to use nginx-ingress-controller with Docker for Mac
[Rails] How to build an environment with Docker
Create a Spring Boot development environment with docker
How to test a class that handles application.properties with SpringBoot (request: pointed out)
[Rails] [Docker] Copy and paste is OK! How to build a Rails development environment with Docker
I tried using Wercker to create and publish a Docker image that launches GlassFish 5.
[Docker] How to update using a container on Heroku and how to deal with Migrate Error
[Docker] How to see the contents of Volumes. Start a container with root privileges.
How to quit Docker for Mac and build a Docker development environment with Ubuntu + Vagrant
Docker command to create Rails project with a single blow in environment without Ruby
Create a docker image that runs a simple Java app
How to create a Java environment in just 3 seconds