Таймаут загрузки s3 на докеризованной установке Digital Ocean

У меня есть хранилище, совместимое с S3, и сервер Droplet на Digital Ocean. Докеризованное приложение Django, которое я запускаю, пытается синхронизировать статические активы с хранилищем. Это не удается с сервера Droplet / контейнера Docker, но не при доступе к тому же хранилищу S3 из моей локальной установки. Я также могу протестировать загрузку прямо с сервера (вне докеризованного приложения), и это тоже работает. Значит, что-то в настройках Docker приводит к тому, что запросы к S3 не работают.

Я сделал простой тестовый пример, в s3upload.py с foobar.txt, присутствующим в той же директории:

from boto3.s3.transfer import S3Transfer
import boto3
import logging

logging.getLogger().setLevel(logging.DEBUG)

client = boto3.client('s3', 
                      aws_access_key_id="…", 
                      aws_secret_access_key="…", 
                      region_name="ams3", 
                      endpoint_url="https://ams3.digitaloceanspaces.com")
transfer = S3Transfer(client)
bucket_name = "…"
transfer.upload_file("foobar.txt", bucket_name, "foobar.txt")

При вызове этой функции из докер-контейнера я вижу следующую ошибку:

Traceback (most recent call last):
  File "/usr/local/lib/python3.13/site-packages/boto3/s3/transfer.py", line 372, in upload_file
    future.result()
    ~~~~~~~~~~~~~^^
  File "/usr/local/lib/python3.13/site-packages/s3transfer/futures.py", line 103, in result
    return self._coordinator.result()
           ~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/local/lib/python3.13/site-packages/s3transfer/futures.py", line 264, in result
    raise self._exception
  File "/usr/local/lib/python3.13/site-packages/s3transfer/tasks.py", line 135, in __call__
    return self._execute_main(kwargs)
           ~~~~~~~~~~~~~~~~~~^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/s3transfer/tasks.py", line 158, in _execute_main
    return_value = self._main(**kwargs)
  File "/usr/local/lib/python3.13/site-packages/s3transfer/upload.py", line 796, in _main
    client.put_object(Bucket=bucket, Key=key, Body=body, **extra_args)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/botocore/client.py", line 569, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/botocore/client.py", line 1023, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (RequestTimeout) when calling the PutObject operation (reached max retries: 4): None

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/app/s3upload.py", line 14, in <module>
    transfer.upload_file("foobar.txt", bucket_name, "foobar.txt")
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/boto3/s3/transfer.py", line 378, in upload_file
    raise S3UploadFailedError(
    ...<3 lines>...
    )
boto3.exceptions.S3UploadFailedError: Failed to upload foobar.txt to [bucketname]/foobar.txt: An error occurred (RequestTimeout) when calling the PutObject operation (reached max retries: 4): None

Сначала я подумал о возможных настройках брандмауэра службы, но вызов извне контейнера docker (на сервере дроплета) работает. Это также подтверждает, что учетные данные и код загрузки как таковые работают.

Поскольку это кажется связанным с Docker, а не с Digital Ocean Droplet или S3, вот моя настройка docker:

Простой докер compose.yml для приложения выглядит примерно так:

services:

  db:
    image: postgres:17
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    env_file:
      - .env

  django-web:
    build: .
    container_name: django-docker
    ports:
      - "80:80"
    expose:
      - "80"
    depends_on:
      - db
    environment:
      SECRET_KEY: ${SECRET_KEY}
      DEBUG: ${DEBUG}

      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

      DATABASE_HOST: ${DATABASE_HOST}
      DATABASE_PORT: ${DATABASE_PORT}
    env_file:
      - .env

volumes:
  postgres_data:

И Dockerfile:

# Stage 1: Build

# Use the official Python runtime image
FROM python:3.13-slim AS builder 
 
# Create the app directory
RUN mkdir /app
 
# Set the working directory inside the container
WORKDIR /app
 
# Set environment variables 
# Prevents Python from writing pyc files to disk
ENV PYTHONDONTWRITEBYTECODE=1
#Prevents Python from buffering stdout and stderr
ENV PYTHONUNBUFFERED=1 
 
# Upgrade pip
RUN pip install --upgrade pip 
 
# Copy the Django project  and install dependencies
COPY requirements.txt  /app/
 
# run this command to install all dependencies 
RUN pip install --no-cache-dir -r requirements.txt



# Stage 2: Run production code

FROM python:3.13-slim

RUN useradd -m -r appuser && mkdir /app && chown -R appuser /app

# Copy the Python dependencies from the builder stage
COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/
COPY --from=builder /usr/local/bin/ /usr/local/bin/

# Set the working directory
WORKDIR /app

# Copy application code
COPY --chown=appuser:appuser . .
 
# Set environment variables to optimize Python
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 
 
# Switch to non-root user
USER appuser

# Expose the Django port
EXPOSE 80

# # Migrate the DB
# RUN ["python", "manage.py", "migrate"]

# Gather static assets

# ---> This is the s3 command not working
# RUN ["python", "manage.py", "collectstatic", "--no-input"]

# Run the app via the gunicorn server
CMD ["gunicorn", "--bind", "0.0.0.0:80", "--workers", "3", "fonts.wsgi"]
# python manage.py migrate && python manage.py collectstatic --no-input && gunicorn myapp.wsgi

Итак, подведем итоги:

В моей локальной тестовой среде:

  • python s3upload.py работает
  • docker compose exec -T django-web python s3upload.py работает

На моем дроплет сервере Digital Ocean:

  • python s3upload.py работает
  • docker compose exec -T django-web python s3upload.py завершается с вышеуказанной ошибкой

Сообщение об ошибке также очень бесполезно. Я видел и другие проблемы с ошибкой boto3.exceptions.S3UploadFailedError: Failed to upload [filename] to [bucketname]/[filename]: An error occurred (RequestTimeout) when calling the PutObject operation (reached max retries: 4):, но последняя None деталь ошибки немного раздражает.

Есть ли какие-либо подсказки, как отладить это дальше или что может быть причиной этого?

Столкнулись с той же проблемой.

Попробуйте понизить зависимость boto3 до версии 1.35.99.

Вернуться на верх