Enable HTTPS for Rails App with acme.sh and Letsencrypt
The Problem
I've got my Rails app successfully running on my Raspberry Pi, I can also expose it to the public (Internet) with an FRP tunnel (something like ngrok or zerotier but is self hosted), but it doesn't support HTTPS, now I need an HTTPS cert to enable HTTPS for the Rails app.
I know Kamal and Dokku does this for me automatically, I can also enable "Proxy" mode in Cloudflare in DNS, such that CF issues the cert for me, or I can also set up a Cloudflare argo tunnel. But sometimes due to the network condition I don't have those options. So, I need to get a cert with acme.sh for my web app.
Start acme.sh in Docker
There is a docker image: neilpang/acme.sh, I can run it on my Pi server. But before that, I need to do two things first.
First thing is, I need to create a folder on the pi and share it with the acme docker container. Because the acme.sh will run in a Docker container, if I want to use the certs issued by it, I need to get it out of the container. One good approach is to use a Docker mount volume, so I create some folders on my server first.
$ cd ~
$ mkdir acme_data
And then after the cert is issued, I need to install the cert to another folder, this is not required, but acme.sh's folder structure may change, so I create another folder to store the certs.
$ cd acme_data
$ mkdir installs
This ~/acme_data/installs folder will be in the acme.sh container later.
Second thing is, because I'm in a local network, I don't have an IPv4 address, so I need to use DNS challenge to ask letsencrypt to issue a cert for me. Acme.sh will need to change my DNS. So I need to tell it how to do it with my DNS provider.
To set this for my DNS provider (Cloudflare), I need to go to Cloudflare to generate an API token. This token should have the permission to manage my DNS record (but not others!). Such that acme.sh can use this token to complete its challenge to prove I own the domain name.
Beware that acme.sh allows two Cloudflare token/key, the "CF_Token" and "CF_Key", the "CF_Token" is what I need here. The "CF_Key" seems to be the key(or another kind of token) that can manage all my Cloudflare account settings, sounds like a dangerous thing, I won't use the "CF_Key". So I can just use the "CF_Token".
Then start the acme docker container, with these two Docker ENV (CF_Token and CF_Email), CF_Email is my Cloudflare account's email.
docker run -it -d --name acme --restart=always --net host -v /home/pi/acme_data:/acme.sh -e CF_Token=<MY_CFToken_From_The_CF_Dashboard> -e CF_Email=<MY_CF_Email> neilpang/acme.sh daemon
After that, register an account for letsencrypt to contact me with acme.sh:
docker exec acme --set-default-ca --server letsencrypt
docker exec acme --register-account -m myname@myemail
And now I can ask for a HTTPS cert, I use DNS challenge here, it will give me an wildcard domain cert:
docker exec acme --issue --dns dns_cf -d printfinn.com -d *.printfinn.com
If the process is successful, I am able to check the certs are already stored in my folder:
$ ls acme_data/printfinn.com_ecc/
backup fullchain.cer printfinn.com.conf printfinn.com.csr.conf
ca.cer printfinn.com.cer printfinn.com.csr printfinn.com.key
Then as said before, install the cert to another folder, maybe I use nginx, or apache, or just want to store it in some folder for future use, like for an FRP tunnel.
# you may use .crt or .cer as you need
docker exec acme --install-cert -d printfinn.com --cert-file /acme.sh/installs/printfinn.com_cert.crt --key-file /acme.sh/installs/printfinn.com.key --fullchain-file /acme.sh/installs/printfinn.com.crt
It gives me a few certs, I need to use the fullchain cert later.
The cert is valid for 90 days, and should usually renew at the 60th's day. To renew the cert automatically, use a cronjob:
# auto update acme.sh in the container
docker exec acme --upgrade --auto-upgrade
docker exec acme --cron
$ crontab -e
# In crontab, put this below:
10 0 * * * docker exec acme --cron > /dev/null
This will renew (if needed) the cert at 0:10 everyday midnight. Once it's renewed, it will be put into the "installs" folder I appointed before automatically. (Have some doubt about this, will check it after my first cert expires :) ).
If I wish to restart nginx or apache or maybe frp, I can append the command after the cronjob:
$ crontab -e
# In crontab, put this below:
10 0 * * * docker exec acme --cron > /dev/null && docker restart nginx-container-or-frp-container
Then I have an HTTPS cert now! And it auto renews!
Cheers!
(Note for myself, the command to start the frp server is:)
docker run \
-d \
--restart=always \
--network=rails_net \
--pull=always \
--name=frp-my-app \
-v /home/pi/acme_data/installs/printfinn.com.crt:/run/frpc/printfinn.com.crt:ro \
-v /home/pi/acme_data/installs/printfinn.com.key:/run/frpc/printfinn.com.key:ro \
natfrp.com/frpc \
--disable_log_color \
-f <frp_config_params>