Doubly-linked list implementation
list_init should initialize all the members of the allocated node before returning.  This way the caller won't have to remember to NULL out the pointers it isn't explicitly setting.

One way a doubly linked list can be implemented is to use a fake node to hold the list head and tail pointers.  In your case, that node would always have a NULL data pointer, and initially the next and prev nodes would point to itself (this could be set up in a new list_create function, which could either initialize an already allocated node, or allocate and return a new one).

This would simplify list_append, as you'd already have a pointer to the last node, and you wouldn't need any extra code to handle an empty list (your current error handling in this case doesn't return after the fprintf, which will result in a crash).  'list_remove_last` would also be simplified.

Walking the list when using that fake node (like you do in list_print and list_find) would need a slightly different end condition.  You'd start at the head, walk to the first node, then stop when you find a node with a NULL data pointer (which would mean you're back at the head node).

Your current list_destroy has one problem, where the variable that is passed to it still points at the (now deleted) list when it returns, so the caller needs to handle this.  Using the fake node scheme, this would leave just the fake head node (which can be cleaned up via a list_destroy function).