dockerize!

changes to sequelize, env loading, ts workflow, minor improvements
This commit is contained in:
Kaushik Narayan R 2025-03-18 18:36:05 -07:00
parent 8c909929d1
commit 57c82dd71c
18 changed files with 236 additions and 992 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
.git
.github
.dockerignore
.gitignore
node_modules
dist
logs
Dockerfile*
.env*

2
.gitignore vendored
View File

@ -109,4 +109,4 @@ dist
# production # production
/build /build
/tsout /dist

View File

@ -1,6 +0,0 @@
import _ from "./config/dotenv.ts";
import { resolve } from "path";
export default {
config: resolve("config", "sequelize.ts"),
};

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
ARG APP_USER_DEFAULT=node
ARG NODE_ENV_DEFAULT=production
FROM node:lts-alpine AS base
ARG NODE_ENV_DEFAULT
ENV NODE_ENV=${NODE_ENV_DEFAULT}
ARG APP_USER_DEFAULT
ENV APP_USER=${APP_USER_DEFAULT}
WORKDIR /usr/src/app
FROM base AS build
COPY --chown=${APP_USER}:${APP_USER} package.json package-lock.json ./
RUN npm ci --include=prod --include=dev
COPY --chown=${APP_USER}:${APP_USER} . .
RUN npm run build
FROM base AS final
COPY --chown=${APP_USER}:${APP_USER} package.json package-lock.json ./
RUN npm ci --include=prod
COPY --from=build --chown=${APP_USER}:${APP_USER} /usr/src/app/dist dist
RUN mkdir dist/logs && chown -R ${APP_USER} dist/logs
USER ${APP_USER}
CMD [ "npm", "start" ]

View File

@ -1,6 +1,7 @@
// TODO: rate limit module is busted (CJS types), do something for rate limiting // TODO: rate limit module is busted (CJS types), do something for rate limiting
// bottleneck (https://npmjs.com/package/bottleneck) looks nice // bottleneck (https://npmjs.com/package/bottleneck) looks nice
import axios, { type AxiosInstance } from "axios"; import axios from "axios";
import type { AxiosInstance } from "axios";
import { baseAPIURL, accountsAPIURL } from "../constants.ts"; import { baseAPIURL, accountsAPIURL } from "../constants.ts";
import logger from "../utils/logger.ts"; import logger from "../utils/logger.ts";

45
compose-core.yaml Normal file
View File

@ -0,0 +1,45 @@
services:
postgres:
container_name: spotify-manager-postgres
image: postgres
restart: on-failure
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: spotify-manager
volumes:
- postgres_data:/var/lib/postgresql/data
user: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d spotify-manager"]
interval: 1s
retries: 5
timeout: 5s
redis:
container_name: spotify-manager-redis
image: redis/redis-stack-server:latest
restart: on-failure
volumes:
- redis_data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 1s
retries: 5
timeout: 3s
web:
container_name: spotify-manager
build:
context: .
init: true
restart: on-failure
ports:
- 127.0.0.1:9001:9001
depends_on:
postgres:
condition: service_healthy
restart: true
redis:
condition: service_healthy
restart: true
volumes:
postgres_data:
redis_data:

9
compose-dev.yaml Normal file
View File

@ -0,0 +1,9 @@
services:
web:
environment:
NODE_ENV: development
env_file:
- .env
- .env.local
- .env.development
- .env.development.local

9
compose-prod.yaml Normal file
View File

@ -0,0 +1,9 @@
services:
web:
environment:
NODE_ENV: production
env_file:
- .env
- .env.local
- .env.production
- .env.production.local

View File

@ -1,18 +0,0 @@
// https://github.com/motdotla/dotenv/issues/133#issuecomment-255298822
// explanation: in ESM, import statements execute first, unlike CJS where it's line order
// so if placed directly in index.ts, the .config gets called after all other imports in index.ts
// and one of those imports is the sequelize loader, which depends on env being loaded
// soln: raise the priority of dotenv to match by placing it in a separate module like this
import { config, type DotenvConfigOutput } from "dotenv";
const result: DotenvConfigOutput = config({
path: [
`.env.${process.env["NODE_ENV"]}.local`,
`.env.${process.env["NODE_ENV"]}`,
".env.local",
".env",
],
});
export default result;

View File

