When I first started writing Dockerfile for building a NodeJS app, I will do:
1
2
3
WORKDIR /app
COPY . /app
RUN npm install
Sure, this is very simple, but every time a change in the source file, the entire dependency tree needs to be re-installed. The only time you need to rebuild is when package.json changes.
One trick is to move package.json elsewhere, and build the dependencies, then move back:
1
2
3
4
5
WORKDIR /app
ADD package.json /tmp
RUN cd /tmp && npm install
COPY . /app
RUN mv /tmp/node_modules /app/node_modules
However, if there are a lot of depending packages with lots of files, the last step will take a long time.
How about using symbolic link to shorten the time?
1
2
3
4
5
WORKDIR /app
ADD package.json /tmp
RUN cd /tmp && npm install
COPY . /app
RUN ln -s /tmp/node_modules /app/node_modules
Well, it works, but not every NPM package is happy about it. Some will complain about the softlink, result in errors.
So, how can we reuse caches as much as possible, and:
Do not need to reinstall dependencies
Do not use symbolic link
Here is a solution, just move down the COPY statement:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Set the working directory, which creates the directory.
WORKDIR /app
# Install dependencies.
ADD package.json /tmp
RUN cd /tmp && npm install
# Move the dependency directory back to the app.
RUN mv /tmp/node_modules /app
# Copy the content into the app directory. The previously added "node_modules"
# directory will not be overridden.
COPY . /app
With this configuration, if package.json stays the same, only the last cache layer will be rebuilt when source codes change.
Register a domain with Amazon Route 53 via CLI is very straightforward, the only complicate thing is to generate the acceptable JSON structure, then somehow enter the JSON string in the command line without meddling from the shell.
Most the command line options require simple string values, but some options like --admin-contact requires a structure data, which is complex to enter in the command line. The easiest way is to forget all other options and use --cli-input-json to get everything in JSON.
First let’s generate the skeleton or template to use:
Now let’s register by running through jq to clean up extra blanks and end of line characters, and then pipe to xargs command by using new line character as the delimiter instead of a blank by default:
Download the latest (5.7.16make sure it is the latest, because 8.x branch might not work) MySQL Docker image:
1
$ docker pull mysql && docker pull mysql:5.7.16
Run a MySQL container with the data directory mounted from the host:
1
2
3
4
5
6
$ docker run \
--detach \
--name mysql \
--restart always \
--volume /srv/mysql:/var/lib/mysql \
mysql
Environment variables (MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER, or MYSQL_PASSWORD) become unnecessary:
Do note that none of the variables below will have any effect if you start the container with a data directory that already contains a database: any pre-existing database will always be left untouched on container startup.[^1]
Now I should be able to access my existing database:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ docker exec -it mysql sh -c 'mysql -uredmine -p redmine'
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.7.16 MySQL Community Server (GPL)
Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
But when accessing MySQL database from another host, we have to update the permissions. Let’s log into MySQL with root user:
1
$ docker exec -it mysql sh -c 'mysql -uroot -p mysql'
Issue SQL commands:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> SELECT Host, User FROM db WHERE Db='redmine';
+-----------+---------+
| Host | User |
+-----------+---------+
| localhost | redmine |
+-----------+---------+
1 row in set (0.00 sec)
mysql> SELECT Host, User FROM user WHERE User='redmine';
+-----------+---------+
| host | user |
+-----------+---------+
| localhost | redmine |
+-----------+---------+
1 row in set (0.00 sec)
Only localhost is allowed for both the database and the user. This is problematic. How does one know the host of the container yet to be created? The easiest way is to allow access from all hosts:
1
2
3
4
5
6
7
mysql> UPDATE db SET Host='%' WHERE Db='redmine';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE user SET Host='%' WHERE User='redmine';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
We can restrict to a set of IP addresses such as 172.17.0.% or sub domains %.example.com. Docker networking uses 172.17.0.x range.
The better way is that when linking containers, should be able to update mysql container /etc/hosts file to include a redmine container field. Then, a wild card is not necessary.
But having to allow all hosts to connect is fine, because all hosts do not mean all hosts. The port is not bind or map to any open port in the host machine. Only the host (172.17.0.1) and other Docker containers inside the host can discover the service provided. Other machines or containers outside the host is not able to access at all.
Restart the container to have the update taking effect:
1
$ docker restart mysql
Redmine
For a basic Redmine setup, there aren’t much to migrate, mainly just three directories, config, files, and log, others are application data. Configuration could be done via environment variables, and if logs weren’t important to migrate, we just need to move the files directory, which is used for file upload.
Let’s Encrypt CA issues short-lived certificates (90 days). Automated renewal process is preferred, recommended, and encouraged. But in a few situations, automated process is not available, here is how to do it manually when SSL certificate was installed with Docker:
First, update the container to the latest version. The latest version can be found from the release page in GitHub.
Let’s Encrypt is a free, open, and automated certificate authority (CA). And its Certbot is a fully-featured, extensible client for Let’s Encrypt CA that can automate the tasks of getting, renewing and even installing SSL certificates.
First, you need to get Certbot. There are a few ways to install Certbot. But with Docker, you don’t need to install, you just need to download the Docker image and run the container. However, the caveat is that this method does not install the certificate automatically respecting to your web server. But if you’re like me, running your server in another Docker container, this might be the way to go.
Let’s start.
First, download the image. You can download the latest version (tag):
An ENTRYPOINT allows you to configure a container that will run as an executable.[^1]
And the command line arguments to docker run becomes the arguments to certbot command. As we saw earlier to obtain the release version by using --version.
You can override the ENTRYPOINT instruction using the docker run --entrypoint flag.[^1]
For example, to override and run the container without executing the certbot command:
1
2
$ docker run -it --rm --name certbot --entrypoint /bin/bash \
quay.io/letsencrypt/letsencrypt:v0.9.1
But we are more concerning about others, such as exposed port and mapped volumes. The exposed port is 443, HTTPS port. The most important volume (directory) is /etc/letsencrypt. All generated keys and issued certificates can be found in there. Directory /var/lib/letsencrypt is the default working directory, some backup stuff are stored. I have yet to find it useful. However, the logs directory /var/log/letsencrypt is not being used. This could be useful if things went haywire.
Let’s Encrypt is a free, open, and automated certificate authority. And its Certbot is a fully-featured, extensible client for Let’s Encrypt CA that can automate the tasks of getting, renewing and even installing SSL certificates.
Sounds great! However, not yet to be simple and automated, especially working cloud providers such as Google Cloud Platform and its Google App Engine or GAE.
But it’s free. Yes, it’s free. Free software works better. Free certificate authority works better than others.
GAE is a managed service. The place to stored SSL certificate is in separate machines (load balancers). The current automated domain validation by Certbot mostly work with a single machine. Therefore, when the machine issues certificate request is not the same machine to be validated, we need find another way, hopefully an automated method to perform domain validation across machines.
Before creating an automated method, let’s see if we can do it manually. Certbot supports a number of different plugins that can be used to obtain and/or install certificates. A plugin is like an extension that supports a particular web server. Let’s see if we can find a plugin that supports GAE.
Here are some supported by Certbot:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ certbot --help plugins
plugins:
Certbot client supports an extensible plugins architecture. See 'certbot
plugins' for a list of all installed plugins and their names. You can
force a particular plugin by setting options provided below. Running
--help will list flags specific to that plugin.
--apache Obtain and install certs using Apache (default: False)
--nginx Obtain and install certs using Nginx (default: False)
--standalone Obtain certs using a "standalone" webserver. (default:
False)
--manual Provide laborious manual instructions for obtaining a
cert (default: False)
--webroot Obtain certs by placing files in a webroot directory.
(default: False)
And there are also a number of third-party plugins, see the User Guide in Certbot Documentation. But there is none for GAE. It looks like there are only three possible options to try: standalone, webroot and manual.
Let’s start with the standalone method, and issue that from the local machine:
If you’re the first time running the command, you will be prompted for email and agreement screens. Both email and agreement can be automated via --email and --agree-tos options. That’s the automated part.
After freeing up the ports 80 and 443, run into some issues:
:: The server could not connect to the client to verify the domain :: Failed to
connect to 0.0.0.0:443 for TLS-SNI-01 challenge, example.com (tls-sni-01):
urn:acme:error:connection :: The server could not connect to the client to verify the
domain :: Failed to connect to 0.0.0.0:443 for TLS-SNI-01 challenge
IMPORTANT NOTES:
- The following errors were reported by the server:
Domain: example.com
Type: connection
Detail: Failed to connect to 0.0.0.0:443 for TLS-SNI-01
challenge
To fix these errors, please make sure that your domain name was
entered correctly and the DNS A record(s) for that domain
contain(s) the right IP address. Additionally, please check that
your computer has a publicly routable IP address and that no
firewalls are preventing the server from communicating with the
client. If you're using the webroot plugin, you should also verify
that you are serving files from the webroot path you provided.
The standalone plugin runs its own simple web server to prove that you control the domain. Ownership or domain validation is the key here. It needs the current computer that just issued the certbot command to have a publicly routable IP address. That’s not going to be happening in my local computer behind NAT. And webroot plugin needs a running web server. It can’t be run from the local machine as well. Domain validation are done automatically with both standalone and webroot plugins. Furthermore, domain validation requests are coming from Let’s Encrypt servers, therefore, you can’t have the machine issuing the certificate request behind a NAT or load balancing methods without properly routing the requests.
Since automated methods mostly require the requester and domain owner to be residing on the same machine, we can try to move the request to the Google cloud. Otherwise, there is one more plugin to try, the manual plugin. The manual method (plugin) helps you obtain a cert by giving you instructions to perform domain validation yourself.
Let’s Encrypt is a free, open, and automated certificate authority. And its Certbot is “a fully-featured, extensible client for the Let’s Encrypt CA (or any other CA that speaks the ACME protocol) that can automate the tasks of obtaining certificates and configuring webservers to use them.”[^1]
There are a number of ways to obtain and install SSL certificates issued by Let’s Encrypt CA. This is about installing Certbot 0.8.0 release on Debian Jessie. But before continuing, a few things to think about:
The Let’s Encrypt Client (Certbot) presently only runs on Unix-ish OSes that include Python 2.6 or 2.7; Python 3.x support will hopefully be added in the future. … currently it supports modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin.[^1]
That’s why using Docker container installation method might be a better choice, because it does not mess up your existing libraries and it can use supported operating systems which might not be the one you are using.
Backports are recompiled packages from testing (mostly) and unstable (in a few cases only, e.g. security updates) in a stable environment so that they will run without new libraries (whenever it is possible) on a Debian stable distribution.
Backports cannot be tested as extensively as Debian stable, and backports are provided on an as-is basis, with risk of incompatibilities with other components in Debian stable. Use with care!
It is therefore recommended to select single backported packages that fit your needs, and not use all available backports.
Again, there’s why it might be a better idea to use a container. But, let’s proceed.
Add a new file named backports.list to /etc/apt/sources.list.d/ directory:
1 upgraded, 28 newly installed, 0 to remove and 163 not upgraded.
Need to get 1,881 kB of archives.
After this operation, 10.5 MB of additional disk space will be used.
Do you want to continue? [Y/n]
APT option -t lets you have simple control over which distribution packages will be retrieved from. In this case, the distribution jessie-backports is used.
Interesting to know that there is letsencrypt package, could this be the old client? Let’s query the APT’s package cache:
1
2
3
4
5
6
7
8
9
10
11
$ apt-cache show letsencrypt
Package: letsencrypt
Source: python-certbot
Version: 0.8.0-1~bpo8+2
Installed-Size: 29
Maintainer: Debian Let's Encrypt
Architecture: all
Depends: certbot
Description-en: transitional dummy package
This is a transitional dummy package for the rename of letsencrypt to certbot.
It can safely be removed.
Yes, it’s a dummy package. It has been renamed. And from the documentation:
Until May 2016, Certbot was named simply letsencrypt or letsencrypt-auto, depending on install method.[^1]
Putting caddy in /usr/local/bin (may require password)
Caddy 0.9.1 (+e8e5595)
Successfully installed
This is different from the Download page, where you get to select additional features (see the &features= URL query parameter).
1
2
$ which caddy
caddy is /usr/local/bin/caddy
Get the installed version:
1
2
$ caddy --version
Caddy 0.9.1 (+e8e5595)
Get help:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ caddy -h
Usage of caddy:
-agree
Agree to the CA's Subscriber Agreement
-ca string
URL to certificate authority's ACME server directory (default"https://acme-v01.api.letsencrypt.org/directory")
-conf string
Caddyfile to load (default"Caddyfile")
-cpu string
CPU cap (default"100%")
-email string
Default ACME CA account email address
-grace duration
Maximum duration of graceful shutdown (default5s)
-host string
Default host
-http2
Use HTTP/2 (default true)
-log string
Process log file
-pidfile string
Path to write pid file
-plugins
List installed plugins
-portstring
Defaultport (default"2015")
-quic
Use experimental QUIC
-quiet
Quiet mode (no initialization output)
-revoke string
Hostname for which to revoke the certificate
-root string
Root path ofdefault site (default".")
-typestring
Typeof server to run (default"http")
-version
Show version
Run Caddy locally:
1
2
3
4
$ caddy
Activating privacy features... done.
http://:2015
WARNING: File descriptor limit 1024 is too low for production servers. At least 8192 is recommended. Fix with "ulimit -n 8192".
A file descriptor is simply a number that the operating system assigns to an open file to keep track of it. Caddy’s primary goal is to be an easy-to-use static file web server. Having high file descriptor limit means it can open more files to serve users at the same time.
1
2
3
$ ulimit -Sn && ulimit -Hn
1024
4096
The current system is too low in both soft and hard limits. But since it’s not in production, warning can be ignored.
Make sure the server working:
1
2
3
4
5
6
7
8
$ http :2015
HTTP/1.1404Not Found
Content-Length: 14
Content-Type: text/plain; charset=utf-8
Server: Caddy
X-Content-Type-Options: nosniff
404Not Found
Response header X-Content-Type-Options: nosniff prevents MIME based attacks, it tells the browser to respect the response content type, not to override.
Status code 404 means working, but just lacks an index file. Let’s create one:
When a Docker image has been updated, will restarting the running container via docker restart pick up the change? Educated guess will be no, because like restarting a process, the memory is still retained. The best way to find out is to give a try.
Step 2 : CMD while true; do echo bar; sleep 5; done
--->Running in 7fc297e12005
---> a6c04345afb9
Removing intermediate container 7fc297e12005
Successfully built a6c04345afb9
Now we have a different image. The image ID is different: a6c0. But the old image is still there:
1
2
3
4
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
example latest a6c04345afb9 24 seconds ago 125.1 MB
<none> <none> 6a56a50ef254 3 minutes ago 125.1MB
Restart the container:
1
2
$ docker restart example
example
Got bar? No still foo all the way with the log. And when you inspect the container, it still uses the old image.
So, docker restart will not pick up the changes from updated image, it will still use the old image built previously. Therefore, the correct way is to drop the container entirely and run it again:
1
2
3
4
$ docker stop example && docker rm example && docker run -d --name example example