diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..926e7a2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Build artifacts +node_modules/ +dist/ +.astro/ + +# CI / Gitea +.gitea/ +.github/ + +# Local dev / IDE +.DS_Store +.vscode/ +.idea/ +*.log +*.tmp +.env +.env.* +!.env.example + +# Git +.git/ +.gitignore + +# Docker (don't include Docker files in the build context of themselves) +Dockerfile +.dockerignore +docker-compose*.yml + +# Docs +docs/ + +# Misc +coverage/ +*.tsbuildinfo diff --git a/.gitea/workflows/build-and-deploy.yml b/.gitea/workflows/build-and-deploy.yml index d7ad58e..771c418 100644 --- a/.gitea/workflows/build-and-deploy.yml +++ b/.gitea/workflows/build-and-deploy.yml @@ -3,7 +3,7 @@ name: Build & Deploy to EasyPanel on: push: branches: - - source-code + - main workflow_dispatch: jobs: diff --git a/.gitea/workflows/lint.yml b/.gitea/workflows/lint.yml index 566fd00..d260dac 100644 --- a/.gitea/workflows/lint.yml +++ b/.gitea/workflows/lint.yml @@ -3,10 +3,10 @@ name: Lint & Test on: push: branches: - - source-code + - main pull_request: branches: - - source-code + - main jobs: build-check: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b8157d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# ===================================================================== +# Stage 1: Build the Astro static site +# ===================================================================== +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install deps with cache layer +COPY package*.json ./ +RUN npm ci --no-audit --no-fund + +# Build +COPY . . +RUN npm run build + +# ===================================================================== +# Stage 2: Serve with nginx +# ===================================================================== +FROM nginx:1.27-alpine + +# Astro outputs to ./dist by default +COPY --from=builder /app/dist /usr/share/nginx/html + +# nginx config: SPA-friendly, gzip, cache headers for static assets +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docs/ci-setup.md b/docs/ci-setup.md index 918b143..023593e 100644 --- a/docs/ci-setup.md +++ b/docs/ci-setup.md @@ -1,6 +1,6 @@ # CI/CD Setup — EasyPanel Deploy -Push to `source-code` triggers `build-and-deploy.yml`: +Push to `main` triggers `build-and-deploy.yml`: 1. Builds the Astro static site into `dist/` 2. Uploads `dist/` as a 7-day artifact 3. Calls EasyPanel tRPC endpoint to trigger a redeploy @@ -9,11 +9,14 @@ Push to `source-code` triggers `build-and-deploy.yml`: Go to **Settings → Actions → Secrets** and add three secrets: -| Name | Example | Where to get it | +| Name | Value (for this site) | Where to get it | |---|---|---| -| `EASYPANEL_TOKEN` | `cmq61ao6h000207qn6mmp2i7u` | EasyPanel → profile/settings → API tokens | -| `EASYPANEL_PROJECT_NAME` | `dealplustech-astroreal` | EasyPanel → project name in the dashboard | -| `EASYPANEL_SERVICE_NAME` | `web` (or whatever you named the app service) | EasyPanel → service name inside the project | +| `EASYPANEL_TOKEN` | `cmq61xwrv000407qn9e2hhfuw` | EasyPanel → profile/settings → API tokens | +| `EASYPANEL_PROJECT_NAME` | `customerwebsite` | EasyPanel → project name in the dashboard | +| `EASYPANEL_SERVICE_NAME` | `dealplustech-astro` | EasyPanel → service name inside the project | + +> ⚠️ Project name ≠ repo name. The site lives in the `customerwebsite` +> project on the panel; the repo is `dealplustech-astroreal`. If any of these are empty, the workflow logs a warning and skips the deploy trigger. The build still runs and the artifact is still uploaded. @@ -22,20 +25,36 @@ trigger. The build still runs and the artifact is still uploaded. The service on the panel side must be: -- **Type: `app`** (Dockerfile-based). The trigger calls - `services.app.deployService` — other service types use different - procedures and won't work. +- **Type: `app`** - **Source: Git**, pointing at this repo (`kunthawat/dealplustech-astroreal`) - on branch `source-code`. -- **Build command:** `npm run build` -- **Output dir:** `dist` + on branch **`main`** (not `source-code`). +- **Build type: `dockerfile`**, **file: `Dockerfile`** at the repo root. + > The workflow used to trigger nixpacks builds, but nixpacks expects a + > `start` script in `package.json` and an Astro static site doesn't + > have one. Switching to a Dockerfile + nginx fixes that. +- **Port: `80`** If you need a different service type later, swap the endpoint in `.gitea/workflows/build-and-deploy.yml` to the matching procedure: -- `services.app.deployService` (Dockerfile) +- `services.app.deployService` (Dockerfile / app) - `services.box.rebuildDockerImage` (low-level) - `services.compose.deployService` (docker-compose) +## Why Dockerfile and not nixpacks + +Astro builds to static files in `dist/` — there is no Node server to run. +Nixpacks tries to find a `start` command and fails. The Dockerfile in +this repo: + +1. **Stage 1** (`node:20-alpine`): runs `npm ci` then `npm run build` to + produce `dist/`. +2. **Stage 2** (`nginx:1.27-alpine`): copies `dist/` to nginx's web root + and serves it. + +`nginx.conf` ships gzip, security headers, 1-year cache for hashed +assets, and a `try_files` fallback for client-side routes (Astro's +file-based routing produces UTF-8 slugs). + ## Verifying the trigger payload If the deploy runs but the panel rejects it, the response in the workflow @@ -47,10 +66,20 @@ To test the payload shape from your local machine: ```bash curl -sS -X POST \ "https://panelwebsite.moreminimore.com/api/trpc/services.app.deployService" \ - -H "Authorization: Bearer *** \ - -H "Content-Type: application/json" \ - -d '{"json":{"projectName":"YOUR_PROJECT","serviceName":"YOUR_SERVICE"}}' + -H "Authorization: Bearer *** -H "Content-Type: application/json" \ + -d '{"json":{"projectName":"customerwebsite","serviceName":"dealplustech-astro"}}' ``` A 2xx response = the panel accepted the trigger. The service will start rebuilding/redploying in the EasyPanel dashboard. + +## Common failure: "Failed to sync changes" (HTTP 500) + +This is a **server-side** error, not a payload problem. It usually means: + +- The EasyPanel service is currently in a state that can't be redeployed + (e.g. the previous container is stuck, or a build is in progress). +- The Docker daemon on the panel host is busy. + +Try again in a few seconds. If it persists, open the EasyPanel dashboard +and check the service's container state. diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d387d8a --- /dev/null +++ b/nginx.conf @@ -0,0 +1,55 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # gzip + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/javascript + text/xml + application/javascript + application/json + application/xml + application/xml+rss + image/svg+xml + font/ttf + font/otf; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Cache static assets aggressively (Astro hashes filenames) + location ~* \.(?:js|css|woff2?|ttf|otf|eot|svg|jpg|jpeg|png|webp|avif|ico)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Don't cache HTML + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # Thai URL slugs (Astro file-based routing produces paths with UTF-8 chars) + location / { + try_files $uri $uri/ $uri.html /index.html; + } + + # healthcheck + location /healthz { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } +}