Set up file and image uploads to S3

So, you want to use S3 to handle image uploads? Here’s the definitive guide, but also see Configure an S3 compatible object storage provider for uploads to see how to configure your app.yml.

S3 registration

Head over to https://aws.amazon.com/free/ and click on Create a Free Account

During the create account process, make sure you provide payment information, otherwise you won’t be able to use S3. There’s no registration fee, you will only be charged for what you use, if you exceed the AWS Free Usage Tier.

Bucket

Go to S3 and click on Create bucket, then fill out the Bucket name. Remember this name because we’ll need it for the next step.

Name of your bucket

Select a Region. You should enter the location (eg. “EU (Frankfurt)”) that is nearest to your users for better performance.

Scroll down a little until you get to the Permissions panel.

  • When you set up the permissions, make sure that you allow public ACLs, otherwise uploads will fail. Uncheck Block all public access and check the bottom two checkboxes. You’ll also have to acknowledge that your settings may make the bucket public in the warning at the top.

User creation

Creating a policy

Sign in to AWS Management Console and search for the “IAM” service to access the AWS Identity and Access Management (IAM) console which enables you to manage access to your AWS resources.

First, click Policies in the sidebar. Then, click on Create Policy and choose the JSON tab:

image

Use the following piece of code as a template for your policy document:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
               "s3:List*",
               "s3:Get*",
               "s3:AbortMultipartUpload",
               "s3:DeleteObject",
               "s3:PutObject",
               "s3:PutObjectAcl",
               "s3:PutObjectVersionAcl",
               "s3:PutLifecycleConfiguration",
               "s3:CreateBucket",
               "s3:PutBucketCORS"
      ],
      "Resource": [
        "arn:aws:s3:::=BUCKET=",
        "arn:aws:s3:::=BUCKET=/*"
      ]
    },
    {
       "Effect": "Allow",
       "Action": [
           "s3:ListAllMyBuckets",
           "s3:ListBucket"
       ],
       "Resource": "*"
    }
  ]
}

:warning: Make sure that these two lines contain the actual name of your bucket. :blush:

image

:star2: If you also intend to do S3 backups, you can include your backup bucket here too like this:

image

Then click on Review Policy and fill out the Name field. For example, you can call it s3-discourse-policy

Then click on Create Policy at the bottom.

Creating a user account

Now that you’ve created the right policy, you’re ready to create the user account. Click on the Users link on the left side of the IAM console and then the Add user button.

Type in a descriptive user name and make sure the “Programmatic access” checkbox is checked.

Setting permissions

The next step in the process is to configure the user’s permissions. Click on the button that says Next: Permissions and then click on «Attach existing policies directly»:
image

Now search for the policy name you created in the previous step (in our case it’s s3-discourse-policy). When you find it, select the checkbox and click on Next: Tags, then Next: Review, then finally Create user.

Here’s the critical step: Make sure you either download the credentials (Download .csv) or you copy and paste somewhere safe both Access key ID and Secret access key values. We will need them in the next step.

Discourse configuration

Now that you’ve properly set up S3, the final step is to configure your Discourse forum. These instructions should work, but the preferred method is to use environment variables and a CDN as described in Configure an S3 compatible object storage provider for uploads.

Make sure you’re logged in with an administrator account and go the Settings section in the admin panel.

Type in “S3” in the textbox on the right to display only the relevant settings:

You will need to:

  • Check the “enable s3 uploads” checkbox to activate the feature
  • Paste in both “Access Key Id” and “Secret Access Key” in their respective text fields
  • Enter =BUCKET= in the “s3 upload bucket” field

You need to append a prefix to the bucket name if you want to use the same bucket for uploads and backups.

Examples of valid bucket settings
  1. Different buckets

    • s3_upload_bucket: =BUCKET=
    • s3_backup_bucket: =BACKUPS=
  2. Different prefixes

    • s3_upload_bucket: =BUCKET=/uploads
    • s3_backup_bucket: =BUCKET=/backups
  3. Prefix for backups

    • s3_upload_bucket: =BUCKET=
    • s3_backup_bucket: =BUCKET=/backups

The “s3_region” setting is optional and defaults to “US East (N. Virginia)”. You should enter the location (eg. “EU (Frankfurt)”) that is nearest to your users for better performance. If you created the bucket manually, you’ll need to select the region you selected during the creation process.

Enjoy

That’s it. From now on, all your images will be uploaded to and served from S3.

Backups

Do you want store backups of your Discourse forum on S3 as well? Take a look at Configure automatic backups for Discourse.

Frequently Asked Questions

I reused the same bucket for uploads and backups and now backups aren’t working. What should I do?

Name of your bucket for backups

The easiest solution is to append a path to the s3_backup_bucket. Here’s an example of how your settings should look afterwards.

  • s3_upload_bucket: =BACKUPS=
  • s3_backup_bucket: =BACKUPS=/backups

You can use the S3 Console to move existing backups into the new folder.

Do I really need to use separate buckets for uploads and backups?

