Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules
npm-debug.log
.next
out
.env
.env.*
docker-compose.yml
.git
.gitignore
coverage
.idea
.vscode
ops/backups
32 changes: 32 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Application
APP_HOST=0.0.0.0
APP_PORT=3000
NEXT_PUBLIC_APP_URL=https://nicebaby.local
NODE_ENV=production

# Database
POSTGRES_DB=nicebaby
POSTGRES_USER=nicebaby
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
DATABASE_URL=postgresql://${POSTGRES_USER}:\${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}

# Redis / queue
REDIS_URL=redis://redis:6379
WORKER_HEARTBEAT_INTERVAL=60000
SCHEDULE_INTERVAL=3600000

# Observability
GRAFANA_ADMIN_USER=admin
LOKI_RETENTION_DAYS=7

# Backups
RESTIC_REPOSITORY=s3:s3.amazonaws.com/your-bucket/nicebaby
RESTIC_BACKUP_CRON=0 */6 * * *
RESTIC_KEEP_DAILY=7
RESTIC_KEEP_WEEKLY=4
RESTIC_KEEP_MONTHLY=6

# Reverse proxy
CADDY_EMAIL=admin@nicebaby.local
CADDY_DOMAIN=nicebaby.local
47 changes: 47 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Build Docker Images

on:
push:
branches: [ main ]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
strategy:
matrix:
image:
- { name: app, dockerfile: Dockerfile.app }
- { name: worker, dockerfile: Dockerfile.worker }
- { name: scheduler, dockerfile: Dockerfile.job }
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to registry
if: ${{ secrets.REGISTRY != '' && secrets.REGISTRY_USERNAME != '' && secrets.REGISTRY_PASSWORD != '' }}
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}

