Frontdoor · Design 2026-05-11

Frontdoor — Self-hosted Docker Registry on R2

Status: Proposal — chưa implement Author: Giang  •  Date: 2026-05-11 Companion docs: SSH-only Workspaces · Batch Job Queue


1. Vấn đề hiện tại

Mọi image (frontdoor/workspace, frontdoor-agent) build local trên Mac, không push đâu cả. Khi:

Cần registry trung tâm để:

  1. Push image build từ Mac (hoặc CI), pull từ worker khác.
  2. Mirror các public image (Docker Hub, ghcr.io) → tăng tốc + giảm rate limit.
  3. Lưu image private (workspace image có thể chứa SSH keys pre-loaded, secrets từ Wrangler).

2. Lựa chọn: ghcr.io vs Docker Hub vs self-host

ghcr.io Docker Hub Self-host registry:2
Setup Tạo PAT, 0 server Tạo account, 0 server 1 docker run
Cost Free unlimited (public), free 500MB private Free 1 private $0.015/GB/tháng R2
Pull rate limit Không (auth) 100/6h (anon) — 200/6h (free) Không
Egress to Lenovo (qua CF Tunnel) Internet Internet Trong CF network (free)
Mirror Docker Hub public images Không Không Có (pull-through cache)
Private image Có (free 500MB) Có (1 free) Có (R2 storage)
Multi-arch
Vulnerability scan, signing, RBAC Có (basic) Có (paid) Cần Harbor
Setup complexity Rất thấp Rất thấp Trung bình
Maintenance 0 0 GC định kỳ, backup

Đề xuất hybrid

Image Lưu ở Lý do
frontdoor-agent (public, mã nguồn mở) ghcr.io Public, free, không secrets
frontdoor/workspace (private, có thể pre-config) registry.6iang.com (self-host) Kiểm soát, có thể chứa config private
Public batch images (python, node, ffmpeg…) registry.6iang.com (mirror) Pull-through cache → giảm Docker Hub rate limit

3. Kiến trúc

Build / developer machine docker build docker login docker push Cloudflare edge DNS · registry.6iang.commirror.6iang.com CF Tunnel(outbound only) Worker host (Mac mini) Worker host stack Traefik :80/:443 registry:2 (port 5000) registry-mirror :5000 Cloudflare R2 — storage backend frontdoor-registry(private images) frontdoor-registry-mirror(cached public images) S3-compatible API · region: auto · endpoint: .r2.cloudflarestorage.com s3 driver

Egress R2 → Mac/Lenovo through CF Tunnel = free (in-network)

Components:


4. Push/Pull flow

Dev / CI Worker CF Tunnel registry:2 R2 bucket

— PUSH (build host) — docker login (htpasswd) 200 OK docker push registry.6iang.com/img:tag PUT /docker/registry/v2/blobs/... ETag PUT manifests

— PULL (worker) — docker pull registry.6iang.com/img:tag GET manifests manifest.json GET blobs (in parallel) image layers

— MIRROR (public image cache) — docker pull python:3.12 (configured registry-mirror) cached blob (no Docker Hub hit)

Push (từ máy build):

  1. docker login registry.6iang.com -u giang → htpasswd verify
  2. docker push registry.6iang.com/frontdoor/workspace:v1
  3. Layers upload qua CF Tunnel → registry → s3 driver → R2 bucket
  4. Manifest commit cuối cùng

Pull (từ worker):

  1. Docker daemon nhận request pull image
  2. Resolve registry.6iang.com qua DNS → CF
  3. CF Tunnel route → registry container
  4. registry serve manifest + blob từ R2
  5. Worker cache layers vào /var/lib/docker/overlay2

Mirror (pull-through cache cho public images):

  1. Worker docker pull python:3.12 (configured với registry-mirror)
  2. Daemon redirect → registry.6iang.com/v2/library/python/manifests/3.12
  3. Registry miss → fetch từ Docker Hub upstream
  4. Cache vào R2 → serve về worker
  5. Pull tiếp theo của worker khác → hit cache, không qua Docker Hub

