diff --git a/README.md b/README.md
index c1ce1e7afbf55a5c276424fd3423eeafd6b3d411..ddfead1dd2a9a620e0528006979249c8746526b8 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,7 @@ should show up in the network list on Riot and other clients.
 
 * For the bot to appear online on Discord you need to run the bridge itself.
 * ``npm start``
+* Particular configuration keys can be overridden by defining corresponding environment variables. For instance, `auth.botToken` can be set with `APPSERVICE_DISCORD_AUTH_BOT_TOKEN`.
 
 [Howto](./docs/howto.md)
 
diff --git a/src/config.ts b/src/config.ts
index 166604317accbb842d349afe1ca26eb5c4500247..2f0eb76241cfca553350e116ea39b9b45e72d171 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+const ENV_PREFIX = "APPSERVICE_DISCORD";
+const ENV_KEY_SEPARATOR = "_";
+const ENV_VAL_SEPARATOR = ",";
+
 /** Type annotations for config/config.schema.yaml */
 export class DiscordBridgeConfig {
     public bridge: DiscordBridgeConfigBridge = new DiscordBridgeConfigBridge();
@@ -27,18 +31,48 @@ export class DiscordBridgeConfig {
 
     /**
      * Apply a set of keys and values over the default config.
-     * @param _config Config keys
+     * @param newConfig Config keys
      * @param configLayer Private parameter
      */
     // tslint:disable-next-line no-any
-    public ApplyConfig(newConfig: {[key: string]: any}, configLayer: any = this) {
+    public applyConfig(newConfig: {[key: string]: any}, configLayer: {[key: string]: any} = this) {
           Object.keys(newConfig).forEach((key) => {
-            if ( typeof(configLayer[key]) === "object" &&
-                    !Array.isArray(configLayer[key])) {
-                this.ApplyConfig(newConfig[key], this[key]);
-                return;
+            if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) {
+                this.applyConfig(newConfig[key], configLayer[key]);
+            } else {
+                configLayer[key] = newConfig[key];
+            }
+        });
+    }
+
+    /**
+     * Override configuration keys defined in the supplied environment dictionary.
+     * @param environment environment variable dictionary
+     * @param path private parameter: config layer path determining the environment key prefix
+     * @param configLayer private parameter: current layer of configuration to alter recursively
+     */
+    public applyEnvironmentOverrides(
+        // tslint:disable-next-line no-any
+        environment: {[key: string]: any},
+        path: string[] = [ENV_PREFIX],
+        // tslint:disable-next-line no-any
+        configLayer: {[key: string]: any} = this,
+    ) {
+        Object.keys(configLayer).forEach((key) => {
+            // camelCase to THICK_SNAKE
+            const attributeKey = key.replace(/[A-Z]/g, (prefix) => `${ENV_KEY_SEPARATOR}${prefix}`).toUpperCase();
+            const attributePath = path.concat([attributeKey]);
+
+            if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) {
+                this.applyEnvironmentOverrides(environment, attributePath, configLayer[key]);
+            } else {
+                const lookupKey = attributePath.join(ENV_KEY_SEPARATOR);
+                if (lookupKey in environment) {
+                    configLayer[key] = (configLayer[key] instanceof Array)
+                        ? environment[lookupKey].split(ENV_VAL_SEPARATOR)
+                        : environment[lookupKey];
+                }
             }
-            configLayer[key] = newConfig[key];
         });
     }
 }
diff --git a/src/discordas.ts b/src/discordas.ts
index 0f4b5e11d6a46c238e1e259316d23b11e06c4da2..1f350c3819a8f6c70c924bbc1f289b7af6f92276 100644
--- a/src/discordas.ts
+++ b/src/discordas.ts
@@ -59,7 +59,8 @@ type callbackFn = (...args: any[]) => Promise<any>;
 
 async function run(port: number, fileConfig: DiscordBridgeConfig) {
     const config = new DiscordBridgeConfig();
-    config.ApplyConfig(fileConfig);
+    config.applyConfig(fileConfig);
+    config.applyEnvironmentOverrides(process.env);
     Log.Configure(config.logging);
     log.info("Starting Discord AS");
     const yamlConfig = yaml.safeLoad(fs.readFileSync(cli.opts.registrationPath, "utf8"));
diff --git a/test/test_config.ts b/test/test_config.ts
index 0979897b55ea157ff94e02386674097ab0516645..badb662a5c4a9929bfdfacb22c45faeef09a965d 100644
--- a/test/test_config.ts
+++ b/test/test_config.ts
@@ -22,10 +22,10 @@ import { DiscordBridgeConfig } from "../src/config";
 
 const expect = Chai.expect;
 
-describe("DiscordBridgeConfig.ApplyConfig", () => {
+describe("DiscordBridgeConfig.applyConfig", () => {
     it("should merge configs correctly", () => {
         const config = new DiscordBridgeConfig();
-        config.ApplyConfig({
+        config.applyConfig({
             bridge: {
                 disableDeletionForwarding: true,
                 disableDiscordMentions: false,
@@ -38,17 +38,40 @@ describe("DiscordBridgeConfig.ApplyConfig", () => {
                 console: "warn",
             },
         });
-        expect(config.bridge.homeserverUrl, "blah");
+        expect(config.bridge.homeserverUrl).to.equal("blah");
         expect(config.bridge.disableTypingNotifications).to.be.true;
         expect(config.bridge.disableDiscordMentions).to.be.false;
         expect(config.bridge.disableDeletionForwarding).to.be.true;
         expect(config.bridge.enableSelfServiceBridging).to.be.false;
         expect(config.bridge.disableJoinLeaveNotifications).to.be.true;
-        expect(config.logging.console, "warn");
+        expect(config.logging.console).to.equal("warn");
+    });
+    it("should merge environment overrides correctly", () => {
+        const config = new DiscordBridgeConfig();
+        config.applyConfig({
+            bridge: {
+                disableDeletionForwarding: true,
+                disableDiscordMentions: false,
+                homeserverUrl: "blah",
+            },
+            logging: {
+                console: "warn",
+            },
+        });
+        config.applyEnvironmentOverrides({
+            APPSERVICE_DISCORD_BRIDGE_DISABLE_DELETION_FORWARDING: false,
+            APPSERVICE_DISCORD_BRIDGE_DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
+            APPSERVICE_DISCORD_LOGGING_CONSOLE: "debug",
+        });
+        expect(config.bridge.disableJoinLeaveNotifications).to.be.true;
+        expect(config.bridge.disableDeletionForwarding).to.be.false;
+        expect(config.bridge.disableDiscordMentions).to.be.false;
+        expect(config.bridge.homeserverUrl).to.equal("blah");
+        expect(config.logging.console).to.equal("debug");
     });
     it("should merge logging.files correctly", () => {
         const config = new DiscordBridgeConfig();
-        config.ApplyConfig({
+        config.applyConfig({
             logging: {
                 console: "silent",
                 files: [
@@ -58,6 +81,6 @@ describe("DiscordBridgeConfig.ApplyConfig", () => {
                 ],
             },
         });
-        expect(config.logging.files[0].file, "./bacon.log");
+        expect(config.logging.files[0].file).to.equal("./bacon.log");
     });
 });
diff --git a/test/test_discordmessageprocessor.ts b/test/test_discordmessageprocessor.ts
index f78d66abded7dacaa4451b546796459cf7776e64..d66dd949e2cbe76da2780a1853e5acd6775ee4b6 100644
--- a/test/test_discordmessageprocessor.ts
+++ b/test/test_discordmessageprocessor.ts
@@ -58,8 +58,8 @@ describe("DiscordMessageProcessor", () => {
             msg.embeds = [];
             msg.content = "Hello World!";
             const result = await processor.FormatMessage(msg);
-            Chai.assert(result.body, "Hello World!");
-            Chai.assert(result.formattedBody, "Hello World!");
+            Chai.assert.equal(result.body, "Hello World!");
+            Chai.assert.equal(result.formattedBody, "Hello World!");
         });
         it("processes markdown messages correctly.", async () => {
             const processor = new DiscordMessageProcessor(
diff --git a/tools/chanfix.ts b/tools/chanfix.ts
index 8d4f4abf9b8510b3ef666184ee640170836fda11..a3f5095231f489dc78aea5c87d4ed20911b9d2f2 100644
--- a/tools/chanfix.ts
+++ b/tools/chanfix.ts
@@ -67,7 +67,8 @@ if (options.help) {
 const yamlConfig = yaml.safeLoad(fs.readFileSync("./discord-registration.yaml", "utf8"));
 const registration = AppServiceRegistration.fromObject(yamlConfig);
 const config = new DiscordBridgeConfig();
-config.ApplyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
+config.applyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
+config.applyEnvironmentOverrides(process.env);
 
 if (registration === null) {
     throw new Error("Failed to parse registration file");
diff --git a/tools/ghostfix.ts b/tools/ghostfix.ts
index 9ce61d6d6102b4bf61566a0379aaae92b085c884..b4e99e9dc95eaf4aab792f9ad39c4f99c2bc7588 100644
--- a/tools/ghostfix.ts
+++ b/tools/ghostfix.ts
@@ -76,7 +76,8 @@ if (options.help) {
 const yamlConfig = yaml.safeLoad(fs.readFileSync("./discord-registration.yaml", "utf8"));
 const registration = AppServiceRegistration.fromObject(yamlConfig);
 const config = new DiscordBridgeConfig();
-config.ApplyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
+config.applyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
+config.applyEnvironmentOverrides(process.env);
 
 if (registration === null) {
     throw new Error("Failed to parse registration file");