🐍 第10章 部署网站到云端空间
📄 一句话概括

Git 是版本管理工具,GitHub 只是把仓库放在网上。 两个概念别混。 init 建仓库、add 放进暂存区、commit 记成一个版本。 commit 之后文件内容没变,变的是 .git 里多了一份快照。 部署要三个文件:requirements.txt 说要装什么、Procfile 说怎么跑、runtime.txt 指定版本。 requirements.txt 里写死版本号很重要。

📝 本章和前面几章的关系

上一篇让外面的人能临时访问到你的机器。这一篇把程序搬到一台一直开着的机器上。 教材第 10 章后半几节是【部署 Flask 网站程序到云端平台】和【认识版本管理工具与 Git】。 再往后是【安装与初设 Git 前端工具】【下载安装与运行 Heroku CLI】 最后一节是【设置 Heroku CLI 与发布文件】。 Git 那部分是真实跑的:git init、git add、git commit、git log 全都执行了, .gitignore 生效也验证过。Heroku 那部分标「平台原样」,因为要账号。 有一句必须提前说:Heroku 2022 年起取消免费额度,学习时先看清计费方式再动手。

一、Git 不等于 GitHub

这两个名字天天一起出现,很多人以为是同一个东西。不是。 Git 是装在你电脑上的版本管理工具,它管的是本地的一个 .git 目录。 就算完全断网、没有任何账号,它也能正常用。 GitHub 是一个网站,它把 Git 仓库放在网上,方便协作和备份。同类的还有 GitLab、Gitee。

打个比方:Git 是本地的账本,GitHub 是帮你保管账本的那个保险箱。没有保险箱,账本照样能用。 演示第一段 git --version 打出来 2.55.0,说明这台机器装了。 Windows 上装完 Git 会带一个 Git Bash,那是个能跑 Linux 命令的终端。

二、一个仓库的三步

git init 建仓库(目录里多一个 .git),git add . 把改动放进暂存区, git commit -m "说明" 把暂存区记成一个版本。三步。 演示里三步退出码都是 0,git log --oneline 打出一行:fa1e12d 第一个版本。 那串字母数字是这次提交的编号,叫 commit hash。 commit 之后文件内容并没有变——变的是 .git 里多了一份快照。 这就是为什么你可以随时回到任何一个 commit 的状态。

「暂存区」这个概念一开始很别扭。为什么不能 add 和 commit 合成一步? 因为它允许你「只提交一部分改动」——改了五个文件,这次只想提交其中两个。 日常最常用的四个命令:git status(现在什么状态)、git diff(改了什么)、 git log --oneline(历史)、git add 加 git commit(记一个版本)。 git status 是最该先敲的那个。 它告诉你哪些文件改了、哪些还没提交,不用猜。

三、什么不该进仓库

在项目根目录建一个 .gitignore,把不该进仓库的东西列进去。 演示加了之后,git status 只看得到 .gitignore 自己。 最该排除的三类。 第一类是 venv/:里面几万个文件,别人能自己建,而且里面的路径是按你的机器写的, 推上去只会把仓库撑大。 第二类是 __pycache__/ 和 *.pyc:那是编译缓存,每次跑都会重新生成,提交上去纯属噪音。

第三类也是最要紧的:带密码的文件。 key.json(第 7.2 篇那个 Google 服务账号)、.env、 数据库密码、API Token。 一旦推上去,就算立刻删掉,历史里还留着。 别人 clone 下来往回翻就能看到。 GitHub 上每天都有密钥因此泄露。 真发生了怎么办?改密码是第一位的——密钥已经泄露了,删文件救不回来。 然后再用 git filter-repo 之类的工具清理历史。

四、部署要的三样东西

文件 写什么 作用
requirements.txt flask==3.1.3 告诉服务器要装哪些包、装哪个版本
Procfile web: python app.py 告诉平台「怎么把你的程序跑起来」
runtime.txt python-3.13.14 指定 Python 版本(可选)

三个文件都放在项目根目录,跟 app.py 平级。 文件名一个字都不能错——平台是按名字找的,不是按内容猜的。 requirements.txt 里写死版本号很重要。 只写 flask,半年后平台装了个新版本, 你的代码可能就跑不起来了——而你自己机器上什么都没改。 写死的方法:flask==3.1.3。 生成它的办法是在虚拟环境里 pip freeze > requirements.txt,它会把当前环境所有包和版本都列出来。

