From bd2ca7bb88a5f57b73fc44e9e90678f483cde6b1 Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Fri, 19 Jul 2024 17:51:48 -0600 Subject: [PATCH 01/12] Added app Dockerfile, minor changes to support it in package.json. Added some missing dependencies that appeared to be required. --- app/.dockerignore | 1 + app/Dockerfile | 63 ++++++++++++++++++++++++++++++++++++++++++++++ app/bun.lockb | Bin 321556 -> 323244 bytes app/package.json | 5 +++- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 app/.dockerignore create mode 100644 app/Dockerfile diff --git a/app/.dockerignore b/app/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/app/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..4c7c2a2 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,63 @@ +# from https://bun.sh/guides/ecosystem/docker, with modifications +# to run a hot-reloading development server + +# use the official Bun image +# see all versions at https://hub.docker.com/r/oven/bun/tags +FROM oven/bun:1 AS base +WORKDIR /app + + +# ----------------------------------------------------------- +# install dependencies for dev and prod into temp directories +# this will cache them and speed up future builds +FROM base AS install + +COPY package.json bun.lockb /temp/dev/ +RUN --mount=type=cache,target=/tmp/bun \ + cd /temp/dev/ && \ + bun install --cache-dir /tmp/bun --frozen-lockfile + +# install with --production (exclude devDependencies) +COPY package.json bun.lockb /temp/prod/ +RUN --mount=type=cache,target=/tmp/bun \ + cd /temp/prod && \ + bun install --cache-dir /tmp/bun --frozen-lockfile --production + + +# ----------------------------------------------------------- +# copy node_modules from temp directory +# then copy all (non-ignored) project files into the image +FROM base AS prerelease +COPY --from=install /temp/dev/node_modules node_modules +COPY . . + +# [optional] tests & build +ENV NODE_ENV=production +RUN bun test +RUN bun run build + + +# ----------------------------------------------------------- +# copy node_modules from dev stage, copy entire app +# source into the image +FROM base AS dev +COPY --from=install /temp/dev/node_modules node_modules +COPY . . +# run the app in hot-reloading development mode +# (this requires a new script, container-dev, in package.json that sets up vite +# to accept connections on any interface, e.g. from outside the container, and +# to always run on port 5713) +CMD ["bun", "run", "container-dev"] + + +# ----------------------------------------------------------- +# copy production dependencies and source code into final image +FROM base AS release +COPY --from=install /temp/prod/node_modules node_modules +COPY --from=prerelease /app/main.tsx . +COPY --from=prerelease /app/package.json . + +# run the app in production mode +USER bun +EXPOSE 3000/tcp +ENTRYPOINT [ "bun", "run", "main.ts" ] diff --git a/app/bun.lockb b/app/bun.lockb index 2a29046f9d83d5d24090d39e13cdfe88c4b193bd..c28634f010da14eabdd8c4c3764940ad08523702 100755 GIT binary patch delta 78020 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 delta 87211 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