5. Setup chi tiết

5.1. Tạo R2 bucket + credentials

# Tạo bucket (qua wrangler)
wrangler r2 bucket create frontdoor-registry

# Tạo R2 API token với quyền Read+Write trên bucket này
# Dashboard: Cloudflare → R2 → Manage R2 API Tokens → Create
# Output: ACCESS_KEY_ID + SECRET_ACCESS_KEY

5.2. Setup htpasswd

mkdir -p ~/registry-auth
docker run --rm --entrypoint htpasswd httpd:2 \
  -Bbn giang YOUR_STRONG_PASSWORD > ~/registry-auth/htpasswd
chmod 600 ~/registry-auth/htpasswd

5.3. Config registry

~/registry-config/config.yml:

version: 0.1
log:
  level: info
  formatter: text
storage:
  s3:
    accesskey: ${R2_ACCESS_KEY_ID}
    secretkey: ${R2_SECRET_ACCESS_KEY}
    region: auto
    regionendpoint: https://${CF_ACCOUNT_ID}.r2.cloudflarestorage.com
    bucket: frontdoor-registry
    encrypt: false
    secure: true
    v4auth: true
    forcepathstyle: true
    rootdirectory: /docker
  delete:
    enabled: true                # cho phép GC xoá blob
  maintenance:
    uploadpurging:
      enabled: true
      age: 168h                   # purge upload dở dang > 7 ngày
      interval: 24h
auth:
  htpasswd:
    realm: Frontdoor
    path: /auth/htpasswd
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
proxy:                            # pull-through cache cho Docker Hub
  remoteurl: https://registry-1.docker.io
  username: ''                    # anonymous; có thể thêm Docker Hub account để tăng rate
  password: ''

Lưu ý: mode proxy biến registry này thành mirror only — không thể push. Để vừa mirror vừa push, cần 2 registry container (port 5000 cho push private, port 5001 cho mirror) hoặc dùng Harbor (có cả 2 trong 1).

5.4. docker-compose entry

Thêm vào docker-compose.yml của frontdoor:

  registry:
    image: registry:2.8
    restart: unless-stopped
    networks: [frontdoor_net]
    environment:
      REGISTRY_STORAGE_S3_ACCESSKEY: ${R2_ACCESS_KEY_ID}
      REGISTRY_STORAGE_S3_SECRETKEY: ${R2_SECRET_ACCESS_KEY}
    volumes:
      - ~/registry-config/config.yml:/etc/docker/registry/config.yml:ro
      - ~/registry-auth:/auth:ro
    labels:
      - traefik.enable=true
      - traefik.http.routers.registry.rule=Host(`registry.6iang.com`)
      - traefik.http.routers.registry.tls=true
      - traefik.http.services.registry.loadbalancer.server.port=5000
      # Tăng max upload size cho image layer lớn
      - traefik.http.middlewares.registry-headers.headers.customresponseheaders.Docker-Distribution-API-Version=registry/2.0

  # Tách container riêng cho mirror (nếu dùng dual mode)
  registry-mirror:
    image: registry:2.8
    restart: unless-stopped
    networks: [frontdoor_net]
    environment:
      REGISTRY_STORAGE_S3_ACCESSKEY: ${R2_ACCESS_KEY_ID}
      REGISTRY_STORAGE_S3_SECRETKEY: ${R2_SECRET_ACCESS_KEY}
      REGISTRY_STORAGE_S3_BUCKET: frontdoor-registry-mirror
      REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io
    labels:
      - traefik.enable=true
      - traefik.http.routers.mirror.rule=Host(`mirror.6iang.com`)
      - traefik.http.routers.mirror.tls=true
      - traefik.http.services.mirror.loadbalancer.server.port=5000

5.5. CF DNS + Tunnel

# /etc/cloudflared/config.yml — thêm 2 ingress rules
ingress:
  - hostname: registry.6iang.com
    service: http://traefik:80
  - hostname: mirror.6iang.com
    service: http://traefik:80
  - hostname: '*.6iang.com'
    service: http://traefik:80

