Installers that don't suck

Larry Garfield
Larry Garfield
Director of Developer Experience
05 Aug 2020

The first part of your web application that users will see is its installer. It’s a surprisingly important piece of your application, yet because it’s only seen once, it’s often an afterthought or doesn’t get the same attention as the user experience of other parts of the system. When it does, however, there is a tendency to focus on a very specific new-user experience only, to the detriment of the multitude of ways that a web application could be deployed, installed, or set up.

At Platform.sh, the Developer Relations team maintains a catalog of ready-to-install templates ranging from stand-alone applications to CMSs to frameworks to example-only configurations. That means we get to see a lot of installers, running the full gamut from “pretty good” to “the installer is so bad we had to completely abandon it.”

So what makes a good installer? Let’s sweat the details.

What does an installer do?

What exactly do we mean when we talk about an installer? It varies somewhat with the application, but there are a few general tasks that installers tend to be responsible for:

  • Asking the user for necessary environment information (DB credentials, etc.).
  • Asking the user for global configuration data, such as a site name, API keys, etc.
  • Writing global configuration to wherever it gets stored.
  • Populating data stores with initial data; that could be empty database tables, sample content, etc.
  • Downloading additional data on the fly, such as sample content or translations.
  • Downloading additional code extensions/plugins/modules/whatever the system calls them.
  • Creating the administrative user.
  • Creating the first non-admin user.

Not every application’s installer does all of these, and some don’t always apply, but that’s largely what they will do. The goal of the installer is that, once it’s done, the user is able to login (as either an admin or normal user) and start using the system.

What’s wrong with this picture?

That’s all well and good, but there are several problems with this approach when running in a cloud-based environment like Platform.sh.

First, the file system is generally read-only. That’s good for security, good for auditability, good for repeatable deployments, and completely incompatible with “write the global configuration to disk” if that configuration is some kind of generated code. Depending on the configuration, it could be written to a user-file part of the disk, but if it’s executable code rather than JSON, YAML, or XML, then that’s unwise from a security standpoint.

Second, in a cloud environment the user may not know what the database credentials are. Often it’s provided through environment variables, or a magically named include file, or some variant on that. The whole point of a cloud environment is that services such as a database or search index can be created declaratively, not through manual setup. In addition, when your application needs to run in multiple environments (either dev/stage/prod or a branch-is-environment setup like Platform.sh), the services you’re connecting to may not even have existed when the installer runs, because it was run on a different branch.

Third, downloading additional modules or extensions may seem like a nice user-friendly feature, but it runs into the same writeable file system problem as before. It also means you cannot easily manage what extensions are installed through Git, which is a major flaw.

Fourth, while this isn’t universal, I have seen systems that try to write their global configuration to the same directory as service credentials. A few even try to write them to the same file. That might seem simple at first, but as soon as you want to take advantage of any cloud-based capabilities at all, it can be a death-knell to your installer.

In general, most installer failings come down to assumptions: specifically, assuming that the application is being installed into the one and only place it will ever run, with a writable file system, by someone with root access, and without really thinking about future upgrades beyond “FTP new files over the old ones.”

That assumption is increasingly very, very wrong.

Better installers

Better installer design accounts for the fact that it may not be installed on The One True Server™ by someone with god-level server access. In fact, it’s optimized instead for needing to know as little as possible.

First off, the application should be able to read environment-specific configuration from environment variables. That includes database credentials, API keys, the installation path of the application, the domain name on which the application is running, and anything else that would be different between production and your local laptop. If those are different, they should be read from, or at least readable from, environment variables.

It’s OK if your application can also read them from a file on disk, be it a .env file to override the environment directly or an application-specific file, as long as it can read from the environment. Java developers may be used to a properties file, which is fine as long as environment variables win over it if defined. That file can be used for local development, but should not be used in production.

It’s also a good idea to have a place for host-specific glue code to be injected, in case the way your host exposes environment information needs to be mapped to the variable names your application expects. The mechanism will vary with every application and host, which is fine; just leave a place for that to happen.

Second, the installer should be able to detect if those environment variables are already populated. If so, and it can already connect to the database, just skip that step in the installer. The user doesn’t have to be in the process at all. That also means there’s nothing to write to disk, so it avoids that problem, too.

Third, any additional global configuration should be written to a non-executable target. The database is a fine place for it. If it must be written to a file, write it to a non-executable format (JSON is a good choice, or ini, or YAML, but not PHP or Javascript) in a directory specific for that purpose. Ideally it’s a non-web-accessible directory separate from where user files go, for additional security. That directory may be writable, but not shared with any environment-dependent information.

And finally, don’t download code in the installer. Virtually every language today has a standard package manager available for it, be it Composer, npm, Pip, Maven, Bundler, Cargo, or what have you. That’s how extensions should be added and managed. That gives you all the same benefits for extensions as you get for your core system, mainly security, auditability, and repeatable deployments.

With those three simple changes—environment variable-based configuration, environment-aware installer steps, and avoiding disk writes—nearly any application can be made “cloud-friendly.” While there are other changes that could make an application fully “cloud native” (depending on which marketer you listen to on what that means), making your application ready for nearly any cloud-based host is really that simple. Not doing so, however, can make your application difficult, or even impossible, to install on a cloud-based host. And really, no one wants that.