From 3773d659cc1d773920b7b66498b3f75165bbd192 Mon Sep 17 00:00:00 2001
From: Dragory <2606411+Dragory@users.noreply.github.com>
Date: Sun, 26 Jun 2022 14:34:54 +0300
Subject: [PATCH] Consolidate .env files. More work on dev containers.

---
 .env.example                                 | 25 +++++++---
 backend/src/api/auth.ts                      | 27 +++--------
 backend/src/api/index.ts                     |  4 +-
 backend/src/api/loadEnv.ts                   |  4 --
 backend/src/api/start.ts                     |  5 +-
 backend/src/data/Phisherman.ts               |  3 +-
 backend/src/env.ts                           | 44 ++++++++++++++++++
 backend/src/index.ts                         | 10 +---
 backend/src/loadEnv.ts                       |  4 --
 backend/src/plugins/Automod/actions/clean.ts | 17 +------
 backend/src/staff.ts                         |  4 +-
 backend/src/utils/crypt.ts                   | 13 ++----
 dashboard/webpack.config.js                  |  2 +-
 docker/development/devenv/Dockerfile         | 17 +++++--
 docker/development/docker-compose.yml        | 48 ++++++++++++--------
 docker/development/nginx/Dockerfile          | 10 ++--
 docker/development/nginx/default.conf        |  6 ++-
 17 files changed, 137 insertions(+), 106 deletions(-)
 delete mode 100644 backend/src/api/loadEnv.ts
 create mode 100644 backend/src/env.ts
 delete mode 100644 backend/src/loadEnv.ts

diff --git a/.env.example b/.env.example
index 917a8d65..8d8052b9 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,4 @@
-ENCRYPTION_KEY=32_character_encryption_key
+KEY=32_character_encryption_key
 
 CLIENT_ID=
 CLIENT_SECRET=
@@ -7,26 +7,37 @@ BOT_TOKEN=
 OAUTH_CALLBACK_URL=
 DASHBOARD_DOMAIN=
 API_DOMAIN=
-PORT=443
+API_PORT=3000
+
+#
+# DOCKER (DEVELOPMENT)
+#
+
+DOCKER_WEB_PORT=443
 
 # The MySQL database running in the container is exposed to the host on this port,
 # allowing access with database tools such as DBeaver
-MYSQL_PORT=3001
+DOCKER_MYSQL_PORT=3001
 # Password for the Zeppelin database user
-MYSQL_PASSWORD=
+DOCKER_MYSQL_PASSWORD=
 # Password for the MySQL root user
-MYSQL_ROOT_PASSWORD=
+DOCKER_MYSQL_ROOT_PASSWORD=
 
 # The development environment container has an SSH server that you can connect to.
 # This is the port that server is exposed to the host on.
-DEVELOPMENT_SSH_PORT=3002
+DOCKER_DEV_SSH_PORT=3002
+DOCKER_DEV_SSH_PASSWORD=password
 
 # Only required if relevant feature is used
 #PHISHERMAN_API_KEY=
 
+#
+# PRODUCTION
+#
+
 # In production, the newest code is pulled from a repository
 # Specify that repository URL here
-PRODUCTION_REPOSITORY=https://github.com/ZeppelinBot/Zeppelin.git
+#PRODUCTION_REPOSITORY=https://github.com/ZeppelinBot/Zeppelin.git
 
 # You only need to set these if you're running an external database.
 # In a standard setup, the database is run in a docker container.
diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts
index c1d19b09..09029bda 100644
--- a/backend/src/api/auth.ts
+++ b/backend/src/api/auth.ts
@@ -9,6 +9,7 @@ import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
 import { ApiUserInfo } from "../data/ApiUserInfo";
 import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
 import { ok } from "./responses";
