From 91f54424ed0262bde612dc26801763837ec4a74a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 16 Jul 2022 22:16:34 +0300 Subject: [PATCH] Early work on prod container --- .devcontainer/devcontainer.json | 2 +- .env.example | 12 ++-- backend/package.json | 4 +- backend/src/env.ts | 9 ++- ...pose.yml => docker-compose.development.yml | 1 + docker-compose.production.yml | 63 +++++++++++++++++++ docker/production/nginx/Dockerfile | 11 ++++ docker/production/nginx/default.conf | 39 ++++++++++++ 8 files changed, 132 insertions(+), 9 deletions(-) rename docker-compose.yml => docker-compose.development.yml (98%) create mode 100644 docker-compose.production.yml create mode 100644 docker/production/nginx/Dockerfile create mode 100644 docker/production/nginx/default.conf diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b44145a2..0d15eb29 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ { "name": "Zeppelin Development", - "dockerComposeFile": "../docker-compose.yml", + "dockerComposeFile": "../docker-compose.development.yml", "service": "devenv", "remoteUser": "ubuntu", diff --git a/.env.example b/.env.example index d98f2662..d2d2a2b2 100644 --- a/.env.example +++ b/.env.example @@ -38,12 +38,16 @@ DOCKER_DEV_SSH_PASSWORD=password #DOCKER_DEV_UID=1000 # -# PRODUCTION +# DOCKER (PRODUCTION) # -# In production, the newest code is pulled from a repository -# Specify that repository URL here -#PRODUCTION_REPOSITORY=https://github.com/ZeppelinBot/Zeppelin.git +DOCKER_PROD_DOMAIN= +DOCKER_PROD_WEB_PORT=443 +DOCKER_PROD_MYSQL_PORT=3001 +# Password for the Zeppelin database user +DOCKER_PROD_MYSQL_PASSWORD= +# Password for the MySQL root user +DOCKER_PROD_MYSQL_ROOT_PASSWORD= # 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/package.json b/backend/package.json index 4761ec2e..6be4713d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,9 +14,9 @@ "start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js", "watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"", "typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js", - "migrate-prod": "npm run typeorm -- migration:run", + "migrate-prod": "cross-env NODE_ENV=production npm run typeorm -- migration:run", "migrate-dev": "npm run build && npm run typeorm -- migration:run", - "migrate-rollback-prod": "npm run typeorm -- migration:revert", + "migrate-rollback-prod": "cross-env NODE_ENV=production npm run typeorm -- migration:revert", "migrate-rollback-dev": "npm run build && npm run typeorm -- migration:revert", "test": "npm run build && npm run run-tests", "run-tests": "ava", diff --git a/backend/src/env.ts b/backend/src/env.ts index e786dd3c..cd453f39 100644 --- a/backend/src/env.ts +++ b/backend/src/env.ts @@ -20,6 +20,7 @@ const envType = z.object({ PHISHERMAN_API_KEY: z.string().optional(), DOCKER_DEV_MYSQL_PASSWORD: z.string().optional(), // Included here for the DB_PASSWORD default in development + DOCKER_PROD_MYSQL_PASSWORD: z.string().optional(), // Included here for the DB_PASSWORD default in production DB_HOST: z.string().optional().default("mysql"), DB_PORT: z @@ -40,6 +41,10 @@ if (fs.existsSync(envPath)) { export const env = envType.parse(toValidate); -if (env.DOCKER_DEV_MYSQL_PASSWORD && !env.DB_PASSWORD) { - env.DB_PASSWORD = env.DOCKER_DEV_MYSQL_PASSWORD; +if (!env.DB_PASSWORD) { + if (process.env.NODE_ENV === "production" && env.DOCKER_PROD_MYSQL_PASSWORD) { + env.DB_PASSWORD = env.DOCKER_PROD_MYSQL_PASSWORD; + } else if (env.DOCKER_DEV_MYSQL_PASSWORD) { + env.DB_PASSWORD = env.DOCKER_DEV_MYSQL_PASSWORD; + } } diff --git a/docker-compose.yml b/docker-compose.development.yml similarity index 98% rename from docker-compose.yml rename to docker-compose.development.yml index 1c836bd2..4a28033e 100644 --- a/docker-compose.yml +++ b/docker-compose.development.yml @@ -1,4 +1,5 @@ version: '3' +name: zeppelin-dev volumes: mysql-data: {} vscode-remote: {} diff --git a/docker-compose.production.yml b/docker-compose.production.yml new file mode 100644 index 00000000..1b03bd00 --- /dev/null +++ b/docker-compose.production.yml @@ -0,0 +1,63 @@ +version: '3' +name: zeppelin-prod +volumes: + mysql-data: {} +services: + nginx: + build: + context: ./docker/production/nginx + args: + API_PORT: ${API_PORT:?Missing API_PORT} + DOCKER_PROD_DOMAIN: ${DOCKER_PROD_DOMAIN:?Missing DOCKER_PROD_DOMAIN} + ports: + - "${DOCKER_PROD_WEB_PORT:?Missing DOCKER_PROD_WEB_PORT}:443" + volumes: + - ./:/zeppelin + + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: ${DOCKER_PROD_MYSQL_ROOT_PASSWORD?:Missing DOCKER_PROD_MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: zeppelin + MYSQL_USER: zeppelin + MYSQL_PASSWORD: ${DOCKER_PROD_MYSQL_PASSWORD?:Missing DOCKER_PROD_MYSQL_PASSWORD} + ports: + - ${DOCKER_PROD_MYSQL_PORT:?Missing DOCKER_PROD_MYSQL_PORT}:3306 + volumes: + - mysql-data:/var/lib/mysql + command: --authentication-policy=mysql_native_password + + prepare_backend: + image: node:16.16 + depends_on: + - mysql + volumes: + - ./:/zeppelin + user: node + command: |- + bash -c "cd /zeppelin/backend && npm ci && npm run build && npm run migrate-prod" + + api: + image: node:16.16 + restart: on-failure + depends_on: + - prepare_backend + volumes: + - ./:/zeppelin + + # Wait for the build_backend container to finish before starting the bot + # See: https://github.com/docker/compose/issues/5007#issuecomment-335815508 + user: node + command: |- + bash -c ' \ + while ping -c1 prepare_backend &>/dev/null; do sleep 1; done; \ + cd /zeppelin/backend && npm run start-api-prod \ + ' + + build_dashboard: + image: node:16.16 + volumes: + - ./:/zeppelin + user: node + command: |- + bash -c "cd /zeppelin/dashboard && npm ci && npm run build" diff --git a/docker/production/nginx/Dockerfile b/docker/production/nginx/Dockerfile new file mode 100644 index 00000000..47df0a26 --- /dev/null +++ b/docker/production/nginx/Dockerfile @@ -0,0 +1,11 @@ +FROM nginx + +ARG API_PORT +ARG DOCKER_PROD_DOMAIN + +RUN apt-get update && apt-get install -y openssl +RUN openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/private/zeppelin-self-signed-cert.key -out /etc/ssl/certs/zeppelin-self-signed-cert.pem -days 3650 -subj "/CN=${DOCKER_PROD_DOMAIN}" -nodes + +COPY ./default.conf /etc/nginx/conf.d/default.conf +RUN sed -ir "s/_API_PORT_/${API_PORT}/g" /etc/nginx/conf.d/default.conf +RUN sed -ir "s/_DOCKER_PROD_DOMAIN_/${DOCKER_PROD_DOMAIN}/g" /etc/nginx/conf.d/default.conf diff --git a/docker/production/nginx/default.conf b/docker/production/nginx/default.conf new file mode 100644 index 00000000..2fcc93c0 --- /dev/null +++ b/docker/production/nginx/default.conf @@ -0,0 +1,39 @@ +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name _DOCKER_PROD_DOMAIN_; + + root /zeppelin/dashboard/dist; + + location / { + index index.html; + try_files $uri $uri/ /index.html; + } + + # Using a variable here stops nginx from crashing if the dev container is restarted or becomes otherwise unavailable + set $backend_upstream "http://api:_API_PORT_"; + + location /api { + # Remove /api/ from the beginning when passing the path to the API process + rewrite /api(/.*)$ $1 break; + + # Using a variable in proxy_pass also requires resolver to be set. + # This is the address of the internal docker compose DNS server. + resolver 127.0.0.11; + + proxy_pass $backend_upstream$uri$is_args$args; + proxy_redirect off; + + client_max_body_size 200M; + } + + ssl_certificate /etc/ssl/certs/zeppelin-self-signed-cert.pem; + ssl_certificate_key /etc/ssl/private/zeppelin-self-signed-cert.key; + + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; +}