No, you don’t, but it’s usually the easiest way to set-up. Essentially you need to either use two different buckets or a prefix for the backup bucket. For example, the following combinations will work:

  1. Different buckets

    • s3_upload_bucket: =BUCKET=
    • s3_backup_bucket: =BACKUPS=
  2. Different prefixes

    • s3_upload_bucket: =BUCKET=/uploads
    • s3_backup_bucket: =BUCKET=/backups
  3. Prefix for backups (not recommended unless you previously reused the same bucket – see above question)

    • s3_upload_bucket: =BACKUPS=
    • s3_backup_bucket: =BACKUPS=/backups

I’ve enabled S3 uploads in my Discourse instance (which has been going for a while); what do I do with the existing local uploads?

To migrate your existing uploads to S3, you can do a couple of rake tasks. To perform this, you need SSH access, root permissions, and have entered the discourse app (as per Administrative Bulk Operations). Oh, and you have to set some environmental variables in app.yml. Not for the faint-hearted.

Once you have done all that you are ready for the rake tasks:

rake uploads:migrate_to_s3

Once that is done, posts that need to be rebaked will be marked accordingly and will be rebaked by a regular task. If you have resources and are in a hurry, you can issue this command (which is recommended by the above rake task):

rake posts:rebake_uncooked_posts

Once the posts are all rebaked (and the uploads are working well) you no longer need to include uploads in your backups. And as a bonus, you will be able to Restore a backup from the command line in the event of catastrophe (just keep a copy of app.yml somewhere).

One-way door

Unlike many configuration decisions in Discourse, note that using S3 is a “one-way door;” that is, a move that cannot easily be reversed. There is no safe or maintained way to move files out of S3 to be in the local uploads. In particular, the migrate_to_s3 moves more than just post images to S3; files for which there is no reverse path. (For more details, see Migrate_from_s3 problems).

Backing up your S3 assets

Using versioning, or syncing to a different region are all good strategies.

106 Likes

This narrative needs to be updated. Or I can’t. After the installation, the images are uploaded to the bucket, but I cannot access them. No matter what I did, it didn’t work. Has anyone tried this recently and got it working?

1 Like

What happens when you try to access them?

Do you have a cdn?

What service did you use?

Are the images uploaded and bucket permissions won’t let you see them?

If the pm need to be updated you’ll g feed to provide more info about what you did and what is happening.

Is the site public? Where

1 Like

I follow the explanation exactly. uploading pictures. However, the images cannot be accessed. Example url: https://awssorucevap24.s3.eu-central-1.amazonaws.com/original/2X/6/62c89621b0764112e0029bc91c957dd49d75d819.jpeg

The permissions section looks like this:

I do not open the ACL when installing Bucket, there is no information about it, and I cannot access it even when I change this setting in my previous attempts.

Additional Note: There is no “Programmatic access” setting when creating a user, it was there a long time ago, it is not there anymore. Could it be related to this? Or can you explain how we can do this in the new system when creating a user?

I also prepared the cloudfront and cdn domain. My only problem is I don’t have access to the files. I hope we can find what I missed :slight_smile:

1 Like

rake uploads:migrate_to_s3
FileStore::ToS3MigrationError: Please provide the following environment variables: (FileStore::ToS3MigrationError)

  • DISCOURSE_S3_BUCKET
  • DISCOURSE_S3_REGION
    and either
  • DISCOURSE_S3_ACCESS_KEY_ID
  • DISCOURSE_S3_SECRET_ACCESS_KEY
    or
  • DISCOURSE_S3_USE_IAM_PROFILE

As I can see, it is neither Access key OR IAM Profile. In my case, I’m using IAM Profie. Any recommendation here?

1 Like

I think you need to define this data in yml file. First, make sure that the upload process works and then migrate.

How did you configure it by the way? I tried to do this, the files were uploading but they were not opening in the browser. It was giving an access denied error.

1 Like

The upload and public read access are working correctly.

I’m using the IAM Role and Policy attached to EC2. Let me know if you want to have more details about this.

Ah, I believe I know what is going on with your case. Check these configs:

Block public access (bucket settings)

Object Ownership

Access control list (ACL) – Probably here is the trick for you

3 Likes

I didn’t see anything in the explanation about turning on the ACL setting, so I tried to do it without touching it every time.

It’s not important anymore for me, I’ll switch to cloudflare R2. This explanation will be very useful for someone else in the future. Thanks.

2 Likes

I am unsure if this is the best configuration, but it’s the way I found it so far. I want to ask the @team to take a look and guide us.

Is this the best way to keep it safe and working correctly?

2 Likes

Could you share more details about this?

1 Like

After some effort, I switched to R2 (let’s not forget the friends who helped, respect). assets and uploads files are published on clouflare. The only problem is that I could not have the theme-javascripts and stylesheets files automatically uploaded to R2. I will research this issue when I have time.

1 Like

Was this a technical or cost decision or both?

1 Like

The first one is technical, I couldn’t install Amazon, but I was already using Cloudflare, so I thought why not install CDN from there. And then I saw that it was a good decision because cloudflare launched this service for us developers against the overly expensive S3 cloud systems. Isn’t it very nice?

