ci: docker compose for easy access

This commit is contained in:
Syahdan 2026-05-25 13:40:09 +07:00
parent a6edf804b4
commit c42b8e2904
11 changed files with 249 additions and 13 deletions

17
.dockerignore Normal file
View file

@ -0,0 +1,17 @@
.git
.jj
node_modules
**/node_modules
.turbo
.cache
coverage
**/dist
**/build
**/*.tsbuildinfo
.env*
**/.env*
!.env.example
!**/.env.example
logs
*.log
docs

15
.env.example Normal file
View file

@ -0,0 +1,15 @@
POSTGRES_DB=minmon
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5432
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/minmon
BETTER_AUTH_SECRET=minmon-compose-secret-change-me-123456
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGIN=http://localhost:3001
NODE_ENV=development
SERVER_PORT=3000
WEB_PORT=3001
VITE_SERVER_URL=http://localhost:3000

View file

@ -159,6 +159,25 @@ bun run dev
- Web: `http://localhost:5173` - Web: `http://localhost:5173`
- API: `http://localhost:3000` - API: `http://localhost:3000`
## Docker Compose Deployment
Run the full stack with PostgreSQL, database initialization, API, and web UI:
```bash
cp .env.example .env
docker compose up -d
```
Default endpoints:
- Web: `http://localhost:3001`
- API: `http://localhost:3000`
On first startup, the database initializer applies the schema and seeds the sample admin only when the users table is empty.
Docker Compose reads the root `.env` file automatically. Edit it before startup if you need different ports, public URLs, credentials, or secrets.
## Project Structure ## Project Structure
```text ```text
@ -186,5 +205,5 @@ See:
## Notes ## Notes
- DNS checking is limited to A record / IPv4 as required. - DNS checking is limited to A record / IPv4 as required.
- This is not a deployment platform and does not include SSH, Docker orchestration, or reverse proxy automation. - Docker Compose is available for running the app stack; the app itself does not include SSH, reverse proxy, or host orchestration automation.
- Full workspace typecheck is still blocked by a pre-existing desktop typing issue for `three`, but the web app and server app are verified. - Full workspace typecheck is still blocked by a pre-existing desktop typing issue for `three`, but the web app and server app are verified.

33
apps/server/Dockerfile Normal file
View file

@ -0,0 +1,33 @@
# syntax=docker/dockerfile:1
FROM oven/bun:1.3.12 AS base
WORKDIR /app
ENV CI=1
COPY package.json bun.lock turbo.json tsconfig.json bts.jsonc ./
COPY apps ./apps
COPY packages ./packages
FROM base AS deps
RUN bun install --frozen-lockfile
FROM deps AS tools
ENV NODE_ENV=development
RUN touch apps/server/.env
FROM deps AS build
ENV NODE_ENV=production
RUN bun run --filter server build
FROM base AS prod-deps
RUN bun install --frozen-lockfile --production
FROM oven/bun:1.3.12 AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/apps/server/dist ./apps/server/dist
EXPOSE 3000
CMD ["bun", "apps/server/dist/index.mjs"]

21
apps/web/Dockerfile Normal file
View file

@ -0,0 +1,21 @@
# syntax=docker/dockerfile:1
FROM oven/bun:1.3.12 AS build
WORKDIR /app
ENV CI=1
ARG VITE_SERVER_URL=http://localhost:3000
ENV VITE_SERVER_URL=${VITE_SERVER_URL}
COPY package.json bun.lock turbo.json tsconfig.json bts.jsonc ./
COPY apps ./apps
COPY packages ./packages
RUN bun install --frozen-lockfile
RUN bun run --filter web build
FROM nginx:1.27-alpine AS runtime
COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/apps/web/dist /usr/share/nginx/html
EXPOSE 80

11
apps/web/nginx.conf Normal file
View file

@ -0,0 +1,11 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

64
docker-compose.yml Normal file
View file

