Handling environment-specific parameters in a web hosting environment


posted | about 5 minutes to read

tags: environment variables nginx apache devops web hosting web development

One of the big roadblocks that I had to figure out when I was setting up some of my more complex websites was how to get environment-specific parameters to apply across environments. When you have things like different database endpoints, different passwords, even different debug options, it’s important that there’s an easy process that doesn’t add too much complexity to what you’re doing. When I was first starting out with multiple environments a few years ago, I had implemented some pretty kludgy solutions involving string replacement, but my views evolved over time and I strongly believe that there are numerous problems with this approach. Instead, I believe it makes more sense to use environment variables to make these changes between environments. Today, I want to spend some time talking about how to use environment variables in a web hosting context for a few different languages, and maybe get a little bit into the philosophy behind why this makes sense from a devops perspective.

Environment Variables for Apache

If you’re running Apache in your environment, setting up environment-specific parameters is fairly trivial. No matter what you’re doing behind the scenes, you can just use the SetEnv VARIABLE_NAME "value" directive in your virtual host file. I’ll get into how to reference the parameters later. Just remember, you want to have these in the individual virtual hosts, not the main Apache config - isolated from other virtual hosts.

Environment Variables for nginx

For nginx, this gets a little more challenging. nginx doesn’t work the same way as Apache, meaning we can’t just set an environment variable and have it work. Instead, we need to rely on other components to do the heavy lifting.

PHP-FPM

I assume that you’re already leveraging PHP-FPM (setup tutorial) in your environment. If not, best to start there. Once you have it set up, the actual process of setting environment variables is similar to Apache - the major difference is that we’re setting parameters using FPM itself. Hop into your FPM pool and add lines env[VARIABLE_NAME] = "value". Make sure clear_env = no is set.

Passenger

For languages that are not PHP, things become a little trickier. Languages like Node or Python don’t offer an easy, built-in way to serve web content using a traditional webserver. So, we need to leverage some more technology.

Enter Passenger. Passenger, in short, is a standalone app server that has a fairly straightforward process to allow it to integrate with nginx (or Apache) fairly easily. After setting up the Passenger repository and installing Passenger (the deployment walkthroughs are extremely straightforward and easy to follow, and customizable based on your technology stack), environment variables for your hosted application can be set using passenger_env_var VARIABLE_NAME value.

Leveraging Environment Variables in Code

So, at this point, we have our environment variables set. Now we need to actually go about using them in code. The idea here is that your standard config stuff normally found in code can be replaced fairly easily using the proper syntax in whatever language we’re working in. Brief examples for some major languages follow.

PHP

In PHP, you can use getenv(). So, for example (and assuming database_endpoint has been set as an environment variable:

// Before
$config['database_endpoint'] = 'db.mydomain.net:3306';

// After
$config['database_endpoint'] = getenv('database_endpoint');

Python

In Python, you can use os.environ. That means you’ll need to import os, but that should be the case already for most projects. To use the same example:

# Before
database_endpoint = "db.mydomain.net:3306"

# After
database_endpoint = os.environ['database_endpoint']

Node

Node also makes this easy; you can just use process.env. Again, with the same example:

// Before
var database_endpoint = 'db.mydomain.net:3306';

// After
var database_endpoint = process.env.database_endpoint;

What This All Means

So, I’ve explained how to leverage environment variables, but I have spent very little time on why you would want to. In fact, there are a number of reasons you’d want to move to this kind of a configuration!

First, it abstracts your configuration stuff out of your source files. If you’re hardcoding this stuff, then you are at the very least stuck keeping potentially sensitive stuff in source control, which you obviously want to avoid. Then, when you start getting into multiple environments, the problem compounds itself - how do you deploy to different regions with potentially different connection strings, error reporting settings, etc.? Without environment variables, you’re stuck with silly solutions like implementing string replacement in your CI pipeline. This doesn’t just add complexity to your build - it also means that the Git repository is no longer a true “source of truth”. You’re now modifying your files every time you deploy, introducing a potential additional point of failure.

By introducing environment variables, you drastically simplify the process. Instead of setting up exclusions for configuration files in your pipeline or implementing string replacements, you can just deploy new builds as-is directly from Git, and they can pick up their config from the environment. Whenever you need to change a parameter, since that’s an environment-specific thing, you just do it in that environment versus modifying your pipelines.

Environment variables don’t just streamline your CI processes. It also makes your pipelines more generalizable. If you’re writing a new project in the same framework, you don’t need to rewrite your CI pipeline - just copy your existing one over and all you’ll have to change is the source repository and the deployment destinations.

Additionally, in a lot of cases, it’s very easy to migrate your code to different platforms with environment variables. Heroku and AWS Lambda, for example, both offer a very easy interface to set environment variables right in the configuration of your function, so if you want to move to serverless there’s an easy path open to you.

I hope this has gone some distance towards showing the benefit of using environment variables in your project. If you have questions about implementation or want to suggest other process improvements, please don’t hesitate to reach out. I’m always happy to hear from you!