- name: Build image
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.image.dockerfile }}
push: ${{ secrets.REGISTRY != '' && secrets.REGISTRY_USERNAME != '' && secrets.REGISTRY_PASSWORD != '' }}
tags: |
${{ secrets.REGISTRY || 'ghcr.io/example' }}/${{ github.repository_owner }}/nicebaby-${{ matrix.image.name }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NODE_VERSION=20.12.2
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@

# vscode
.vscode

# docker secrets
docker/secrets/*
43 changes: 43 additions & 0 deletions Dockerfile.app
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# syntax=docker/dockerfile:1.6

ARG NODE_VERSION=20.12.2
FROM node:${NODE_VERSION}-alpine AS base

RUN apk add --no-cache libc6-compat bash wget
WORKDIR /app

FROM base AS deps
COPY package.json package-lock.json* pnpm-lock.yaml* bun.lockb* ./
RUN \
if [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then npm install -g pnpm && pnpm install --frozen-lockfile; \
elif [ -f bun.lockb ]; then npm install -g bun && bun install --frozen-lockfile; \
else npm install; fi

FROM base AS builder
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup -g 1001 -S nodejs \
&& adduser -S nextjs -G nodejs

WORKDIR /app
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/package.json ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1

CMD ["node", "server.js"]
27 changes: 27 additions & 0 deletions Dockerfile.job
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# syntax=docker/dockerfile:1.6

ARG NODE_VERSION=20.12.2
FROM node:${NODE_VERSION}-alpine AS base
RUN apk add --no-cache libc6-compat bash
WORKDIR /app

FROM base AS deps
COPY package.json package-lock.json* pnpm-lock.yaml* bun.lockb* ./
RUN \
if [ -f package-lock.json ]; then npm ci --omit=dev; \
elif [ -f pnpm-lock.yaml ]; then npm install -g pnpm && pnpm install --frozen-lockfile --prod; \
elif [ -f bun.lockb ]; then npm install -g bun && bun install --frozen-lockfile --production; \
else npm install --omit=dev; fi

FROM base AS runner
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs \
&& adduser -S scheduler -G nodejs

WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./
COPY scripts ./scripts

USER scheduler
CMD ["node", "scripts/scheduled-job.js"]
27 changes: 27 additions & 0 deletions Dockerfile.worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# syntax=docker/dockerfile:1.6

ARG NODE_VERSION=20.12.2
FROM node:${NODE_VERSION}-alpine AS base
RUN apk add --no-cache libc6-compat bash
WORKDIR /app

FROM base AS deps
COPY package.json package-lock.json* pnpm-lock.yaml* bun.lockb* ./
RUN \
if [ -f package-lock.json ]; then npm ci --omit=dev; \
elif [ -f pnpm-lock.yaml ]; then npm install -g pnpm && pnpm install --frozen-lockfile --prod; \
elif [ -f bun.lockb ]; then npm install -g bun && bun install --frozen-lockfile --production; \
else npm install --omit=dev; fi

FROM base AS runner
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs \
&& adduser -S worker -G nodejs

WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./
COPY scripts ./scripts

USER worker
CMD ["node", "scripts/worker.js"]
97 changes: 5 additions & 92 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# NiceBaby.io

A self-hosted newborn companion built with Next.js 14. This project scaffolds the front-end experience for tracking feeds,
diapers, sleep, growth, medical appointments, and curated guidance for new parents. It also outlines the surrounding
architecture for API services, data modeling, and Docker-based deployment.
diapers, sleep, growth, medical appointments, and curated guidance for new parents. It now ships with production-grade
Docker images, observability, and backup automation for home-lab and small scale deployments.

## Getting Started
## Getting Started (Local Development)

```bash
npm install
Expand All @@ -13,102 +13,15 @@ npm run dev

Open http://localhost:3000 in your browser to explore the product blueprint.

## Environment Variables

Create a `.env` file (or export the variables) before running the application:

```bash
DATABASE_URL="postgresql://user:password@localhost:5432/nicebaby"
NEXTAUTH_SECRET="$(openssl rand -hex 32)"
NEXTAUTH_URL="http://localhost:3000"
```

`DATABASE_URL` should point to the PostgreSQL instance backing Prisma. `NEXTAUTH_SECRET` secures JWT sessions and should
be a long, random string in production. `NEXTAUTH_URL` must reflect the public URL where the app is served.

## Database & Prisma

Prisma is configured under `prisma/schema.prisma` with models for caregivers, babies, and the primary log tables. Helpful
commands are exposed through `package.json` scripts:

```bash
# Apply schema changes locally (creates a new migration if needed)
npm run prisma:migrate

# Re-generate the Prisma client after editing the schema
npm run prisma:generate

# Apply existing migrations in production environments
npm run prisma:deploy

# Inspect and edit data visually
npm run prisma:studio
```

The repository includes an initial migration at `prisma/migrations/0001_init/migration.sql` that provisions the schema
for all caregivers, babies, and log tables.

## Seeding Guidance

To load starter caregivers, babies, or log entries, create a `prisma/seed.ts` script that hashes passwords with
[`bcryptjs`](https://www.npmjs.com/package/bcryptjs) and inserts records via the Prisma client. Example outline:

```ts
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import { hash } from 'bcryptjs';

const prisma = new PrismaClient();

async function main() {
const passwordHash = await hash('changeme', 12);

const caregiver = await prisma.caregiver.upsert({
where: { email: 'caregiver@example.com' },
update: {},
create: {
email: 'caregiver@example.com',
name: 'Primary Caregiver',
passwordHash,
babies: {
create: {
name: 'Baby Doe',
birthDate: new Date('2024-01-01')
}
}
}
});

console.log('Seeded caregiver', caregiver.email);
}

main()
.catch((error) => {
console.error(error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
```

Then execute (after wiring a seed command in `package.json` such as `"prisma": { "seed": "ts-node prisma/seed.ts" }`):

```bash
npx prisma db seed
```

Adjust the script to suit production data-loading needs (multiple caregivers, cross-linked babies, and initial log
entries). For JavaScript environments, rename the file to `seed.js` and swap the `import` statements for `require`.

## Project Highlights

- **Next.js App Router** foundation with typed routes and Tailwind CSS styling.
- **Component-driven sections** that describe the core modules (tracking, playbooks, deployment) to guide future builds.
- **Data blueprints** (`data/*.ts`) capturing roadmap items for backend, analytics, and self-hosting strategies.
- **Production operations** blueprint with Docker images, observability, backups, and CI automation.

## Next Steps

- Extend the new authentication and log APIs with UI components for recording daily activity.
- Add dashboards, reminders, and background jobs based on the outlined modules.
- Containerize the application with Docker Compose including database, worker, and reverse proxy services.

10 changes: 10 additions & 0 deletions app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NextResponse } from 'next/server';
import { trackRequest } from '../metrics/route';

export const runtime = 'nodejs';

export async function GET(request: Request) {
const response = NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() });
trackRequest(request.method, response.status);
return response;
}
34 changes: 34 additions & 0 deletions app/api/metrics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import client from 'prom-client';
import { NextResponse } from 'next/server';

export const runtime = 'nodejs';

const register = client.register;

if (!(global as any).__METRICS_DEFAULTS__) {
client.collectDefaultMetrics({ register });
(global as any).__METRICS_DEFAULTS__ = true;
}

let httpRequestCounter = register.getSingleMetric('http_requests_total') as client.Counter | undefined;
if (!httpRequestCounter) {
httpRequestCounter = new client.Counter({
name: 'http_requests_total',
help: 'Count of HTTP requests by status code',
labelNames: ['method', 'status'],
});
}

export async function GET() {
return new NextResponse(await register.metrics(), {
status: 200,
headers: {
'Content-Type': register.contentType,
'Cache-Control': 'no-store',
},
});
}

export function trackRequest(method: string, status: number) {
httpRequestCounter?.inc({ method, status: status.toString() });
}
Loading