+import { env } from "../env";
 
 interface IPassportApiUser {
   apiKey: string;
@@ -54,22 +55,6 @@ function simpleDiscordAPIRequest(bearerToken, path): Promise<any> {
 export function initAuth(app: express.Express) {
   app.use(passport.initialize());
 
-  if (!process.env.CLIENT_ID) {
-    throw new Error("Auth: CLIENT ID missing");
-  }
-
-  if (!process.env.CLIENT_SECRET) {
-    throw new Error("Auth: CLIENT SECRET missing");
-  }
-
-  if (!process.env.OAUTH_CALLBACK_URL) {
-    throw new Error("Auth: OAUTH CALLBACK URL missing");
-  }
-
-  if (!process.env.DASHBOARD_URL) {
-    throw new Error("DASHBOARD_URL missing!");
-  }
-
   passport.serializeUser((user, done) => done(null, user));
   passport.deserializeUser((user, done) => done(null, user));
 
@@ -101,9 +86,9 @@ export function initAuth(app: express.Express) {
       {
         authorizationURL: "https://discord.com/api/oauth2/authorize",
         tokenURL: "https://discord.com/api/oauth2/token",
-        clientID: process.env.CLIENT_ID,
-        clientSecret: process.env.CLIENT_SECRET,
-        callbackURL: process.env.OAUTH_CALLBACK_URL,
+        clientID: env.CLIENT_ID,
+        clientSecret: env.CLIENT_SECRET,
+        callbackURL: env.OAUTH_CALLBACK_URL,
         scope: ["identify"],
       },
       async (accessToken, refreshToken, profile, cb) => {
@@ -132,9 +117,9 @@ export function initAuth(app: express.Express) {
     passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
     (req: Request, res: Response) => {
       if (req.user && req.user.apiKey) {
-        res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
+        res.redirect(`https://${env.DASHBOARD_DOMAIN}/login-callback/?apiKey=${req.user.apiKey}`);
       } else {
-        res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?error=noAccess`);
+        res.redirect(`https://${env.DASHBOARD_DOMAIN}/login-callback/?error=noAccess`);
       }
     },
   );
diff --git a/backend/src/api/index.ts b/backend/src/api/index.ts
index cd2ae878..e8ed699a 100644
--- a/backend/src/api/index.ts
+++ b/backend/src/api/index.ts
@@ -1,8 +1,8 @@
 import { connect } from "../data/db";
 import { setIsAPI } from "../globals";
-import "./loadEnv";
+import { apiEnv } from "./loadApiEnv";
 
-if (!process.env.KEY) {
+if (!apiEnv.KEY) {
   // tslint:disable-next-line:no-console
   console.error("Project root .env with KEY is required!");
   process.exit(1);
diff --git a/backend/src/api/loadEnv.ts b/backend/src/api/loadEnv.ts
deleted file mode 100644
index 0bbc5063..00000000
--- a/backend/src/api/loadEnv.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import path from "path";
-
-require("dotenv").config({ path: path.resolve(process.cwd(), "../.env") });
-require("dotenv").config({ path: path.resolve(process.cwd(), "api.env") });
diff --git a/backend/src/api/start.ts b/backend/src/api/start.ts
index 8089dac0..27d982ab 100644
--- a/backend/src/api/start.ts
+++ b/backend/src/api/start.ts
@@ -8,12 +8,13 @@ import { initGuildsAPI } from "./guilds/index";
 import { clientError, error, notFound } from "./responses";
 import { startBackgroundTasks } from "./tasks";
 import multer from "multer";
+import { env } from "../env";
 
 const app = express();
 
 app.use(
   cors({
-    origin: process.env.DASHBOARD_URL,
+    origin: `https://${env.DASHBOARD_DOMAIN}`,
   }),
 );
 app.use(
@@ -48,7 +49,7 @@ app.use((req, res, next) => {
   return notFound(res);
 });
 
-const port = (process.env.PORT && parseInt(process.env.PORT, 10)) || 3000;
+const port = env.API_PORT;
 app.listen(port, "0.0.0.0", () => console.log(`API server listening on port ${port}`)); // tslint:disable-line
 
 startBackgroundTasks();
diff --git a/backend/src/data/Phisherman.ts b/backend/src/data/Phisherman.ts
index 9ab1ef00..2f7b90fc 100644
--- a/backend/src/data/Phisherman.ts
+++ b/backend/src/data/Phisherman.ts
@@ -6,9 +6,10 @@ import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
 import moment from "moment-timezone";
 import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
 import crypto from "crypto";
+import { env } from "../env";
 
 const API_URL = "https://api.phisherman.gg";
-const MASTER_API_KEY = process.env.PHISHERMAN_API_KEY;
+const MASTER_API_KEY = env.PHISHERMAN_API_KEY;
 
 let caughtDomainTrackingMap: Map<string, Map<string, number[]>> = new Map();
 
diff --git a/backend/src/env.ts b/backend/src/env.ts
new file mode 100644
index 00000000..4698d9b3
--- /dev/null
+++ b/backend/src/env.ts
@@ -0,0 +1,44 @@
+import path from "path";
+import fs from "fs";
+import dotenv from "dotenv";
+import { rootDir } from "./paths";
+import { z } from "zod";
+
+const envType = z.object({
+  KEY: z.string().length(32),
+
+  CLIENT_ID: z.string(),
+  CLIENT_SECRET: z.string(),
+  BOT_TOKEN: z.string(),
+
+  OAUTH_CALLBACK_URL: z.string().url(),
+  DASHBOARD_DOMAIN: z.string(),
+  API_DOMAIN: z.string(),
+
+  STAFF: z.preprocess((v) => String(v).split(","), z.array(z.string())).optional(),
+
+  PHISHERMAN_API_KEY: z.string().optional(),
+
+  API_PORT: z.number().min(1).max(65535),
+
+  DOCKER_MYSQL_PASSWORD: z.string().optional(), // Included here for the DB_PASSWORD default in development
+
+  DB_HOST: z.string().optional().default("mysql"),
+  DB_PORT: z.number().optional().default(3306),
+  DB_USER: z.string().optional().default("zeppelin"),
+  DB_PASSWORD: z.string().optional(), // Default is set to DOCKER_MYSQL_PASSWORD further below
+  DB_DATABASE: z.string().optional().default("zeppelin"),
+});
+
+let toValidate = {};
+const envPath = path.join(rootDir, "../.env");
+if (fs.existsSync(envPath)) {
+  const buf = fs.readFileSync(envPath);
+  toValidate = dotenv.parse(buf);
+}
+
+export const env = envType.parse(toValidate);
+
+if (env.DOCKER_MYSQL_PASSWORD && !env.DB_PASSWORD) {
+  env.DB_PASSWORD = env.DOCKER_MYSQL_PASSWORD;
+}
diff --git a/backend/src/index.ts b/backend/src/index.ts
index f5438991..31b2c0a8 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -10,7 +10,6 @@ import { connect } from "./data/db";
 import { GuildLogs } from "./data/GuildLogs";
 import { LogType } from "./data/LogType";
 import { DiscordJSError } from "./DiscordJSError";
-import "./loadEnv";
 import { logger } from "./logger";
 import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availablePlugins";
 import { RecoverablePluginError } from "./RecoverablePluginError";
@@ -37,12 +36,7 @@ import { runPhishermanCacheCleanupLoop, runPhishermanReportingLoop } from "./dat
 import { hasPhishermanMasterAPIKey } from "./data/Phisherman";
 import { consumeQueryStats } from "./data/queryLogger";
 import { EventEmitter } from "events";
-
-if (!process.env.KEY) {
-  // tslint:disable-next-line:no-console
-  console.error("Project root .env with KEY is required!");
-  process.exit(1);
-}
+import { env } from "./env";
 
 // Error handling
 let recentPluginErrors = 0;
@@ -413,5 +407,5 @@ connect().then(async () => {
   bot.initialize();
   logger.info("Bot Initialized");
   logger.info("Logging in...");
-  await client.login(process.env.TOKEN);
+  await client.login(env.BOT_TOKEN);
 });
diff --git a/backend/src/loadEnv.ts b/backend/src/loadEnv.ts
deleted file mode 100644
index d0991965..00000000
--- a/backend/src/loadEnv.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import path from "path";
-
-require("dotenv").config({ path: path.resolve(process.cwd(), "../.env") });
-require("dotenv").config({ path: path.resolve(process.cwd(), "bot.env") });
diff --git a/backend/src/plugins/Automod/actions/clean.ts b/backend/src/plugins/Automod/actions/clean.ts
index 56452d79..27ee1cb6 100644
--- a/backend/src/plugins/Automod/actions/clean.ts
+++ b/backend/src/plugins/Automod/actions/clean.ts
@@ -4,8 +4,6 @@ import { LogType } from "../../../data/LogType";
 import { noop } from "../../../utils";
 import { automodAction } from "../helpers";
 
-const cleanDebugServer = process.env.TEMP_CLEAN_DEBUG_SERVER;
-
 export const CleanAction = automodAction({
   configType: t.boolean,
   defaultConfig: false,
@@ -29,26 +27,13 @@ export const CleanAction = automodAction({
       }
     }
 
-    if (pluginData.guild.id === cleanDebugServer) {
-      const toDeleteFormatted = Array.from(messageIdsToDeleteByChannelId.entries())
-        .map(([channelId, messageIds]) => `- ${channelId}: ${messageIds.join(", ")}`)
-        .join("\n");
-      // tslint:disable-next-line:no-console
-      console.log(`[DEBUG] Cleaning messages (${ruleName}):\n${toDeleteFormatted}`);
-    }
-
     for (const [channelId, messageIds] of messageIdsToDeleteByChannelId.entries()) {
       for (const id of messageIds) {
         pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, id);
       }
 
       const channel = pluginData.guild.channels.cache.get(channelId as Snowflake) as TextChannel;
-      await channel.bulkDelete(messageIds as Snowflake[]).catch((err) => {
-        if (pluginData.guild.id === cleanDebugServer) {
-          // tslint:disable-next-line:no-console
-          console.error(`[DEBUG] Failed to bulk delete messages (${ruleName}): ${err}`);
-        }
-      });
+      await channel.bulkDelete(messageIds as Snowflake[]).catch(noop);
     }
   },
 });
diff --git a/backend/src/staff.ts b/backend/src/staff.ts
index 7c14f0b6..6950ee33 100644
--- a/backend/src/staff.ts
+++ b/backend/src/staff.ts
@@ -1,6 +1,8 @@
+import { env } from "./env";
+
 /**
  * Zeppelin staff have full access to the dashboard
  */
 export function isStaff(userId: string) {
-  return (process.env.STAFF ?? "").split(",").includes(userId);
+  return (env.STAFF ?? []).includes(userId);
 }
diff --git a/backend/src/utils/crypt.ts b/backend/src/utils/crypt.ts
index bba4c09a..d6208f1c 100644
--- a/backend/src/utils/crypt.ts
+++ b/backend/src/utils/crypt.ts
@@ -1,21 +1,14 @@
 import { spawn, Worker, Pool } from "threads";
-import "../loadEnv";
 import type { CryptFns } from "./cryptWorker";
 import { MINUTES } from "../utils";
+import { env } from "../env";
 
-if (!process.env.KEY) {
-  // tslint:disable-next-line:no-console
-  console.error("Environment value KEY required for encryption");
-  process.exit(1);
-}
-
-const KEY = process.env.KEY;
 const pool = Pool(() => spawn(new Worker("./cryptWorker"), { timeout: 10 * MINUTES }), 8);
 
 export async function encrypt(data: string) {
-  return pool.queue((w) => w.encrypt(data, KEY));
+  return pool.queue((w) => w.encrypt(data, env.KEY));
 }
 
 export async function decrypt(data: string) {
-  return pool.queue((w) => w.decrypt(data, KEY));
+  return pool.queue((w) => w.decrypt(data, env.KEY));
 }
diff --git a/dashboard/webpack.config.js b/dashboard/webpack.config.js
index e4b78765..7fb9f221 100644
--- a/dashboard/webpack.config.js
+++ b/dashboard/webpack.config.js
@@ -1,4 +1,4 @@
-require("dotenv").config();
+require("dotenv").config({ path: path.resolve(process.cwd(), "../.env") });
 
 const path = require("path");
 const VueLoaderPlugin = require("vue-loader/lib/plugin");
diff --git a/docker/development/devenv/Dockerfile b/docker/development/devenv/Dockerfile
index 95e3e128..ac278942 100644
--- a/docker/development/devenv/Dockerfile
+++ b/docker/development/devenv/Dockerfile
@@ -1,18 +1,27 @@
 FROM ubuntu:20.04
 
 ARG DOCKER_UID
+ARG DOCKER_DEV_SSH_PASSWORD
 
 ENV DEBIAN_FRONTEND=noninteractive
 ENV TZ=UTC
 
+# Set up some core packages
+RUN apt-get update
+RUN apt-get install -y sudo git curl
+
 # Set up SSH access
-RUN apt-get update && apt-get install -y openssh-server sudo git
+RUN apt-get install -y openssh-server iptables
 RUN mkdir /var/run/sshd
 RUN useradd -rm -d /home/ubuntu -s /bin/bash -g root -G sudo -u "${DOCKER_UID}" ubuntu
-RUN echo 'ubuntu:password' | chpasswd
+RUN echo "ubuntu:${DOCKER_DEV_SSH_PASSWORD}" | chpasswd
 
-# Install Node.js 16
+# Set up proper permissions for volumes
+RUN mkdir -p /home/ubuntu/zeppelin /home/ubuntu/.vscode-remote /home/ubuntu/.vscode-server /home/ubuntu/.cache/JetBrains
+RUN chown -R ubuntu /home/ubuntu
+
+# Install Node.js 16 and packages needed to build native packages
 RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
-RUN apt-get install -y nodejs
+RUN apt-get install -y nodejs gcc g++ make python3
 
 CMD /usr/sbin/sshd -D -e
diff --git a/docker/development/docker-compose.yml b/docker/development/docker-compose.yml
index c5721914..351cf21d 100644
--- a/docker/development/docker-compose.yml
+++ b/docker/development/docker-compose.yml
@@ -1,28 +1,33 @@
 version: '3'
+volumes:
+  mysql-data: {}
+  vscode-remote: {}
+  vscode-server: {}
+  jetbrains-data: {}
 services:
-#  nginx:
-#    user: "${UID:?Missing UID}:${GID:?Missing GID}"
-#    build:
-#      context: ./nginx
-#      args:
-#        API_DOMAIN: ${API_DOMAIN:?Missing API_DOMAIN}
-#        API_PORT: ${API_PORT:?Missing API_PORT}
-#        DASHBOARD_DOMAIN: ${DASHBOARD_DOMAIN:?Missing DASHBOARD_DOMAIN}
-#        DASHBOARD_PORT: ${DASHBOARD_PORT:?Missing DASHBOARD_PORT}
-#    ports:
-#      - ${PORT:?Missing PORT}:443
-#    volumes:
-#      - ./:/zeppelin
-#
+  nginx:
+    build:
+      context: ./nginx
+      args:
+        API_DOMAIN: ${API_DOMAIN:?Missing API_DOMAIN}
+        API_PORT: ${API_PORT:?Missing API_PORT}
+        DASHBOARD_DOMAIN: ${DASHBOARD_DOMAIN:?Missing DASHBOARD_DOMAIN}
+    ports:
+      - ${DOCKER_WEB_PORT:?Missing DOCKER_WEB_PORT}:443
+    volumes:
+      - ../../:/zeppelin
+
   mysql:
     image: mysql:8.0
     environment:
-      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD?:Missing MYSQL_ROOT_PASSWORD}
+      MYSQL_ROOT_PASSWORD: ${DOCKER_MYSQL_ROOT_PASSWORD?:Missing DOCKER_MYSQL_ROOT_PASSWORD}
       MYSQL_DATABASE: zeppelin
       MYSQL_USER: zeppelin
-      MYSQL_PASSWORD: ${MYSQL_PASSWORD?:Missing MYSQL_PASSWORD}
+      MYSQL_PASSWORD: ${DOCKER_MYSQL_PASSWORD?:Missing DOCKER_MYSQL_PASSWORD}
     ports:
-      - ${MYSQL_PORT:?Missing MYSQL_PORT}:3306
+      - ${DOCKER_MYSQL_PORT:?Missing DOCKER_MYSQL_PORT}:3306
+    volumes:
+      - mysql-data:/var/lib/mysql
 #
 #  backend:
 #    image: node:16
@@ -50,7 +55,12 @@ services:
       args:
         DOCKER_UID: ${DOCKER_UID:?Missing DOCKER_UID}
         DOCKER_GID: ${DOCKER_GID:?Missing DOCKER_GID}
+        DOCKER_DEV_SSH_PASSWORD: ${DOCKER_DEV_SSH_PASSWORD:?Missing DOCKER_DEV_SSH_PASSWORD}
     ports:
-      - "${DEVELOPMENT_SSH_PORT:?Missing DEVELOPMENT_SSH_PORT}:22"
+      - "${DOCKER_DEV_SSH_PORT:?Missing DOCKER_DEV_SSH_PORT}:22"
     volumes:
-      - ../../:/zeppelin
+      - ../../:/home/ubuntu/zeppelin
+      - ~/.ssh:/home/ubuntu/.ssh
+      - vscode-remote:/home/ubuntu/.vscode-remote
+      - vscode-server:/home/ubuntu/.vscode-server
+      - jetbrains-data:/home/ubuntu/.cache/JetBrains
diff --git a/docker/development/nginx/Dockerfile b/docker/development/nginx/Dockerfile
index 7058f8b0..2f63ba2f 100644
--- a/docker/development/nginx/Dockerfile
+++ b/docker/development/nginx/Dockerfile
@@ -2,11 +2,13 @@ FROM nginx
 
 ARG API_DOMAIN
 ARG DASHBOARD_DOMAIN
+ARG API_PORT
 
 RUN apt-get update && apt-get install -y openssl
-RUN openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/private/api-cert.key -out /etc/ssl/certs/api-cert.pem -days 365 -subj '/CN=*.${API_DOMAIN}' -nodes
-RUN openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/private/dashboard-cert.key -out /etc/ssl/certs/dashboard-cert.pem -days 365 -subj '/CN=*.${DASHBOARD_DOMAIN}' -nodes
+RUN openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/private/api-cert.key -out /etc/ssl/certs/api-cert.pem -days 3650 -subj '/CN=*.${API_DOMAIN}' -nodes
+RUN openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/private/dashboard-cert.key -out /etc/ssl/certs/dashboard-cert.pem -days 3650 -subj '/CN=*.${DASHBOARD_DOMAIN}' -nodes
 
 COPY ./default.conf /etc/nginx/conf.d/default.conf
-RUN sed -ir "s/_API_DOMAIN_/$(echo ${API_DOMAIN} | sed -ir 's///g')/g"
-RUN sed -ir "s/_DASHBOARD_DOMAIN_/$(echo ${DASHBOARD_DOMAIN} | sed 's/\./\\\\./g')/g"
+RUN sed -ir "s/_API_DOMAIN_/$(echo ${API_DOMAIN} | sed 's/\./\\./g')/g" /etc/nginx/conf.d/default.conf
+RUN sed -ir "s/_DASHBOARD_DOMAIN_/$(echo ${DASHBOARD_DOMAIN} | sed 's/\./\\./g')/g" /etc/nginx/conf.d/default.conf
+RUN sed -ir "s/_API_PORT_/${API_PORT}/g" /etc/nginx/conf.d/default.conf
diff --git a/docker/development/nginx/default.conf b/docker/development/nginx/default.conf
index 6aea2aeb..126df16b 100644
--- a/docker/development/nginx/default.conf
+++ b/docker/development/nginx/default.conf
@@ -4,7 +4,9 @@ server {
   server_name _API_DOMAIN_;
 
   location / {
-    proxy_pass backend:3000;
+    # Using a variable here stops nginx from crashing if the dev container is restarted or becomes otherwise unavailable
+    set $backend_upstream devenv;
+    proxy_pass http://$backend_upstream:_API_PORT_;
 
     client_max_body_size 200M;
   }
@@ -23,7 +25,7 @@ server {
 server {
   listen 443 ssl http2;
   listen [::]:443 ssl http2;
-  server_name dashboard.dev.zeppelin.gg;
+  server_name _DASHBOARD_DOMAIN_;
 
   root /zeppelin/dashboard/dist;