Ubuntu 20.04.3 上快速搭建 Apache CGI 运行环境实战指南

发布时间:2026/10/21 12:52:02
Ubuntu 20.04.3 上快速搭建 Apache CGI 运行环境实战指南 1. 为什么在 20.04.3 上折腾 CGI 运行环境先把结论摆在前面Ubuntu 20.04.3 LTS 这个版本本身并不“老”它的软件源里依然躺着完整的 Apache2 和 CGI 支持模块真正让人头疼的不是系统而是很多人一上来就照着十年前的教程去改httpd.conf结果发现文件根本不存在然后就开始怀疑人生。我前后在三台不同配置的机器上装过这套环境踩的坑基本都集中在“配置文件路径变了”“模块没启用”“权限没配对”这三件事上。CGI 这个东西说白了就是让 Web 服务器在收到请求时去调用一个外部程序把这个程序的输出直接当作 HTTP 响应返回给浏览器。它不像现在的各种应用服务器那样常驻内存而是“来一个请求起一个进程处理完就退出”。这种模式在今天看来确实笨重但它的好处是极其直观你写一个脚本给它可执行权限放到指定目录浏览器一访问就能跑。对于要理解 Web 服务器和外部程序之间到底怎么交互的人来说CGI 是最短的那条路径。那为什么偏偏选 Ubuntu 20.04.3 LTS 来做这件事因为 LTS 版本意味着长期支持软件包版本稳定Apache2 的默认配置和模块管理方式a2enmod/a2dismod已经非常成熟。你不需要去编译源码也不需要手动下载一堆依赖apt一条命令就能把基础环境拉起来。这套环境适合谁适合刚接触 Web 后端、想搞明白“请求进来之后到底发生了什么”的开发者也适合需要快速搭一个内部工具页面、又不想引入重型框架的运维人员。我见过太多人在这件事上绕远路有人去装 Nginx 然后发现 CGI 支持要额外编译有人去改/etc/apache2/apache2.conf里的全局配置结果把整个站点搞挂。其实在 Ubuntu 20.04.3 上正确做法是“用 Apache2 的模块化机制把 CGI 当作一个可插拔的能力来启用”而不是去动主配置文件。下面我就按我实际操作的顺序把每一步为什么这么做、不这么做会怎样全部拆开讲清楚。2. 装 Apache2 之前先想清楚的两件事2.1 用系统包还是自己编译这个问题我一开始也纠结过。自己编译 Apache 的好处是版本可控、编译选项可定制但代价是你得自己处理依赖、自己写 systemd 服务文件、自己管升级。在 Ubuntu 20.04.3 上系统源里的 Apache2 版本是 2.4.x这个版本对 CGI 的支持非常完善mod_cgi和mod_cgid都是现成的。我实测下来用apt安装的版本在稳定性和可维护性上完胜自己编译尤其是当你只是想要一个能跑 CGI 的环境时完全没有必要去折腾源码。提示如果你后续要跑的是高并发场景CGI 本身就不适合那时候你要考虑的是 FastCGI 或者应用服务器而不是在 Apache 上死磕。CGI 的定位就是“轻量、直观、够用就好”。2.2 先更新源还是直接装我习惯在装任何东西之前先跑一次apt update这不是形式主义。Ubuntu 20.04.3 的初始镜像里软件源索引可能已经过时直接apt install有时候会碰到 404 或者依赖版本对不上的情况。先更新索引再安装能避免很多莫名其妙的报错。这一步花不了多少时间但能省掉后面排查“为什么包找不到”的麻烦。sudo apt update sudo apt install apache2 -y装完之后Apache2 会自动启动并且设置好开机自启。你可以用systemctl status apache2确认一下状态。如果看到active (running)说明基础 Web 服务已经跑起来了。这时候在浏览器里访问本机地址应该能看到 Apache 的默认欢迎页面。这个页面本身不重要重要的是它证明“服务器能响应 HTTP 请求”这件事已经成立了。3. 把 CGI 模块真正启起来的关键操作3.1 为什么a2enmod cgi是核心一步Apache2 在 Ubuntu 下的设计哲学是“核心精简功能模块化”。也就是说默认安装出来的 Apache 只加载了最基础的模块CGI 相关的功能是关闭的。你要做的不是去配置文件里手动加LoadModule而是用a2enmod这个工具来启用。这个工具会在/etc/apache2/mods-enabled/目录下创建指向mods-available/里对应配置文件的符号链接Apache 启动时会自动读取这个目录。sudo a2enmod cgi执行完这条命令你会看到它提示Enabling module cgi。但这里有个细节很多人会忽略在有些环境下CGI 模块的实际名称是cgid而不是cgi。这两个模块的区别在于mod_cgi适用于多线程环境mod_cgid适用于多进程环境。Ubuntu 的 Apache2 默认用的是 prefork MPM多进程模型所以理论上应该启用cgid。但实际上a2enmod cgi在大多数情况下也能正常工作因为 Apache 会根据当前 MPM 自动选择合适的模块。我实测下来的稳妥做法是先执行a2enmod cgi然后检查/etc/apache2/mods-enabled/下是否有cgi.load或cgid.load。如果没有再手动执行sudo a2enmod cgid。两个都启用也不会冲突Apache 会自己选。3.2 重启还是 reload启用模块之后必须让 Apache 重新加载配置。这里有两种方式systemctl restart apache2和systemctl reload apache2。restart 是彻底重启进程reload 是平滑重载配置。对于只是启用了一个模块这种操作reload 就够了而且不会中断正在处理的请求。但如果你不确定配置有没有语法错误我建议先用sudo apache2ctl configtest检查一下看到Syntax OK再 reload。sudo apache2ctl configtest sudo systemctl reload apache2注意如果你在configtest阶段看到报错千万不要强行 reload 或 restart否则 Apache 可能会起不来。先把报错信息里的文件路径和行号找出来改完再继续。4. 目录配置CGI 脚本到底该放哪4.1 默认的/usr/lib/cgi-bin能用吗Ubuntu 的 Apache2 默认配置里/usr/lib/cgi-bin/这个目录是被映射到 URL 路径/cgi-bin/的。也就是说你把脚本放到这个目录下浏览器访问http://你的地址/cgi-bin/脚本名就能执行。这个默认配置在/etc/apache2/conf-available/serve-cgi-bin.conf里定义内容大致是IfModule mod_alias.c IfModule mod_cgi.c Define ENABLE_USR_LIB_CGI_BIN /IfModule IfModule mod_cgid.c Define ENABLE_USR_LIB_CGI_BIN /IfModule IfDefine ENABLE_USR_LIB_CGI_BIN ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ Directory /usr/lib/cgi-bin AllowOverride None Options ExecCGI -MultiViews SymLinksIfOwnerMatch Require all granted /Directory /IfDefine /IfModule这段配置的意思是如果 CGI 模块被启用了就把/cgi-bin/这个 URL 前缀映射到/usr/lib/cgi-bin/目录并且允许执行 CGI 脚本。这个默认配置对于快速验证来说是够用的但我不建议把实际项目放在这里原因有两个一是这个目录在系统升级时可能会被覆盖二是它的权限管理比较粗糙。4.2 自定义目录的完整配置流程我更推荐的做法是在/var/www/下建一个自己的 CGI 目录比如/var/www/cgi-bin/然后单独写一个配置文件来管理它。这样做的好处是配置和系统默认配置分离升级不会互相影响权限也能按需设置。第一步创建目录并设置权限sudo mkdir -p /var/www/cgi-bin sudo chown -R www-data:www-data /var/www/cgi-bin sudo chmod -R 755 /var/www/cgi-bin这里把属主设为www-data是因为 Apache 的工作进程就是以这个用户身份运行的。如果你不设置脚本执行时可能会因为权限不足而失败。755的权限意味着目录可读可执行文件可读可执行但只有属主能写。对于 CGI 脚本来说可执行权限是必须的否则 Apache 会返回 403。第二步新建一个配置文件sudo nano /etc/apache2/conf-available/custom-cgi.conf内容如下ScriptAlias /mycgi/ /var/www/cgi-bin/ Directory /var/www/cgi-bin AllowOverride None Options ExecCGI -MultiViews SymLinksIfOwnerMatch Require all granted /Directory这里我把 URL 前缀设成了/mycgi/是为了和默认的/cgi-bin/区分开避免混淆。Options ExecCGI是核心没有这一行Apache 会把脚本当普通文本文件返回而不是执行它。-MultiViews是关闭内容协商防止 Apache 自作主张地匹配文件。SymLinksIfOwnerMatch是允许符号链接但要求链接目标的所有者匹配这是一个安全折中。第三步启用配置并重载sudo a2enconf custom-cgi sudo apache2ctl configtest sudo systemctl reload apache24.3 权限配置里最容易踩的坑我在这上面栽过跟头所以特别说一下。CGI 脚本要能被 Apache 执行需要满足三个条件脚本文件本身有可执行权限、脚本所在目录对www-data用户可读可执行、脚本里引用的解释器路径正确。很多人只记得第一条忘了后两条。还有一个隐蔽的坑是脚本的换行符。如果你在 Windows 上编辑脚本然后传到 Linux文件里会带有\r\n换行符这会导致脚本解释器找不到正确的路径报出bad interpreter的错误。解决办法是用dos2unix转换或者在编辑器里设置成 Unix 换行。sudo apt install dos2unix -y dos2unix /var/www/cgi-bin/你的脚本5. 写一个能跑通的最小 CGI 脚本5.1 脚本的基本结构CGI 脚本的核心逻辑其实很简单输出一个 HTTP 头空一行然后输出正文。不管你用什么语言写这个结构都是一样的。我用 Bash 和 Python 各写一个例子你可以根据自己熟悉的语言选一个。Bash 版本#!/bin/bash echo Content-Type: text/html echo echo htmlbody echo h1CGI 环境测试/h1 echo p当前时间$(date)/p echo /body/htmlPython 版本#!/usr/bin/env python3 print(Content-Type: text/html) print() print(htmlbody) print(h1CGI 环境测试/h1) print(p当前时间{}/p.format(__import__(datetime).datetime.now())) print(/body/html)注意 Python 版本里我用了#!/usr/bin/env python3而不是#!/usr/bin/python3。这两种写法在大多数情况下都能工作但env的方式更灵活它会从当前环境的 PATH 里找 python3避免因为 Python 安装路径不同而找不到解释器。5.2 赋予权限并测试把脚本保存到/var/www/cgi-bin/test.sh或/var/www/cgi-bin/test.py然后sudo chmod x /var/www/cgi-bin/test.sh浏览器访问http://你的地址/mycgi/test.sh如果看到页面显示当前时间说明整个链路已经通了。如果看到的是脚本源码说明ExecCGI没生效如果看到 500 错误说明脚本执行出错了这时候要去查 Apache 的错误日志。sudo tail -f /var/log/apache2/error.log这个日志会告诉你具体是哪一行、什么原因导致的失败。我遇到过的错误包括脚本没有可执行权限、脚本里的解释器路径写错、脚本输出里缺少Content-Type头、脚本里有语法错误。基本上看日志都能定位到。5.3 关于Content-Type的一个细节很多人写 CGI 脚本时只输出Content-Type: text/html但忘了后面那个空行。这个空行是必须的它用来分隔 HTTP 头和正文。没有这个空行浏览器会把整个输出都当成头信息结果就是页面一片空白或者报错。我在刚开始的时候也犯过这个错排查了半天才发现是少了一个echo 。6. 排查 CGI 不工作的完整思路6.1 从浏览器表现反推问题当你访问 CGI 地址却得不到预期结果时浏览器的表现其实已经给了你很多线索。我整理了一个对照表你可以按这个顺序去排查浏览器表现可能原因排查方向显示脚本源码ExecCGI未启用检查配置文件里是否有Options ExecCGI403 Forbidden权限不足检查目录和文件的权限、属主404 Not Found路径映射错误检查ScriptAlias配置和实际文件路径500 Internal Server Error脚本执行失败查看error.log检查脚本语法和解释器路径页面空白缺少Content-Type或空行检查脚本输出格式这个表是我在实际排查中总结出来的基本上按这个顺序走一遍问题都能定位到。6.2 日志里到底看什么Apache 的错误日志在/var/log/apache2/error.log访问日志在/var/log/apache2/access.log。排查 CGI 问题时错误日志是主要信息来源。你会看到类似这样的记录[Wed Oct 11 10:23:45.123456 2023] [cgi:error] [pid 1234] [client 192.168.1.100:54321] AH01215: (2)No such file or directory: exec of /var/www/cgi-bin/test.sh failed这条记录的意思是Apache 尝试执行/var/www/cgi-bin/test.sh但是找不到文件或者没有执行权限。AH01215是 Apache 的错误码你可以根据这个码去查更详细的解释。另一条常见的错误是[Wed Oct 11 10:25:12.654321 2023] [cgi:error] [pid 1234] [client 192.168.1.100:54322] AH01215: /usr/bin/env: python3\r: No such file or directory这个就是我前面说的换行符问题\r暴露了脚本是在 Windows 下编辑的。用dos2unix转换一下就好了。6.3 一个容易被忽略的 SELinux 问题虽然 Ubuntu 默认不启用 SELinux但如果你之前装过一些安全相关的软件可能会把 AppArmor 或 SELinux 带进来。这种情况下即使文件权限看起来没问题Apache 也可能因为安全策略而无法执行脚本。你可以用aa-status查看 AppArmor 的状态如果发现有针对 Apache 的限制策略需要调整或者临时禁用。sudo aa-status提示生产环境不要随意禁用安全模块应该去调整策略而不是一关了之。我这里只是提醒你排查时要把这个因素考虑进去。7. 让 CGI 环境更实用的几个调整7.1 开启更详细的错误显示默认情况下Apache 只会在错误日志里记录详细信息浏览器上只显示一个笼统的 500 错误。调试阶段你可以临时让浏览器也显示错误详情这样排查会快很多。在配置文件里加上Directory /var/www/cgi-bin AllowOverride None Options ExecCGI -MultiViews SymLinksIfOwnerMatch Require all granted SetEnvIf Request_URI ^/mycgi/ cgi_debug1 /Directory然后在脚本里根据cgi_debug环境变量决定是否输出详细错误。不过更简单的做法是直接看日志因为日志里的信息永远比浏览器上能显示的更全。7.2 限制 CGI 的执行范围CGI 的安全模型是“谁都能执行”这在内部环境没问题但如果你的服务器暴露在公网就需要额外小心。一个基本的做法是限制只有特定后缀的文件才能被执行Directory /var/www/cgi-bin AllowOverride None Options ExecCGI -MultiViews SymLinksIfOwnerMatch Require all granted AddHandler cgi-script .cgi .sh .py /Directory这样只有.cgi、.sh、.py结尾的文件会被当作 CGI 脚本执行其他文件即使有可执行权限也不会被执行。这是一个很实用的安全加固措施。7.3 用ScriptAlias还是AddHandler这两种方式都能让 CGI 跑起来但适用场景不同。ScriptAlias是把整个目录标记为 CGI 目录目录下所有文件都被当作 CGI 脚本。AddHandler是按文件后缀来匹配更灵活。我个人的习惯是如果整个目录都是 CGI 脚本用ScriptAlias如果目录里混合了静态文件和脚本用AddHandler。两种方式可以同时使用不冲突。8. 我在这套环境上积累的几条实操心得第一条每次改完 Apache 配置先configtest再 reload这个习惯能帮你省掉至少一半的“服务器起不来”问题。我见过有人改完配置直接 restart结果 Apache 启动失败整个站点挂掉还得去查日志回滚。第二条CGI 脚本里的路径尽量用绝对路径。因为 Apache 执行脚本时的工作目录不一定是脚本所在目录用相对路径去读文件很容易找不到。比如你要读一个配置文件写/var/www/cgi-bin/config.ini比写./config.ini可靠得多。第三条脚本的输出一定要有Content-Type和空行这是 HTTP 协议的要求不是 Apache 的怪癖。我刚开始学的时候觉得这个空行很多余后来理解了 HTTP 报文结构才明白它的必要性。第四条如果你要传参数给 CGI 脚本GET 请求的参数会放在QUERY_STRING环境变量里POST 请求的数据会从标准输入进来。Bash 里可以用$QUERY_STRING读取Python 里可以用os.environ和sys.stdin读取。这块内容展开又是一大篇但核心就是“环境变量加标准输入”这两个通道。第五条CGI 脚本每次请求都会启动一个新进程所以脚本的启动开销直接影响响应速度。如果你的脚本是用 Python 写的可以考虑用-S参数跳过一些初始化或者把耗时的操作缓存起来。但说实话如果你对性能有要求CGI 本身就不是正确选择应该去看 FastCGI 或者 WSGI。这套环境搭起来之后你可以用它来做很多事写一个简单的系统信息面板、做一个内部用的文件上传工具、甚至跑一个玩具级的 Web 应用。它的价值不在于性能而在于让你真正理解“Web 服务器如何与外部程序协作”这件事。我每次给新人讲 Web 后端入门都会让他们先在这套环境上写一个 CGI 脚本跑通之后再去看框架很多概念就自然通了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询