慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
M
MIT News - Artificial intelligence
博客园 - 叶小钗
MyScale Blog
MyScale Blog
V
Visual Studio Blog
月光博客
月光博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
I
InfoQ
有赞技术团队
有赞技术团队
阮一峰的网络日志
阮一峰的网络日志
Jina AI
Jina AI
V
V2EX
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale
Last Week in AI
Last Week in AI
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
博客园 - Franky

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python)
用Docker建立部落格平台 #5:添加Dockerfile + 部署到Clouderized
David Tio · 2026-05-24 · via DEV Community

🐳 使用 Docker 建立部落格平台 #5:添加 Dockerfile + 部署到 Clouderized

快速一行指令: 寫一個 Dockerfile,建立一個影像,並使用一個 git push 部署到 Clouderized,讓你的 Flask 部落格自動啟動 HTTPS 並無需伺服器照看.


🔧 開始前:為 Blogger 迁移進行 URL 控制

在接觸 Docker 之前,有一個來自第四集的修復措施現在值得執行。

目前我們的平台從檔案名稱建立 URL:hey-markdown.md 變成 /2026/04/hey-markdown.html。這對於新帖子有效,但對於遷移過來的 Blogger URL 則無效:

Blogger URL 基於檔案名稱的 URL
/2026/03/docker-rootless-on-ubuntu-2026-guide.html /2026/03/docker-rootless-ubuntu.html

我們在 frontmatter 中添加 canonical_url 並用它來做兩件事:

  1. 此平台實際為文章服務的 URL — 如此則遷移的文章能保留他們的 Blogger 路徑
  2. <link rel="canonical"> — 這會告訴 Google 這頁面與舊 Blogger URL 的內容相同

更新content/posts/hey-markdown.md

---
title: "Hey Markdown"
date: 2026-04-25
description: "The first post written in Markdown  no more writing HTML by hand."
tags: [meta, blog]
canonical_url: "https://blog.dtio.app/2026/04/old-markdown.html"
---

進入全螢幕模式 離開全螢幕模式

這種不匹配是故意的:檔案名稱與服務路徑可以不同。

加入一個 URL Helper 到app.py

打開app.py. 在最上面添加這個匯入:

from urllib.parse import urlparse

進入全螢幕模式 離開全螢幕模式

然後再加一個get_post_path()以下函數parse_post():

def get_post_path(meta):
    """Determine the URL path for a post from its canonical_url."""
    if meta.get('canonical_url'):
        return urlparse(meta['canonical_url']).path
    # Fallback: auto-generate from date + slug
    slug = meta.get('slug', 'unknown')
    date = meta.get('date', '')
    if hasattr(date, 'year'):
        return f'/{date.year}/{date.month:02d}/{slug}.html'
    return f'/{slug}.html'

進入全螢幕模式 離開全螢幕模式

這個輔助工具從中提取 URL 路徑canonical_url,帶有日期+縮略名的備用方案.

更新 get_all_posts()

在現有的 get_all_posts() 函數中,添加路徑欄位:

def get_all_posts():
    posts = []
    posts_dir = 'content/posts'
    for filename in os.listdir(posts_dir):
        if not filename.endswith('.md'):
            continue
        filepath = os.path.join(posts_dir, filename)
        post = parse_post(filepath)
        post['slug'] = filename[:-3]
        post['path'] = get_post_path(post)
        posts.append(post)
    posts.sort(key=lambda p: p.get('date', ''), reverse=True)
    return posts

進入全螢幕模式 退出全螢幕模式

每篇帖子現在有一個 path 欄位 — 它應該被服務的確切 URL.

更新路徑

尋找現有的路徑在app.py:

@app.route('/<int:year>/<int:month>/<slug>')
def post(year, month, slug):
    filepath = f'content/posts/{slug}.md'
    if not os.path.exists(filepath):
        abort(404)
    post = parse_post(filepath)
    return render_template('post.html', post=post)

進入全螢幕模式 離開全螢幕模式

用這個動態查詢替換它:

@app.route('/<int:year>/<int:month>/<path:slug>.html')
def post(year, month, slug):
    target_path = f'/{year}/{month:02d}/{slug}.html'
    all_posts = get_all_posts()
    matching_post = next((p for p in all_posts if p['path'] == target_path), None)

    if not matching_post:
        abort(404)

    post_data = parse_post(f'content/posts/{matching_post["slug"]}.md')
    post_data['path'] = matching_post['path']
    return render_template('post.html', post=post_data)

