Can't seem to upload images in Volume

I’m hosting Django (backend) on Fly and React (frontend) on Cloudflare.

Here’s my frontend code to upload an image:

export const createPostApi = async (imageFile, text) => {
    const formData = new FormData();
    formData.append("text", text);
    if (imageFile) formData.append("image", imageFile);

    const response = await api.post("/create-post/", formData, {
        headers: { "Content-Type": "multipart/form-data" },
    });
    return response.data;
};

which goes to this view:

@api_view(['POST'])
def CreatePost(request):
    data = request.data.copy()
    data['text'] = normalize_whitespace(data.get('text', '')).strip()
    serializer = PostSerializer(data=request.data, context={'request': request})
    serializer.is_valid(raise_exception=True)
    serializer.save(user=request.user)

    return Response(serializer.data, status=status.HTTP_201_CREATED)

and here’s the serializer:

class PostSerializer(serializers.ModelSerializer):
    is_mine         = serializers.SerializerMethodField()
    username        = serializers.CharField(source='user.username', read_only=True)
    name            = serializers.CharField(source='user.first_name', read_only=True)
    profile_picture = serializers.ImageField(source='user.profile_picture', read_only=True)
    like_count      = serializers.SerializerMethodField()
    is_liked        = serializers.SerializerMethodField()
    comment_count   = serializers.SerializerMethodField()
    formatted_date  = serializers.SerializerMethodField()
    is_edited       = serializers.SerializerMethodField()

    def get_is_mine(self, obj):
        request = self.context.get('request', None)
        if not request or not request.user.is_authenticated:
            return False
        return obj.user == request.user

    def get_like_count(self, obj):
        return obj.likes.count()

    def get_is_liked(self, obj):
        request = self.context.get('request')
        return request.user in obj.likes.all() if request and request.user.is_authenticated else False

    def get_comment_count(self, obj):
        return obj.comments.count()

    def get_formatted_date(self, obj):
        return obj.created_at.strftime("%d/%m/%Y %H:%M")
    
    def get_is_edited(self, obj):
        return obj.edited

    class Meta:
        ...

and the Post model:

class Post(models.Model):
    user = models.ForeignKey(MyUser, on_delete=models.CASCADE, related_name='posts')
    image = models.ImageField(upload_to=post_upload_path, blank=True, null=True)
    text = models.TextField(max_length=1000, blank=False)
    created_at = models.DateTimeField(auto_now_add=True)
    likes = models.ManyToManyField(MyUser, related_name='liked_posts', blank=True)
    edited = models.BooleanField(default=False)

    def __str__(self):
        return f"{self.user.username}'s post"

and the post_upload_path:

def post_upload_path(instance, filename):
    ext = os.path.splitext(filename)[1]
    ts = timezone.now().strftime("%Y%m%d%H%M%S")
    uid = uuid.uuid4().hex
    return f"posts/post_{instance.user.id}_{ts}_{uid}{ext}"

and settings.py:

MEDIA_URL = '/api/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

Anyway, I hope this is enough background. This works in local (classic). But when I mount and try to upload it on a Fly volume, it refuses to work. Here’s what I’ve tried:

> fly status
App
  Name     = appname
  Owner    = personal
  Hostname = appname.fly.dev
  Image    = appname:deployment-0123456789

Machines
PROCESS ID              VERSION REGION  STATE   ROLE    CHECKS  LAST UPDATED
app     e1234567891000  17      yyz     started                 2025-06-05T10:32:33Z

> fly volumes list  
ID                      STATE   NAME            SIZE    REGION  ZONE    ENCRYPTED       ATTACHED VM     CREATED AT  
vol_somerandomid1010    created lipupona_vol    1GB     yyz     75ec    true            e1234567891000  3 hours ago

So it seems the volume is attached to my VM. Here’s my relevant fly.toml:

[[statics]]
  guest_path  = "/code/media"
  url_prefix  = "/api/media/"

I tried this:

root@e1234567891000:/code# ls /code/media/posts/
post_1_20250604160752_9bfd744909a2478b90fd067c115be9fc.jpg

This is a post that’s on my local machine. So it shows the posts on my local machine but does not let me upload. I also tried:

root@e1234567891000:/code# echo "Hello" > /code/media/test.txt

and this works when I go to appname.fly.dev/api/media/test.txt.

That’s pretty much all. I don’t know where the upload goes and why it doesn’t work. Any kind of help is appreciated. Thank you.

It sounds like file uploads locally is working in Docker, but when you try it in Fly with a mounted drive, it fails. I would expect this to be a permissions issue to start with.

Where does Django write logs? Can you shell into your Fly machine and tail them? I would imagine they’d be in /var/log/*, but they may also be wherever your Django project is stored on the disk.

Actually, sorry, the local image is a remains from when I was testing locally. When I upload through my app on Fly, it does not show up locally either. They show up because they were already there when I ran fly deploy.

Replied in main thread sorry

Ah, righto. Just to check then, and apologies if you have already covered this:

  • does the app work locally in your laptop using Docker
  • do uploads work locally in Docker?

I think we need to work out if the app works before we get into Fly specifics.

To be honest, I haven’t tried running it on a local Docker. Just ran the server locally and upload worked. I’ll try local Docker if you believe it’ll help. Will get back to you! Thanks.

Always. There’s a few Fly oddities, and most of the challenge is to see whether a problem is app-related or Fly related. If there is an issue with the app, then it definitely won’t work in Fly.

That said, if you can find a Django log file remotely, that may short-circuit that process. It may tell you what the issue is (e.g. it is trying to write a file to disk but is running into permission problems).

Thank you. Where can I find the log file? Or produce one if that’s done manually? I appreciate all the help!

Where can I find the log file?

See my guess earlier in the thread.

What does “MEDIA_ROOT” resolve to? It has to be /code/media. That’s what I’d double check.

Oops edited the wrong thing…

1 Like

That’s in settings. Here’s my mounts:

[mounts]
  destination = '/code/media'
  source = 'appname_vol'

Sorry I forgot to mention it in the original post.
Oh and /media is not actually under /api, /api is just the link to view an image. /media is on the main folder.

What does “MEDIA_ROOT” resolve to? It has to be /code/media. That’s what I’d double check.

I did python manage.py shell -c "from django.conf import settings; print(settings.MEDIA_ROOT)" and it printed /code/media, so that part is good, I believe?

yea it should be good. I’m not familiar with the python code but it sounds like a mount path issue if it’s working locally but not on fly.

On a side note, I’d upload directly to S3/R2 so you can bypass the bandwidth on fly. You’re paying for storage + bandwidth.

2 Likes

And this also avoids the risk of permanent data loss, :cooking: (due to the single Volume).

1 Like

Sigh. Honestly, I have no idea what I’ve changed (I just kinda shuffled around the things back and forth with the directories and whatnot, but… then I came back to pretty much what I had?) Anyway, it’s fixed, somehow. I’m kind of mad at myself for not knowing how it didn’t work, then worked. I appreciate you guys’ help though, I’m considering moving to S3.

if you changed the mount, I think you have to destroy and recreate the volume. That might be why it didn’t work after your change.

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.