Using Docker env vars in .NET Core
Tweet Mon 06 February 2017One really good thing about packing your apps with Docker is that you're building once and deploying many times in many different places (environments), because of this it's a good idea to make your app configurable at the environment level (not just files). This means that you should avoid having config files in the container that can be different in each environment you run the container, "An app’s config is everything that is likely to vary between deploys" (The 12 Factor App).
But why environment variables?
The idea comes from the 12 factor app methodology (https://12factor.net/config), where it says that we should prefer using this type of configuration because having files can compromise sensitive information (security in Docker is not that mature yet because you can easily inspect the container and see all the environment variables, but that's an interesting talk for another time).
I'm not going to repeat what you already read in the previous link but I want to focus in the following: "In a twelve-factor app, env vars are granular controls, each fully orthogonal to other env vars. They are never grouped together as 'environments', but instead are independently managed for each deploy. This is a model that scales up smoothly as the app naturally expands into more deploys over its lifetime." (The 12 Factor App)
So, the idea is that we should always have our app prepared to behave accordingly in each environment, we should not have to re-compile our code just because we need to change some configuration, imagine doing that in all of the software you use in your computer.
That's good, but how can this be done in .NET Core?
I'm glad you ask. By this moment I hope you are convinced that env vars are a good option and lucky for us this is how Docker works, we just need to know how to achieve this behavior in .NET Core. For this, there's also good documentation here but in this post I will focus only on how to read env vars that comes from Docker.
.NET Core has a really good integration with env vars, actually the default template for a Web API use it as default, so we just need to make a few changes to our code in the Startup.cs class, create a new POCO class and use it in our Controllers by injecting the configuration we need.
Changes in the app
While I list the changes we need to make in the app, I'm going to give some explanation in each step. I have uploaded the code sample in Github, so in I couldn't explain myself enough you can see the source code here. Here's the list of steps:
- Make sure that in the Startup.cs class of your Web API project you have the line to add the env vars. This should be in the constructor of the class where the builder of the configuration is being defined. The builder definition should include the call "AddEnvironmentVariables()". Something like this:
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
... other method calls
.AddEnvironmentVariables();
- You need to create a POCO class where you define all the properties that you're interested to read from the environment. In my case I want to treat the profile name as an env var, so I created a class called EnvironmentConfig.cs and I included a public string property called "ProfileName" (his name should match with the env var, the framework makes reflection to fill the properties with env vars and because this is being done at startup, it won't affect performance when serving requests, this is also because in the container you will have only the environment variables you define when running the container).
public class EnvironmentConfig
{
public string ProfileName { get; set; }
}
- Add the new configuration class in the Startup.cs services definition. This means that in the method called "ConfigureServices" you should include this line:
services.Configure<EnvironmentConfig>(Configuration);
- Finally, we just use it in the Controller (or wherever you want) by injecting the configuration in the constructor of the class. I recommend having a local var in the class, then you need to specify in the constructor the injection of the configuration class we just defined, but it should be done in the .NET Core way by using the 'IOptions' interface. In my case I want to include it in the ValuesController.cs so my constructor looks like this:
private readonly EnvironmentConfig _configuration;
public ValuesController(IOptions<EnvironmentConfig> configuration)
{
_configuration = configuration.Value;
}
In case you don't see it, one of the cool things by doing it in this way is that if you don't specify an env var called "ProfileName" your app won't crash (unless you want/need to) because when you read the property of the configuration class it will be empty.
And that's all, if you want to deep dive in how configuration can be modified/used please read the following article here
How to test and debug this with VS?
I assume you already have added Docker support to your solution, if not take a look to this article. In your solution, open the docker-compose.override.yml file inside docker-compose section and in the environment list the ProfileName var, take a look:
Go and debug in Docker mode and the app should read that value from the container environment.
But what about Docker?
Yes, I know I almost forgot about this. But you know what? This is really simple, I'm going to put here the way I run my container so you can see how to define the env var that my .NET Core app will read.
docker run -e ProfileName='Christian' -d -p 9000:80 christianhxc/hellodocker:env
Now I need to go to: http://localhost:9000/api/values and boom! I see my name there. Of course, you don't need to specify all env vars in the run command, you can make use of docker compose to group them, but remember: don't include the any config file with the source code in version control.
You don't need to follow this guide to test it, just run the above command with different values :)