diff options
| -rw-r--r-- | .dockerignore | 5 | ||||
| -rw-r--r-- | Dockerfile.prod | 44 | ||||
| -rw-r--r-- | README.md | 139 | ||||
| -rw-r--r-- | docker-compose.prod.yml | 27 | ||||
| -rw-r--r-- | nginx.conf | 29 |
5 files changed, 234 insertions, 10 deletions
diff --git a/.dockerignore b/.dockerignore index e06b666..7b46340 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,8 @@ node_modules dist .git *.log +.ask_user.md +ai_prompt.md +docker_readme.md +README.md +.tool-versions diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..a1a792c --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,44 @@ +# -------------------------------------------------------------------------- +# Dockerfile.prod — Production multi-stage build for the Bicycle Wheel +# Circumference app. +# +# Stage 1 (build): +# • Installs dependencies and runs `npm run build` to produce a static +# bundle in /app/dist. +# +# Stage 2 (production): +# • Copies the built assets into an Nginx Alpine image. +# • Uses a custom nginx.conf that handles SPA routing (all paths +# fall back to index.html). +# • Serves on port 80 — Traefik/Dokploy will handle TLS termination. +# -------------------------------------------------------------------------- + +# ---- Build stage ---- +FROM node:25-alpine AS build + +WORKDIR /app + +# Install dependencies first (layer caching optimisation) +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy the rest of the source and build +COPY . . +RUN npm run build + +# ---- Production stage ---- +FROM nginx:stable-alpine AS production + +# Remove default Nginx site config +RUN rm /etc/nginx/conf.d/default.conf + +# Add custom Nginx config for SPA routing +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built assets from the build stage +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +# Nginx runs in the foreground by default with the official image +CMD ["nginx", "-g", "daemon off;"] @@ -2,20 +2,139 @@ A small learning app (React + TypeScript + Vite) to measure bicycle wheel circumference by rolling the wheel or by entering diameter. This project is a port of the tire size calculator that once lived on the Cateye website but was removed. It was reimplemented here for usefulness and learning. -## Quick start +## Tech -- Build: - docker compose build +React · TypeScript · Vite · TailwindCSS · DaisyUI · Docker · Nginx -- Start (detached): - docker compose up -d +--- -- Stop and remove containers/networks: - docker compose down +## Development (Docker) -Served on port 5173 — open http://127.0.0.1:5173 or http://localhost:5173 +```bash +docker compose build +docker compose up -d +``` -## Tech +Served on port 5173 — open http://localhost:5173 + +See [docker_readme.md](docker_readme.md) for the full development Docker guide. + +--- + +## Production Static Build (Docker) + +The production setup uses a **multi-stage Docker build**: + +1. **Build stage** — Installs dependencies and runs `npm run build` to produce static assets in `dist/`. +2. **Production stage** — Copies the built assets into an Nginx Alpine image that serves them on port 80. + +### Build and run locally + +```bash +docker compose -f docker-compose.prod.yml up -d --build +``` + +Then open http://localhost:80. + +> **Note:** The `ports` mapping in `docker-compose.prod.yml` is commented out by default (intended for Dokploy/Traefik). Uncomment the `ports` line for local testing: +> +> ```yaml +> ports: +> - "80:80" +> ``` + +### Files involved + +| File | Purpose | +| ------------------------- | -------------------------------------------------------- | +| `Dockerfile.prod` | Multi-stage build: Node (build) → Nginx (serve) | +| `docker-compose.prod.yml` | Production compose file (single `web` service) | +| `nginx.conf` | Nginx config with SPA fallback routing and gzip | + +--- + +## Deploying to Dokploy + +Dokploy is a self-hosted PaaS that uses Traefik as a reverse proxy. The production compose file is ready for Dokploy out of the box. + +### Prerequisites + +- A VPS with [Dokploy installed](https://docs.dokploy.com/get-started/installation) +- A domain name with a DNS **A record** pointing to your server's IP +- Your GitHub repository connected to Dokploy (see below) + +### Step 1 — Connect GitHub to Dokploy + +1. Log in to the Dokploy dashboard (`http://<your-server-ip>:3000`). +2. Go to **Settings → Git Providers**. +3. Click **"+ Add Git Provider"** and follow the prompts to create a **GitHub App**. +4. After creation, click **"Install"** next to the provider and grant access to this repository (or all repositories). + +### Step 2 — Create a Project + +1. Go to **Dashboard → Projects**. +2. Click **"+ Create Project"**, give it a name (e.g. "Tire Calculator"), and click **"Create"**. + +### Step 3 — Create a Compose Service + +1. Inside the project, click **"+ Create Service"** → **"Compose"**. +2. Fill in: + - **Name**: e.g. "tirecalc" + - **Compose Type**: `docker-compose` (default) +3. Click **"Create"**. + +### Step 4 — Configure the Source + +1. On the compose page, set the source to **"GitHub"**. +2. Select your **GitHub Provider**, **Repository**, and **Branch** (e.g. `main`). +3. Set **Compose Path** to: + ``` + ./docker-compose.prod.yml + ``` +4. Click **"Save"**. + +### Step 5 — Add a Domain + +1. Go to the **"Domains"** tab. +2. Click **"+ Add Domain"** and fill in: + - **Host**: your domain (e.g. `tirecalc.example.com`) + - **Service Name**: `web` (must match the service name in `docker-compose.prod.yml`) + - **Port**: `80` + - **HTTPS**: toggle **on** + - **Certificate Type**: `letsencrypt` +3. Click **"Save"**. + +> Make sure your DNS A record is already pointing to the server. Let's Encrypt needs this to issue a certificate. + +### Step 6 — Deploy + +1. Click the **"Deploy"** button. +2. Monitor progress in the **"Deployments"** tab. +3. Once the status shows **"Done" ✅**, the app is live at your domain. + +### Step 7 — Enable Auto-Deploy (optional) + +Auto-deploy is enabled by default when using the GitHub App integration. Every push to the configured branch will trigger a new deployment automatically. + +If you prefer manual webhook control: + +1. Find the **webhook URL** in the compose settings: + ``` + https://<your-dokploy-host>/api/deploy/compose/<refreshToken> + ``` +2. In your GitHub repo, go to **Settings → Webhooks → Add webhook**. +3. Set: + - **Payload URL**: the webhook URL above + - **Content type**: `application/json` + - **Events**: "Just the push event" +4. Click **"Add webhook"**. -React - TypeScript - Vite - TailwindCSS - DaisyUI - Docker +### Troubleshooting +| Problem | Likely cause | Fix | +| --- | --- | --- | +| 502 Bad Gateway | Container not running or wrong port | Check deployment logs; verify domain port is `80` and service name is `web` | +| Site not loading | DNS not configured | Add an A record pointing your domain to the server IP | +| SSL error | DNS not propagated yet | Wait a few minutes for propagation; check Traefik logs | +| Build fails | Missing dependencies or TypeScript errors | Check deployment logs for build output; run `npm run build` locally to reproduce | +| Stale content after deploy | Browser cache | Hard-refresh (`Ctrl-Shift-R`); Vite hashes asset filenames so this is rare | diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..131e865 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------- +# docker-compose.prod.yml — Production deployment for the Bicycle Wheel +# Circumference app. +# +# Builds a static bundle and serves it with Nginx on port 80. +# Designed for use with Dokploy (Traefik handles TLS and routing). +# +# Local usage: +# docker compose -f docker-compose.prod.yml up -d --build +# → open http://localhost:80 +# +# Dokploy usage: +# Set the Compose Path to ./docker-compose.prod.yml in Dokploy. +# Dokploy will inject Traefik labels and the dokploy-network +# automatically — do NOT add them here. +# -------------------------------------------------------------------------- + +services: + web: + build: + context: . + dockerfile: Dockerfile.prod + restart: unless-stopped + # No ports mapping needed when behind Traefik/Dokploy. + # Uncomment the line below for local testing without a reverse proxy. + # ports: + # - "80:80" diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..f36fef4 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,29 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Serve static files directly; fall back to index.html for SPA routing + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets aggressively (Vite hashes filenames) + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Don't log favicon/robots requests + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + # Gzip compression for text-based assets + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; +} |
