diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..43bd4ed --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +.git +.gitignore +.gitea +.claude +.dockerignore +Dockerfile +docker-compose.yml +node_modules +**/node_modules +server/dist +server/data.db +server/data.db-journal +server/data.db-shm +server/data.db-wal +web/dist +web/tsconfig.tsbuildinfo +weed-tracker +*.log +.env +.env.local diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..6d48a62 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,40 @@ +name: Build and push image + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: gitea.thewrightserver.net + username: ${{ gitea.repository_owner }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - id: meta + uses: docker/metadata-action@v5 + with: + images: gitea.thewrightserver.net/josh/apothecary + tags: | + type=ref,event=branch + type=sha,prefix=,format=short + type=semver,pattern={{version}} + type=raw,value=latest,enable={{is_default_branch}} + + - uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d638583 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# syntax=docker/dockerfile:1.7 + +# --- builder: compile server + web --- +FROM node:20-alpine AS builder +RUN apk add --no-cache python3 make g++ +WORKDIR /build + +COPY package*.json ./ +COPY server/package*.json ./server/ +COPY web/package*.json ./web/ +RUN npm ci && \ + npm --prefix server ci && \ + npm --prefix web ci + +COPY server ./server +COPY web ./web +RUN npm --prefix server run build && \ + npm --prefix web run build + +# --- prod deps: production-only node_modules with native bindings rebuilt --- +FROM node:20-alpine AS prod-deps +RUN apk add --no-cache python3 make g++ +WORKDIR /deps +COPY server/package*.json ./ +RUN npm ci --omit=dev + +# --- runtime --- +FROM node:20-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production \ + PORT=4000 \ + APOTHECARY_DB=/data/apothecary.db + +COPY --from=prod-deps /deps/node_modules ./node_modules +COPY --from=builder /build/server/dist ./dist +COPY --from=builder /build/server/package.json ./package.json +COPY --from=builder /build/web/dist ./public + +RUN mkdir -p /data +VOLUME ["/data"] +EXPOSE 4000 + +CMD ["node", "dist/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..250bb70 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + apothecary: + image: gitea.thewrightserver.net/josh/apothecary:latest + container_name: apothecary + restart: unless-stopped + ports: + - "4000:4000" + environment: + PORT: "4000" + APOTHECARY_DB: /data/apothecary.db + volumes: + - apothecary-data:/data + +volumes: + apothecary-data: diff --git a/server/src/index.ts b/server/src/index.ts index 51a48aa..e2cb42c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,3 +1,5 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; import cors from "cors"; import express from "express"; import { seedIfEmpty } from "./seed.js"; @@ -5,6 +7,9 @@ import { bootstrapRouter } from "./routes/bootstrap.js"; import { productsRouter } from "./routes/products.js"; import { catalogRouter } from "./routes/catalog.js"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PUBLIC_DIR = path.resolve(__dirname, "..", "public"); + seedIfEmpty(); const app = express(); @@ -15,6 +20,12 @@ app.use("/api", bootstrapRouter); app.use("/api", productsRouter); app.use("/api", catalogRouter); +app.use(express.static(PUBLIC_DIR)); +app.get("*", (req, res, next) => { + if (req.path.startsWith("/api")) return next(); + res.sendFile(path.join(PUBLIC_DIR, "index.html")); +}); + const PORT = Number(process.env.PORT ?? 4000); app.listen(PORT, () => { console.log(`[apothecary] api listening on http://localhost:${PORT}`);