@ -2,13 +2,21 @@ import type { SequelizeOptions } from "sequelize-typescript";
import logger from "../utils/logger.ts"; import logger from "../utils/logger.ts";
type ConnConfigs = Record<string, SequelizeOptions>; interface SeqOptsWithURI extends SequelizeOptions {
use_env_variable: string;
}
type ConnConfigs = Record<string, SeqOptsWithURI>;
// env-specific config // env-specific config
const connConfigs: ConnConfigs = { const connConfigs: ConnConfigs = {
development: {}, development: {
test: {}, use_env_variable: "DB_URI",
},
test: {
use_env_variable: "DB_URI",
},
production: { production: {
use_env_variable: "DB_URI",
// dialectOptions: { // dialectOptions: {
// ssl: true, // ssl: true,
// }, // },

View File

@ -1,5 +1,3 @@
import _ from "./config/dotenv.ts";
import { promisify } from "util"; import { promisify } from "util";
import express from "express"; import express from "express";
import session from "express-session"; import session from "express-session";

View File

@ -1,5 +1,5 @@
"use strict"; "use strict";
import { type Migration } from "sequelize-cli"; import type { Migration } from "sequelize-cli";
export default { export default {
up: async function (queryInterface, Sequelize) { up: async function (queryInterface, Sequelize) {
await queryInterface.createTable("playlists", { await queryInterface.createTable("playlists", {

View File

@ -1,5 +1,5 @@
"use strict"; "use strict";
import { type Migration } from "sequelize-cli"; import type { Migration } from "sequelize-cli";
export default { export default {
up: async function (queryInterface, Sequelize) { up: async function (queryInterface, Sequelize) {
await queryInterface.createTable("links", { await queryInterface.createTable("links", {

View File

@ -8,14 +8,16 @@ import playlists from "./playlists.ts";
import logger from "../utils/logger.ts"; import logger from "../utils/logger.ts";
// Initialize
if (!process.env["NODE_ENV"]) if (!process.env["NODE_ENV"])
throw new TypeError("Node environment not defined"); throw new TypeError("Node environment not defined");
if (!process.env["DB_URI"])
throw new TypeError("Database connection URI not defined");
// Initialize
const config = seqConfig[process.env["NODE_ENV"]]; const config = seqConfig[process.env["NODE_ENV"]];
const seqConn: Sequelize = new Sequelize(process.env["DB_URI"], config); if (!config) throw new TypeError("Unknown environment");
const dbURI = process.env[config.use_env_variable];
if (!dbURI) throw new TypeError("Database connection URI not defined");
const seqConn: Sequelize = new Sequelize(dbURI, config);
try { try {
await seqConn.authenticate(); await seqConn.authenticate();

1039
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,9 @@
"exports": "./index.ts", "exports": "./index.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development tsx watch index.ts", "build": "tsc",
"test_setup": "npm i && cross-env NODE_ENV=test npx sequelize-cli db:migrate", "migrate": "npx sequelize-cli db:migrate --config dist/config/sequelize.js --models-path dist/models --migrations-path dist/migrations",
"test": "NODE_ENV=test tsx index.ts", "start": "npm run migrate && node dist/index.js"
"prod": "NODE_ENV=production tsx index.ts"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -26,7 +25,6 @@
"connect-redis": "^8.0.1", "connect-redis": "^8.0.1",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"express-validator": "^7.2.0", "express-validator": "^7.2.0",
@ -36,6 +34,7 @@
"redis": "^4.7.0", "redis": "^4.7.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"sequelize": "^6.37.6", "sequelize": "^6.37.6",
"sequelize-cli": "^6.6.2",
"sequelize-typescript": "^2.1.6", "sequelize-typescript": "^2.1.6",
"serializr": "^3.0.3", "serializr": "^3.0.3",
"winston": "^3.17.0" "winston": "^3.17.0"
@ -49,10 +48,6 @@
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"@types/sequelize": "^4.28.20", "@types/sequelize": "^4.28.20",
"@types/validator": "^13.12.2", "@types/validator": "^13.12.2",
"cross-env": "^7.0.3",
"nodemon": "^3.1.9",
"sequelize-cli": "^6.6.2",
"tsx": "^4.19.3",
"typescript": "^5.8.2" "typescript": "^5.8.2"
} }
} }

View File

@ -1,13 +1,13 @@
{ {
"exclude": [ "exclude": [
"boilerplates", "boilerplates",
"tsout" "dist"
], ],
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */ /* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */ /* Projects */
"incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
"composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
@ -63,7 +63,7 @@
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */ // "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./tsout", /* Specify an output folder for all emitted files. */ "outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */

View File

@ -1,6 +1,7 @@
import path from "path"; import path from "path";
import { createLogger, transports, config, format, type Logger } from "winston"; import { createLogger, transports, config, format } from "winston";
import type { Logger } from "winston";
const { combine, timestamp, printf } = format; const { combine, timestamp, printf } = format;