CLI Architecture
At its core, the Thermostat CLI is a Node.js CLI application. However, unlike a typical CLI application, it does not install a binary that runs directly on the user's system, it instead runs in a Docker container. This somewhat complicates the implementation of certain parts of Thermostat, but it makes the overall development process for Thermostat easier because it is all encapsulated in Docker containers. You can see a general overview of how the Thermostat CLI operates in the image below, and these components are described in more detail in the sections below.
Bootstrapping Process
Because Thermostat runs entirely within Docker, it requires a bootstrapping process to properly set up the correct host files and Docker configuration for the container. This bootstrapping process is designed to be as simple as possible to initiate (as illustrated by the installation script which is very simple), it is initiated simply by running the Docker image with access to the Docker daemon and no other configuration parameters. This bootstrapping process is also used for updating the Thermostat installation as well, since part of the bootstrapping process is cleaning up any old Thermostat installation files. You can learn more about the bootstrapping process by reading the setup code, but the basic process is as follows:
- Check if bootstrapping is needed: When the Thermostat container initially starts it checks to see if it is running without any arguments and in a container not named "thermostat", if so it starts the bootstrapping process.
- Stop and rename the old Thermostat container if it exists: Thermostat looks for an existing container named "thermostat" and stops it and renames it to "old_thermostat" if so. If any of the following steps fails, it will restore this old Thermostat container to be named "thermostat" again and will restart it.
- Start the new Thermostat container: Thermostat copies itself, creating a new Docker container using the same Docker image as the one it is currently running, but initializing it with all of the necessary Docker configuration values (primarily all the necessary bind mounts that it needs to function). The old Docker container then waits for this new container to initialize itself.
- Set up host SSH key: As discussed more in the Host Files section, Thermostat uses SSH to control the host machine from within the Docker container, and to enable this it generates an SSH public/private key pair and adds the public key to the
authorized_keys
file in the root user's.ssh
folder. It then connects on port 22 to the host machine to check that it has SSH access. - Update bind mounts: As discussed more in the Host Files section, Thermostat applications can be stored anywhere on the host machine, and so it uses bind mounts to hard-link the application files into the application data folder (typically located at
/var/lib/thermostat/apps
). It uses SSH to communicate with the host machine to ensure all these bind mounts have been properly established. - Update services: Thermostat checks to see which services are in use by the currently deployed Thermostat applications, and it updates any of them that are out of date with the current proper configuration. It uses Docker Compose as the primary tool for managing the configuration of the services.
- Update command script: As discussed more in the Host Files section, while Thermostat runs primarily in Docker, it does require a minimal Bash script that triggers the Docker execution when the user runs the
thermostat
command on their host machine. This setup ensures that this command script is set up properly. - Old Thermostat containers are removed: At this point the new Thermostat container is fully initialized and running normally, and so the old bootstrap container deletes the outdated "old_thermostat" container and then removes itself, leaving the new version of Thermostat fully bootstrapped.
Host Files
While Thermostat runs primarily in Docker, there are some host files it uses:
- The Thermostat data folder (which by default is at
/var/lib/thermostat
) is where Thermostat stores its stateful data that needs to persist between restarts. This contains files like configuration files for Thermostat and its services, database files, log files, etc. Application code and data folders are bind mounted into this folder when an application is deployed. - The Thermostat CLI bash script that triggers the Docker execution of the full CLI application is located at
/usr/local/bin/thermostat
. - Thermostat accesses the root user's home directory in order to modify the
.ssh/authorized_keys
file so it can add an SSH key that allows it to communicate via SSH with the host machine.
SSH Agent Authentication
Thermostat communicates with the Thermostat API using its SSH API, which authenticates the user based on their SSH public key. In order for Thermostat to properly authenticate the user that runs the thermostat
command, it needs access to their SSH public key, which it gets by accessing their SSH agent. SSH agents are a standard way to pass along SSH identities between sessions, its primary purpose is to allow users to do nested SSH sessions while preserving their identity, but it serves well for Thermostat's use case as well. When you run the thermostat
CLI command it bind-mounts the SSH agent sockfile into the Docker container that runs the command so Thermostat can use it to authenticate with the Thermostat API.
Anatomy of an Application
The primary purpose of Thermostat is to run applications. Applications have two primary parts, code and data. The application code is a Git repository that contains all the source code and configuration files for the project, and the data folder is for mutable files that can't be stored in the Git repository like log files, secrets, user uploads, etc. In addition to the code and data, each deployment of an application has a "stage" value that is specified at deployment time which specifies the environment the application runs in. The primary purpose of the stage is to determine where the app pulls its data from and pushes its data to, each stage has a separate storage pool used for uploading/downloading data. It is important that these different environments use different data sets since for example it would be very bad to accidentally overwrite data in the client's production BigCommerce store when a developer is just testing things out on their local development environment. You can also optionally specify a "substage" which is a unique version of a stage that still uses that stage's data. This allows you to deploy, for example, a development:v1 project and a development:v2 project simultaneously and they will both push and pull data from the "development" environment in the Thermostat API, but they will be uniquely identified as distinct deployments.
In order to make the code repository usable in a Thermostat application, it must contain a Thermostat configuration file named thermostat.yml
that defines how Thermostat should run the application. One of the primary purposes of this file is to specify how the code and data folders should be linked together so that some folders (for example log file folders) can be excluded from the Git repository but still have their data saved to a secure backup, you can learn more about this in the data links configuration section. Technically it is possible to run a Thermostat application with a configuration file that is blank (except for the version number), but this would not actually do anything for you, you need to configure scripts and services to really make it useful.
Scripts
One of the primary purposes of Thermostat is to provide an easy way to run saved commands in Docker containers. This allows you to easily work on projects no matter what software you have installed on your local machine, for example with Thermostat you can easily build a project that uses Node.js 14 even if your host machine is running Node.js 18, just by specifying node:14
as the image to run the build script in Thermostat. This is invaluable for enabling long-term maintenance of a wide variety of projects that may have widely different dependencies. Some scripts are run during the deployment or removal process as described by the thermostat deploy and thermostat remove commands, but most commands simply serve as a way to document and reuse common procedures.
Services
Services are common application components like web servers and databases that can be easily configured using Thermostat. Thermostat services let you take a bare Git repository with some PHP files in it and spin up a full production ready deployment that serves the website with Nginx and PHP with just a few lines of configuration.
Fahrenheit currently hosts these services, though more are planned in the future:
- Nginx: This is the primary web server that we typically use with Thermostat, you configure it by providing the path to a Nginx configuration file that is stored in the code Git repository. Thermostat will test the configuration for validity and automatically load and serve the configuration when the application is deployed.
- Certbot: Certbot is the tool we use to automatically provision LetsEncrypt SSL certificates. It automatically integrates with the Nginx service to provision the certificates and serve the websites with SSL with minimal configuration, you can see an example of this here. You configure it just by providing a list of domains that you want it to generate SSL certificates for.
- Caddy: Caddy is an alternative web server that we use for some projects due to its ability to automatically provision SSL certificates. You configure it similar to how you configure Nginx, just by providing the path to a Caddy configuration file in the code Git repository. Note that you can't run the Nginx service and the Caddy service at the same time because they both try to bind ports 80 and 443.
- PHP 7.3-8.2: These services run the PHP FPM process with a standard set of extensions and production-ready configuration files. The primary purpose of these services is to be used with the Nginx service, as it is configured to automatically connect to these services just by connecting to
php-7.3
,php-7.4
,php-8.1
, orphp-8.1
. If you want files to be writable by the PHP process, be sure to store those files in the "data" folder and make them owned by82:82
(which is the user and group ID that the PHP process uses). - MariaDB: MariaDB is the primary database that we use for most of our applications. You configure it by providing the name of the database you want it to create and optionally a path to where in the data folder you want to store the backup for the database contents. You also need to specify where the service should find the database password in the .env file in the data folder.
- Redis: We use Redis for some of our websites as a caching database. You configure it by specifying where the service should find the database password in the .env file in the data folder.
If you need to use other databases or supplementary tools that don't exist as Thermostat services yet, it is recommended that you use Docker compose commands in the start
and stop
scripts to spin up Docker containers for the tools when the application is deployed and to remove the containers when the application is removed, you can see an example of this here.