This guide is for those utilizing the Let’s Encrypt service but do not want to run a separate certbot docker image, or simply want an easy way to set up NGINX as your HTTPS service front. The set up will utilize the acme-challenge for verifying the DNS name.
There are multiple options when you want to set up a front for your services. You could go with a fully fledged API gateway solution, providing all sorts of features. In a larger production environment you probably also want to add service discovery support. However, if you have a finite number of microservices and want to utilize a single lightweight docker image as your service front, then read on to see how to set up a such solution.
The full example can be found at https://bitbucket.org/zuunr/https-nginx-front. Download or fork it and follow the README to configure it.
Preparing the image
As the base image we will use the default NGINX docker image https://hub.docker.com/_/nginx. On that image we will install the certbot command in order to create and renew certificates.
FROM nginx:1.15.6-alpine EXPOSE 80 443 RUN apk update \ && apk add certbot
We also need to make sure certbot is executed at startup as well as a scheduled job to renew the certs whenever they are about to expire. For that job we will set up a cron job to execute a certbot renewal twice a day.
Since we need to start both nginx, crond and certbot at image startup we create a script to handle this (since there can only be one entry point in the Dockerfile). The updated Dockerfile will look like this:
FROM nginx:1.15.6-alpine EXPOSE 80 443 COPY shell/nginx_start.sh /home/nginx_start.sh RUN apk update \ && apk add certbot \ && echo '0 0,12 * * * certbot renew --post-hook "nginx -s reload"' > /etc/crontabs/root \ && dos2unix /home/nginx_start.sh \ && chmod +x /home/nginx_start.sh CMD ["/home/nginx_start.sh"]
Let’s have a look at the nginx_start.sh script:
#!/bin/sh if [ "$CERTBOT_MODE" = "PROD" ] then echo "Starting certbot in PROD mode" sleep 5 && /home/init_certbot.sh & crond elif [ "$CERTBOT_MODE" = "STAGING" ] then echo "Starting certbot in STAGING mode" sleep 5 && /home/init_certbot_staging.sh & crond else echo "Starting certbot in disabled mode" fi nginx -g "daemon off;"
As seen above, we have introduced a new environment variable to be able to start the image in different modes in case you don’t want the certbot to start creating certificates on startup. The PROD and STAGING modes will start certbot scripts (delayed 5 seconds) as well as the crond command before starting the NGINX server.
Looking at the init_certbot.sh scripts, we are setting up certbot with the webroot configuration in a non-interactive mode. We also introduce two new environment variables, REGISTER_EMAIL and DOMAINS. The cert-name is set to a fixed value so that the path to the certificate will stay the same regardless of domains added.
The init_certbot_staging.sh will look the same, except an added –staging flag to the command.
See certbot documentation for more information: https://certbot.eff.org/docs/using.html#certbot-commands
#!/bin/sh rm -rf /etc/letsencrypt certbot certonly --webroot -w /usr/share/nginx/html --email $REGISTER_EMAIL --non-interactive --agree-tos --cert-name generatedcert --domains $DOMAINS nginx -s reload
Why are we removing the /etc/letsencrypt folder the first thing we do?
Well if NGINX is configured to use SSL certificates, it will not start if there are no certificates available. To get around that, we create a dummy self signed certificate to be installed at startup. We are removing the self signed certificate before the real certificates are installed. Create the self signed certificates yourself, or use http://www.selfsignedcertificate.com
Here is the complete Dockerfile from https://bitbucket.org/zuunr/https-nginx-front. In the example repository, we have also added a html template for HTTP errors.
FROM nginx:1.15.6-alpine EXPOSE 80 443 ENV REGISTER_EMAIL your@email.com ENV DOMAINS www.yoursite.com,yoursite.com COPY webconfig/nginx.conf /etc/nginx/nginx.conf COPY webconfig/dummy_fullchain.pem /etc/letsencrypt/live/generatedcert/fullchain.pem COPY webconfig/dummy_privkey.pem /etc/letsencrypt/live/generatedcert/privkey.pem COPY shell/init_certbot.sh /home/init_certbot.sh COPY shell/init_certbot_staging.sh /home/init_certbot_staging.sh COPY shell/nginx_start.sh /home/nginx_start.sh COPY dist /usr/share/nginx/html/error RUN apk update \ && apk add certbot \ && echo '0 0,12 * * * certbot renew --post-hook "nginx -s reload"' > /etc/crontabs/root \ && dos2unix /home/init_certbot.sh \ && dos2unix /home/init_certbot_staging.sh \ && dos2unix /home/nginx_start.sh \ && chmod +x /home/init_certbot.sh \ && chmod +x /home/init_certbot_staging.sh \ && chmod +x /home/nginx_start.sh CMD ["/home/nginx_start.sh"]
Configure NGINX
Looking at the nginx.conf, we start with the base NGINX configuration but we disable the include /etc/nginx/conf.d/*.conf; and add two server directives.
The first server, regular HTTP on port 80, is used for the acme-challenge and for redirecting requests to the HTTPS server on port 443.
The second server contains the certificate configuration as well as the reverse proxy to your microservice. The example also contains an /error mapping for 404 and 50x errors.
user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; gzip on; #include /etc/nginx/conf.d/*.conf; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; server { listen 80; server_name yoursite.com; location /.well-known { root /usr/share/nginx/html; } location / { return 301 https://$host$request_uri; } } server { listen 443 ssl; server_name yoursite.com; ssl_certificate /etc/letsencrypt/live/generatedcert/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/generatedcert/privkey.pem; error_page 404 /error/404.html; error_page 500 502 503 504 /error/50x.html; location /error { add_header Cache-Control "public"; root /usr/share/nginx/html; index 404.html index.htm; } location /v1/my-services/api { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; proxy_pass http://10.0.0.0:8080; } } }
The server_name should be updated with your DNS name(s), and the location /v1/my-services/api should be renamed/replaced with correct mapping for your microservice.
If you are running the microservice in a Kubernetes environment, locate the ClusterIP of your service and enter it as the proxy_pass value. Add as many locations as your solution requires. In the above example we are using standard HTTP for internal requests from the NGINX and the microservice, i.e. the NGINX acts as a HTTPS terminator.
Running the image
Be sure you have updated the REGISTER_EMAIL and DOMAINS environment values in the Dockerfile before building the docker image. Also don’t forget to point your DNS name to your external IP address to use.
docker run -it --rm -p 443:443 -p 80:80 -e CERTBOT_MODE=STAGING my-nginx-service-front
The above command will start the image in STAGING mode, i.e. not creating any real certificates. Set the CERTBOD_MODE to PROD to enable real certs. That’s all!
For this simple case, the certs will be removed each time the container is destroyed, but you could easily place those certs on a mounted volume. You could also automate the configuration of the nginx.conf if you want to use this solution for a larger share of microservices.
Mikael