jekyll build builds your site and generates the _site directory in a ready-to-deploy state. You can upload the contents of the _site directory to your web-host and it will happily serve the site for your customers.
jekyll serve is technically comprised of two steps:
The first step runs jekyll build internally with site.url set to http://localhost:4000 — all HTML files inside _site will now have that hard-coded — and uploading the contents of _site will return errors.
The next step starts a local webserver which gets mounted at the baseurl you specify in the config file ("/" by default) and serves the current contents of the _site directory.
With the local webserver running, you can open your browser and point it to http://localhost:4000/<baseurl> and behold your site that only you can access.
If you’re happy with what you see, you can rest assured and run jekyll build to get your site ready for remote deploy.
bundle exec <command> is just the same as running above commands with the major difference that Jekyll now only sees gems listed in the Gemfile in your site. This allows you to have multiple versions of Jekyll and plugins installed in your system but use just one particular version for a given build session.
Like I mentioned above, jekyll serve hardcodes urls with local host name by default. However, you can override that by setting the environment variable JEKYLL_ENV to "production". For example: