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:无法上传任何带有表单的文件的主要内容,如果未能解决你的问题,请参考以下文章

用Docker部署Django+uWSGI+Nginx

django 和 gunicorn 在 docker 容器内运行时,无法在 ec2 上使用 nginx 提供静态文件

腾讯云docker部署django uwsgi nginx

腾讯云docker部署django uwsgi nginx

腾讯云docker部署django uwsgi nginx

AWS ECS 使用 docker 和 nginx,如何将我的 nginx 配置放入容器中?