pip freeze 的缺点是把一堆不相干的包也写进去(比如你调试时装的)。 更干净的做法是手写,只写真正需要的。

五、部署的完整动作

heroku login 登录,heroku create 应用名 建一个应用,git push heroku main 推代码, heroku open 打开网址,heroku logs --tail 看日志。 注意第三个命令还是 git push,只是推到了 heroku 这个远程仓库。 所以会 Git 和会部署,在这里是同一件事。 平台收到代码后做四件事:按 requirements.txt 装包、按 runtime.txt 选 Python 版本、 按 Procfile 启动、然后给你一个网址。

这就是「部署」的全部:把你的代码搬到一台一直开着的机器上,让它按 Procfile 里写的方式跑起来, 再对外开一个端口。 这一段在演示里标了「平台原样 · 本沙箱不执行」——要 Heroku 账号。 而且 Heroku 2022 年起取消免费额度,学习时先看清计费方式再动手。 不想花钱也有别的选择(Render、Fly.io、PythonAnywhere 都有免费档),流程大同小异: 推代码、给配置、看日志。

六、最容易翻车的三处

现象 多半是 怎么查
启动就崩 requirements.txt 少写了包 heroku logs --tail 看报错栈
能跑但访问超时 端口没读环境变量 PORT 平台上端口由环境变量给,不能写死
文件上传后不见了 本地磁盘是临时的 要持久化得用对象存储或数据库

第二条最坑。 本地跑 python app.py 默认 5000 端口,平台却会给你分配一个随机端口, 放在环境变量 PORT 里。 必须写成 port = int(os.environ.get('PORT', 5000))。 不改的话,平台一直等不到你的服务起来,最后报超时——而日志里什么都看不出来。 第三条也常踩。 很多平台的文件系统是临时的:重启一次,你上传的文件全没了。 这不是 bug, 是设计——容器随时可能被销毁重建。

要存住文件,得用对象存储(第 11 章会碰到)或者数据库(第 12 章)。 「本地磁盘能写」这件事,在云上是不成立的。

七、动手 Demo:Git 与部署:把代码推到别人的机器上

先看这台机器有没有 git,再在一个临时目录里建仓库、提交、看历史。 然后加 .gitignore,看 git status 少掉了哪些东西。 最后把部署要的三个文件真写出来看内容。Heroku 那几条命令标平台原样。

import os, subprocess, tempfile

def git(*args, cwd=None):
    r = subprocess.run(['git'] + list(args), capture_output=True, text=True, cwd=cwd)
    return r.returncode, (r.stdout + r.stderr).strip()

print('① 先看有没有 git')
rc, out = git('--version')
print('  git --version →', out if rc == 0 else '没装')
print('  Git 是版本管理工具,不是 GitHub。GitHub 只是把 Git 仓库放在网上。两个概念别混。')

d = tempfile.mkdtemp()
open(os.path.join(d, 'app.py'), 'w').write("print('hi')\n")
os.makedirs(os.path.join(d, 'venv', 'Lib'), exist_ok=True)
open(os.path.join(d, 'venv', 'Lib', 'a.py'), 'w').write('x = 1\n')
os.makedirs(os.path.join(d, '__pycache__'), exist_ok=True)
open(os.path.join(d, '__pycache__', 'a.pyc'), 'w').write('bin')
open(os.path.join(d, 'key.json'), 'w').write('{"secret": "don\'t commit"}')

print('② 一个仓库的三步')
git('init', '-q', cwd=d)
print('  git init -q                  退出码 %d' % git('init', '-q', cwd=d)[0])
git('add', 'app.py', cwd=d)
print('  git add .                    退出码 %d' % git('add', '.', cwd=d)[0])
print('  git commit -q -m 第一个版本     退出码 %d'
      % git('-c', 'user.name=demo', '-c', '[email protected]',
            'commit', '-q', '-m', '第一个版本', cwd=d)[0])
print('  git log --oneline:')
for ln in git('log', '--oneline', cwd=d)[1].splitlines():
    print('    ' + ln)
print('  init 建仓库(多一个 .git 目录),add 把改动放进暂存区,commit 把暂存区记成一个版本。')
print('  commit 之后文件内容并没有变——变的是 .git 里多了一份快照。')

