給 AI 助理讀的:如何把 user 的專案部署到 1gb.us
如果 user 請你把他的專案部署到 1gb.us,請完整讀完這份指南,再依專案類型執行對應情境。
請先判斷專案類型,選擇且只選擇一種情境執行。兩種情境使用的端點完全不同,切勿混用。
| 判斷條件 | 選擇 |
|---|---|
| 專案只有 HTML / CSS / JS 檔案(無 Dockerfile) | → 情境 A(靜態網站):使用 /upload/ |
| 專案有 Dockerfile(Node、Python、Go、PocketBase…) | → 情境 B(Docker 動態站):使用 /images/ + /deploy/ |
使用端點:PUT /upload/<site>/<path>
逐檔 PUT 上傳。略過 .git、node_modules、原始檔資料夾;上傳 build 後的成品。
curl -u <user>:<pass> -T ./index.html \
https://deploy.1gb.us/upload/<site>/index.html
curl -u <user>:<pass> -T ./assets/style.css \
https://deploy.1gb.us/upload/<site>/assets/style.css
# 驗證:
curl -sI https://<site>.1gb.us/ # 預期 200
使用端點:PUT /images/ + POST /deploy/(注意:不是 /upload/)
docker build --platform linux/amd64 -t <site>-app .
伺服器架構是 linux/amd64。ARM Mac 必須加上 --platform linux/amd64,否則 container 無法執行。
macOS / Linux:
docker save <site>-app | gzip | curl -u <user>:<pass> \
-T - https://deploy.1gb.us/images/<site>.tar.gz
Windows(PowerShell)注意:PowerShell 的 | 管道會損壞二進制串流,導致上傳的 tar.gz 解壓失敗。請改為分步執行:
# Windows PowerShell 分步指令:
docker save <site>-app -o app.tar
# 用 7-Zip 或 gzip 工具壓縮成 app.tar.gz,然後:
curl -u <user>:<pass> -T app.tar.gz `
https://deploy.1gb.us/images/<site>.tar.gz
行內 JSON(簡單場景):
curl -u <user>:<pass> -X POST \
-H 'Content-Type: application/json' \
-d '{"port": 8090, "env": {"KEY": "value"}}' \
https://deploy.1gb.us/deploy/<site>
port(選填):app 在 container 內監聽的 port。如果省略,平台會自動從 Dockerfile 的 EXPOSE 偵測。如果指定了錯誤的 port,部署會成功但網站打不開(回 "Not deployed")。env(選填):環境變數 key-value⚠️ Port 防呆機制:deploy 回應的 JSON 會包含以下診斷欄位,AI 請務必檢查:
"reachable": true → 部署成功且已驗證連通"reachable": false + "warnings" → 平台偵測到問題:
PORT_MISMATCH:指定的 port 沒回應,但偵測到另一個 port 有回應。依 warnings[0].fix 的指示重新 deploy 即可修復。NOT_REACHABLE:container 完全沒回應。請檢查 app 是否 bind 到 0.0.0.0(不是 127.0.0.1),以及 log 是否有錯。"port_source":告訴你 port 從哪裡來(user-specified / auto-detected from EXPOSE / default)檔案化設定(推薦用於多環境變數):將 JSON 存成 deploy.json,便於版控與閱讀:
# deploy.json
{
"port": 3000,
"env": {
"DATABASE_URL": "sqlite:///data/app.db",
"NODE_ENV": "production",
"API_KEY": "your-key-here"
}
}
curl -u <user>:<pass> -X POST \
-H 'Content-Type: application/json' \
-d @deploy.json \
https://deploy.1gb.us/deploy/<site>
sleep 3
curl -sI https://<site>.1gb.us/
首次部署需要上傳 tarball(步驟 2 + 3)。之後如果只是重啟或更新設定,直接 POST /deploy 即可,不需要重新上傳——平台會自動重用已載入的 Docker image。
# 重新部署(不需要重新上傳 image)
curl -u <user>:<pass> -X POST \
-H 'Content-Type: application/json' \
-d '{"port": 3000}' \
https://deploy.1gb.us/deploy/<site>
注意:如果你用 DELETE 刪掉了站,image 也會一併移除。之後要重新部署就必須再上傳一次 tarball。
curl -u <user>:<pass> https://deploy.1gb.us/logs/<site>
curl -u <user>:<pass> -X DELETE \
https://deploy.1gb.us/deploy/<site>
# 回: {"ok": true, "site": ..., "container_removed": true,
# "image_removed": true, "data_removed": true}
| Method | URL | 用途 | 情境 |
|---|---|---|---|
| PUT | /upload/<site>/<path> | 上傳靜態檔案 | A |
| PUT | /images/<site>.tar.gz | 上傳 Docker image | B |
| POST | /deploy/<site> | 啟動 container | B |
| GET | /logs/<site> | 查 container log | B |
| DELETE | /deploy/<site> | 刪站(container + image + data) | A / B |
| 項目 | 限制 |
|---|---|
| 站名 | a–z、0–9、連字號;1–30 字元 |
| 保留字(禁用) | www, api, deploy, ssh, admin, test, root, mail, blog, traefik |
| Container 記憶體 | 200 MB 硬上限 |
| Container CPU | 0.5 核 |
| 單檔上傳 | 200 MB |
| Docker image 上傳 | 500 MB |
| CPU 架構 | 僅支援 linux/amd64 |
Container 內寫到 /data/ 的檔案會在重新部署後保留。適合存:SQLite 檔、上傳的圖片、PocketBase 資料等。
| 選項 | 狀態 |
|---|---|
SQLite(檔案放 /data/) | ✅ 推薦 |
| 外部 DB(Supabase / Neon)用環境變數連線 | ✅ 可用 |
| 自己帶 MySQL / Postgres container | ❌ 禁止——server 只有 1 GB RAM |
| 技術 | 記憶體 | 適合 |
|---|---|---|
| 純 HTML / Astro SSG | 0 MB | 部落格、作品集、文件站 |
| PocketBase | ~10 MB | CMS、會員系統、API(含後台) |
| Hono + better-sqlite3 | ~40 MB | TypeScript API |
| FastAPI + SQLite | ~60 MB | Python API |
避開:Next.js、Laravel、Django、Rails、Spring Boot——太吃資源。
detail 欄位,請呈現給 userport 設錯,或 image 還沒上傳完FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY pocketbase /pocketbase
EXPOSE 8090
CMD ["/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/data/pb_data"]
TypeScript 全棧,約 40 MB RAM。SQLite 檔案放 /data/app.db,重部署不會掉。
專案結構:
my-app/
├── Dockerfile
├── package.json
├── tsconfig.json
├── drizzle.config.ts
└── src/
├── index.ts # Hono app
├── db.ts # Drizzle + SQLite
└── schema.ts # Table 定義
package.json:
{
"name": "my-app",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:push": "drizzle-kit push"
},
"dependencies": {
"hono": "^4.6.0",
"@hono/node-server": "^1.13.0",
"better-sqlite3": "^11.3.0",
"drizzle-orm": "^0.36.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.0",
"@types/node": "^22.0.0",
"drizzle-kit": "^0.28.0",
"tsx": "^4.19.0",
"typescript": "^5.6.0"
}
}
src/schema.ts:
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
src/db.ts:
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema.js';
import { mkdirSync } from 'fs';
mkdirSync('/data', { recursive: true });
const sqlite = new Database('/data/app.db');
sqlite.pragma('journal_mode = WAL');
export const db = drizzle(sqlite, { schema });
src/index.ts:
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { db } from './db.js';
import { posts } from './schema.js';
import { desc, eq } from 'drizzle-orm';
// 首次啟動建表
db.run(`CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at INTEGER NOT NULL
)`);
const app = new Hono();
app.get('/', (c) => c.html(`
<h1>My Hono App</h1>
<p><a href="/api/posts">GET /api/posts</a></p>
`));
app.get('/api/posts', async (c) => {
const all = await db.select().from(posts).orderBy(desc(posts.createdAt));
return c.json(all);
});
app.post('/api/posts', async (c) => {
const body = await c.req.json();
const [row] = await db.insert(posts)
.values({ title: body.title, content: body.content })
.returning();
return c.json(row, 201);
});
app.delete('/api/posts/:id', async (c) => {
const id = Number(c.req.param('id'));
await db.delete(posts).where(eq(posts.id, id));
return c.json({ ok: true });
});
const port = 3000;
serve({ fetch: app.fetch, port });
console.log(`listening on ${port}`);
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Dockerfile:
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
FROM node:20-alpine
WORKDIR /app
RUN apk add --no-cache python3 make g++ \
&& npm i -g [email protected] \
&& apk del python3 make g++
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
部署步驟:
# 1. 本地 build(ARM Mac 記得加 platform)
docker build --platform linux/amd64 -t myapp .
# 2. 上傳
docker save myapp | gzip | curl -u <user>:<pass> \
-T - https://deploy.1gb.us/images/<site>.tar.gz
# 3. 啟動(port 3000)
curl -u <user>:<pass> -X POST \
-H 'Content-Type: application/json' \
-d '{"port": 3000}' \
https://deploy.1gb.us/deploy/<site>
# 4. 驗證
curl https://<site>.1gb.us/api/posts
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]