A self-hosted CMS-powered notes application deployed on Alibaba Cloud ECS via BaoTa Panel, featuring ISR static acceleration, Lexical rich text editing, and OSS image pipeline.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 + Turbopack |
| CMS | Payload CMS 3 |
| Database | SQLite |
| Storage | Alibaba Cloud OSS (S3-compatible) |
| Styling | Tailwind CSS 4 + shadcn/ui |
| Language | TypeScript |
Browser → nginx → Docker → SQLite + OSS
Build time: generateStaticParams() → pre-render /posts/[slug] as static HTML
Edit time: Payload afterChange hook → POST /api/revalidate → revalidatePath()
Runtime: pages served from cache, auto-regenerate every 60s
Images are stored on Alibaba Cloud OSS with on-the-fly resize. The app server is completely bypassed for image delivery.
- Node.js 22+
- pnpm
git clone <repo-url>
cd payload-notes
cp .env.example .env.local
# Edit .env.local with your OSS credentials
pnpm dev # http://localhost:3000| Script | Purpose |
|---|---|
pnpm dev |
Start dev server with Turbopack |
pnpm build |
Production build |
pnpm lint |
ESLint |
pnpm payload:gen-importmap |
Regenerate Payload admin import map |
pnpm docker:build |
Build Docker image (tag: commitHash-timestamp) |
pnpm docker:push |
Push latest local image to ACR |
One-time setup to prepare the server environment.
Install BaoTa Panel
ECS console → Instance details → Extensions → Search "BaoTa" → Install
Install via BaoTa
- Docker Manager
- Nginx
Configure ACR
- BaoTa → Docker → Image Registry → Add Registry
- Fill in ACR registry address, username, and password
Prepare environment variables
scp .env.local root@<ECS_IP>:/opt/notes/.env.localReference .env.example for all required variables.
Option A: Local build
pnpm docker:build # tag: commitHash-timestamp
pnpm docker:push # push to ACR
# Or specify a version tag
TAG=v1.1.0 pnpm docker:build
TAG=v1.1.0 pnpm docker:pushFirst time, login to ACR locally:
docker login <ACR_REGISTRY> -u <username>Option B: CI build (GitHub Actions)
Push a git tag to trigger automatic build and push:
git tag v1.1.0
git push origin v1.1.0CI reuses the same docker/build.sh and docker/push.sh scripts.
First deploy:
- Create data directory:
mkdir -p /opt/notes/db - BaoTa → Docker → Image Management → Pull your image
- Create container:
- Port:
127.0.0.1:3000:3000 - Volume:
/opt/notes/db:/app/db - Env vars: from
.env.local - Restart: Always
- Port:
- BaoTa → Websites → Add Site → Reverse Proxy:
- Name:
notes-app - Target:
http://127.0.0.1:3000
- Name:
- BaoTa → Websites → Site Settings → Config → Add to
location /block:proxy_set_header Origin "https://$host";
- BaoTa → Websites → SSL → Issue Let's Encrypt certificate
Update deploy:
- BaoTa → Docker → Image Management → Pull latest image
- BaoTa → Docker → Containers → Recreate with same config
SQLite database is stored at /opt/notes/db/database.db on ECS:
# Manual backup
cp /opt/notes/db/database.db /opt/notes/db/backup-$(date +%Y%m%d).db
# Keep last 7 days
find /opt/notes/db -name "backup-*.db" -mtime +7 -deletecurl http://localhost:3000/api/health
# Returns: { "status": "ok" }docker logs notes-app
docker ps -a
docker exec -it notes-app sh
docker restart notes-appSee docs/pitfalls.md for real-world issues encountered during development and deployment.
MIT