5.6. Client cấu hình mirror

Trên Mac (mỗi worker):

// /etc/docker/daemon.json (Linux) hoặc Docker Desktop Settings (Mac)
{
  "registry-mirrors": ["https://mirror.6iang.com"]
}

Sau đó docker pull python:3.12 sẽ đi qua mirror tự động.


6. Đăng nhập và sử dụng

# Login (one-time per host)
docker login registry.6iang.com -u giang
# Password: (từ htpasswd)

# Tag + push
docker tag frontdoor/workspace:latest registry.6iang.com/frontdoor/workspace:v1
docker push registry.6iang.com/frontdoor/workspace:v1

# Multi-arch via buildx (push trực tiếp)
docker buildx build \
  --platform linux/arm64,linux/amd64 \
  -t registry.6iang.com/frontdoor/workspace:v1 \
  --push ./image

# Pull từ Lenovo
docker login registry.6iang.com -u giang
docker pull registry.6iang.com/frontdoor/workspace:v1

7. Garbage collection

Registry không tự xoá blob khi delete tag — cần GC định kỳ.

Cron job

# /etc/cron.weekly/registry-gc
#!/bin/bash
docker exec frontdoor-registry \
  /bin/registry garbage-collect \
  --delete-untagged=true \
  /etc/docker/registry/config.yml

Hoặc qua agent job

Thêm vào agent jobs.go:

case "registryGC":
    return jobRegistryGC(ctx, docker)

Cron Trigger trên CF Pages enqueue job này mỗi tuần. Agent exec lệnh trong registry container.


8. Cost estimate (R2)

Mục Giá Ước lượng
Storage $0.015 / GB / tháng 50 GB image = $0.75/tháng
Class A operations (push, list) $4.50 / 1M ~1000/tháng = miễn phí
Class B operations (pull, head) $0.36 / 1M ~50k/tháng = miễn phí
Egress đến CF network Free unlimited
Egress ngoài CF Free 10GB/tháng thường không xảy ra

Tổng: ~$1/tháng cho registry ~50GB. Rẻ hơn ghcr.io paid tier nhiều.


9. So sánh storage backend

R2 Local disk (Mac) NFS shared
Setup API token 1 volume mount Cần NFS server
Backup Tự replicated Cần backup riêng Tuỳ NFS
Multi-host scale ✓ same R2 cho mọi node ✗ phải sync ✓ qua NFS
Disaster recovery Cloudflare global Mac chết = mất NFS server chết = mất
Cost $0.015/GB/m "Free" (đã có ổ) Server cost
Latency push từ Mac ~50ms (R2 SIN/HKG) ~1ms <1ms LAN
Latency pull về Mac ~50ms ~1ms <1ms LAN
Latency pull về Lenovo (sau khi setup) ~50ms qua CF Phải tunnel ra Mac Phải tunnel ra Mac

R2 thắng khi có >1 worker. Single-host Mac thì local disk nhanh hơn nhưng mất ưu thế khi thêm Lenovo.


10. Bảo mật

Threat Mitigation
htpasswd bị brute force Cloudflare WAF rate limit + chỉ allow IP từ workers (qua CF Access Service Token cho UI ops)
Image bị poison (man-in-the-middle) HTTPS (CF cert), content-addressable storage (image digest verify)
Push image độc lên Chỉ Giang có credentials. Audit qua R2 access log
Stolen credentials Revoke htpasswd entry → regen → push lại
R2 bucket public mistake Bucket private mặc định, set policy explicit
Mirror pull credential Anonymous → có thể bị Docker Hub rate limit. Thêm Docker Hub account vào config để authenticated pull (200/6h → 5000/day)

11. Comparison: Harbor

Khi nào cân nhắc Harbor thay vì registry:2:

Feature registry:2 Harbor
Pull-through proxy cache
Multi-arch
Web UI ✗ (CLI only) ✓ (đẹp, search, history)
Vulnerability scan (Trivy)
Image signing (Cosign)
RBAC (per-project)
Replication giữa registries
Garbage collection UI ✗ (CLI)
Setup 1 container docker-compose 6-7 service
Tài nguyên ~50 MB RAM ~500 MB RAM, postgres + redis

