Self-Hosted Apps: Configuration Management With Secrets

I host a few small web projects using CapRover, a simple web-based container orchestration framework built upon Docker Services. For non-critical hobby apps, it is perfect. I simply run CapRover on a VPS with DigitalOcean and containerize all my running applications; forgoing all systemd setup and dependency installation.

As I was moving my applications into the Docker containers, I wanted to have a consistent configuration framework. One that was as simple as possible from a management perspective, not overkill for hobby apps and available for use in almost any Docker Service environment. This was my solution:

  1. Store configuration files in a local github repo that would be encrypted and backed up in multiple places (Cloud, Scheduled Backup, etc.) I didn’t want to push to a hosted git instance as there would be secrets included in the files.
  2. Pass base64-encoded configuration files as environment variables to the containers.

That’s it! Simple, easy to understand, and ergonomic enough. So I create a file:

{
  "db": "user:password@127.0.0.1:5000"
}

Then I produce the base64 encoding:

% base64 -i config.json
ewogICJkYiI6ICJ1c2VyOnBhc3N3b3JkQDEyNy4wLjAuMTo1MDAwIgp9Cg

Then in the CapRover UI (or wherever I choose to deploy) I create an ENV variable accessible to that application container:

APP_CONFIG=ewogICJkYiI6ICJ1c2VyOnBhc3N3b3JkQDEyNy4wLjAuMTo1MDAwIgp9Cg

This can be easily retrieved on startup code that looks like this (Go example):

func GetConfig() *Config {
    configStr, err := base64.StdEncoding.DecodeString(os.Getenv("APP_CONFIG"))
    if err != nil {
        return nil
    }

    var config Config
    err = json.Unmarshal([]byte(configStr), &config)
    if err != nil {
        return nil
    }

    return &config
}

For my purposes, this is good enough. Yes, the drawback is that I manually have to copy/paste the config values, but I don’t run enough apps to necessary warrant. This could potentially be automated.

The next stage would be to create an authenticated configservice that would provide the latest configuration file from the git repo when requested. I decided that the effort wasn’t worth it, and I didn’t want to add another runtime dependency to my hobby apps. By indulging in such things, I would end up in a world where my ‘simple’ app requires more and more external dependencies.

Related post