fix(deploy): switch from nixpacks to Dockerfile + change branch to main
Some checks failed
Build & Deploy to EasyPanel / build-and-deploy (push) Has been cancelled
Lint & Test / build-check (push) Has been cancelled

Nixpacks auto-detect could not find a 'start' script in package.json
and bailed out. Astro builds to static files in dist/ — there is no
Node server to start. Switching to a Dockerfile + nginx fixes the
'No start command could be found' error from EasyPanel.

The workflow also pointed at the source-code branch, but the panel's
git source ref for the dealplustech-astro service is 'main', so the
trigger was firing for the wrong ref. Both workflows now run on push
to main.

- Dockerfile: multi-stage node:20-alpine build + nginx:1.27-alpine serve
- nginx.conf: gzip, security headers, 1-year cache for hashed assets,
  try_files fallback for UTF-8 slugs (Astro file-based routing)
- .dockerignore: keep build context small (skip CI, docs, .gitea, IDE)
- build-and-deploy.yml + lint.yml: branch source-code -> main
- docs/ci-setup.md: corrected project + service names (customerwebsite
  / dealplustech-astro), documented the Dockerfile rationale, added a
  note for the 'Failed to sync changes' server-side error
This commit is contained in:
hermes
2026-06-09 10:28:46 +07:00
parent 3efaf4d661
commit d73e48351f
6 changed files with 165 additions and 18 deletions

34
.dockerignore Normal file
View File

@@ -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

View File

@@ -3,7 +3,7 @@ name: Build & Deploy to EasyPanel
on:
push:
branches:
- source-code
- main
workflow_dispatch:
jobs:

View File

@@ -3,10 +3,10 @@ name: Lint & Test
on:
push:
branches:
- source-code
- main
pull_request:
branches:
- source-code
- main
jobs:
build-check:

29
Dockerfile Normal file
View File

@@ -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;"]

View File

@@ -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.

55
nginx.conf Normal file
View File

@@ -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;
}
}