docker容器中的django + nginx:无法上传任何带有表单的文件
Posted
技术标签:
【中文标题】docker容器中的django + nginx:无法上传任何带有表单的文件【英文标题】:django + nginx in docker containers: can not upload any file with forms 【发布时间】:2017-01-27 05:22:32 【问题描述】:我正在尝试将我的应用程序迁移到 docker 容器,但遇到了使用 django 表单上传文件的问题。一切正常,但是当我尝试从我的应用程序中的表单上传任何文件时,我什么也得不到,比如 multipart/form-data
未在 html 表单标签中设置。但是,如果我直接安装应用程序而不使用 docker,它会设置并且一切正常。无论如何,这是我的配置,我希望有人可以帮助我。
这是我的 docker-compose.yml
version: '2'
services:
db_postgres:
build:
context: .
dockerfile: dockerfiles/docker-postgres/Dockerfile
args:
- db_user=username
- db_name=databasename
- db_pass=password
environment:
LC_ALL: C.UTF-8
app:
restart: always
build:
context: .
dockerfile: dockerfiles/docker-app/Dockerfile
links:
- db_postgres:db_postgres
nginx:
restart: always
build: dockerfiles/docker-nginx
volumes_from:
- app
ports:
- "80:80"
- "443:443"
links:
- app:applink
这是应用程序 Dockerfile:
FROM ubuntu:16.04
RUN \
apt-get update && \
apt-get install -y python-pip python-dev build-essential python-virtualenv && \
apt-get install -y libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev && \
apt-get install -y libpq-dev libffi-dev && \
apt-get install -y libssl-dev git
RUN mkdir app
COPY requrements.txt /app
RUN pip install --upgrade pip
RUN pip install -r /app/requrements.txt
ADD . /app
WORKDIR /app
VOLUME ["/app/staticfiles/", "/app/media/", "/app/protected/"]
# I tried this but it seams no effect at all
# this is media folders where users can upload their files
# nginx can read from this folders with no problem
# I tried to docker exec and nginx can even write there
# anyways I tried to start nginx as root
RUN chown www-data:www-data -R /app/media/
RUN chown www-data:www-data -R /app/protected/
ADD dockerfiles/docker-app/django_entrypoint.sh .
RUN chmod +x django_entrypoint.sh
CMD ["./django_entrypoint.sh"]
EXPOSE 8000
这里是 django_entrypoint.sh
#!/bin/bash
NAME=my_app_name
USER=www-data
GROUP=www-data
NUM_WORKERS=8
DJANGO_WSGI_MODULE=my_application.wsgi
python manage.py makemigrations
python manage.py migrate
echo "Collenting staticfiles..."
python manage.py collectstatic --noinput > /dev/null
python manage.py initadmin
python manage.py init_default_settings
exec gunicorn $DJANGO_WSGI_MODULE:application \
--name $NAME \
--workers $NUM_WORKERS \
--user=$USER --group=$GROUP \
--bind=:8000 \
--log-level=debug \
--capture-output
这里是 nginx Dockerfile:
FROM ubuntu:16.04
# Install Nginx.
RUN apt-get update && \
apt-get install -y nginx && \
rm -rf /var/lib/apt/lists/* && \
echo "\ndaemon off;" >> /etc/nginx/nginx.conf && \
chown -R www-data:www-data /var/lib/nginx
# Define mountable directories.
VOLUME ["/etc/nginx/sites-available", "/etc/nginx/certs", "/etc/nginx/conf.d"]
ADD confgfile /etc/nginx/sites-available/
RUN rm /etc/nginx/sites-enabled/default && rm /etc/nginx/sites-available/default
RUN ln -s /etc/nginx/sites-available/ctrd /etc/nginx/sites-enabled/ctrd
# Define default command.
CMD ["nginx"]
# Expose ports.
EXPOSE 80
EXPOSE 443
这里是 nginx 的配置文件:
server
listen 80;
# I pasted my server ip in sever name
server_name 175.116.110.231;
client_max_body_size 300M;
error_log stdout debug;
location /static/
alias /app/staticfiles/;
location /media/
alias /app/media/;
location /
try_files $uri @proxy;
location @proxy
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-FILE $request_body_file;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://applink:8000;
location /private-uploads/
internal;
alias /app/protected/records/;
该应用程序运行良好,但是当我想上传任何文件时,它什么也不做,没有文件,错误日志中也没有错误。如果 filefield 不能为空(django 表单中的 requred=True),我得到了错误。 在浏览器工具中请求:
Request URL:http://175.116.110.231/edit-avatar/2
Request Method:POST
Status Code:302 Found
Remote Address:175.116.110.231:80
Response Headers
view source
Connection:keep-alive
Content-Language:ru
Content-Type:text/html; charset=utf-8
Date:Mon, 19 Sep 2016 13:26:59 GMT
Location:/profile/
Server:nginx/1.10.0 (Ubuntu)
Transfer-Encoding:chunked
Vary:Accept-Language, Cookie
X-Frame-Options:SAMEORIGIN
Request Headers
view source
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4
Cache-Control:max-age=0
Connection:keep-alive
Content-Length:30316
Content-Type:multipart/form-data; boundary=----WebKitFormBoundarySCXVqlqPsCHyaHtU
Cookie:JSESSIONID=dummy; sessionid=htnbd9coal4ws2ansxzze8bhtu4fq6do; csrftoken=tCD5cVrR0IUGkjkkbJKDsxdRrtyLIUGbOIHkjHKjhkjhmnVJhvKUGkBDjZ
DNT:1
Host:175.116.110.231
Origin:http://175.116.110.231
Referer:http://175.116.110.231/edit-avatar/2
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.90 Safari/537.36 Vivaldi/1.4.589.11
Request Payload
------WebKitFormBoundarySCXVqlqPsCHyaHtU
Content-Disposition: form-data; name="csrfmiddlewaretoken"
zaAvb1KoCCovbuCbik261UDeZgQbJdumzcvpcqOHqTKIrRN826lEoeb5AvU7SrG6
------WebKitFormBoundarySCXVqlqPsCHyaHtU
Content-Disposition: form-data; name="avatar"; filename="ava2.jpg"
Content-Type: image/jpeg
------WebKitFormBoundarySCXVqlqPsCHyaHtU--
启动时更新 gunicorn 调试输出:
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [1] [DEBUG] Current configuration:
ctrd_app_1 | secure_scheme_headers: 'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'
ctrd_app_1 | proxy_protocol: False
ctrd_app_1 | worker_connections: 1000
ctrd_app_1 | statsd_host: None
ctrd_app_1 | max_requests_jitter: 0
ctrd_app_1 | post_fork: <function post_fork at 0x7fb7e74ee230>
ctrd_app_1 | pythonpath: None
ctrd_app_1 | enable_stdio_inheritance: False
ctrd_app_1 | worker_class: sync
ctrd_app_1 | ssl_version: 3
ctrd_app_1 | suppress_ragged_eofs: True
ctrd_app_1 | syslog: False
ctrd_app_1 | syslog_facility: user
ctrd_app_1 | when_ready: <function when_ready at 0x7fb7e74e5ed8>
ctrd_app_1 | pre_fork: <function pre_fork at 0x7fb7e74ee0c8>
ctrd_app_1 | cert_reqs: 0
ctrd_app_1 | preload_app: False
ctrd_app_1 | keepalive: 2
ctrd_app_1 | accesslog: None
ctrd_app_1 | group: 33
ctrd_app_1 | graceful_timeout: 30
ctrd_app_1 | do_handshake_on_connect: False
ctrd_app_1 | spew: False
ctrd_app_1 | workers: 8
ctrd_app_1 | proc_name: django_ctrd_app
ctrd_app_1 | sendfile: None
ctrd_app_1 | pidfile: None
ctrd_app_1 | umask: 0
ctrd_app_1 | on_reload: <function on_reload at 0x7fb7e74e5d70>
ctrd_app_1 | pre_exec: <function pre_exec at 0x7fb7e74ee7d0>
ctrd_app_1 | worker_tmp_dir: None
ctrd_app_1 | post_worker_init: <function post_worker_init at 0x7fb7e74ee398>
ctrd_app_1 | limit_request_fields: 100
ctrd_app_1 | on_exit: <function on_exit at 0x7fb7e74eee60>
ctrd_app_1 | config: None
ctrd_app_1 | logconfig: None
ctrd_app_1 | check_config: False
ctrd_app_1 | statsd_prefix:
ctrd_app_1 | proxy_allow_ips: ['127.0.0.1']
ctrd_app_1 | pre_request: <function pre_request at 0x7fb7e74ee938>
ctrd_app_1 | post_request: <function post_request at 0x7fb7e74eea28>
ctrd_app_1 | user: 33
ctrd_app_1 | forwarded_allow_ips: ['127.0.0.1']
ctrd_app_1 | worker_int: <function worker_int at 0x7fb7e74ee500>
ctrd_app_1 | threads: 1
ctrd_app_1 | max_requests: 0
ctrd_app_1 | limit_request_line: 4094
ctrd_app_1 | access_log_format: %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"
ctrd_app_1 | certfile: None
ctrd_app_1 | worker_exit: <function worker_exit at 0x7fb7e74eeb90>
ctrd_app_1 | chdir: /app
ctrd_app_1 | paste: None
ctrd_app_1 | default_proc_name: calltrade.wsgi:application
ctrd_app_1 | errorlog: -
ctrd_app_1 | loglevel: debug
ctrd_app_1 | capture_output: True
ctrd_app_1 | syslog_addr: udp://localhost:514
ctrd_app_1 | syslog_prefix: None
ctrd_app_1 | daemon: False
ctrd_app_1 | ciphers: TLSv1
ctrd_app_1 | on_starting: <function on_starting at 0x7fb7e74e5c08>
ctrd_app_1 | worker_abort: <function worker_abort at 0x7fb7e74ee668>
ctrd_app_1 | bind: [':8000']
ctrd_app_1 | raw_env: []
ctrd_app_1 | reload: False
ctrd_app_1 | limit_request_field_size: 8190
ctrd_app_1 | nworkers_changed: <function nworkers_changed at 0x7fb7e74eecf8>
ctrd_app_1 | timeout: 30
ctrd_app_1 | ca_certs: None
ctrd_app_1 | django_settings: None
ctrd_app_1 | tmp_upload_dir: None
ctrd_app_1 | keyfile: None
ctrd_app_1 | backlog: 2048
ctrd_app_1 | logger_class: gunicorn.glogging.Logger
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [1] [INFO] Starting gunicorn 19.6.0
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [1] [DEBUG] Arbiter booted
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [1] [INFO] Using worker: sync
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [38] [INFO] Booting worker with pid: 38
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [39] [INFO] Booting worker with pid: 39
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [44] [INFO] Booting worker with pid: 44
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [47] [INFO] Booting worker with pid: 47
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [48] [INFO] Booting worker with pid: 48
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [51] [INFO] Booting worker with pid: 51
ctrd_app_1 | [2016-09-19 16:33:47 +0000] [54] [INFO] Booting worker with pid: 54
ctrd_app_1 | [2016-09-19 16:33:48 +0000] [59] [INFO] Booting worker with pid: 59
ctrd_app_1 | [2016-09-19 16:33:48 +0000] [1] [DEBUG] 8 workers
我也在 gunicorn 容器上尝试了 8000:8000 端口暴露,它也无法保存文件,所以原因不是 nginx,而是我的 docker 卷配置错误。有人可以解释 docker 卷吗?请检查我的卷配置,我一定不明白它应该如何工作。 请帮忙。
这是媒体服务视图:
@login_required
def private_media_response(request, username, filename):
"""
In the nginx setting we can use something like here:
location /private-uploads/
internal;
alias /place/to/private/media/;
"""
user = request.user
if user.username == username:
response = HttpResponse()
url = '/private-uploads/0/1'.format(username, filename)
response.status_code = 200
response['X-Accel-Redirect'] = url.encode('utf-8')
response['X-Accel-Buffering'] = 'yes'
return response
else:
return HttpResponseForbidden("Restricted Access")
问题是没有得到文件,我不能用表单保存它。 我保存文件的所有观点都类似于模型更新 CBV 或此标准视图:
@login_requred
def file_save_view(request, **kwags):
if request.POST:
form = MyForm(request.POST, request.FILES)
if form.is_valid()
form.save()
else:
form = myForm();
return render(request, 'some_template.html', 'form':form)
标准模型将文件数据保存在媒体中,对于我创建的受保护文件FileSystemStorage
location = os.path.join(settings.BASE_DIR, 'protected')
fs = FileSystemStorage(location=location)
然后在模型中使用它:
def get_upload_path(instance, filename):
user = instance.user
return 'documents/0/1'.format(user, filename)
class MyModel(models.Model):
date = models.DateTimeField(auto_now=True, blank=False)
user = models.ForeignKey(User)
document = models.FileField(
upload_to=get_upload_path,
storage=fs,
)
pass
这段代码有点抽象,但它适用于标准部署方式。我猜卷一定有问题。
【问题讨论】:
如果我尝试保存不受支持的文件类型 - 我在表单错误消息中收到有关它的消息,那么 django 可以看到该文件,但由于某种原因无法保存?也许我对卷做错了,我应该如何用 docker 保存媒体文件? 为什么你认为这个问题与 Docker 或 nginx 或处理文件处理的实际代码以外的任何其他东西有关......这是你唯一没有包括的东西?跨度> 因为如果我将应用程序部署到单个服务器,代码工作正常,主机上只有 nginx + django + gunicorn。代码中没有什么特别之处,只是一些基于类的视图,用文件字段更新模型。无论如何,我不确定 100%。 【参考方案1】:所以,你似乎在某种程度上误解了卷(尽管我没有看到任何会导致你的问题的东西)。
卷来自 Docker 用于其映像的“写入时复制”文件系统。每个层只包含自上次使用以来未更改的新信息 - 这使您可以高效地构建图像,而无需为从该图像开始的每个容器复制大量数据。
一个卷在说“不要为此目录使用写入文件系统的副本”。所以VOLUME ["/etc/nginx/sites-available", "/etc/nginx/certs", "/etc/nginx/conf.d"]
实际上是在告诉 Docker 将这些目录保留在您构建的映像之外。这不是您想要的...我看到您在将配置目录标记为卷之后将配置文件添加到您的 nginx 配置...这些文件更改不会传播到您正在构建的映像中。
关于你的实际问题 - 我不知道那是什么。部署配置都与“保存文件”无关。唯一可能的地方是,如果您尝试从 django 保存它,然后使用 nginx 将其备份...但是您说该文件没有被保存,这意味着 nginx 与它无关。我会向您的应用程序本身添加一些调试输出,以尝试找出发生了什么。如果它返回 200 而没有按照它所说的去做(将一些文件保存到磁盘)......这就是我要开始的地方。
【讨论】:
谢谢你,保罗,我将学习更多关于卷的文档,感谢你的笔记,它们帮助我理解更多。我将尝试使用音量并将结果发布在这里。 代码是基于类的更新视图,它只是用文件字段更新一些模型。它可以在没有 docker 的情况下工作。我发现该文件一直到 django 应用程序,因为 django 可以提供它的类型和大小。它接缝它没有权限将其写入磁盘,但我在日志中没有任何关于它的报告,需要更深入地调试它。 docker 卷可以以某种方式覆盖应该保存文件的位置吗? 你没有提供代码,所以这是一个随机猜测......目录不存在吗?您在 Dockerfile 中创建/app/protected/
,但 nginx 正在尝试从 /app/protected/records
读取...
是的,因为这是受保护的文件,它们应该通过查看文件的权限来提供服务,并返回 nginx 'X-Accel-Redirect' 响应以服务内部请求。但这不仅是文件保存不起作用的地方,还有很多地方都有标准视图,可以从 /media/ 目录保存和读取媒体图像。我在第一条消息中添加了私人媒体服务视图。
是的,目录存在,当 docker exec 到具有该卷的容器时,我可以看到其中的公共文件。我可以在那里创建任何文件,并且在重新启动容器后,文件仍然存在。我不明白一些事情:如果我从包含的应用程序(例如 /media/)创建卷而没有将其安装在主机上,我可以将文件保存在那里吗?我应该在 dockersystem 的哪个位置保存用户媒体文件?我应该在 dockerfile 末尾创建卷吗?【参考方案2】:
对我来说,问题出在非常奇怪的地方。
有型号:
class Document(Model):
name = CharField()
file = ImageField(
upload_to='documents/%Y/%m/',
default='default.doc'
)
如果我在这里删除default
,它会起作用!但是默认版本在 docker 之外可以正常工作。
【讨论】:
非常奇怪......我面临类似的问题,wagtail 文件上传不起作用,但 django 管理员的文件可以。以上是关于docker容器中的django + nginx:无法上传任何带有表单的文件的主要内容,如果未能解决你的问题,请参考以下文章