diff --git a/.devcontainer.json b/.devcontainer.json index 51cc3fb6..23194b65 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -2,9 +2,12 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node-postgres { "name": "Bun & Postgres", - "dockerComposeFile": "docker-compose.yml", - "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "dockerComposeFile": [ + "docker/docker-compose.yml", + "docker/docker-compose.workspace.yml" + ], + "service": "workspace", + "workspaceFolder": "/app", // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/devcontainers/features/github-cli:1": {}, @@ -16,14 +19,6 @@ "ghcr.io/itsmechlark/features/postgresql:1.3.3": {} }, "waitFor": "onCreateCommand", - "updateContentCommand": "bun i", - "postAttachCommand": { - "app": "bun dev", - "studio": "bun db:studio", - "docs": "bun docs" - }, - // "postCreateCommand": "bun db:migrate && bun db:seed", - "postCreateCommand": "./scripts/create-db.sh", "customizations": { "vscode": { "extensions": [ diff --git a/bun.lockb b/bun.lockb index b7a0577d..5a77b067 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ff36c26b..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: '3.8' - -services: - db: - image: postgres:16 - restart: unless-stopped - volumes: - - postgres-data:/var/lib/postgresql/data - environment: - - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - POSTGRES_DB=${POSTGRES_DB} - healthcheck: - test: - [ - "CMD-SHELL", - "PGPASSWORD=$${POSTGRES_PASSWORD} psql -U $${POSTGRES_USER} -d $${POSTGRES_DB} -c 'SELECT 1;' || exit 1" - ] - interval: 1s - timeout: 5s - retries: 10 - # This allows accessing externally from "localhost" in addition to "127.0.0.1" - ports: - - ${POSTGRES_PORT}:5432 - - app: - image: node:20-bullseye - restart: unless-stopped - - volumes: - - ..:/workspaces:cached - - ../scripts:/scripts:cached - - # Overrides default command so things don't shut down after the process ends. - command: sleep infinity - - # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. - network_mode: service:db - # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. - # (Adding the "ports" property to this file will not forward from a Codespace.) - - # depends_on: - # - db - # environment: - # - POSTGRES_HOST=db # connect to the Postgres container with Docker Networking - # - POSTGRES_PORT=5432 # use internal port of Postgres container - -volumes: - postgres-data: diff --git a/docker/.env b/docker/.env new file mode 100644 index 00000000..d0509337 --- /dev/null +++ b/docker/.env @@ -0,0 +1,10 @@ +# This is needed for devcontainers. We can't specify the .env files in the devcontainer.json file +# so we need to rely on docker compose to pick up this file by default... +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=medium +POSTGRES_HOST=0.0.0.0 +POSTGRES_PORT=5432 +JWT_SECRET=supersecretkey +JWT_ALGORITHM=HS256 +APP_PORT=3000 \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..299cf708 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,13 @@ +FROM oven/bun:1.0.6-slim + +WORKDIR /app + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y git + +COPY package.json /app/package.json +COPY bun.lockb /app/bun.lockb + +RUN bun install + diff --git a/docker/docker-compose.workspace.yml b/docker/docker-compose.workspace.yml new file mode 100644 index 00000000..63b9318c --- /dev/null +++ b/docker/docker-compose.workspace.yml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + workspace: + build: + context: .. + dockerfile: docker/Dockerfile + restart: unless-stopped + command: ["sleep", "infinity"] + volumes: + - ..:/app + + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..962b49d1 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,57 @@ +version: '3.8' + +x-common: + &commmon + build: + context: .. + dockerfile: docker/Dockerfile + depends_on: + db: + condition: service_healthy + env_file: + - ../environments/local/.env.db + - ../environments/local/.env.app + environment: + - POSTGRES_HOST=db + - POSTGRES_PORT=${POSTGRES_PORT} + volumes: + - ..:/app + + +services: + db: + image: postgres:16 + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_PORT=${POSTGRES_PORT} + healthcheck: + test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}"] + interval: 5s + timeout: 30s + retries: 10 + ports: + - ${POSTGRES_PORT}:${POSTGRES_PORT} + + db-init: + <<: *commmon + command: ["bun", "db:migrate", "&&", "bun", "db:seed"] + restart: no + + app: + <<: *commmon + restart: unless-stopped + command: ["bun", "dev"] + depends_on: + db-init: + condition: service_completed_successfully + ports: + - ${APP_PORT}:${APP_PORT} + +volumes: + postgres-data: + diff --git a/environments/local/.env.app b/environments/local/.env.app new file mode 100644 index 00000000..b97f5c03 --- /dev/null +++ b/environments/local/.env.app @@ -0,0 +1,3 @@ +JWT_SECRET=supersecretkey +JWT_ALGORITHM=HS256 +APP_PORT=3000 \ No newline at end of file diff --git a/.env b/environments/local/.env.db similarity index 64% rename from .env rename to environments/local/.env.db index 941631c2..699982d9 100644 --- a/.env +++ b/environments/local/.env.db @@ -3,6 +3,4 @@ POSTGRES_PASSWORD=postgres POSTGRES_DB=medium POSTGRES_HOST=0.0.0.0 POSTGRES_PORT=5432 -JWT_SECRET=supersecretkey -JWT_ALGORITHM=HS256 -APP_PORT=3000 + diff --git a/scripts/create-db.sh b/scripts/create-db.sh deleted file mode 100755 index 34c8d9ee..00000000 --- a/scripts/create-db.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Source .env file to get environment variables -source .env - -# TODO: find a fix for database ('medium') creation without having to rebuild - -# Run wait-for-db.sh with the sourced environment variables -# ./scripts/wait-for-db.sh db:5432 --strict --timeout=60 -- psql -h db -U ${POSTGRES_USER} -d ${POSTGRES_DB} -c 'SELECT 1;' - -# TODO: find a better way to wait for database ('medium') creation than waiting 10 seconds -sleep 10 - -bun db:migrate -bun db:seed diff --git a/scripts/create-start-container-with-env.sh b/scripts/create-start-container-with-env.sh index 1fe39d67..8e2dbb39 100755 --- a/scripts/create-start-container-with-env.sh +++ b/scripts/create-start-container-with-env.sh @@ -3,22 +3,24 @@ # this script extends docker-compose up by supporting .env files in the same way as bun # see: https://bun.sh/docs/runtime/env#setting-environment-variables -# Default env file -env_file=".env" +current_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +cd "$current_path/.." + + +env_prefix="environments" +# Default env folder +env_folder="$env_prefix/local" # Determine which env file to use based on NODE_ENV if [ "$NODE_ENV" == "production" ]; then - env_file=".env.production" + env_folder="$env_prefix/production" elif [ "$NODE_ENV" == "development" ]; then - env_file=".env.development" + env_folder="$env_prefix/development" elif [ "$NODE_ENV" == "test" ]; then - env_file=".env.test" + env_folder="$env_prefix/test" fi -# If .env.local exists, use that instead -if [ -f ".env.local" ]; then - env_file=".env.local" -fi + # Run docker-compose with the selected env file and pass along any arguments -docker-compose --env-file $env_file up "$@" +docker-compose --env-file "$env_folder/.env.db" --env-file "$env_folder/.env.app" -f docker/docker-compose.yml up "$@" diff --git a/scripts/wait-for-db.sh b/scripts/wait-for-db.sh deleted file mode 100755 index 6ca54a4a..00000000 --- a/scripts/wait-for-db.sh +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env bash - -# wait-for-db.sh modifies wait-for-it.sh to work with postgresql by waiting for a specific database to be ready -# see original: https://github.com/vishnubob/wait-for-it - -# Use this script to test if a given TCP host/port are available - -WAITFORIT_cmdname=${0##*/} - -echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } - -usage() -{ - cat << USAGE >&2 -Usage: - $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] - -h HOST | --host=HOST Host or IP under test - -p PORT | --port=PORT TCP port under test - Alternatively, you specify the host and port as host:port - -s | --strict Only execute subcommand if the test succeeds - -q | --quiet Don't output any status messages - -t TIMEOUT | --timeout=TIMEOUT - Timeout in seconds, zero for no timeout - -- COMMAND ARGS Execute command with args after the test finishes -USAGE - exit 1 -} - -wait_for() -{ - if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then - echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" - else - echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" - fi - WAITFORIT_start_ts=$(date +%s) - while : - do - if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then - nc -z $WAITFORIT_HOST $WAITFORIT_PORT - WAITFORIT_result=$? - else - (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 - WAITFORIT_result=$? - fi - if [[ $WAITFORIT_result -eq 0 ]]; then - WAITFORIT_end_ts=$(date +%s) - echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" - break - fi - sleep 1 - done - return $WAITFORIT_result -} - -wait_for_wrapper() -{ - # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 - if [[ $WAITFORIT_QUIET -eq 1 ]]; then - timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & - else - timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & - fi - WAITFORIT_PID=$! - trap "kill -INT -$WAITFORIT_PID" INT - wait $WAITFORIT_PID - WAITFORIT_RESULT=$? - if [[ $WAITFORIT_RESULT -ne 0 ]]; then - echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" - fi - return $WAITFORIT_RESULT -} - -# process arguments -while [[ $# -gt 0 ]] -do - case "$1" in - *:* ) - WAITFORIT_hostport=(${1//:/ }) - WAITFORIT_HOST=${WAITFORIT_hostport[0]} - WAITFORIT_PORT=${WAITFORIT_hostport[1]} - shift 1 - ;; - --child) - WAITFORIT_CHILD=1 - shift 1 - ;; - -q | --quiet) - WAITFORIT_QUIET=1 - shift 1 - ;; - -s | --strict) - WAITFORIT_STRICT=1 - shift 1 - ;; - -h) - WAITFORIT_HOST="$2" - if [[ $WAITFORIT_HOST == "" ]]; then break; fi - shift 2 - ;; - --host=*) - WAITFORIT_HOST="${1#*=}" - shift 1 - ;; - -p) - WAITFORIT_PORT="$2" - if [[ $WAITFORIT_PORT == "" ]]; then break; fi - shift 2 - ;; - --port=*) - WAITFORIT_PORT="${1#*=}" - shift 1 - ;; - -t) - WAITFORIT_TIMEOUT="$2" - if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi - shift 2 - ;; - --timeout=*) - WAITFORIT_TIMEOUT="${1#*=}" - shift 1 - ;; - --) - shift - WAITFORIT_CLI=("$@") - break - ;; - --help) - usage - ;; - *) - echoerr "Unknown argument: $1" - usage - ;; - esac -done - -if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then - echoerr "Error: you need to provide a host and port to test." - usage -fi - -WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} -WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} -WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} -WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} - -# Check to see if timeout is from busybox? -WAITFORIT_TIMEOUT_PATH=$(type -p timeout) -WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) - -WAITFORIT_BUSYTIMEFLAG="" -if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then - WAITFORIT_ISBUSY=1 - # Check if busybox timeout uses -t flag - # (recent Alpine versions don't support -t anymore) - if timeout &>/dev/stdout | grep -q -e '-t '; then - WAITFORIT_BUSYTIMEFLAG="-t" - fi -else - WAITFORIT_ISBUSY=0 -fi - -if [[ $WAITFORIT_CHILD -gt 0 ]]; then - wait_for - WAITFORIT_RESULT=$? - exit $WAITFORIT_RESULT -else - if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then - wait_for_wrapper - WAITFORIT_RESULT=$? - else - wait_for - WAITFORIT_RESULT=$? - fi -fi - -if [[ $WAITFORIT_CLI != "" ]]; then - if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then - echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" - exit $WAITFORIT_RESULT - fi - exec "${WAITFORIT_CLI[@]}" -else - exit $WAITFORIT_RESULT -fi - -# wait for db -if [ "$WAITFORIT_TIMEOUT" -gt 0 ]; then - echo "wait-for-db.sh: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" -else - echo "wait-for-db.sh: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" - - # Initialize variables for the loop - max_retries=5 - count=0 - - # Loop to retry database connection - while [[ $count -lt $max_retries ]] - do - PGPASSWORD=${POSTGRES_PASSWORD} psql -h $WAITFORIT_HOST -U ${POSTGRES_USER} -d ${POSTGRES_DB} -c 'SELECT 1;' &>/dev/null - exit_status=$? - if [ $exit_status -eq 0 ]; then - echo "wait-for-db.sh: database is ready" - exit 0 - fi - echo "wait-for-db.sh: database not ready yet. Retrying... ($((count+1))/$max_retries)" - ((count++)) - sleep 5 - done - - # If it reaches here, it means it failed to connect to the database after max_retries - echo "wait-for-db.sh: Failed to connect to database after $max_retries retries." - exit 1 -fi