Backup upload failure due to uninitialized TransferManager

After updating to v2026.8.0-latest.1 +4, my backups have started failing. These are the logs: log.txt.zip (22.1 KB)

In particular, here’s the section that looks relevant:

[2026-08-04 03:37:52] Uploading archive...
[2026-08-04 03:37:52] EXCEPTION: uninitialized constant Aws::S3::TransferManager
[2026-08-04 03:37:52] /var/www/discourse/lib/s3_helper.rb:453:in 'S3Helper#transfer_manager'
/var/www/discourse/lib/s3_helper.rb:332:in 'S3Helper#upload_file'
/var/www/discourse/lib/backup_restore/s3_backup_store.rb:48:in 'BackupRestore::S3BackupStore#upload_file'
/var/www/discourse/lib/backup_restore/creator.rb:437:in 'BackupRestore::Creator#upload_archive'
/var/www/discourse/lib/backup_restore/creator.rb:41:in 'BackupRestore::Creator#run'
/var/www/discourse/lib/backup_restore.rb:13:in 'BackupRestore.backup!'
/var/www/discourse/app/jobs/regular/create_backup.rb:10:in 'Jobs::CreateBackup#execute'
/var/www/discourse/app/jobs/base.rb:313:in 'block (2 levels) in Jobs::Base#perform'

I’m not sure how to solve this issue.

I updated to v2026.9.0-latest +565, but I still ran into the same exact issue.

The Discourse version you’re running includes an aws-sdk-s3 version that should provide Aws::S3::TransferManager.

Are you using the official Discourse Docker installation? If you’re using the official Docker setup, was the container fully rebuilt as part of the upgrade?

Yes, I believe I’m using the official Discourse Docker installation. Is there a way for me to know for sure? It’s been a while since I’ve performed the installation.

To update, I ran ./launcher rebuild app. I assume this means the container was fully rebuilt, but maybe I’m mistaken?

A few more details that may be relevant:

  • Running Ubuntu 24.04.4 LTS
  • app.yml installs the official docker_manager and discourse-doc-categories plugins in the after_code hook. There are no other plugins defined in app.yml.
  • I’m trying to store the backups in an S3-compatible storage provided by Backblaze, and this had worked beforehand.

One thing that may help narrow this down is checking which aws-sdk-s3 gem is actually being loaded inside the running container.

The Discourse change which introduced Aws::S3::TransferManager also
bumped aws-sdk-s3 from 1.182.0 to 1.227.0:

At the Discourse revision you linked, Gemfile.lock should still be using
aws-sdk-s3 1.227.0, which does provide Aws::S3::TransferManager.

Could you try entering the container:

cd /var/discourse
./launcher enter app

Then, inside the container:

cd /var/www/discourse
bundle info aws-sdk-s3
bundle exec ruby -e '
require "aws-sdk-s3"
puts "gem: #{Gem.loaded_specs["aws-sdk-s3"]&.full_name}"
puts "path: #{Gem.loaded_specs["aws-sdk-s3"]&.full_gem_path}"
p Aws::S3.autoload?(:TransferManager)
p Aws::S3::TransferManager
'

It may also be useful to check the application itself under Rails:

RAILS_ENV=production bundle exec rails runner '
puts "aws-sdk-s3: #{Gem.loaded_specs["aws-sdk-s3"]&.version}"
p Aws::S3.autoload?(:TransferManager)
p Aws::S3::TransferManager
'

Then leave the container with:

logout

If that reports an older aws-sdk-s3, or TransferManager is missing,
that would explain the backup exception and would suggest that the container’s installed gems don’t match the current Discourse
Gemfile.lock.

If it reports 1.227.0 and successfully resolves
Aws::S3::TransferManager, then the issue is more unusual and we’d know to look at what differs between the Sidekiq backup process and that interactive environment.

Since you’re using ./launcher rebuild app, that does sound like the standard discourse_docker workflow; the commands above should tell us more precisely what actually ended up in the rebuilt container.

It looks like I’ve got an old version of aws-sdk-s3.