進入全螢幕模式 離開全螢幕模式

這個路徑匹配 .html URL,找到匹配的 post['path'],並載入正確的 markdown 檔案.

更新首頁連結

templates/index.html中,將手動構建的 URL 替換為文章的路徑:

<a href="{{ post.path }}">{{ post.title }}</a>

進入全螢幕模式 退出全螢幕模式

而對於"閱讀更多"鏈接:

<a href="{{ post.path }}" class="text-teal-400 text-sm hover:text-teal-300 font-medium transition-colors duration-200">
    Read more →
</a>

進入全螢幕模式 退出全螢幕模式

添加標準鏈接標籤

templates/post.html中,在內部添加標準鏈接標籤<head> 在 meta 描述之後:

<meta name="description" content="{{ post.description }}">
{% if post.canonical_url %}
<link rel="canonical" href="{{ post.canonical_url }}">
{% endif %}

開啟全螢幕模式 關閉全螢幕模式

The {% if %} 開啟,讓標籤對沒有 canonical_url 的文章是可選的。

更新 Frontmatter Schema

這是目前未來的全 Frontmatter Schema:

Field 用於
title <title> 標籤,<h1> 在文章頁面,文章列表
date URL 生成,排序,顯示
description <meta name="description">,文章縮略預覽在首頁
tags 文章頁面上的標籤藥丸,最終在Ep12中標籤頁面
canonical_url 首頁連結 + <link rel="canonical"> 用於Blogger遷移(可選)

在遷移文章時,將 canonical_url 設為與原Blogger URL一致。


✅ 步驟 1:在地端確認一切正常

在觸及 Docker 之前,確保平台在 URL 更改後仍然正確運行

啟動您的 venv 並運行應用程式:

$ source venv/bin/activate
$ python app.py

進入全螢幕模式 離開全螢幕模式

