Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ node_modules/
tmp/
*.log
rowetech
bin/
dist/

# Database
data/*.db
Expand Down
68 changes: 68 additions & 0 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -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.
66 changes: 33 additions & 33 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]