Automatically create and renew website SSL certificates using the Let's Encrypt free certificate authority and its client certbot. Built on top of the Nginx server running on Debian. OpenSSL is used to automatically create the Diffie-Hellman parameters used during the initial handshake of some ciphers.
ℹ️ The very first time this container is started it might take a long time before before it is ready to respond to requests. Read more about this in the Diffie-Hellman parameters section.
This container requests SSL certificates from Let's Encrypt, with the help of their certbot script, which they provide for the absolutely bargain price of free! If you like what they do, please donate.
This repository was originally forked from @henridwyer
by
@staticfloat
, before it was forked again by me. However, the changes to
the code has since become so significant that this has now been detached as its
own independent repository (while still retaining all the history). Migration
instructions, from @staticfloat
's image, can be found
here.
Some of the more significant additions to this container:
- Handles multiple server names when
requesting certificates
(i.e. both
example.com
andwww.example.com
). - Will create Diffie-Hellman parameters if they are defined.
- Uses the parent container's
/docker-entrypoint.d/
folder. - Will report correct exit code when stopped/killed/failed.
- Stricter when it comes to checking that all files exist.
- Easily to force renewal of certificates if necessary.
- You can tune your own renewal interval.
- Builds for multiple architectures available on Docker Hub.
-
This guide expects you to already own a domain which points at the correct IP address, and that you have both port
80
and443
correctly forwarded if you are behind NAT. Otherwise I recommend DuckDNS as a Dynamic DNS provider, and then either search on how to port forward on your router or maybe find it here. -
Tips on how to make a proper server config file, and how to create a simple test, can be found under the Good to Know section.
-
I don't think it is necessary to mention if you managed to find this repository, however, I have been proven wrong before so I want to make it clear that this is a Dockerfile which requires Docker to function.
CERTBOT_EMAIL
: Your e-mail address. Used by Let's Encrypt to contact you in case of security issues.
STAGING
: Set to1
to use Let's Encrypt's staging servers (default:0
)DHPARAM_SIZE
: The size of the Diffie-Hellman parameters (default:2048
)RSA_KEY_SIZE
: The size of the RSA encryption keys (default:2048
)RENEWAL_INTERVAL
: Time interval between certbot's renewal checks (default:8d
)
/etc/letsencrypt
: Stores the obtained certificates and the Diffie-Hellman parameters
This option is for if you have downloaded this entire repository.
Place any additional server configuration you desire inside the
nginx_conf.d/
folder and run the following command in
your terminal while residing inside the src/
folder.
docker build --tag jonasal/nginx-certbot:local .
This option is for if you make your own Dockerfile
.
This image exist on Docker Hub under jonasal/nginx-certbot
, which means
you can make your own Dockerfile
for a cleaner folder structure. Just add a
command where you copy in your own server configuration files (make sure
that none of your files replace those already present).
FROM jonasal/nginx-certbot:latest
COPY conf.d/* /etc/nginx/conf.d/
Don't forget to build it!
docker build --tag jonasal/nginx-certbot:local .
Irregardless what option you chose above you run it with the following command:
docker run -it -p 80:80 -p 443:443 \
--env CERTBOT_EMAIL=your@email.org \
-v $(pwd)/nginx_secrets:/etc/letsencrypt \
--name nginx-certbot jonasal/nginx-certbot:local
You should be able to detach from the container by holding
Ctrl
and pressingp
+q
after each other.
An example of a docker-compose.yaml
file can be found in the
examples/
folder. The default parameters that are found inside
the nginx-certbot.env
file will be overwritten by any environment variables
you set inside the .yaml
file.
Move the .yaml
file (and optionally the .env
file) into the src/
folder, and then build and start with the following commands. Just remember to
place any additional server configs you want inside the
nginx_conf.d/
folder beforehand.
docker-compose build --pull
docker-compose up
In case you are just experimenting with setting this up I suggest you set the
environment variable STAGING=1
, since this will change the challenge URL to
the staging one. This will not give you "proper" certificates, but it has
ridiculous high rate limits compared to the non-staging
production certificates.
Include it like this:
docker run -it -p 80:80 -p 443:443 \
--env CERTBOT_EMAIL=your@email.org \
--env STAGING=1 \
--name nginx-certbot jonasal/nginx-certbot:latest
As an example of a barebone (but functional) SSL server in Nginx you can
look at the file example_server.conf
inside the examples/
directory. By replacing 'yourdomain.org
' with your own domain you can
actually use this config to quickly test if things are working properly.
Place the modified config inside the nginx_conf.d/
folder, build
the container and then run it as described above.
Let the container do it's magic for a while, and
then try to visit your domain. You should now be greeted with the string
"Let's Encrypt certificate successfully installed!
".
The included script will go through all configuration files (*.conf*
) it
finds inside Nginx's /etc/nginx/conf.d/
folder, and create requests from the
file's content. In every unique file it will find any line that says:
ssl_certificate_key /etc/letsencrypt/live/yourdomain.org/privkey.pem;
and only extract the part which here says "yourdomain.org
", and this will
henceforth be used as the "primary domain" for this config file. It will then
find all the lines that contain server_name
and make a list of all the domain
names that exist on the same line. So a file containing something like this:
server {
listen 443 ssl;
server_name yourdomain.org www.yourdomain.org;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.org/privkey.pem;
...
}
server {
listen 443 ssl;
server_name sub.yourdomain.org;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.org/privkey.pem;
...
}
will share the same certificate file (the "primary domain"), but the certbot command will include all listed domain variants. The limitation is that you should write all your server blocks that have the same "primary domain" in the same file. The certificate request from the above file will then become something like this (duplicates will be removed):
certbot ... -d yourdomain.org -d www.yourdomain.org -d sub.yourdomain.org
This container will automatically start a certbot certificate renewal check
after the time duration that is defined in the environmental variable
RENEWAL_INTERVAL
has passed. After certbot has done its stuff, the code will
return and wait the defined time before triggering again.
This process is very simple, and is just a while [ true ];
loop with a sleep
at the end:
while [ true ]; do
# Run certbot...
sleep "$RENEWAL_INTERVAL"
done
So when setting the environmental variable, it is possible to use any string
that is recognized by sleep
, e.g. 3600
or 60m
or 1h
. Read more about
which values that are allowed in its manual.
The default is 8d
, since this allows for multiple retries per month, while
keeping the output in the logs at a very low level. If nothing needs to be
renewed certbot won't do anything, so it should be no problem setting it lower
if you want to. The only thing to think about is to not to make it longer than
one month, because then you would miss the window where certbot would deem
it necessary to update the certificates.
Regarding the Diffie-Hellman parameter it is recommended that you have one for
your server, and in Nginx you define it by including a line that starts with
ssl_dhparam
in the server block (see
examples/example_server.conf
). However, you can make a
config file without it and Nginx will work just fine with ciphers that don't
rely on the Diffie-Hellman key exchange (more info about ciphers).
The larger you make these parameters the longer it will take to generate them.
I was unlucky and it took me 65 minutes to generate a 4096 bit parameter on an
old 3.0GHz CPU. This will vary greatly between runs as some randomness is
involved. A 2048 bit parameter, which is still secure today, can probably be
calculated in about 1-3 minutes on a modern CPU (this process will only have to
be done once, since one of these parameters is good for the rest of your
website's lifetime). To modify the size of the parameter you may set the
DHPARAM_SIZE
environment variable. Default is 2048
if nothing is provided.
It is also possible to have all your server configs point to the same
Diffie-Hellman parameter on disk. There is no negative effects in doing this for
home use (source 1 & source 2). For persistence you should place it
inside the dedicated folder /etc/letsencrypt/dhparams/
, which is inside the
predefined Docker volume. There is, however, no requirement to do
so, since a missing parameter will be created where the config file expects the
file to be. But this would mean that the script will have to re-create these
every time you restart the container, which may become a little bit tedious.
You can also create this file on a completely different (faster?) computer and
just mount/copy the created file into this container. This is perfectly fine,
since it is nothing "private/personal" about this file. The only thing to
think about in that case would perhaps be to use a folder that is not under
/etc/letsencrypt/
, since that would otherwise cause a double mount.
It might be of interest to manually trigger a renewal of the certificates, and
that is why the run_certbot.sh
script is possible to run standalone at any
time from within the container.
However, the preferred way of requesting a reload of all the configuration files
is to send in a SIGHUP
to the container:
docker kill --signal=HUP <container_name>
This will terminate the sleep timer and make the renewal loop start again from the beginning.
While this will be enough in the majority of the cases, it might sometimes be
necessary to force a renewal of the certificates even though certbot thinks
it could keep them for a while longer (like when this happened). It is
therefore possible to add "force" as an argument, when calling the
run_certbot.sh
script, to have it append the --force-renewal
flag to the
requests made.
docker exec -it <container_name> /scripts/run_certbot.sh force
This will request new certificates irregardless of then they are set to expire.
NOTE: Using "force" will make new requests for all you certificates, so don't run it too often since there are some limits to requesting production certificates.
The two images are not that different when it comes to building/running, since
this repository was originally a fork. So just like in @staticfloat
's setup
you need to get your own *.conf
files into the container's
/etc/nginx/conf.d/
folder, and then you should be able to start this one
just like you did with his.
This can either be done by copying your own files into the container at
build time, or you can mount a local folder to the
aforementioned location. In the latter case you need to make sure you include
the two files present in this repository's
src/nginx_conf.d/
folder, since these are required in
order for certbot to request certificates.
Why you need to include these files yourself is because this gives the user more freedom in how they can configure their server blocks, i.e. you can create your own "redirector" if you do not like mine.
The only obligatory environment variable for starting this container is the
CERTBOT_EMAIL
one, just like in @staticfloat
's case, but I
have exposed a couple of more that can be changed from their
defaults if you like. Then there is of course any environment variables read by
the parent container as well, but those are probably not as important.
If you were using templating before, you should probably look into "template" files used by the Nginx parent container, since this is not something I have personally implemented in mine.
- Fix dependencies so that it is possible to build in 32-bit ARM architectures (issue #24).
- Added GitHub Actions/Workflows so that each tag now is built for multiple arches (issue #28).
- Fix that scripts inside
/docker-entrypoint.d/
were never run (issue #21). - Fix for issue where the script failed in case the
/etc/letsencrypt/dhparams
folder was missing (issue #20).
- Move over to semantic versioning.
- The version number will now be given like this:
[MAJOR].[MINOR].[PATCH]
- This is done to signify that I feel like this code is stable, since I have been running this for quite a while.
- The version number will now be given like this:
- Build from a defined version of Nginx.
- This is done to facilitate a way to lock this container to a more specific version.
- This also allows us to more often trigger rebuilds of this container on Docker Hub.
- New tags are available on Docker Hub.
- There will now be tags on the following form:
- latest
- 1.0.0
- 1.0.0-nginx1.19.7
- There will now be tags on the following form:
- Container now listens to
SIGHUP
and will reload all configs if this signal is received.- More details can be found in the commit message: bf2c135
- Made Docker image slightly smaller by including
--no-install-recommends
. - There is now also a
dev
branch/tag if you are brave and want to run experimental builds. - JonasAlfredsson/docker-nginx-certbot is now its own independent repository (i.e. no longer just a fork).
- It is now possible to manually trigger a renewal of
certificates.
- It is also possible to include "force" to add
--force-renewal
to the request.
- It is also possible to include "force" to add
- The "clean exit" trap now handle that parent container changed to
SIGQUIT
as stop signal. - The "certbot" server block (in Nginx) now prints to stdout by default.
- Massive refactoring of both code and files:
- Our "start command" file is now called
start_nginx_certbot.sh
instead ofentrypoint.sh
. - Both
create_dhparams.sh
andrun_certbot.sh
can now be run by themselves inside the container. - I have added
set -e
in most of the files so the program exit as intended when unexpected errors occurs. - Added
{}
and""
around most of the bash variables. - Change some log messages and where they appear.
- Our "start command" file is now called
- Our
/scripts/startup/
folder has been removed.- The parent container will run any
*.sh
file found inside the/docker-entrypoint.d/
folder.
- The parent container will run any
- Made so that the container now exits gracefully and reports the correct exit
code.
- More details can be found in the commit message: 43dde6e
- Bash script now correctly monitors both the Nginx and the certbot renewal
process PIDs.
- If either one of these processes dies, the container will exit with the same exit code as that process.
- This will also trigger a graceful exit for the rest of the processes.
- Removed unnecessary and empty
ENTRYPOINT
from Dockerfile. - A lot of refactoring of the code, cosmetic changes and editing of comments.
- Fixed the regex used in all of the
sed
commands.- Now makes sure that the proper amount of spaces are present in the right places.
- Now allows comments at the end of the lines in the configs.
# Nice!
- Made the expression a little bit more readable thanks to the
-r
flag.
- Now made certbot solely responsible for checking if the certificates needs to
be renewed.
- Certbot is actually smart enough to not send any renewal requests if it doesn't have to.
- The time interval used to trigger the certbot renewal check is now user
configurable.
- The environment variable to use is
RENEWAL_INTERVAL
.
- The environment variable to use is
- Added
--cert-name
flag to the certbot certificate request command.- This allows for both adding and subtracting domains to the same certificate file.
- Makes it possible to have path names that are not domain names (but this is not allowed yet).
- Made the file parsing functions smarter so they only find unique file paths.
- Cleaned up some log output.
- Updated the
docker-compose
example. - Fixed some spelling in the documentation.
- Python 2 is EOL, so it's time to move over to Python 3.
- From now on Docker Hub will also automatically build with tags.
- Lock the version by specifying the tag:
jonasal/nginx-certbot:0.11
- Lock the version by specifying the tag:
- Update to new ACME v2 servers.
- I am now confident enough to remove the version suffixes.
nginx:mainline
is now using Debian 10 Buster.- Updated documentation.
- Make both Nginx and the update script child processes of the
entrypoint.sh
script. - Container will now die along with Nginx like it should.
- The Diffie-Hellman parameters now have better permissions.
- Container now exist on Docker Hub under
jonasal/nginx-certbot:latest
- More documentation.
@JonasAlfredsson
enters the battle.- Diffie-Hellman parameters are now automatically generated.
- Nginx now handles everything HTTP related -> certbot set to webroot mode.
- Better checking to see if necessary files exist.
- Will now request a certificate that includes all domain variants listed
on the
server_name
line. - More extensive documentation.
- Ditch cron, it never liked me anyway. Just use
sleep
and awhile
loop instead.
- Complete rewrite, build this image on top of the
nginx
image, and runcron
/certbot
alongsidenginx
so that we can have Nginx configs dynamically enabled as we get SSL certificates.
- Add
nginx_auto_enable.sh
script to/etc/letsencrypt/
so that users can bring Nginx up before SSL certs are actually available.
- Change the name to
docker-certbot-cron
, update documentation, strip out even more stuff I don't care about.
- Rip out a bunch of stuff because
@staticfloat
is a monster, and likes to do things his way
- Add support for webroot mode.
- Run certbot once with all domains.
- Upgraded to use certbot client
- Changed image to use alpine linux
- Initial release