diff --git a/.env.example b/.env.example index 4d98c47..eafbda3 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ IP2LOCATION_API_KEY=*********** COOKIE_SECRET=*********** FORM_SUBMISSION_ACTION_URL=*********** +DB_PATH=*[OPTIONAL_CAN_BE_REMOVED]* +PORT=*[OPTIONAL_CAN_BE_REMOVED]* +HOST=*[OPTIONAL_CAN_BE_REMOVED]* diff --git a/.gitignore b/.gitignore index 0ac103a..029634e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ sqlite.db node_modules/ dist/ +data/* +!data/.gitkeep .env diff --git a/.nvmrc b/.nvmrc index 4b5ebaf..1117d41 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.6.0 +18.20.5 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..90590dd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:18-alpine3.20 + +WORKDIR /app +VOLUME data +EXPOSE 3000 + +COPY package.json package-lock.json tsconfig.json .nvmrc /app/ +COPY src /app/src +COPY static /app/static + +COPY .env /app + +RUN npm ci +RUN echo "DB_PATH=./data/sqlite.db" >> /app/.env +RUN echo "PORT=3000" >> /app/.env +RUN echo "HOST=0.0.0.0" >> /app/.env + +CMD ["npm", "run", "start:ci"] diff --git a/README.md b/README.md index ab6fbc3..f09ead5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ You can run import script: The file should include one domain per line. This script will ingest the domains and ignore any duplicates. This data is not part of this repository. +If you are running the application via docker, provide additional flag `--dbFilePath=./data/sqlite.db`, assuming you are running it in specified `WORKDIR`. + ## Development ### JSON schema @@ -48,6 +50,32 @@ Endpoints use JSON schema to define the constraints of inputs. Run `npm run sche ## Deployment +### Docker (recommended) + +Make sure you have all your secrets define in `.env`, see `.env.example` to see what these are. The values for `HOST`, `PORT` and `DB_PATH` will be overwritten when the image is built. + +If you have existing database, place it in this location: `./data/sqlite.db` (the name must stay the same), otherwise database will be created at that location. + +Build the image: + +```bash +$ docker build -t czdomains . +``` + +Run the container (recommended settings for server environment): + +```bash +$ docker run -d --name czdomains -p 3000:3000 --restart always -v $(pwd)/data:/app/data czdomains +``` + +This will run the application in daemon mode (in the background) and make sure it is automatically restarted. +The volume `-v` flag is important so the database can be found at the right place and data is persisted. +You can customize the host port with `-p` flag if you need to, e.g. if you want to have it accessible on port 80, change it to `-p 80:3000`. + +The application will automatically run all the migrations when it starts. If you need to import data, follow the instructions in the `Populating database with data` section. + +### Bare metal + Make sure you have all your secrets define in `.env`, copy `.env.example` to see what these are. Application can be started with `npm run start`. You should provide `PORT` environment variable to specify which port to run the webserver on. You can add this variable to `.env` file as well. diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 6aa92f7..15a4226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "undici": "^5.22.1" }, "engines": { - "node": ">= 18.6.0" + "node": "18.20.5" } }, "node_modules/@ampproject/remapping": { diff --git a/package.json b/package.json index f36e1d6..0ac2d57 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "scripts": { "build": "tsc -p tsconfig.json", "prebuild": "npm run clean", + "prestart:ci": "DB_PATH=$(node -e 'console.log(require(\"minidotenv\")()(\"DB_PATH\"))'); tsx src/migrate.ts --dbFilePath=$DB_PATH", + "start:ci": "NODE_ENV=production ts-node --transpile-only src/index.ts", "start": "NODE_ENV=production ts-node --transpile-only src/index.ts", "dev": "NODE_ENV=development ts-node-dev --respawn --transpile-only src/index.ts", "dev:debug": "NODE_ENV=development ts-node-dev --inspect-brk --respawn --transpile-only src/index.ts", @@ -24,7 +26,7 @@ "url": "git+https://github.com/comatory/czdomains.git" }, "engines": { - "node": ">= 18.6.0" + "node": "18.20.5" }, "keywords": [ "server", diff --git a/src/import.ts b/src/import.ts index ce4fa85..2ec1931 100644 --- a/src/import.ts +++ b/src/import.ts @@ -14,6 +14,12 @@ const cliOptions = yargs(process.argv.slice(2)) description: 'Path to file with domain names', demandOption: true, }) + .options('dbFilePath', { + alias: 'db', + type: 'string', + description: 'Path to the sqlite database file', + demandOption: false, + }) .parseSync(); const HTTPS_RE = /^http(s?):\/\//i; @@ -110,7 +116,7 @@ void (async ({ filePath }: typeof cliOptions) => { console.info('Connecting to database...'); - const dbPath = join(__dirname, '..', './sqlite.db'); + const dbPath = join(__dirname, '..', cliOptions.dbFilePath ?? './sqlite.db'); const db = await open({ filename: dbPath, driver: sqlite3.Database, diff --git a/src/lib/app.ts b/src/lib/app.ts index 31ed2a4..aa06474 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -15,8 +15,8 @@ import { submitPlugin } from './routes/submit/routes'; /** * Function that builds and runs the Fastify server. */ -export async function createApp() { - const server = await configureServices(fastify()); +export async function createApp(options: { dbFilePath: string | undefined }) { + const server = await configureServices(fastify(), options); configureParsers(server); configureHooks(server); diff --git a/src/lib/config/services.ts b/src/lib/config/services.ts index da7cee3..e156e0f 100644 --- a/src/lib/config/services.ts +++ b/src/lib/config/services.ts @@ -5,8 +5,8 @@ import { createDatabase } from '../services/db'; type UnwrapPromise = T extends Promise ? U : T; export type Services = UnwrapPromise>; -async function createServices() { - const db = await createDatabase(); +async function createServices({ dbFilePath }: { dbFilePath?: string }) { + const db = await createDatabase({ dbFilePath }); return { db, @@ -15,8 +15,10 @@ async function createServices() { export async function configureServices( app: FastifyInstance, + options: { dbFilePath: string | undefined }, ): Promise { - const services = await createServices(); + const { dbFilePath } = options; + const services = await createServices({ dbFilePath }); app.decorate('services', services); diff --git a/src/lib/server.ts b/src/lib/server.ts index c8a115c..42aab41 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -3,11 +3,14 @@ import { createApp } from './app'; const PORT = Number.isNaN(Number(process.env.PORT)) ? 3000 : Number(process.env.PORT); +const HOST = process.env.HOST; + +const DB_FILE_PATH = process.env.DB_PATH ?? 'sqlite.db'; (async () => { - const app = await createApp(); + const app = await createApp({ dbFilePath: DB_FILE_PATH }); - app.listen({ port: PORT }, (err, address) => { + app.listen({ port: PORT, host: HOST }, (err, address) => { if (err) { console.error(err); process.exit(1); diff --git a/src/lib/services/db.ts b/src/lib/services/db.ts index c94ef83..1679858 100644 --- a/src/lib/services/db.ts +++ b/src/lib/services/db.ts @@ -4,9 +4,9 @@ import { cwd } from 'node:process'; import { open } from 'sqlite'; import sqlite3 from 'sqlite3'; -export async function createDatabase() { +export async function createDatabase({ dbFilePath }: { dbFilePath?: string }) { const db = await open({ - filename: join(cwd(), 'sqlite.db'), + filename: join(cwd(), dbFilePath ?? 'sqlite.db'), driver: sqlite3.Database, }); diff --git a/src/lib/test-utils/app.ts b/src/lib/test-utils/app.ts index c3c2652..2669741 100644 --- a/src/lib/test-utils/app.ts +++ b/src/lib/test-utils/app.ts @@ -5,5 +5,5 @@ type UnwrapPromise = T extends Promise ? U : T; export type TestApp = UnwrapPromise>; export async function createTestApp(): Promise { - return await createApp(); + return await createApp({ dbFilePath: undefined }); } diff --git a/src/migrate.ts b/src/migrate.ts index 48d3ebc..86435b0 100644 --- a/src/migrate.ts +++ b/src/migrate.ts @@ -15,6 +15,12 @@ const cliOptions = yargs(process.argv.slice(2)) type: 'boolean', description: 'List all migrations', }) + .options('dbFilePath', { + alias: 'db', + type: 'string', + description: 'Path to the sqlite database file', + require: false, + }) .help() .parseSync(); @@ -64,7 +70,16 @@ async function migrate(client: Database, version?: string) { void (async (options: typeof cliOptions) => { verbose(); - const client = new Database(join(__dirname, '..', 'sqlite.db')); + + if (options.filePath) { + console.info(`Override detected. Using database file: ${options.filePath}`); + } + + const dbPath = options.dbFilePath + ? join(__dirname, '..', options.dbFilePath) + : join(__dirname, '..', 'sqlite.db'); + + const client = new Database(dbPath); try { if (options.list) {