$ bundle info aws-sdk-s3
  * aws-sdk-s3 (1.177.0)
	Summary: AWS SDK for Ruby - Amazon S3
	Homepage: https://github.com/aws/aws-sdk-ruby
	Source Code: https://github.com/aws/aws-sdk-ruby/tree/version-3/gems/aws-sdk-s3
	Changelog: https://github.com/aws/aws-sdk-ruby/tree/version-3/gems/aws-sdk-s3/CHANGELOG.md
	Path: /var/www/discourse/vendor/bundle/ruby/3.4.0/gems/aws-sdk-s3-1.177.0

Looking into this reminded me that I used this workaround to get around an issue with AWS SDK not working with Backblaze. Catching up on the topic, it looks like Backblaze updated their API, which makes the workaround unnecessary.

After I removed the workaround, the backup succeeded! Thanks for the help. :heart:

One thought from this: temporary compatibility templates like this may be safer if they have some kind of self-expiring/guard condition, rather than continuing to override Discourse dependencies indefinitely.

For example, with temporary source backports, i’m planning to use hooks in production that first check whether the upstream fix is already present, and only apply the patch if it is still needed.

In this case, something along these lines could potentially have prevented the old aws-sdk-s3 1.177.0 pin from continuing once Discourse itself started depending on newer SDK functionality:

hooks:
  after_bundle_exec:
    - exec:
        cd: $home
        cmd:
          - |
            if grep -q "Aws::S3::TransferManager" lib/s3_helper.rb; then
              echo "Discourse now requires Aws::S3::TransferManager; skipping obsolete Backblaze aws-sdk-s3 downgrade"
            else
              echo "Applying temporary Backblaze aws-sdk-s3 compatibility pin"
              bundle config set frozen false
              sed -i 's/gem "aws-sdk-s3", require: false/gem "aws-sdk-s3", "1.177.0", require: false/' Gemfile
              bundle update aws-sdk-s3
              bundle add aws-sdk-core --version 3.215
            fi

That particular check is just illustrative - a more general version check or an explicit failure when Discourse moves past the expected SDK version would probably be better.

Even failing loudly would likely be preferable to silently producing a container whose gems no longer match the versions expected by Discourse.

While I agree that failing loudly would have been nice, it was the case beforehand that I wanted the gems to not match the versions expected by Discourse, because the newer aws-sdk-s3 version sent headers that Backblaze did not support and I wanted the backups to work.

I don’t know if there was a check I could have proactively set ahead of time to notify me when the workaround was no longer necessary. I implemented the workaround a year and a half ago in April 2025, before a fix was available.

I think the best I could have done is add an expiration date, to force me to check whether or not the workaround is still necessary. Functionality like this might be nice. Or, maybe the onus is upon me to be more proactive and set reminders to check whether or not the workaround is still necessary. Having some notes helped me out here, in that respect.

A small follow-up after running into a very similar situation myself today.

I had a temporary source backport in app.yml for an unmerged Discourse PR. The hook used:

git apply --check /tmp/patch &&
  git apply /tmp/patch

Upstream subsequently refactored one of the affected areas, so the old patch no longer applied cleanly. git apply --check did exactly what I wanted: the patch was not partially applied, and the rebuild failed loudly rather than producing a subtly inconsistent checkout.

There was an important downside, though: because this happened during ./launcher rebuild app, the failed rebuild left the app container unavailable until I removed the obsolete hook and rebuilt again.

So I think I would refine my earlier suggestion slightly. For temporary production workarounds/backports, the safest approach seems to be a combination of:

  • an explicit review/expiry date, as you suggested;
  • a semantic/version guard where one can be made reliable;
  • a preflight applicability check before beginning a production rebuild where practical;
  • and still using git apply --check immediately before git apply as the final safety net.

In my case the expiry/review reminder would have told me to revisit the workaround, while the applicability check prevented the stale patch from silently modifying a newer Discourse checkout.

So your point about an expiration date makes more sense to me now: there often isn’t a perfect automatic test for “is this workaround still necessary?”, particularly when the original workaround deliberately makes the installation differ from upstream.