print('③ 什么东西不该进仓库')
open(os.path.join(d, '.gitignore'), 'w').write('venv/\n__pycache__/\n*.pyc\nkey.json\n')
print('  加了 .gitignore 之后,git status 只看到:')
for ln in git('status', '--porcelain', cwd=d)[1].splitlines():
    print('    ' + ln)
print('  venv 目录里几万个文件不该进仓库:别人能自己建,而且里面的路径是按你的机器写的。')
print('  key.json、.env 这种带密码的文件尤其不能进。')
print('  一旦推上去,就算立刻删掉,历史里还留着。')

print('④ 部署要的三样东西')
print('| 文件 | 写什么 | 作用 |')
print('|---|---|---|')
print('| requirements.txt | flask==3.1.3 | 告诉服务器要装哪些包、装哪个版本 |')
print('| Procfile | web: python app.py | 告诉平台「怎么把你的程序跑起来」 |')
print('| runtime.txt | python-3.13.14 | 指定 Python 版本(可选,平台常自带) |')
open(os.path.join(d, 'requirements.txt'), 'w').write('flask==3.1.3\n')
open(os.path.join(d, 'Procfile'), 'w').write('web: python app.py\n')
print("  requirements.txt 内容:%r" % open(os.path.join(d, 'requirements.txt')).read().strip())
print("  Procfile 内容:%r" % open(os.path.join(d, 'Procfile')).read().strip())
print('  requirements.txt 里写死版本号很重要。')

print('⑤ 部署的完整动作')
print('  [平台原样 · 本沙箱不执行]')
print('  | # 本书第 10-6、10-7 节(平台原样,本沙箱不执行:要 Heroku 账号)')
print('  | heroku login')
print('  | heroku create my-flask-app')
print('  | git push heroku main')
print('  | heroku open')
print('  | heroku logs --tail')
print('  Heroku 2022 年起取消免费额度,学习时先看清计费方式再动手')
print('  注意第三个命令还是 git push,只是推到了 heroku 这个远程仓库。')

print('⑥ 部署最容易翻车的三处')
print('| 现象 | 多半是 | 怎么查 |')
print('|---|---|---|')
print('| 启动就崩 | requirements.txt 少写了包 | heroku logs --tail 看报错栈 |')
print('| 能跑但访问超时 | 端口没读环境变量 PORT | 平台上端口由环境变量给,不能写死 5000 |')
print('| 文件上传后不见了 | 本地磁盘是临时的 | 要持久化得用对象存储或数据库 |')
print("  正确写法:port = int(os.environ.get('PORT', 5000))")

实跑输出:

--------------------------------------------------------------------
Git 与部署:把代码推到别人的机器上
--------------------------------------------------------------------

  ① 先看有没有 git
  --------------------------------------------------------------------
    git --version → git version 2.55.0.windows.3
  Git 是版本管理工具,不是 GitHub。GitHub 只是把 Git 仓库放在网上。两个概念别混。

  ② 一个仓库的三步
  --------------------------------------------------------------------
    git init -q                  退出码 0
    git add .                    退出码 0
    git commit -q -m 第一个版本       退出码 0
    git log --oneline:
      fa1e12d 第一个版本
  init 建仓库(多一个 .git 目录),add 把改动放进暂存区,commit 把暂存区记成一个版本。三步。
  commit 之后文件内容并没有变——变的是 .git 里多了一份快照。
  所以你可以随时回到任何一个 commit 的状态,这就是「版本管理」的全部意义。

  ③ 什么东西不该进仓库
  --------------------------------------------------------------------
    加了 .gitignore 之后,git status 只看到:
      ?? .gitignore
  venv 目录里几万个文件不该进仓库:别人能自己建,而且里面的路径是按你的机器写的,
  推上去只会把仓库撑大。
  key.json、.env 这种带密码的文件尤其不能进。
  一旦推上去,就算立刻删掉,历史里还留着——GitHub 上每天都有密钥因此泄露。

  ④ 部署要的三样东西
  --------------------------------------------------------------------