1 Like

It’s nice. I will take a deeper look at it.

And what about integrating into discourse? How was that experience?

Are you also using Cloudflare WAF?

1 Like

No I haven’t used it, actually I’m not sure if I’ll need it. But I use cloudflare “ARGO”.

2 Likes

I have an S3 bucket for image uploads with ACL enabled and an IAM role properly configured and assigned to the EC2 instance running my standalone instance. However, the S3 image URLs in my test posts are not viewable. I would like to host S3 images in a bucket that can only be viewed by logged-in Discourse users. Why aren’t URL references to S3 images including a pre-signed URL for proper access? If I access the file via the S3 console and request a pre-signed url via AWS for a defined amount of time like 10 minutes then the image will load as expected so I know it can work.

1 Like

Are we sure that it must be stored in the app.yml file?

I did it like this;

# enter the app
cd /var/discourse
./launcher enter app

# pass the environment variables with the command at runtime
DISCOURSE_S3_BUCKET=my-bucket DISCOURSE_S3_REGION=us-east-1 DISCOURSE_S3_ACCESS_KEY_ID=abcdefg DISCOURSE_S3_SECRET_ACCESS_KEY=zxywqrst rake uploads:migrate_to_s3

I did it this way since I did not want my access key to stick around in the YAML file because I back that up in various places. Passing the AWS secret key through the environment is ~generally~ a little “safer”. I think, right? Not sure how long the scrollback history inside the app container persists, I am guessing it gets wiped after the container restarts.

I wanted to instead configure S3 access the “normal” way by storing the AWS Access Key and Secret Key in the server’s ~/.aws/credentials file, however, I am not entire sure how that would work with the app running inside the container.

Also, as per the guide here, you are instructed to just copy/paste the Access and Secret keys into the Discourse Admin Settings web UI and Save them there; it is not clear to me where these keys are getting stored in the back-end, since Saving them here does not populate the app.yml file with them. So I am guessing they are in the Postgres db somehwere? Hopefully encrypted, maybe?

I am hoping that by not storing the AWS Access and Secret keys in the app.yml, I will not run into any issues in the future? Is there some other process that requires the keys to live there?

1 Like

I went ahead and enabled AWS Cloudfront for a CDN cache in front of my S3 bucket which was setup as described here. However, configuring Cloudfront prompted for some modifications to the S3 access policies. In particular, there seem to be some policies that would restrict S3 bucket access to only Cloudfront, instead of the public access that this guide suggests. Is anyone able to review what the correct S3 bucket permissions policies should be if you are using Cloudfront CDN with your S3 bucket on Discourse? Currently, my CDN is working, but I am not sure if I need to remove any extraneous access permissions from the S3 bucket.

1 Like

I used this bucket policy shown in the first post of this topic, on a new Aws bucket. But at this day, it is giving Json Syntax error.

Error giving policy given in the OP/1st post of the topic: Gives "Missing Principal", "Unsupported Policy" errors in Line 4, 23, 26, 29, and when I happen to fix those errors somehow by reading aws docus, then surfaced "Json Syntax Error" towards the end of code.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
               "s3:List*",
               "s3:Get*",
               "s3:AbortMultipartUpload",
               "s3:DeleteObject",
               "s3:PutObject",
               "s3:PutObjectAcl",
               "s3:PutObjectVersionAcl",
               "s3:PutLifecycleConfiguration",
               "s3:CreateBucket",
               "s3:PutBucketCORS"
      ],
      "Resource": [
        "arn:aws:s3:::your-uploads-bucket",
        "arn:aws:s3:::your-uploads-bucket/*"
      ]
    },
    {
       "Effect": "Allow",
       "Action": [
           "s3:ListAllMyBuckets",
           "s3:ListBucket"
       ],
       "Resource": "*"
    }
  ]
}
The one I tried creating, which doesn't show any syntax errors in console, except one "Invalid Principal" error, which I couldn't overcome #### Used my own bucket names
{
  "Id": "Policy1725098748430",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1725098741436",
      "Action": [
        "s3:AbortMultipartUpload",
        "s3:CreateBucket",
        "s3:DeleteObject",
        "s3:GetObject",
        "s3:List*",
        "s3:ListBucket",
        "s3:ListBucketMultipartUploads",
        "s3:PutBucketCORS",
        "s3:PutLifecycleConfiguration",
        "s3:PutObject",
        "s3:PutObjectAcl",
        "s3:PutObjectVersionAcl"
      ],
      "Effect": "Allow",
      "Resource": [
	"arn:aws:s3:::t9478010203",
	"arn:aws:s3:::t9478010203/backups/",
	"arn:aws:s3:::t9478010203/uploads/"
	],
      "Principal": {
        "AWS": [
          "123456789012"
        ]
      }
    }
  ]
}
1 Like

This topic really needs a refresh - I was just in S3/IAM fiddling with the policy and noticed the errors too. Seems to work fine fortunately!

I wonder if someone with the skills and knowledge required would mind taking a look and updating what the policy should be. @pfaffman?

5 Likes