Đề xuất: Bắt đầu với registry:2. Move sang Harbor khi:

  • Cần share workspace với team (multi-user) → RBAC quan trọng
  • Compliance yêu cầu vuln scan
  • Hoặc >5 workers cần fine-grained access control

12. Implementation plan

Rollout (~3.5 giờ) Storage R2 bucket + API token Auth htpasswd file Config config.yml + docker-compose Network DNS + Tunnel ingress Verify Test push từ Mac Mirror Setup mirror.6iang.com Configure registry-mirrors Ops GC cron + alert Migrate Push workspace images Lenovo Test pull (khi máy về) d0 d2 d4 d6 d8 d10 d12 d14

Bước Hành động Effort
1 Tạo R2 bucket + API token 15 phút
2 Tạo htpasswd file 5 phút
3 Viết config.yml + docker-compose entry 30 phút
4 DNS + Tunnel ingress 15 phút
5 Test push từ Mac 15 phút
6 Test pull từ Lenovo (khi máy về) 15 phút
7 Setup mirror.6iang.com 30 phút
8 Cấu hình registry-mirrors trên workers 10 phút
9 GC cron + alert 30 phút
10 Migrate workspace image push 30 phút
Tổng ~3.5 giờ

13. Push workspace image lên registry

Build workspace image (Dockerfile mới, SSH-only):

# 1. Build local (Mac)
cd ~/workspace/frontdoor/image
docker buildx build \
  --platform linux/arm64,linux/amd64 \
  -t registry.6iang.com/frontdoor/workspace:v1 \
  -t registry.6iang.com/frontdoor/workspace:latest \
  --push .

buildx build --push build multi-arch và push trực tiếp, không qua local image store — gọn hơn docker push riêng.

Cập nhật agent dùng image từ registry:

// agent/main.go — đổi env name + default
WorkspaceImage: envOr("WORKSPACE_IMAGE", "registry.6iang.com/frontdoor/workspace:latest"),

Chỉ cần set env trong setup → WORKSPACE_IMAGE=registry.6iang.com/frontdoor/workspace:latest. Agent restart pull tự động.

Lenovo về chạy setup.sh với cùng env → tạo workspace pull cùng image từ registry, không cần build local.


14. Open questions

  • Container chạy registry ở đâu? Mac mini hiện đang chạy frontdoor stack. Thêm 1 service vào docker-compose.yml. Tài nguyên: ~50MB RAM, ~5% CPU.
  • Backup R2 sang đâu? R2 tự replicated cross-zone. Optional: rclone copy weekly sang Backblaze B2 (chi phí ~$0.005/GB/m). Có cần không cho 50GB image?
  • Push từ GitHub Actions vào registry self-host? Cần expose registry với credentials trong GH secrets. Hoặc giữ flow: GitHub Actions build → push ghcr.io → cron job mirror ghcr → self-host registry.
  • Image signing? cosign sign registry.6iang.com/... — không bắt buộc cho single-user nhưng nice. Cần thêm step trong CI.
  • Mirror Docker Hub có vi phạm ToS? Pull-through cache cho personal use OK. Đừng share mirror.6iang.com public.
  • Multiple registries cho phép push lên ghcr.io VÀ self-host cùng lúc? Có — tag image với 2 prefix + push 2 lần. Hoặc dùng skopeo copy giữa 2 registries.

15. Tóm tắt

Khi nào dùng Choice
1 worker, demo, học Build local, không push
1 worker, agent open-source ghcr.io
≥2 worker, private image registry:2 + R2 (self-host)
Team ≥3 user, audit, vuln scan Harbor

Cho roadmap hiện tại (Mac + Lenovo soon): self-host registry:2 backed bằng R2, kèm mirror cho Docker Hub. Setup ~3.5 giờ. Cost ~$1/tháng. Maintenance: GC mỗi tuần (cron).