@ -0,0 +1,64 @@
name: minmon
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:?POSTGRES_DB must be set in root .env}
POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER must be set in root .env}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in root .env}
ports:
- "${POSTGRES_PORT:?POSTGRES_PORT must be set in root .env}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
db-init:
build:
context: .
dockerfile: apps/server/Dockerfile
target: tools
environment:
<<: *server-environment
command: ["sh", "-c", "bun run db:push && bun run --filter @minmon/db db:seed:if-empty"]
depends_on:
postgres:
condition: service_healthy
restart: "no"
server:
build:
context: .
dockerfile: apps/server/Dockerfile
environment:
<<: *server-environment
ports:
- "${SERVER_PORT:?SERVER_PORT must be set in root .env}:3000"
depends_on:
postgres:
condition: service_healthy
db-init:
condition: service_completed_successfully
restart: unless-stopped
web:
build:
context: .
dockerfile: apps/web/Dockerfile
args:
VITE_SERVER_URL: ${VITE_SERVER_URL:?VITE_SERVER_URL must be set in root .env}
ports:
- "${WEB_PORT:?WEB_PORT must be set in root .env}:80"
depends_on:
server:
condition: service_started
restart: unless-stopped
volumes:
postgres_data:

View file

@ -114,6 +114,25 @@ bun run dev
- Web: `http://localhost:5173` - Web: `http://localhost:5173`
- API: `http://localhost:3000` - API: `http://localhost:3000`
## Docker Compose Deployment
Run the deployable stack with one command:
```bash
cp .env.example .env
docker compose up -d
```
Default Docker endpoints:
- Web: `http://localhost:3001`
- API: `http://localhost:3000`
The Compose stack starts PostgreSQL, applies the schema, and seeds the sample admin only if the users table is empty.
Docker Compose reads the root `.env` file automatically. Edit it before startup if you need different ports, public URLs, credentials, or secrets.
## Current Behavior Notes ## Current Behavior Notes
### List Ordering ### List Ordering
@ -157,4 +176,4 @@ bun x tsc --noEmit -p packages/api/tsconfig.json
- Full workspace typecheck may still fail because of a pre-existing `apps/desktop` typing issue for `three`. - Full workspace typecheck may still fail because of a pre-existing `apps/desktop` typing issue for `three`.
- The verified implementation path is the web app + server app + database push + database seed. - The verified implementation path is the web app + server app + database push + database seed.
- DNS checking is limited to A record / IPv4. - DNS checking is limited to A record / IPv4.
- This project is not a deployment platform and does not include SSH, Docker orchestration, or reverse proxy automation. - Docker Compose is available for running the app stack; the app itself does not include SSH, reverse proxy, or host orchestration automation.

View file

@ -15,6 +15,7 @@
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"db:seed": "bun --env-file=../../apps/server/.env run ./src/seed.ts", "db:seed": "bun --env-file=../../apps/server/.env run ./src/seed.ts",
"db:seed:if-empty": "bun --env-file=../../apps/server/.env run ./src/seed-if-empty.ts",
"db:start": "docker compose up -d", "db:start": "docker compose up -d",
"db:watch": "docker compose up", "db:watch": "docker compose up",
"db:stop": "docker compose stop", "db:stop": "docker compose stop",

View file

@ -0,0 +1,34 @@
import dotenv from "dotenv";
dotenv.config({
path: new URL("../../../apps/server/.env", import.meta.url).pathname,
});
import { db } from "./index";
import { seed } from "./seed";
async function seedIfEmpty() {
const existingUser = await db.query.user.findFirst({
columns: {
id: true,
},
});
if (existingUser) {
console.info("Database already has users; skipping sample seed.");
return;
}
await seed();
console.info("Seeded auth admin and monitoring dashboard sample data.");
console.info("Admin login: admin@minmon.local / admin12345");
}
seedIfEmpty()
.then(() => {
process.exit(0);
})
.catch((error) => {
console.error("Failed to initialize seed data.", error);
process.exit(1);
});

View file

@ -261,11 +261,12 @@ async function seedMonitoringData() {
); );
} }
async function seed() { export async function seed() {
await seedAuthAdmin(); await seedAuthAdmin();
await seedMonitoringData(); await seedMonitoringData();
} }
if (import.meta.main) {
seed() seed()
.then(() => { .then(() => {
console.info("Seeded auth admin and monitoring dashboard sample data."); console.info("Seeded auth admin and monitoring dashboard sample data.");
@ -276,3 +277,4 @@ seed()
console.error("Failed to seed dashboard sample data.", error); console.error("Failed to seed dashboard sample data.", error);
process.exit(1); process.exit(1);
}); });
}