This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Rebuilt inside the image
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
packages/database/src/generated
|
||||
|
||||
# Secrets and local data - these must never be baked into an image layer
|
||||
**/.env
|
||||
**/.env.*
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
# VCS, tooling and editor noise
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
.vscode
|
||||
.idea
|
||||
**/*.log
|
||||
|
||||
# The build files themselves
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
docker build -t git.alexav.gg/${{ gitea.repository }}:latest .
|
||||
docker build -t git.alexav.gg/${{ gitea.repository }}:${{ gitea.sha }} .
|
||||
|
||||
- name: Login to Gitea Registry
|
||||
run: |
|
||||
echo "${{ secrets.RUNNER_TOKEN }}" | docker login git.alexav.gg -u ${{ gitea.actor }} --password-stdin
|
||||
|
||||
- name: Push Docker Image
|
||||
run: |
|
||||
docker push git.alexav.gg/${{ gitea.repository }}:latest
|
||||
docker push git.alexav.gg/${{ gitea.repository }}:${{ gitea.sha }}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Prisma 7 supports Node 20.19+, 22.12+ and 24.x and prints an unsupported
|
||||
# warning on newer majors, so this tracks the current LTS rather than latest.
|
||||
ARG NODE_VERSION=24-slim
|
||||
ARG PNPM_VERSION=11.25.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# base - shared runtime, with pnpm baked in so no stage fetches it later
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:${NODE_VERSION} AS base
|
||||
ARG PNPM_VERSION
|
||||
ENV PNPM_HOME=/pnpm \
|
||||
PATH=/pnpm:$PATH \
|
||||
CHECKPOINT_DISABLE=1
|
||||
RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate
|
||||
WORKDIR /app
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build - install, generate the Prisma client, compile all three packages
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM base AS build
|
||||
|
||||
# better-sqlite3's install step is `prebuild-install || node-gyp rebuild`, so a
|
||||
# toolchain has to exist for the fallback. It never reaches a runtime stage.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Manifests first: dependencies only reinstall when the lockfile actually moves.
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY apps/backend/package.json apps/backend/
|
||||
COPY apps/frontend/package.json apps/frontend/
|
||||
COPY packages/database/package.json packages/database/
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
# `prisma generate` resolves datasource.url through prisma7.config.ts, so the
|
||||
# variable must exist - but generate only writes the client, it never connects.
|
||||
ENV DATABASE_URL="file:/tmp/build.db"
|
||||
RUN pnpm --filter database db:generate \
|
||||
&& pnpm --filter database build \
|
||||
&& pnpm --filter backend build \
|
||||
&& pnpm --filter frontend build
|
||||
|
||||
# Everything is compiled, so drop devDependencies in place. Safe here because
|
||||
# every runtime import in both apps resolves to a production dependency; the
|
||||
# one devDependency reference, `import("vite")` in server.js, sits in the dev
|
||||
# branch that --prod never takes.
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
pnpm install --prod --frozen-lockfile
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# backend - Express API. Not published to the host; the frontend proxies to it.
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM base AS backend
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY --from=build /app /app
|
||||
|
||||
# Applying migrations on boot keeps a fresh volume usable. `migrate deploy` only
|
||||
# applies migrations it has not already recorded, so restarts are a no-op.
|
||||
COPY <<'EOF' /usr/local/bin/blog-backend
|
||||
#!/bin/sh
|
||||
set -e
|
||||
cd /app/packages/database
|
||||
./node_modules/.bin/prisma migrate deploy
|
||||
cd /app/apps/backend
|
||||
exec node dist/index.js
|
||||
EOF
|
||||
RUN chmod +x /usr/local/bin/blog-backend
|
||||
|
||||
# The SQLite file lives on a volume mounted here.
|
||||
RUN mkdir -p /data && chown node:node /data
|
||||
USER node
|
||||
WORKDIR /app/apps/backend
|
||||
|
||||
# src/index.ts calls app.listen(3000) directly - env.PORT is parsed but unused.
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||
|
||||
CMD ["blog-backend"]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# frontend - SSR server: renders / and /post/:slug, proxies /api to the backend
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM base AS frontend
|
||||
ENV NODE_ENV=production \
|
||||
PORT=5173 \
|
||||
BACKEND_ORIGIN=http://backend:3000
|
||||
|
||||
COPY --from=build /app /app
|
||||
|
||||
USER node
|
||||
WORKDIR /app/apps/frontend
|
||||
|
||||
EXPOSE 5173
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD ["node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||5173)+'/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||
|
||||
CMD ["node", "server.js", "--prod"]
|
||||
@@ -1 +1,2 @@
|
||||
.env
|
||||
.env
|
||||
dist
|
||||
@@ -4,7 +4,8 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts"
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Backend is intentionally not published to the host: the browser only ever
|
||||
# talks to the frontend, which proxies /api to it over the internal network.
|
||||
# That keeps sessions same-origin, exactly as the Vite dev proxy does.
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
command: ["redis-server", "--save", "60", "1"]
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
target: backend
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
# No default in env.ts - the server exits on boot without it.
|
||||
SESSION_SECRET: ${SESSION_SECRET:?set SESSION_SECRET in .env}
|
||||
DATABASE_URL: file:/data/blog.db
|
||||
REDIS_URL: redis://redis:6379
|
||||
volumes:
|
||||
- blog-data:/data
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
target: frontend
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 5173
|
||||
BACKEND_ORIGIN: http://backend:3000
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-5173}:5173"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
blog-data:
|
||||
redis-data:
|
||||
Reference in New Issue
Block a user