(GlVCNZ53H%oRsfs@t0ys7%k>LJMis4w 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 diff --git a/app/package.json b/app/package.json index ecded39..5d3333a 100644 --- a/app/package.json +++ b/app/package.json @@ -3,6 +3,7 @@ "scripts": { "build": "vite build", "dev": "vite", + "container-dev": "vite --host --port 5713", "lint": "FIX=true ./lint.sh", "preview": "vite preview", "test": "bun run test:lint && bun run test:types", @@ -18,6 +19,7 @@ "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", + "@tanstack/react-query": "^5.36.2", "@tanstack/react-table": "^8.15.3", "classnames": "^2.5.1", "csv-stringify": "^6.4.6", @@ -34,7 +36,8 @@ "react-time-ago": "^7.3.1", "react-to-text": "^2.0.1", "react-use": "^17.5.0", - "use-debounce": "^10.0.0" + "use-debounce": "^10.0.0", + "use-query-params": "^2.2.1" }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.2.1", From b781d966be28c0fcb76aa8ce497782306ab5dcdb Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Fri, 19 Jul 2024 17:57:10 -0600 Subject: [PATCH 02/12] Adds docker-compose config, stack-runner script, dummy backend, folders where other services will be implemented. Updates README. --- .env.TEMPLATE | 5 ++ .gitignore | 1 + README.md | 44 +++++++++ backend/docker/Dockerfile | 42 +++++++++ backend/docker/install.R | 11 +++ backend/dummy.R | 7 ++ backend/entrypoint.R | 25 ++++++ backend/launch_api.sh | 4 + backend/server/tcp_utils.R | 41 +++++++++ cluster/README.md | 4 + docker-compose.override.yml | 36 ++++++++ docker-compose.prod.yml | 1 + docker-compose.yml | 32 +++++++ run_stack.sh | 174 ++++++++++++++++++++++++++++++++++++ services/postgres/README.md | 9 ++ 15 files changed, 436 insertions(+) create mode 100644 .env.TEMPLATE create mode 100644 .gitignore create mode 100644 backend/docker/Dockerfile create mode 100644 backend/docker/install.R create mode 100644 backend/dummy.R create mode 100644 backend/entrypoint.R create mode 100755 backend/launch_api.sh create mode 100644 backend/server/tcp_utils.R create mode 100644 cluster/README.md create mode 100644 docker-compose.override.yml create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100755 run_stack.sh create mode 100644 services/postgres/README.md diff --git a/.env.TEMPLATE b/.env.TEMPLATE new file mode 100644 index 0000000..ea25a60 --- /dev/null +++ b/.env.TEMPLATE @@ -0,0 +1,5 @@ +DEFAULT_ENV=dev + +POSTGRES_USER=molevolvr +POSTGRES_PASSWORD= +POSTGRES_DB=molevolvr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f10862a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.env diff --git a/README.md b/README.md index e69de29..6e426ae 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,44 @@ +# MolEvolvR Stack + +This repo contains the implementation of the MolEvolvR stack, i.e.: +- `app`: the frontend, written in Vue +- `backend`: a backend written in [Plumber](https://www.rplumber.io/index.html) +- `cluster`: the containerized SLURM "cluster" on which jobs are run +- `services`: a collection of services on which the stack relies: + - `postgres`: configuration for a PostgreSQL database, which stores job information + +Most of the data processing is accomplished via the `MolEvolvR` package, which +is currently available at https://github.com/JRaviLab/molevol_scripts. The stack +simply provides a user-friendly interface for accepting and monitoring the +progress of jobs, and orchestrates running the jobs on SLURM. The jobs +themselves call methods of the package at each stage of processing. + +## Running the Stack in Development + +To run the stack, you will need to have Docker and Docker Compose installed. + +First, copy `.env.TEMPLATE` to `.env` and fill in the necessary values. You +should supply a random password for the `POSTGRES_PASSWORD` variable. Of note +is the `DEFAULT_ENV` variable, which gives `run_stack.sh` a default environment +in which to operate; in development, this should be set to `dev`. + +Then, you can run the following command to bring up the stack: + +```bash +./run_stack.sh +``` + +This will start the stack in development mode, which automatically reloads the +backend or frontend when there are changes to their source. + +You should then be able to access the frontend at `http://localhost:5173`. + +## Production + +To run the stack in production, you can run the following + +```bash +./run_stack.sh prod +``` + +This will start the stack in production mode. diff --git a/backend/docker/Dockerfile b/backend/docker/Dockerfile new file mode 100644 index 0000000..c64ec02 --- /dev/null +++ b/backend/docker/Dockerfile @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1.7 + +# this Dockerfile should be used with the ./backend/ folder as the context +# and ./backend/docker/Dockerfile as the dockerfile + +FROM rocker/tidyverse:4.3 + +# install ccache, a compiler cache +RUN apt-get update && apt-get install -y ccache + +# install some common cmd line tools +RUN apt-get update && apt-get install -y curl + +# acquire drip, a plumber auto-reloader, and install +ENV DRIP_URL="https://rdrip.netlify.app/builds/drip_0.1.0_linux_amd64.zip" +RUN mkdir -p /tmp/software/ && \ + wget -L -O /tmp/software/drip.zip ${DRIP_URL} && \ + unzip /tmp/software/drip.zip -d /tmp/software && \ + mv /tmp/software/drip /usr/local/bin && \ + chmod +x /usr/local/bin/drip + +# acquire atlas, a schema manager +RUN curl -sSf https://atlasgo.sh | sh + +# configure ccache env vars +ENV PATH="/usr/lib/ccache:${PATH}" +ENV CCACHE_DIR="/tmp/ccache" + +# install dependencies into the image +COPY ./docker/install.R /tmp/install.r +RUN Rscript /tmp/install.r + +# RUN --mount=type=cache,target=/usr/local/lib/R/site-library \ +# Rscript /tmp/install.r + +WORKDIR /app + +# copy the app into the image +COPY . /app + +# run the app +CMD ["/app/launch_api.sh"] diff --git a/backend/docker/install.R b/backend/docker/install.R new file mode 100644 index 0000000..76c03d7 --- /dev/null +++ b/backend/docker/install.R @@ -0,0 +1,11 @@ +# install packages depended on by the molevolvr API server +install.packages( + c( + "plumber", # REST API framework + "DBI", # Database interface + "RPostgres", # PostgreSQL-specific impl. for DBI + "dbplyr", # dplyr for databases + "box" # allows R files to be referenced as modules + ), + Ncpus = 6 +) diff --git a/backend/dummy.R b/backend/dummy.R new file mode 100644 index 0000000..151297f --- /dev/null +++ b/backend/dummy.R @@ -0,0 +1,7 @@ +# Load the plumber package +library(plumber) + +#* @get / +function() { + "Hello, world!" +} diff --git a/backend/entrypoint.R b/backend/entrypoint.R new file mode 100644 index 0000000..8530e69 --- /dev/null +++ b/backend/entrypoint.R @@ -0,0 +1,25 @@ +options(box.path = "/app") + +box::use( + plumber[plumb], + server/tcp_utils[wait_for_port] +) + +# receive the target port as the env var API_PORT, or 9050 if unspecified +target_port <- as.integer(Sys.getenv("API_PORT", unset=9050)) + +# workaround for https://github.com/siegerts/drip/issues/3, in which +# reloading fails due to the port being in use. we just wait, polling +# occasionally, for up to 60 seconds for the port to become free. +if (wait_for_port(target_port, poll_interval = 1, verbose = TRUE)) { + pr <- plumb("./dummy.R")$run( + host="0.0.0.0", + port=target_port, + debug=TRUE + ) +} +else { + stop( + paste0("Failed to start the API server; port ", target_port, " still occupied after wait timeout exceeded" + ) +} diff --git a/backend/launch_api.sh b/backend/launch_api.sh new file mode 100755 index 0000000..9560958 --- /dev/null +++ b/backend/launch_api.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# pass off to drip to control serving and reloading the API +drip diff --git a/backend/server/tcp_utils.R b/backend/server/tcp_utils.R new file mode 100644 index 0000000..4b27dfd --- /dev/null +++ b/backend/server/tcp_utils.R @@ -0,0 +1,41 @@ +#' Utility functions for working with TCP ports + +#' Check if a port is in use +#' @param port The port to check +#' @param host The IP for which to check the port +#' @return TRUE if the port is in use, FALSE otherwise +is_port_in_use <- function(port, host = "127.0.0.1") { + connection <- try(suppressWarnings(socketConnection(host = host, port = port, timeout = 1, open = "r+")), silent = TRUE) + if (inherits(connection, "try-error")) { + return(FALSE) # Port is not in use + } else { + close(connection) + return(TRUE) # Port is in use + } +} + +#' Wait for a port to become free +#' @param port The port to wait for +#' @param timeout The maximum time to wait in seconds +#' @param poll_interval The interval between checks in seconds +#' @param host The IP for which to check the port +#' @param verbose Whether to print messages to the console +#' @return TRUE if the port is free, FALSE if the timeout is reached +wait_for_port <- function(port, timeout = 60, poll_interval = 5, host = "127.0.0.1", verbose = TRUE) { + start_time <- Sys.time() + end_time <- start_time + timeout + + while (Sys.time() < end_time) { + if (!is_port_in_use(port, host)) { + if (verbose) { cat("Port", port, "is now free\n") } + return(TRUE) + } + if (verbose) { cat("Port", port, "is in use. Checking again in", poll_interval, "seconds...\n") } + Sys.sleep(poll_interval) + } + + if (verbose) { + cat(paste0("Timeout of ", timeout, "s reached, but port ", port, " is still in use, aborting\n")) + } + return(FALSE) +} diff --git a/cluster/README.md b/cluster/README.md new file mode 100644 index 0000000..c67e635 --- /dev/null +++ b/cluster/README.md @@ -0,0 +1,4 @@ +# MolEvolvR Cluster + +This folder will eventually contain Dockerfiles for building images for a SLURM +controller and worker nodes. diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..1ab4de6 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,36 @@ +services: + backend: + volumes: + - ./backend/:/app/ + # - ./backend/api/:/app/api/ + # - ./backend/schema/:/app/schema/ + # - ./backend/entrypoint.R:/app/entrypoint.R + # - ./backend/run_tests.sh:/app/run_tests.sh + ports: + - "9050:9050" + environment: + - "POSTGRES_DEV_HOST=dev-db" + - "PLUMBER_DEBUG=1" + depends_on: + - "dev-db" + + app: + build: + context: ./app + target: dev + volumes: + - ./app/src:/app/src + environment: + - 'VITE_API=http://localhost:9050' + ports: + - "5713:5713" + + db: + ports: + - "5460:5432" + + # used by atlas to create migrations + dev-db: + image: postgres:16 + env_file: + - .env diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..25e9fb3 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1 @@ +# currently empty, but will add production services \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6b28837 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +services: + backend: + image: molevolvr-backend + platform: linux/amd64 + build: + context: ./backend + dockerfile: ./docker/Dockerfile + env_file: + - .env + environment: + - API_PORT=9050 + - "POSTGRES_HOST=db" + depends_on: + db: + condition: service_healthy + + app: + image: molevolvr-frontend + build: ./app + depends_on: + - backend + + db: + image: postgres:16 + env_file: + - .env + healthcheck: + test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'"] + interval: 30s + timeout: 60s + retries: 5 + start_period: 80s diff --git a/run_stack.sh b/run_stack.sh new file mode 100755 index 0000000..f45fcbf --- /dev/null +++ b/run_stack.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash + +# NOTES: +# ------- +# This script launches the molevolvr stack in the specified target environment. +# It's invoked as ./run_stack [target_env] [docker-compose args]; if +# [target_env] is not specified, it will attempt to infer it from the repo's +# directory name, and aborts with an error message if it doesn't find a match. +# the remainder of the arguments are passed along to docker compose. +# +# for example, to launch the stack in the "prod" environment with the "up -d" +# command, you would run: ./run_stack prod up -d +# +# the available environments differ in a variety of ways, including: +# - which services they run (prod runs 'nginx', for example, but the dev-y envs +# don't) +# - cores and memory constraints that are applied to the SLURM containers, in +# environments where the job scheduler is enabled +# - what external resources they mount as volumes into the container; for +# example, each environment mounts a different job results folder, but +# environments that process jobs use the same blast and iprscan folders, since +# they're gigantic +# +# these differences between environments are implemented by invoking different +# sets of docker-compose.yml files. with the exception of the "app" environment, +# the "root" compose file, docker-compose.yml, is always used first, and then +# depending on the environment other compose files are added in, which merge +# with the root compose configuration. since the app environment only runs the +# app, it has a separate compose file, docker-compose.apponly.yml, rather than +# merging with the root and killing nearly all the services except the app +# service. +# +# see the following for details on the semantics of merging compose files: +# https://docs.docker.com/compose/multiple-compose-files/merge/ +# +# the current environments are as follows (contact FSA for details): +# - prod: the production environment, which runs the full stack, including the +# shiny app, the job scheduler, and the accounting database. it's the most +# resource-intensive environment, and is intended for use in production. +# - dev/staging: these are effectively dev environments that specific users run +# on the server for testing purposes. +# - app: a development environment that runs only the frontend and backend, and +# not the job scheduler or the accounting database. it's intended for use in +# frontend development, where you don't need to submit jobs or query the +# accounting database. + + +# if 1, skips invoking ./build_images.sh before running the stack +SKIP_BUILD=${SKIP_BUILD:-0} + +# command to run after the stack has launched, e.g. +# in cases where you want to tail some containers after launch +# (by default, it does nothing) +POST_LAUNCH_CMD=":" +# if 1, clears the screen before running the post-launch command +DO_CLEAR="0" + +# helper function to print a message and exit with a specific code +# in one command +function fatal() { + echo "${1:-fatal error, aborting}" + exit ${2:-1} +} + +# =========================================================================== +# === entrypoint +# =========================================================================== + +# source the .env file and export its contents +# among other things, we'll use the DEFAULT_ENV var in it to set the target env +set -a +source .env +set +a + +# check if the first argument is a valid target env, and if not attempt +# to infer it from the script's parents' directory name +case $1 in + "prod"|"staging"|"dev"|"app") + TARGET_ENV=$1 + shift + echo "* Selected target environment: ${TARGET_ENV}" + ;; + *) + # attempt to resolve the target env from the host environment + # (e.g., the hostname, possibly the repo root directory name, etc.) + + # get the name of the script's parent directory + PARENT_DIR=$(basename $(dirname $(realpath $0))) + HOSTNAME=$(hostname) + + # check if the parent directory name contains a valid target env + if [[ "${HOSTNAME}" = "jravilab" ]]; then + TARGET_ENV="prod" + STRATEGY="via hostname ${HOSTNAME}" + elif [[ ! -z "${DEFAULT_ENV}" ]]; then + TARGET_ENV="${DEFAULT_ENV}" + STRATEGY="via DEFAULT_ENV" + else + echo -e \ + "ERROR: No valid target env specified, and could not infer" \ + "target environment from parent directory name:\n${PARENT_DIR}" + exit 1 + fi + + echo "* Inferred target environment: ${TARGET_ENV} (${STRATEGY:-n/a})" +esac + +case ${TARGET_ENV} in + "prod") + DEFAULT_ARGS="up -d" + COMPOSE_CMD="docker compose -f docker-compose.yml -f docker-compose.prod.yml" + DO_CLEAR="1" + # watch the logs after, since we detached after bringing up the stack + POST_LAUNCH_CMD="docker compose logs -f" + ;; + "dev") + DEFAULT_ARGS="up -d" + COMPOSE_CMD="docker compose -f docker-compose.yml -f docker-compose.override.yml" + DO_CLEAR="1" + # watch the logs after, since we detached after bringing up the stack + POST_LAUNCH_CMD="${COMPOSE_CMD} logs -f" + ;; + "app") + # launches just the services necessary to run the shiny app, for frontend development. + # note that you won't be able to submit jobs or query the accounting database. + DEFAULT_ARGS="up" + COMPOSE_CMD="docker compose -f docker-compose.apponly.yml" + DO_CLEAR="1" + SKIP_BUILD="1" # don't build images for the app environment, since it uses so few of them + # watch the logs after, since we detached after bringing up the stack + # POST_LAUNCH_CMD="${COMPOSE_CMD} logs -f app" + ;; + *) + echo "ERROR: Unknown target environment: ${TARGET_ENV}" + exit 1 +esac + +# ensure that docker compose can see the target env, so it can, e.g., namespace hosts to their environment +export TARGET_ENV=${TARGET_ENV} + +# if any arguments were specified after the target env, use those instead of the default +if [ $# -gt 0 ]; then + DEFAULT_ARGS="$@" + DO_CLEAR="0" # don't clear so we can see the output +fi + +# check if a "control" command is the current first argument; if so, skip the build +if [[ "$1" =~ ^(down|restart|logs)$ ]]; then + echo "* Skipping build, since we're running a control command: $1" + SKIP_BUILD=1 + # also skip the post-launch command so we don't get stuck, e.g., tailing + POST_LAUNCH_CMD="" +fi + +# if SKIP_BUILD is 0 and 'down' isn't the docker compose command, build images +# for the target env. +# each built image is tagged with its target env, so they don't collide with +# each other; in the case of prod, the tag is "latest". +if [ "${SKIP_BUILD}" -eq 0 ]; then + if [ "${TARGET_ENV}" == "prod" ] || [ "${TARGET_ENV}" == "app" ]; then + IMAGE_TAG="latest" + else + IMAGE_TAG="${TARGET_ENV}" + fi + + echo "* Building images for ${TARGET_ENV} (tag: ${IMAGE_TAG})" + # ./build_images.sh ${IMAGE_TAG} || fatal "Failed to build images for ${TARGET_ENV}" + ${COMPOSE_CMD} build || fatal "Failed to build images for ${TARGET_ENV}" +fi + +echo "Running: ${COMPOSE_CMD} ${DEFAULT_ARGS}" +${COMPOSE_CMD} ${DEFAULT_ARGS} && \ +( [[ ${DO_CLEAR} = "1" ]] && clear || exit 0 ) && \ +${POST_LAUNCH_CMD} diff --git a/services/postgres/README.md b/services/postgres/README.md new file mode 100644 index 0000000..3cbf910 --- /dev/null +++ b/services/postgres/README.md @@ -0,0 +1,9 @@ +# PostgreSQL Configuration + +This folder will eventually contain configuration for the PostgreSQL instance +that runs within the MolEvolvR stack. + +The instance is responsible for: +- keeping records of analysis submissions +- tracking job status between the backend and SLURM controller +- recording any artifacts that aren't better stored on the filesystem. From 61cad30398e783831f1aa8940c1f0bbb65aacd66 Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 10:14:03 -0600 Subject: [PATCH 03/12] Update README.md Co-authored-by: Vincent Rubinetti --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e426ae..5ebbc8a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MolEvolvR Stack This repo contains the implementation of the MolEvolvR stack, i.e.: -- `app`: the frontend, written in Vue +- `app`: the frontend webapp, written in React - `backend`: a backend written in [Plumber](https://www.rplumber.io/index.html) - `cluster`: the containerized SLURM "cluster" on which jobs are run - `services`: a collection of services on which the stack relies: From 5c04aeea9c0ff7a3973048a7e5413d04057283b8 Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 10:30:37 -0600 Subject: [PATCH 04/12] Update README.md Co-authored-by: Vincent Rubinetti --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ebbc8a..3e0d0b3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ themselves call methods of the package at each stage of processing. ## Running the Stack in Development -To run the stack, you will need to have Docker and Docker Compose installed. +To run the stack, you'll need to [install Docker and Docker Compose](https://www.docker.com/). First, copy `.env.TEMPLATE` to `.env` and fill in the necessary values. You should supply a random password for the `POSTGRES_PASSWORD` variable. Of note From 17e193ec878af668472e469547320c9239c6e7c3 Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 13:34:24 -0600 Subject: [PATCH 05/12] Simplified stages, switched to invoking vite directly --- app/Dockerfile | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index 4c7c2a2..7336d66 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -9,33 +9,16 @@ WORKDIR /app # ----------------------------------------------------------- # install dependencies for dev and prod into temp directories -# this will cache them and speed up future builds FROM base AS install COPY package.json bun.lockb /temp/dev/ -RUN --mount=type=cache,target=/tmp/bun \ - cd /temp/dev/ && \ - bun install --cache-dir /tmp/bun --frozen-lockfile +RUN cd /temp/dev/ && \ + bun install --frozen-lockfile # install with --production (exclude devDependencies) COPY package.json bun.lockb /temp/prod/ -RUN --mount=type=cache,target=/tmp/bun \ - cd /temp/prod && \ - bun install --cache-dir /tmp/bun --frozen-lockfile --production - - -# ----------------------------------------------------------- -# copy node_modules from temp directory -# then copy all (non-ignored) project files into the image -FROM base AS prerelease -COPY --from=install /temp/dev/node_modules node_modules -COPY . . - -# [optional] tests & build -ENV NODE_ENV=production -RUN bun test -RUN bun run build - +RUN cd /temp/prod && \ + bun install --frozen-lockfile --production # ----------------------------------------------------------- # copy node_modules from dev stage, copy entire app @@ -47,7 +30,7 @@ COPY . . # (this requires a new script, container-dev, in package.json that sets up vite # to accept connections on any interface, e.g. from outside the container, and # to always run on port 5713) -CMD ["bun", "run", "container-dev"] +CMD [ "vite", "dev", "--host", "--port", "5713" ] # ----------------------------------------------------------- @@ -57,7 +40,7 @@ COPY --from=install /temp/prod/node_modules node_modules COPY --from=prerelease /app/main.tsx . COPY --from=prerelease /app/package.json . -# run the app in production mode -USER bun -EXPOSE 3000/tcp -ENTRYPOINT [ "bun", "run", "main.ts" ] +# produce a bundle that'll then be served via a reverse http proxy, e.g. nginx +# (you'll want /app/dist to be mapped to a volume that's served by the reverse +# http proxy) +CMD [ "vite", "build" ] From 516e1beb8dde3068bc1c7a18981b5785999ae533 Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 13:35:31 -0600 Subject: [PATCH 06/12] Adds support for opening browser to frontend on normal stack launch --- .env.TEMPLATE | 3 +++ run_stack.sh | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/.env.TEMPLATE b/.env.TEMPLATE index ea25a60..513fb17 100644 --- a/.env.TEMPLATE +++ b/.env.TEMPLATE @@ -1,5 +1,8 @@ DEFAULT_ENV=dev +# if 0, doesn't open a browser to the frontend webapp on a normal stack launch +DO_OPEN_BROWSER=1 + POSTGRES_USER=molevolvr POSTGRES_PASSWORD= POSTGRES_DB=molevolvr diff --git a/run_stack.sh b/run_stack.sh index f45fcbf..845bd50 100755 --- a/run_stack.sh +++ b/run_stack.sh @@ -54,6 +54,8 @@ SKIP_BUILD=${SKIP_BUILD:-0} POST_LAUNCH_CMD=":" # if 1, clears the screen before running the post-launch command DO_CLEAR="0" +# if 1, opens the browser window to the app after launching the stack +DO_OPEN_BROWSER=${DO_OPEN_BROWSER:-1} # helper function to print a message and exit with a specific code # in one command @@ -62,6 +64,19 @@ function fatal() { exit ${2:-1} } +# cross-platform helper function to open a browser window +function open_browser() { + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + xdg-open "$1" + elif [[ "$OSTYPE" == "darwin"* ]]; then + open "$1" + elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + start "$1" + else + echo "WARNING: Unsupported OS: $OSTYPE, unable to open browser" + fi +} + # =========================================================================== # === entrypoint # =========================================================================== @@ -150,6 +165,8 @@ if [[ "$1" =~ ^(down|restart|logs)$ ]]; then SKIP_BUILD=1 # also skip the post-launch command so we don't get stuck, e.g., tailing POST_LAUNCH_CMD="" + # also skip opening a browser window + DO_OPEN_BROWSER=0 fi # if SKIP_BUILD is 0 and 'down' isn't the docker compose command, build images @@ -171,4 +188,8 @@ fi echo "Running: ${COMPOSE_CMD} ${DEFAULT_ARGS}" ${COMPOSE_CMD} ${DEFAULT_ARGS} && \ ( [[ ${DO_CLEAR} = "1" ]] && clear || exit 0 ) && \ +( + [[ ${DO_OPEN_BROWSER} = "1" ]] && \ + open_browser "http://localhost:5713" +) && ${POST_LAUNCH_CMD} From 99f4dbcd3d4724f145b8fed120d9e5072643444d Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 13:37:39 -0600 Subject: [PATCH 07/12] Removed container-dev script, fixed vite dev server invocation in Dockerfile --- app/Dockerfile | 3 ++- app/package.json | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index 7336d66..08ddf18 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -30,7 +30,7 @@ COPY . . # (this requires a new script, container-dev, in package.json that sets up vite # to accept connections on any interface, e.g. from outside the container, and # to always run on port 5713) -CMD [ "vite", "dev", "--host", "--port", "5713" ] +CMD [ "vite", "--host", "--port", "5713" ] # ----------------------------------------------------------- @@ -44,3 +44,4 @@ COPY --from=prerelease /app/package.json . # (you'll want /app/dist to be mapped to a volume that's served by the reverse # http proxy) CMD [ "vite", "build" ] + \ No newline at end of file diff --git a/app/package.json b/app/package.json index 5d3333a..e4c1cd7 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,6 @@ "scripts": { "build": "vite build", "dev": "vite", - "container-dev": "vite --host --port 5713", "lint": "FIX=true ./lint.sh", "preview": "vite preview", "test": "bun run test:lint && bun run test:types", From f2a86674247eef43e05ae94e68ab746dcfef9da5 Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 14:41:45 -0600 Subject: [PATCH 08/12] Update app/Dockerfile Co-authored-by: Vincent Rubinetti --- app/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Dockerfile b/app/Dockerfile index 08ddf18..0296603 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -27,7 +27,7 @@ FROM base AS dev COPY --from=install /temp/dev/node_modules node_modules COPY . . # run the app in hot-reloading development mode -# (this requires a new script, container-dev, in package.json that sets up vite +# set up vite # to accept connections on any interface, e.g. from outside the container, and # to always run on port 5713) CMD [ "vite", "--host", "--port", "5713" ] From 2df63b992850fce64c6fe44207ae5c84cd8be1ab Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 14:43:17 -0600 Subject: [PATCH 09/12] Copies in entire app for 'release' target --- app/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index 0296603..66b81d0 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -37,8 +37,7 @@ CMD [ "vite", "--host", "--port", "5713" ] # copy production dependencies and source code into final image FROM base AS release COPY --from=install /temp/prod/node_modules node_modules -COPY --from=prerelease /app/main.tsx . -COPY --from=prerelease /app/package.json . +COPY . . # produce a bundle that'll then be served via a reverse http proxy, e.g. nginx # (you'll want /app/dist to be mapped to a volume that's served by the reverse From 471f14b234c214588791899f93414e2c501a238b Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 14:43:50 -0600 Subject: [PATCH 10/12] Minor tweak to comment following VR's suggestion --- app/Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index 66b81d0..346625e 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -27,9 +27,8 @@ FROM base AS dev COPY --from=install /temp/dev/node_modules node_modules COPY . . # run the app in hot-reloading development mode -# set up vite -# to accept connections on any interface, e.g. from outside the container, and -# to always run on port 5713) +# set up vite to accept connections on any interface, e.g. from outside the +# container, and to always run on port 5713) CMD [ "vite", "--host", "--port", "5713" ] From 881100dfaff413bebd94a00c28c0e8e567aa619e Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 15:05:32 -0600 Subject: [PATCH 11/12] Adds WIP "prod" config. Factors frontend URL into var, fixes abort when DO_OPEN_BROWSER=0. --- docker-compose.prod.yml | 29 ++++++++++++++++++++++++++++- run_stack.sh | 14 ++++++++++---- services/caddy/Caddyfile | 5 +++++ 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 services/caddy/Caddyfile diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 25e9fb3..600f84e 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1 +1,28 @@ -# currently empty, but will add production services \ No newline at end of file +volumes: + app_bundle: + caddy_data: + caddy_config: + +services: + app: + image: molevolvr-frontend + build: + context: ./app + target: release + volumes: + - app_bundle:/app/dist + depends_on: + - backend + + caddy: + image: caddy:2 + ports: + - "80:80" + - "443:443" + volumes: + - app_bundle:/srv + - ./services/caddy/Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + depends_on: + - app diff --git a/run_stack.sh b/run_stack.sh index 845bd50..d17d3df 100755 --- a/run_stack.sh +++ b/run_stack.sh @@ -57,6 +57,9 @@ DO_CLEAR="0" # if 1, opens the browser window to the app after launching the stack DO_OPEN_BROWSER=${DO_OPEN_BROWSER:-1} +# the URL to open when we invoke the browser +FRONTEND_URL=${FRONTEND_URL:-"http://localhost:5713"} + # helper function to print a message and exit with a specific code # in one command function fatal() { @@ -124,9 +127,11 @@ case ${TARGET_ENV} in "prod") DEFAULT_ARGS="up -d" COMPOSE_CMD="docker compose -f docker-compose.yml -f docker-compose.prod.yml" - DO_CLEAR="1" + DO_CLEAR="0" + # never launch the browser in production + DO_OPEN_BROWSER=0 # watch the logs after, since we detached after bringing up the stack - POST_LAUNCH_CMD="docker compose logs -f" + POST_LAUNCH_CMD="${COMPOSE_CMD} logs -f" ;; "dev") DEFAULT_ARGS="up -d" @@ -189,7 +194,8 @@ echo "Running: ${COMPOSE_CMD} ${DEFAULT_ARGS}" ${COMPOSE_CMD} ${DEFAULT_ARGS} && \ ( [[ ${DO_CLEAR} = "1" ]] && clear || exit 0 ) && \ ( - [[ ${DO_OPEN_BROWSER} = "1" ]] && \ - open_browser "http://localhost:5713" + [[ ${DO_OPEN_BROWSER} = "1" ]] \ + && open_browser "${FRONTEND_URL}" \ + || exit 0 ) && ${POST_LAUNCH_CMD} diff --git a/services/caddy/Caddyfile b/services/caddy/Caddyfile new file mode 100644 index 0000000..b183e66 --- /dev/null +++ b/services/caddy/Caddyfile @@ -0,0 +1,5 @@ +# serve /srv +:80 { + root * /srv + file_server +} From 44e23c9379a7c36e208b30d03c8a72c01bf6344c Mon Sep 17 00:00:00 2001 From: Faisal Alquaddoomi Date: Tue, 30 Jul 2024 15:06:02 -0600 Subject: [PATCH 12/12] app/Dockerfile: removes prod pkg install, since it's unused. Adds comments to explain why. --- app/Dockerfile | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index 346625e..7508ba7 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -15,10 +15,15 @@ COPY package.json bun.lockb /temp/dev/ RUN cd /temp/dev/ && \ bun install --frozen-lockfile -# install with --production (exclude devDependencies) -COPY package.json bun.lockb /temp/prod/ -RUN cd /temp/prod && \ - bun install --frozen-lockfile --production +# FA: the production-only install is currently commented out since we always +# require the dev dependencies, specifically vite, to run *or* build the app. +# i'm leaving it here because perhaps someday we'll think of a reason why we +# want just the production dependencies. + +# # install with --production (exclude devDependencies) +# COPY package.json bun.lockb /temp/prod/ +# RUN cd /temp/prod && \ +# bun install --frozen-lockfile --production # ----------------------------------------------------------- # copy node_modules from dev stage, copy entire app @@ -35,7 +40,7 @@ CMD [ "vite", "--host", "--port", "5713" ] # ----------------------------------------------------------- # copy production dependencies and source code into final image FROM base AS release -COPY --from=install /temp/prod/node_modules node_modules +COPY --from=install /temp/dev/node_modules node_modules COPY . . # produce a bundle that'll then be served via a reverse http proxy, e.g. nginx