인셔셔RSS 관심 있는 블로그, 뉴스, 기술 정보를 효율적으로 추적하고 읽으세요
원문 읽기 InertiaRSS에서 열기

추천 피드

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를 만지기 전에, 에피소드 4에서 한 수정 사항이 지금 바로 적용할 가치가 있습니다.

현재 우리 플랫폼은 파일 이름으로 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

우리는 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"
---

전체 화면 모드 입력 전체 화면 모드 종료

일치하지 않는 것은 의도적입니다: 파일 이름과 제공 경로는 다를 수 있습니다.

app.py

Open app.py

from urllib.parse import urlparse

위에 이 import를 추가하세요: 전체 화면 모드로 전환

Exit fullscreen modeget_post_path()그런 다음 parse_post() 아래에 __JHSNS_SEG_aed961b0_27__ 함수를 추가하세요:

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'

전체 화면 모드로 전환 Exit fullscreen mode

이 헬퍼는 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']을 찾아 올바른 마크다운 파일을 로드합니다.

홈페이지 링크를 업데이트하세요

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 name="description" content="{{ post.description }}">
{% if post.canonical_url %}
<link rel="canonical" href="{{ post.canonical_url }}">
{% endif %}

전체 화면 모드 전체 화면 모드 종료

{% if %} 경비대는 canonical_url 없는 게시물에 태그를 선택 사항으로 유지합니다.

업데이트된 프롬터마트 스키마

이제 앞장에 있는 전체 스키마입니다:

필드 사용용
title <title> 태그, <h1> 게시물 페이지에서, 게시물 목록
date URL 생성, 정렬, 표시
description <meta name="description">, 홈페이지에서의 요약
tags 게시물 페이지에서의 태그 패일, 결국 E12에서 태그 페이지로 이동
canonical_url 홈페이지 링크 + <link rel="canonical"> 블로거 이전용 (선택 사항)

게시물을 이전할 때, canonical_url를 원래 블로거 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으로 푸시하기 전에 저장소에 속하지 않는 것을 커밋하지 않도록 확인하세요. 프로젝트 루트에 .gitignore를 생성하세요

venv/
__pycache__/
*.pyc
.env

전체 화면 모드로 전환 전체 화면 모드 종료

이것은 당신의 가상 환경, 캐시 파일, 그리고 어떤 로컬 설정도 버전 관리에서 제외합니다.


🌐 단계 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.gitdavidtio을 클라우더라이즈된 사용자 이름으로 사용하고 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. Clouderized에 의해 표시된 cfargotunnel 대상을 복사하세요.
  6. 도메인용 CNAME 레코드를 만들고 cfargotunnel 대상(

Clouderized Domains view for tioblog custom domain setup

)을 가리키세요.

DNS 전파가 완료되면, 앱은 본인의 도메인에서 서비스되면서 Clouderized는 여전히 라우팅과 HTTPS를 처리합니다.__JHSNS_SEG_aed961b0_163__작은 창작자 플랫폼에 대해 이것이 왜 중요한가:

  • 한 개의 배포 단위(당신의 Docker 이미지)
  • 수동으로 유지 관리되는 역방향 프록시 및 인증서 설정을 피할 수 있습니다
  • 동일한 Git 워크플로우로 콘텐츠 및 앱 변경 사항을 배송할 수 있습니다

🧪 단계 8: 활성 상태 확인

브라우저를 엽니다 및 라이브 URL을 확인합니다:

https://davidtio-tioblog.clouderized.com

전체 화면 모드 입력 전체 화면 모드 종료

홈페이지가 로드되고 게시물이 목록에 표시되며, .html 게시물 URL이 작동하는지 확인하십시오:

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 경로를 올바른 마크다운 파일로 해석합니다
  • 홈페이지 링크는{{ post.path }}
  • 선택적 정규화 태그가post.html
  • Dockerfile에 추가되었습니다.dockerignore.gitignore
  • 앱이 컨테이너에서 실행되며, docker builddocker run
  • 배포가 Clouderized를 통해 공개 HTTPS 호스팅에 전송되었습니다.
  • Domains에 DNS가 제공된 cfargotunnel 대상으로 매핑될 수 있습니다.
  • 배포 흐름은 Git 기반으로, 향후 시리즈 게시물에 대한 발행 작업을 반복 가능하게 유지합니다

🚀 다음 소식

문제점: 게시물을 수정할 때마다 이미지를 다시 빌드해야 변경 사항을 볼 수 있습니다. 콘텐츠 중심 블로그의 목적을 무색하게 합니다.

다음 에피소드: 바인드 마운트. 우리는 마운트할 것입니다content/ 폴더를 직접 머신에서 컨테이너로 가져와서 — 게시물을 편집하거나, 페이지를 새로고침하거나, 즉시 변경 사항을 확인하세요. 재건축이 필요 없습니다.


이 도움이 되셨습니까? 네트워크와 공유하거나, 아래에 댓글을 남기세요.


SEO 메타데이터

  • 제목: Docker로 블로그 플랫폼 만들기 #5: Dockerfile 추가 + Clouderized에 배포
  • 메타 설명: Flask 블로그용 Dockerfile을 작성하고 이미지를 빌드한 다음 clouderized.com에 배포하세요 — git.clouderized.com/davidtio/tioblog으로 푸시하고, 자동 빌드, 자동 라우팅, 자동 HTTPS를 davidtio-tioblog.clouderized.com에서 수행하세요.