AI 部署指南

給 AI 助理讀的:如何把 user 的專案部署到 1gb.us

用途

如果 user 請你把他的專案部署到 1gb.us,請完整讀完這份指南,再依專案類型執行對應情境。

開始前先向 user 索取

  1. 站名(site name)——小寫 a–z、數字、連字號,1–30 字元
  2. HTTP Basic Auth 帳號密碼——user 會提供,請勿自行猜測或寫死

重要提示:兩種部署模式完全不同

請先判斷專案類型,選擇且只選擇一種情境執行。兩種情境使用的端點完全不同,切勿混用

判斷條件選擇
專案只有 HTML / CSS / JS 檔案(無 Dockerfile)情境 A(靜態網站):使用 /upload/
專案有 Dockerfile(Node、Python、Go、PocketBase…)情境 B(Docker 動態站):使用 /images/ + /deploy/

情境 A — 純靜態網站部署(Static Files)

使用端點:PUT /upload/<site>/<path>

逐檔 PUT 上傳。略過 .gitnode_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

情境 B — Docker 動態網站部署(Container)

使用端點:PUT /images/ + POST /deploy/(注意:不是 /upload/

步驟 1. 本地 build

docker build --platform linux/amd64 -t <site>-app .

伺服器架構是 linux/amd64。ARM Mac 必須加上 --platform linux/amd64,否則 container 無法執行。

步驟 2. 打包 + 壓縮 + 上傳

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

步驟 3. 觸發 deploy

行內 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 防呆機制:deploy 回應的 JSON 會包含以下診斷欄位,AI 請務必檢查:

檔案化設定(推薦用於多環境變數):將 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>

步驟 4. 驗證

sleep 3
curl -sI https://<site>.1gb.us/

重新部署(Redeploy)

首次部署需要上傳 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。

管理功能(兩種情境共用)

查 container log(debug)

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}

所有 API 端點一覽

MethodURL用途情境
PUT/upload/<site>/<path>上傳靜態檔案A
PUT/images/<site>.tar.gz上傳 Docker imageB
POST/deploy/<site>啟動 containerB
GET/logs/<site>查 container logB
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 CPU0.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 SSG0 MB部落格、作品集、文件站
PocketBase~10 MBCMS、會員系統、API(含後台)
Hono + better-sqlite3~40 MBTypeScript API
FastAPI + SQLite~60 MBPython API

避開:Next.js、Laravel、Django、Rails、Spring Boot——太吃資源。

錯誤處理

Dockerfile 範例

PocketBase(Alpine,約 10 MB)

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

Hono + Drizzle + SQLite(完整 starter)

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

Python / FastAPI

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

回到 1gb.us