造訪 http://localhost:8000 並檢查:

  • 首頁 — 列出貼文,連結指向正確的路徑
  • 貼文頁面 — 正確渲染,<link rel="canonical"> 存在于 HTML 源碼中(檢視原始碼並搜尋 canonical
  • 標籤藥丸 — 在貼文頁面上正確顯示

一旦本地一切正常運作,我們就準備好將其容器化了。


📦 第 2 步:添加一個 .dockerignore

在撰寫 Dockerfile 之前,告訴 Docker 要從影像中排除什麼。在專案根目錄中建立 .dockerignore

venv/
__pycache__/
*.pyc
.git/
.gitignore

全螢幕模式 退出全螢幕模式

這能讓影像保持輕量,並避免發送本地環境檔案.


📄 第 3 步:撰寫 Dockerfile

建立Dockerfile 在專案根目錄:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["python", "app.py"]

進入全螢幕模式 退出全螢幕模式

為何這個順序很重要:

  • 先複製 requirements.txt 以便依賴安裝可以被快取
  • 之後再複製應用程式碼,以便常規程式碼編輯能快速重新建構

🔨 第 4 步:建立影像

確保你位於專案根目錄(Dockerfile所在地):

$ docker build -t tioblog .

進入全螢幕模式 離開全螢幕模式

完成後,確認圖片是否存在:

$ docker images tioblog
IMAGE            ID             DISK USAGE   CONTENT SIZE   EXTRA
tioblog:latest   05dbc52b9852        231MB         55.6MB

進入全螢幕模式 離開全螢幕模式


🚀 第5步:執行它

$ docker run -p 8000:8000 tioblog

進入全螢幕模式 離開全螢幕模式

造訪http://localhost:8000。部落格載入 — 和以前一樣,但現在在容器內運行.


📝 步驟 6: 添加 .gitignore

在推送至 Clouderized 之前,確保你沒有提交不應該存入 repo 的東西。在專案根目錄中建立.gitignore

venv/
__pycache__/
*.pyc
.env

Enter fullscreen mode Exit fullscreen mode

這能讓你的虛擬環境、快取檔案以及任何本地設定都離開版本控制


🌐 步驟 7: 部署到 Clouderized

Clouderized 是這系列中的部署目標,因為它專為簡單的容器應用程式發送進行了優化:推送到 Git、自動建構、自動部署、預設啟用 HTTPS

對於這篇部落格,部署是:

$ git init
$ git add .
$ git commit -m "Initial commit for tioblog"
$ git branch -M main
$ git remote add origin https://git.clouderized.com/davidtio/tioblog.git
$ git push -u origin main

進入全螢幕模式 退出全螢幕模式

https://git.clouderized.com/davidtio/tioblog.git 使用 davidtio 作為 Clouderized 使用者名稱,並使用 tioblog 作為專案名稱.
對於您的應用程式,請使用:

https://git.clouderized.com/<username>/<project>.git

進入全螢幕模式 退出全螢幕模式

一分鐘或兩分鐘後,您的部落格在:

https://davidtio-tioblog.clouderized.com

進入全螢幕模式 退出全螢幕模式

相同 Dockerfile,現已以 HTTPS 公開執行.

添加您的自訂域

Clouderized 也支援自訂域。對於這個部落格,生產環境運行在 blog.dtio.app.

  1. 前往 https://dash.clouderized.com
  2. 打開 tioblog 應用程式卡片
  3. 點擊 Domains
  4. 添加您的自訂域(例如 blog.dtio.app
  5. 複製cfargotunnel由Clouderized顯示的目標
  6. 為您的域名建立一個CNAME記錄,並將其指向cfargotunnel目標

Clouderized Domains view for tioblog custom domain setup

DNS傳播後,您的應用程式會在您自己的域名上提供服務,而Clouderized則繼續處理路由和HTTPS

對小型創作者平台而言,這為何重要:

  • 你保持一個部署單位(你的 Docker 映像)
  • 你避免手動維護反向代理和證書設定
  • 你可以使用相同的 Git 工作流程發布內容和應用程式變更

🧪 步驟 8:驗證它已啟動

打開你的瀏覽器並檢查實時 URL:

https://davidtio-tioblog.clouderized.com

進入全螢幕模式 退出全螢幕模式

確認首頁載入、文章已列出,且.html的文章連結有效:

https://davidtio-tioblog.clouderized.com/2026/04/hey-markdown.html

進入全螢幕模式 離開全螢幕模式

這與Blogger風格的URL相符,以便更順暢地遷移.

另外請確認HTTPS已啟用且憑證有效。在Clouderized上這是自動完成的,因此您可以專注於內容和產品工作,而不是基礎設施任務。


✅ 你已建立的內容

tiohub-blog/
├── Dockerfile         ← new
├── .dockerignore      ← new
├── app.py             (get_post_path, post['path'] in get_all_posts, route adds post['path'])
├── requirements.txt
├── static/
│   └── js/
│       ├── tailwind.config.js
│       └── code-blocks.js
├── templates/
│   ├── index.html     (links use {{ post.path }} instead of manual URLs)
│   └── post.html      (<link rel="canonical"> added)
└── content/
    └── posts/
        ├── hey-markdown.md  (canonical_url added to frontmatter)
        └── second-post.md

進入全螢幕模式 退出全螢幕模式

變更內容:

  • canonical_urlpost.path 現在驅動穩定的後續 URL
  • 動態路徑解析 URL 路徑到正確的 markdown 檔案
  • 首頁連結使用 {{ post.path }}
  • 選擇性加入规范標籤到 post.html
  • Dockerfile.dockerignore,以及.gitignore新增了
  • 應用程式在容器內運行,搭配docker builddocker run
  • 部署已推送至Clouderized,供公眾透過HTTPS主機使用
  • 的自訂域名可在Domains中設定,DNS指向所提供的cfargotunnel目標
  • 部署流程是 Git 為本的,這讓發布操作對未來的系列文章保持可重複

🚀 即將推出

一個問題:每當你編輯文章時,你必須重新建立圖像才能看到變化。這破壞了內容驅動型博客的初衷

下一集:綁定掛載。我們將掛載content/ 資料夾從您的電腦直接複製到容器中 — 編輯一篇帖子、刷新頁面、立即看到變更。不需要重新構建.


您覺得這有幫助嗎? 與您的網絡分享或留下評論.


SEO 元數據

  • 標題: 使用 Docker 建立部落格平台 #5:添加 Dockerfile + 部署到 Clouderized
  • Meta 描述: 請為您的 Flask 博客編寫一個 Dockerfile,建立影像,並部署到 clouderized.com — 推送到 git.clouderized.com/davidtio/tioblog,自動建立,自動路由,並自動 HTTPS 在 davidtio-tioblog.clouderized.com。