🐳 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를 프롬트마터에 추가하고 이를 두 가지 일에 사용합니다:
- 이 플랫폼이 게시물을 제공하는 실제 URL — 이로 인해 이전 게시물은 그 Blogger 경로를 유지합니다
-
<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
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'
이 헬퍼는 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.git는 davidtio을 클라우더라이즈된 사용자 이름으로 사용하고 tioblog을 프로젝트 이름으로 사용합니다.
앱에 대해서는:
https://git.clouderized.com/<username>/<project>.git
한두 분 정도 지나면, 당신의 블로그는 다음 주소에서 활성화됩니다:
https://davidtio-tioblog.clouderized.com
동일한 Dockerfile, 지금 HTTPS로 공개적으로 실행 중입니다.
커스텀 도메인 추가
Clouderized도 커스텀 도메인을 지원합니다. 이 블로그의 경우, 운영은 blog.dtio.app에서 이루어집니다.
https://dash.clouderized.com- 으로 이동
tioblog앱 카드 열기 Domains- 커스텀 도메인을 추가하세요 (예:
blog.dtio.app) - Clouderized에 의해 표시된
cfargotunnel대상을 복사하세요. - 도메인용
CNAME레코드를 만들고cfargotunnel대상(
)을 가리키세요.
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_url와post.path이 이제 안정적인 후킹 URL을 제어합니다 - 동적 경로는 URL 경로를 올바른 마크다운 파일로 해석합니다
- 홈페이지 링크는
{{ post.path }} - 선택적 정규화 태그가
post.html -
Dockerfile에 추가되었습니다.dockerignore와.gitignore는 - 앱이 컨테이너에서 실행되며,
docker build와docker 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에서 수행하세요.












