diff --git a/.dockerignore b/.dockerignore index cd7103a..388382b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,8 @@ node_modules/ tmp/ *.log rowetech +bin/ +dist/ # Database data/*.db diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..6a05f9a --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,68 @@ +# Deployment + +This project has two deploy targets: + +| Target | What it runs | Status | +|--------|--------------|--------| +| **Vercel** (`vercel.json` + `make build-static`) | A **static snapshot** exported by `cmd/build` (no server, no DB, no live admin/contact) | Current — leave in place until cutover | +| **Dokploy** (`Dockerfile`) | The **real dynamic app** (`cmd/server`) — live admin, contact form persistence, SQLite | Go-live target | + +The two are independent: the `Dockerfile`/`.dockerignore` don't touch the Vercel +path, and `make build-static` / `vercel.json` are untouched. + +## Dokploy setup + +The app is a single static binary (Go + embedded goose migrations) serving on +`PORT` (default `3000`), with SQLite stored under `/app/data`. + +1. **Create an Application** in Dokploy pointing at this Git repo, branch `main`. +2. **Build Type: Dockerfile** — Dokploy will use the repo's `Dockerfile` + (multi-stage; `CGO_ENABLED=0`, ~28 MB final image). +3. **Add a persistent Volume** so the database survives redeploys: + | Setting | Value | + |---------|-------| + | Mount Type | Volume Mount | + | Volume Name | `rowetech-data` | + | Mount Path | `/app/data` | +4. **Environment variables:** + + | Variable | Required | Notes | + |----------|----------|-------| + | `ENV` | — | Defaults to `production` (set in the Dockerfile). JSON logs. | + | `PORT` | — | Defaults to `3000`. The app binds whatever `PORT` is set. | + | `DATABASE_URL` | — | Defaults to `/app/data/rowetech.db` (inside the volume). | + | `SITE_NAME` | recommended | e.g. `RoweTech Machine & Engineering` | + | `SITE_URL` | recommended | Public URL, e.g. `https://rowetech.example.com` (used in meta tags). | + | `CLERK_SECRET_KEY` | **yes (see warning)** | Enables admin auth. | + | `CLERK_PUBLISHABLE_KEY` | **yes (see warning)** | Enables admin auth. | + | `ADMIN_EMAILS` | recommended | Comma-separated admin emails allowed into `/admin`. | + | `SMTP_HOST` / `SMTP_PORT` / `SMTP_USERNAME` / `SMTP_PASSWORD` / `SMTP_FROM` | optional | Enables contact-form notification emails. | + | `CONTACT_NOTIFICATION_EMAILS` | optional | Where contact submissions are emailed (defaults to admin emails). | + +5. **Health check path:** `/health` (returns `200 {"status":"ok"}`). +6. **Domain:** point your domain at the app; container listens on `PORT`. +7. **Deploy.** Migrations run automatically on boot (goose, embedded), so the + volume is brought to schema on first start and after any migration changes. + +> ### ⚠️ Security: configure Clerk before exposing publicly +> Admin pages (`/admin/*`) are gated by Clerk. **If `CLERK_SECRET_KEY` / +> `CLERK_PUBLISHABLE_KEY` are not set, `/admin` is unauthenticated and open to +> anyone.** Always set the Clerk keys (and `ADMIN_EMAILS`) on the Dokploy app +> before pointing a public domain at it. +> +> Note: the dev-only auth bypass (`/auth/dev/login`) is compiled **out** of the +> production image (it requires `-tags dev`), so it cannot be reached on Dokploy. + +## Local container test + +```bash +docker build -t rowetech:local . +docker run --rm -p 3010:3000 -v rowetech-data:/app/data rowetech:local +# then: curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3010/health +``` + +## Vercel (current, until cutover) + +No change. Vercel runs `make build-static` (`installCommand`/`buildCommand` in +`vercel.json`), which exports the static site to `dist/`. Leave it as-is until +the Dokploy deployment is verified live, then cut the domain over. diff --git a/Dockerfile b/Dockerfile index 2e73f1d..4b61e99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,53 +1,53 @@ -# Build stage -FROM golang:1.23-alpine AS builder +# syntax=docker/dockerfile:1 -WORKDIR /app +# ---- Build stage ---- +FROM golang:1.26-alpine AS builder + +# Node is needed only to build the Tailwind CSS bundle. +RUN apk add --no-cache nodejs npm -# Install build dependencies -RUN apk add --no-cache git nodejs npm +WORKDIR /app -# Copy go mod files +# Go module cache layer. COPY go.mod go.sum ./ RUN go mod download -# Install templ -RUN go install github.com/a-h/templ/cmd/templ@latest +# Node deps for Tailwind (cached separately from source). +COPY package.json package-lock.json* ./ +RUN npm install -# Copy source code +# Application source. COPY . . -# Generate templ files -RUN templ generate - -# Build CSS -RUN npm install && npm run css +# Generate templ + sqlc via the versions pinned in go.mod's `tool` directive +# (go tool), build the minified CSS, then compile a static binary. +# CGO_ENABLED=0 is safe because modernc.org/sqlite is pure Go. +RUN go tool templ generate \ + && go tool sqlc generate -f sqlc/sqlc.yaml \ + && npx @tailwindcss/cli -i static/css/input.css -o static/css/output.css --minify \ + && CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/server ./cmd/server -# Build the Go binary -RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server +# ---- Runtime stage ---- +FROM alpine:3.21 -# Runtime stage -FROM alpine:latest +RUN apk add --no-cache ca-certificates tzdata WORKDIR /app -# Install ca-certificates for HTTPS -RUN apk --no-cache add ca-certificates - -# Copy binary and static files -COPY --from=builder /app/server . +# Binary + static assets. Goose migrations are embedded in the binary +# (migrations/embed.go), so they are not copied here. +COPY --from=builder /app/server ./server COPY --from=builder /app/static ./static -COPY --from=builder /app/migrations ./migrations -# Create data directory -RUN mkdir -p /app/data +# SQLite database lives here. Mount a persistent Dokploy volume at /app/data +# so data survives redeploys (DATABASE_URL points inside it). The app creates +# the directory on boot if missing. +VOLUME ["/app/data"] -# Expose port -EXPOSE 3000 +ENV PORT=3000 \ + ENV=production \ + DATABASE_URL=/app/data/rowetech.db -# Set environment variables -ENV PORT=3000 -ENV ENV=production -ENV DATABASE_URL=/app/data/rowetech.db +EXPOSE 3000 -# Run the server CMD ["./server"]