前言
最近,在做一个好玩的,以后有可能的话,说不定可以发展成自己的品牌 HackTheWhat。
但是,无论如何,这不是重点,重点是想为这个品牌做一个有趣的网站。
于是乎,想到了最近用的很方便的Docker,我也算是时代的眼泪了,这么久才开始接触Docker。
正文
1. 文件目录
我们这里会用Docker起 3个服务:
web
:Python应用(FastAPI + Gunicorn
)nginx
:反向代理(80 + 443)cerbot
:使用webroot 模式签发/续签证书
Nginx负责TLS终止,并且把流量反向代理到web:8000
;Let's Encrypt 使用cron
定时跑 certbot renew
,完成后 reload Nginx。
我们先来展示一下我们的文件目录:
web-example/
├─ docker-compose.yml
├─ app/
│ ├─ Dockerfile
│ ├─ requirements.txt
│ ├─ gunicorn.conf.py
│ └─ app.py
└─ nginx/
└─ conf.d/
├─ site\_http.conf
├─ site\_https.conf.disabled
└─ ssl\_params.conf
接下来是我们的文件内容,我们需要分别创建这些文件
1.1 docker-compose.yml
其中container_name
字段请 自行更改。
version: "3.8"
services:
web:
build: ./app
container_name: example-web
command: gunicorn -k uvicorn.workers.UvicornWorker -c gunicorn.conf.py app:app
volumes:
- ./app:/app
expose:
- "8000"
restart: unless-stopped
nginx:
image: nginx:1.27-alpine
container_name: example-nginx
depends_on:
- web
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/certbot:/var/www/certbot
- ./letsencrypt:/etc/letsencrypt
restart: unless-stopped
certbot:
image: certbot/certbot:latest
container_name: example-certbot
volumes:
- ./letsencrypt:/etc/letsencrypt
- ./nginx/certbot:/var/www/certbot
1.2 app/Dockerfile
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-c", "gunicorn.conf.py", "app:app"]
1.3 app/requirements.txt
fastapi>=0.111,<1
uvicorn[standard]>=0.30,<1
gunicorn>=21,<22
1.4 app/gunicorn.conf.py
import multiprocessing
bind = "0.0.0.0:8000"
workers = max(2, multiprocessing.cpu_count() * 2 + 1)
worker_class = "uvicorn.workers.UvicornWorker"
timeout = 60
keepalive = 5
accesslog = "-"
errorlog = "-"
1.5 app/app.py
其中title
,还有html页面中的example 请自行更改。
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
app = FastAPI(title="example")
@app.get("/healthz")
async def healthz():
return {"status": "ok"}
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
return '''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Example</title>
<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:2rem;max-width:820px}code{background:#f4f4f8;padding:.15rem .35rem;border-radius:.35rem}</style>
</head>
<body>
<h1>Example</h1>
<p>It works 🎉 — served by <strong>FastAPI + Gunicorn</strong> behind <strong>Nginx</strong> in Docker.</p>
<p>Edit <code>app/app.py</code> to customize your homepage.</p>
<p>Interactive docs: <a href="/docs">/docs</a></p>
</body>
</html>'''
1.6 nginx/conf.d/site_http.conf
其中 server
中的 server_name
字段请自行更改
# Initial HTTP-only config (use this before you obtain certificates)
server {
listen 80;
server_name example.com www.example.com;
# ACME challenge location for certbot
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Proxy all traffic to the Python app
location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://web:8000;
}
}
1.7 nginx/conf.d/site_https.conf.disabled
其中 server
中的 server_name
和 ssl_certificate
和 ssl_certificate_key
字段 请自行更改。
# Enable HTTPS by renaming this file to: site_https.conf
# (and remove/rename site_http.conf), then reload nginx.
# This expects certs at /etc/letsencrypt/live/example.com/...
# Keep port 80 for ACME + redirect to HTTPS
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/nginx/conf.d/ssl_params.conf;
location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://web:8000;
}
}
1.8 nginx/conf.d/ssl_params.conf
# Reasonable modern TLS settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
# Enable HSTS (preload is optional; enable only when you're sure)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
2. 启动HTTP(给Certbot验证使用)
进入到 docker目录 /path/to/example-web
,输入以下命令:
docker compose up -d web nginx
# 打开 http://example.com 看是否能访问,这里的example 是自己的域名
3. 申请证书(webroot模式)
启动HTTP后,我们申请证书:
EMAIL=you@example.com # 换成你的邮箱
# 下面example.com请替换成自己的域名
docker compose run --rm certbot certonly \
--webroot -w /var/www/certbot \
-d example.com -d www.example.com \
--email "$EMAIL" --agree-tos --no-eff-email
4. 切换到HTTPS的Nginx config 并且热重载Nginx
HTTPS证书申请到后,我们方可切换至HTTPS的 Nginx config,否则有小概率可能报错:
mv nginx/conf.d/site_https.conf.disabled nginx/conf.d/site_https.conf
rm -f nginx/conf.d/site_http.conf
docker compose exec nginx nginx -t && docker compose exec nginx nginx -s reload
# 访问 https://example.com
5. 自动续期Let's Encrypt证书(宿主机cron
)
设置自动续费:
crontab -e
# 每周一凌晨 3 点续期并重载 Nginx(/path/to/example-web改成你的路径)
0 3 * * 1 cd /path/to/example-web && \
docker compose run --rm certbot renew && \
docker compose exec nginx nginx -s reload
注意:如果遇到Permission Denied
的情况,我们切换至su
(root权限)即可。
6. 后续更新相关
如果后续想要简短的更新Docker服务,我们的更新分为三种:
只改页面/路由代码(不加新依赖)
新增第三方库(需要修改
requirements.txt
)添加静态文件或模板(
*.png
,*.html
)
6.1 只改页面/路由代码(不加新依赖)
修改
app/app.py
让应用重载,输入
docker compose restart web
6.2 新增第三方库(需要修改requirements.txt
)
编辑
app/requirements.txt
(比如加上jinja2
等)重新构建并启动:
docker compose build web
docker compose up -d web
6.3 添加静态文件或模板(*.png
, *.html
)
我们可以直接使用Nginx,来直接服务静态。
新建静态目录:
app/static
在
nginx/conf.d/site_https.conf
里加入:
location /static/ {
alias /var/www/static/;
access_log off;
expires 7d;
}
把静态目录挂载给 Nginx 容器(在
docker-compose.yml
的nginx
服务里加一行):
services:
nginx:
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/certbot:/var/www/certbot
- ./letsencrypt:/etc/letsencrypt
- ./app/static:/var/www/static:ro # ← 新增
重新加载 Nginx:
docker compose up -d nginx
docker compose exec -T nginx nginx -t && sudo docker compose exec -T nginx nginx -s reload
6.4 什么时候需要动Nginx?
只改 Python 页面/路由:不需要动 Nginx。
改了 Nginx 配置或证书:需要
nginx -t && nginx -s reload
(见上面命令)。
总结
这个时代,有了GPT,学习起来,真方便呀!
参考
[1] ChatGPT