| 文件 | 写什么 | 作用 |
|---|---|---|
| requirements.txt | flask==3.1.3 | 告诉服务器要装哪些包、装哪个版本 |
| Procfile | web: python app.py | 告诉平台「怎么把你的程序跑起来」 |
| runtime.txt | python-3.13.14 | 指定 Python 版本(可选,平台常自带) |

    requirements.txt 内容:'flask==3.1.3'
    Procfile 内容:'web: python app.py'
  requirements.txt 里写死版本号很重要。
  只写 flask,半年后平台装了个新版本,你的代码可能就跑不起来了——而你自己机器上什么都没改。

  ⑤ 部署的完整动作
  --------------------------------------------------------------------

  [平台原样 · 本沙箱不执行]
  | # 本书第 10-6、10-7 节(平台原样,本沙箱不执行:要 Heroku 账号)
  | heroku login
  | heroku create my-flask-app
  | git push heroku main
  | heroku open
  | heroku logs --tail
  Heroku 2022 年起取消免费额度,学习时先看清计费方式再动手

  注意第三个命令还是 git push,只是推到了 heroku 这个远程仓库。
  平台收到代码后按 Procfile 装包、启动,然后给你一个网址。
  这就是「部署」的全部:把你的代码搬到一台一直开着的机器上,让它按 Procfile 里写的方式跑起来,
  再对外开一个端口。

  ⑥ 部署最容易翻车的三处
  --------------------------------------------------------------------

| 现象 | 多半是 | 怎么查 |
|---|---|---|
| 启动就崩 | requirements.txt 少写了包 | heroku logs --tail 看报错栈 |
| 能跑但访问超时 | 端口没读环境变量 PORT | 平台上端口由环境变量给,不能写死 5000 |
| 文件上传后不见了 | 本地磁盘是临时的 | 要持久化得用对象存储或数据库 |

  第二条最坑:本地跑 python app.py 默认 5000 端口,平台却会给你分配一个随机端口,
  必须写成 os.environ.get('PORT', 5000)。不改的话,平台一直等不到你的服务起来,最后报超时。

第二段三步都真跑了。 注意 commit 那一步多带了 user.name 和 user.email,因为临时目录里没配身份,Git 会拒绝提交。 你本机配过一次就不用每次带。 第三段那个 git status --porcelain 输出只有一行 ?? .gitignore,说明 venv、pycache、 key.json 全被忽略了。要是没加 .gitignore,这里会列出一堆。 第四段那两个文件内容要看清:requirements 写的是 flask==3.1.3,双等号锁版本; Procfile 写的是 web: python app.py,冒号后面必须有空格。 第六段第二条最坑。 平台给你的端口是随机的,放在环境变量 PORT 里,写死 5000 的话平台一直等不到服务起来,最后报超时, 而日志里什么都看不出来。

30 秒实验

  1. 在一个真实项目目录里跑 git init、git status,看它列出了哪些文件, 再想想哪些该加进 .gitignore。
  2. 在虚拟环境里跑 pip freeze,看它列了多少个包,再想想为什么手写 requirements.txt 更干净。
  3. 把演示第二段的 commit 命令去掉那两行 -c user.name,看它报什么错。

常见误区

  1. 「Git 和 GitHub 是一回事。」Git 是本地的工具,GitHub 是放仓库的网站。 断网、没账号,Git 照样能用。
  2. 「commit 会改文件内容。」不改。它是在 .git 里多存一份快照。 工作目录里的文件一直是你写的样子。
  3. 「密钥提交了再删掉就没事。」历史里还留着。 别人 clone 下来往回翻就能看到。 第一件事是改密码。
自测三问(点开对答案) 1. Git 和 GitHub 是什么关系? —— Git 是装在本地的版本管理工具,管的是项目里那个 `.git` 目录,断网也能用。 GitHub 是一个网站,把 Git 仓库放到网上,方便协作和备份。GitLab、Gitee 是同类。 没有 GitHub,Git 照样工作。 2. 为什么 `requirements.txt` 里要写 `flask==3.1.3` 而不是只写 `flask`? —— 因为不锁版本的话,平台装包时会拿当时的最新版。 半年后出了新版本,你的代码可能就跑不起来了——而你自己机器上什么都没改。 锁版本保证「部署出来的环境和你本地一致」。 3. 本地跑得好好的,部署上去访问超时,最可能是什么原因? —— 端口写死了。本地默认 5000,云平台会给你分配一个随机端口,放在环境变量 `PORT` 里。 要写成 `int(os.environ.get('PORT', 5000))`。 不改的话平台一直等不到服务起来,最后报超时,日志里还看不出问题。

第 10 章结束。下一篇进第 11 章:让程序自己去网上抓东西。

相关笔记