GitLab Runner中Docker化Flask应用数据库连接失败问题
问题背景
我有一个Flask Web应用,采用PyTest与MySQL数据库开展API自动化测试。本地运行时,无论是否Docker化,测试均能成功完成。但在基于Alpine Linux的GitLab Runner中执行Docker化流程时出现异常:Docker启动的服务能连接数据库,但通过docker exec执行pytest时,Flask测试客户端的登录POST请求触发报错,提示数据库连接为NoneType,同时出现Access denied for user的权限错误。曾尝试添加--network host参数,但未解决问题。
GitLab流水线配置
before_script: - apk update - apk add openssh - apk add --no-cache git stages: - tests tests: stage: tests script: - docker rm --force /myapp 2>/dev/null || true - docker image build --no-cache -t myapp . - docker run --name myapp -d -e GOOGLE_APPLICATION_CREDENTIALS="service_account.json" -p 5050:5050 myapp - docker ps - docker exec -e GOOGLE_APPLICATION_CREDENTIALS="service_account.json" myapp pytest
错误输出
Docker运行输出
$ docker run --name myapp -d -e GOOGLE_APPLICATION_CREDENTIALS="service_account.json" -p 5050:5050 myapp 096864c06de5fb2e8195cc1b87ab7635626759cb690dfc2d76eb640b3679c400 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 096864c06de5 myapp "gunicorn -b 0.0.0.0…" 1 second ago Up Less than a second 0.0.0.0:5050->5050/tcp, :::5050->5050/tcp myapp 790892bfc88e b0757c55a1fd "docker-entrypoint.s…" 8 minutes ago Up 8 minutes runner-uz9hfgtk-project-3669-concurrent-0-1be2fbd9916f7e7c-build df641fdce08f f8517f1ae7c9 "/usr/bin/dumb-init …" 6 days ago Up 6 days runner-uz9hfgtk-project-3740-concurrent-0-b0186612731c8779-docker-0-wait-for-service
Pytest错误输出
$ docker exec -e GOOGLE_APPLICATION_CREDENTIALS="service_account.json" myapp pytest ============================= test session starts ============================== platform linux -- Python 3.8.18, pytest-7.4.4, pluggy-1.4.0 rootdir: /docker-myapp plugins: anyio-4.1.0, pylama-7.7.1 collected 50 items Running tests... ==================================== ERRORS ==================================== _ ERROR client = <FlaskClient <Flask 'app_factory'>> @pytest.fixture() def auth_client(client): basic_auth = get_basic_auth().popitem() > response = client.post( "/login", json={ "email": "test@test.com", "password": "password", }, headers={"Authorization": _basic_auth_str(basic_auth[0], basic_auth[1])}, ) tests/conftest.py:33: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ /usr/local/lib/python3.8/site-packages/werkzeug/test.py:1039: in post return self.open(*args, **kw) /usr/local/lib/python3.8/site-packages/flask/testing.py:222: in open return Client.open( /usr/local/lib/python3.8/site-packages/werkzeug/test.py:993: in open response = self.run_wsgi_app(environ.copy(), buffered=buffered) /usr/local/lib/python3.8/site-packages/werkzeug/test.py:884: in run_wsgi_app rv = run_wsgi_app(self.application, environ, buffered=buffered) /usr/local/lib/python3.8/site-packages/werkzeug/test.py:1119: in run_wsgi_app app_rv = app(environ, start_response) /usr/local/lib/python3.8/site-packages/flask/app.py:2464: in __call__ return self.wsgi_app(environ, start_response) /usr/local/lib/python3.8/site-packages/flask/app.py:2450: in wsgi_app response = self.handle_exception(e) /usr/local/lib/python3.8/site-packages/flask/app.py:1867: in handle_exception reraise(exc_type, exc_value, tb) /usr/local/lib/python3.8/site-packages/flask/_compat.py:39: in reraise raise value /usr/local/lib/python3.8/site-packages/flask/app.py:2447: in wsgi_app response = self.full_dispatch_request() /usr/local/lib/python3.8/site-packages/flask/app.py:1952: in full_dispatch_request rv = self.handle_user_exception(e) /usr/local/lib/python3.8/site-packages/flask/app.py:1821: in handle_user_exception reraise(exc_type, exc_value, tb) /usr/local/lib/python3.8/site-packages/flask/_compat.py:39: in reraise raise value /usr/local/lib/python3.8/site-packages/flask/app.py:1950: in full_dispatch_request rv = self.dispatch_request() /usr/local/lib/python3.8/site-packages/flask/app.py:1936: in dispatch_request return self.view_functions[rule.endpoint](**req.view_args) /usr/local/lib/python3.8/site-packages/flask_httpauth.py:172: in decorated return self.ensure_sync(f)(*args, **kwargs) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @app.route("/login", methods=["POST"]) @auth.login_required def _login(): connection = config_parcer() response, status = get_login.login( request, connection, app.config["SECRET_KEY"], bcrypt ) > connection.close() E AttributeError: 'NoneType' object has no attribute 'close' main.py:818: AttributeError ---------------------------- Captured stdout setup ----------------------------- (1045, "Access denied for user 'app_user'@'XX.XXX.XX.XXX' (using password: YES)") Connection refused. Attempts count: 1 (1045, "Access denied for user 'app_user'@'XX.XXX.XX.XXX' (using password: YES)") Connection refused. Attempts count: 2 (1045, "Access denied for user 'app_user'@'XX.XXX.XX.XXX' (using password: YES)") Connection refused. Attempts count: 3 'NoneType' object has no attribute 'cursor'
排查方向与解决方案
补充缺失的环境变量:
docker exec仅传递了GOOGLE_APPLICATION_CREDENTIALS,但可能缺少数据库连接所需的其他变量(如DB_HOST、DB_USER、DB_PASSWORD)。本地运行时这些变量可能默认存在,但GitLab Runner中未传递。- 解决:在
docker exec命令中补充所有数据库相关环境变量,或确保容器启动时已通过Dockerfile ENV、docker run -e加载这些变量,让exec进程继承容器环境。
- 解决:在
检查测试环境配置:
pytest可能加载了生产环境的数据库配置,而非测试环境配置。本地测试时默认使用本地测试库,但GitLab Runner中测试进程尝试访问生产库却无权限。- 解决:检查
config_parcer()函数,确认测试环境下是否正确加载测试数据库配置;确保GitLab Runner所在IP段被数据库允许访问,测试账号权限配置正确。
- 解决:检查
添加服务就绪等待逻辑:
docker run后立刻执行pytest,此时gunicorn服务可能未完全启动,数据库连接池未初始化。本地机器性能较好,服务启动快,未触发该问题。- 解决:在
docker run和docker exec之间添加等待逻辑,比如用curl轮询应用健康检查接口(如http://localhost:5050/health),直到返回成功状态再执行测试。
- 解决:在
排查网络权限问题:
GitLab Runner的Docker网络可能存在特殊配置,导致容器内测试进程无法访问数据库。- 解决:检查数据库防火墙规则,确认Runner所在IP段是否被允许;或使用Docker Compose将应用与测试数据库放在同一自定义网络中,避免网络隔离问题。
总结
核心问题是测试进程无法获取有效数据库连接,大概率由环境变量缺失、测试配置错误、服务未就绪或网络权限限制导致。逐一排查上述方向,重点确保测试进程的环境与本地一致,且数据库访问权限配置正确。
内容的提问来源于stack exchange,提问作者George Rouvalis

