Adapted from the official upgrade notes and starting with a v3.5.3 non-Docker source install:
# back everything up first, then as mastodon user
cd ~/live
git fetch && git checkout v4.0.2
# do next step only if you're stuck on ruby 3.0.3
sed -e -i 's/3.0.4/3.0.3/' .ruby-version
bundle install
yarn install
SKIP_POST_DEPLOYMENT_MIGRATIONS=true RAILS_ENV=production bundle exec rails db:migrate
RAILS_ENV=production bundle exec rails assets:precompile
# restart everything as root
systemctl restart mastodon-streaming
systemctl restart mastodon-sidekiq
systemctl restart mastodon-web
# as mastodon user
RAILS_ENV=production bundle exec rails db:migrate
# restart everything again as root
systemctl restart mastodon-streaming
systemctl restart mastodon-sidekiq
systemctl restart mastodon-web