{"id":4742,"date":"2024-12-12T10:18:54","date_gmt":"2024-12-12T15:18:54","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=4742"},"modified":"2024-12-12T10:18:55","modified_gmt":"2024-12-12T15:18:55","slug":"introducing-fly-io","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/introducing-fly-io\/","title":{"rendered":"Introducing Fly.io"},"content":{"rendered":"\n<p><a href=\"https:\/\/fly.io\/\">Fly.io<\/a>&nbsp;is an increasingly popular infrastructure platform. Fly is a place to deploy your applications, similar to Vercel or Netlify, but with some different tradeoffs.<\/p>\n\n\n\n<p>This post will introduce the platform, show how to deploy web apps, stand up databases, and some other fun things. If you leave here wanting to learn more, <a href=\"https:\/\/fly.io\/docs\/\">the docs&nbsp;are here<\/a>&nbsp;and are outstanding.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What is Fly?<\/h2>\n\n\n\n<p>Where platforms like Vercel and Netlify run your app on serverless functions which spin up and die off as needed (typically running on AWS Lambda), Fly runs your machines on actual VM&#8217;s, running in their infrastructure. These VMs can be configured to scale up as your app&#8217;s traffic grows, just like with serverless functions. But as the continuously run, there is no cold start issues. That said, if you&#8217;re on a budget, or your app isn&#8217;t that important (or both) you can also configure Fly to scale your app down to zero machines when traffic dies. You&#8217;ll be billed essentially nothing during those periods of inactivity, though your users will see a cold start time if they&#8217;re the first to hit your app during an inactive period.<\/p>\n\n\n\n<p>To be perfectly frank, the cold start problem has been historically exaggerated, so please don&#8217;t pick a platform just to avoid cold starts.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why VMs?<\/h2>\n\n\n\n<p>You might be wondering why, if cold starts aren&#8217;t a big deal in practice, one should care about Fly using VMs instead of cloud functions. For me there&#8217;s two reasons: the ability to execute long-running processes, and the ability to run anything that will run in a Docker image. Let&#8217;s dive into both.<\/p>\n\n\n\n<p>The ability to handle long-running processes greatly expands the range of apps Fly can run. They have turn-key solutions for Phoenix LiveView, Laravel, Django, Postgres, and lots more. Anything you ship on Fly will be via a Dockerfile (don&#8217;t worry, they&#8217;ll help you generate them). That means anything you can put into a Dockerfile, can be run by Fly. If there&#8217;s a niche database you&#8217;ve been wanting to try (Neo4J, CouchDB, etc), just stand one up via a Dockerfile (and both of those DBs have official images), and you&#8217;re good to go. New databases, new languages, new anything: if there&#8217;s something you&#8217;ve been wanting to try, you can run it on Fly if you can containerize it; and anything can be containerized.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">But&#8230; I don&#8217;t know Docker<\/h2>\n\n\n\n<p>Don&#8217;t worry, Fly will, as you&#8217;re about to see, help you scaffold a Dockerfile from any common app framework. We&#8217;ll take a quick look at what&#8217;s generated, and explain the high points.<\/p>\n\n\n\n<p>That said, Docker is one of the most valuable tools for a new engineer to get familiar with, so if Fly motivates you to learn more, so much the better! <\/p>\n\n\n\n<p class=\"learn-more\">If you&#8217;d like to go deeper on Docker, our course <a href=\"https:\/\/frontendmasters.com\/courses\/complete-intro-containers-v2\/?utm_source=boost&amp;utm_medium=blog&amp;utm_campaign=boost\">Complete Intro to Containers<\/a> from Brian Holt is fantastic.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Let&#8217;s launch an app!<\/h2>\n\n\n\n<p>Let&#8217;s ship something. We&#8217;ll create a brand new Next.js app, using the standard scaffolding&nbsp;<a href=\"https:\/\/nextjs.org\/docs\/app\/api-reference\/cli\/create-next-app\">here<\/a>.<\/p>\n\n\n\n<p>We&#8217;ll create an app, run&nbsp;<code>npm i<\/code>&nbsp;and then&nbsp;<code>npm run dev<\/code>&nbsp;and verify that it works.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"764\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-1-next-app.png?resize=1024%2C764&#038;ssl=1\" alt=\"screenshot of a running Next.js app\" class=\"wp-image-4752\" style=\"width:631px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-1-next-app.png?resize=1024%2C764&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-1-next-app.png?resize=300%2C224&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-1-next-app.png?resize=768%2C573&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-1-next-app.png?w=1276&amp;ssl=1 1276w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n<\/div>\n\n\n<p>Now let&#8217;s deploy it to Fly. If you haven&#8217;t already, install the Fly CLI, and sign up for an account. Instructions can be found in the first few steps of the\u00a0<a href=\"https:\/\/fly.io\/docs\/getting-started\/launch\/\">quick start guide<\/a>.<\/p>\n\n\n\n<p>To deploy an app on Fly, you need to containerize your app. We\u00a0<em>could<\/em>\u00a0manually piece together a valid Dockerfile that would run our Next app, and then run\u00a0<code>fly deploy<\/code>. But that&#8217;s a tedious process. Thankfully Fly has made life easier for us. Instead, we can just run\u00a0<code>fly launch<\/code>\u00a0from our app&#8217;s root directory.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"419\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-2-fly-launch-initial.png?resize=1024%2C419&#038;ssl=1\" alt=\"\" class=\"wp-image-4754\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-2-fly-launch-initial.png?resize=1024%2C419&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-2-fly-launch-initial.png?resize=300%2C123&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-2-fly-launch-initial.png?resize=768%2C314&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-2-fly-launch-initial.png?w=1144&amp;ssl=1 1144w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>Fly easily <em>detected<\/em> Next.js, and then made some best guesses as to deployment settings. It opted for the third cheapest deployment option. Here&#8217;s <a href=\"https:\/\/fly.io\/docs\/about\/pricing\/\">Fly&#8217;s full pricing information<\/a>. Fly let&#8217;s us accept these defaults, or tweak them. Let&#8217;s hit yes to tweak. We should be taken to the fly.io site, where our app is in the process of being set up.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"751\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-3-default-settings.png?resize=1024%2C751&#038;ssl=1\" alt=\"\" class=\"wp-image-4756\" style=\"width:700px\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-3-default-settings.png?resize=1024%2C751&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-3-default-settings.png?resize=300%2C220&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-3-default-settings.png?resize=768%2C563&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-3-default-settings.png?resize=1536%2C1126&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-3-default-settings.png?w=1850&amp;ssl=1 1850w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>For fun, let&#8217;s switch to the cheapest option, and change the region to Virginia (what AWS would call us-east-1).<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"751\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-4-updated-settings.png?resize=1024%2C751&#038;ssl=1\" alt=\"\" class=\"wp-image-4757\" style=\"width:700px\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-4-updated-settings.png?resize=1024%2C751&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-4-updated-settings.png?resize=300%2C220&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-4-updated-settings.png?resize=768%2C563&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-4-updated-settings.png?resize=1536%2C1126&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-4-updated-settings.png?w=1830&amp;ssl=1 1830w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>Hit confirm, and return to your command line. It should finish setting everything up, which should look like this, in part.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"454\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-5-cli-finish.png?resize=1024%2C454&#038;ssl=1\" alt=\"\" class=\"wp-image-4758\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-5-cli-finish.png?resize=1024%2C454&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-5-cli-finish.png?resize=300%2C133&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-5-cli-finish.png?resize=768%2C341&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-5-cli-finish.png?resize=1536%2C681&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-5-cli-finish.png?resize=2048%2C909&amp;ssl=1 2048w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>If we head over to our&nbsp;<a href=\"https:\/\/fly.io\/dashboard\">Fly dashboard<\/a>, we should see something like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"349\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-6-fly-dashboard.png?resize=1024%2C349&#038;ssl=1\" alt=\"\" class=\"wp-image-4759\" style=\"width:700px\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-6-fly-dashboard.png?resize=1024%2C349&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-6-fly-dashboard.png?resize=300%2C102&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-6-fly-dashboard.png?resize=768%2C262&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-6-fly-dashboard.png?w=1424&amp;ssl=1 1424w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>We can then click that app and see the app&#8217;s details<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"1018\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-7-app-in-dashboard.png?resize=1024%2C1018&#038;ssl=1\" alt=\"\" class=\"wp-image-4760\" style=\"width:700px\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-7-app-in-dashboard.png?resize=1024%2C1018&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-7-app-in-dashboard.png?resize=300%2C298&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-7-app-in-dashboard.png?resize=150%2C150&amp;ssl=1 150w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-7-app-in-dashboard.png?resize=768%2C763&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-7-app-in-dashboard.png?resize=1536%2C1527&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-7-app-in-dashboard.png?w=1624&amp;ssl=1 1624w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>And lastly, we can go to the URL listed, and see the app actually running!<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"866\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-8-app-running.png?resize=1024%2C866&#038;ssl=1\" alt=\"\" class=\"wp-image-4761\" style=\"width:700px\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-8-app-running.png?resize=1024%2C866&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-8-app-running.png?resize=300%2C254&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-8-app-running.png?resize=768%2C649&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-8-app-running.png?w=1202&amp;ssl=1 1202w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Looking closer<\/h2>\n\n\n\n<p>There&#8217;s a number of files that Fly created for us. The two most important are the Dockerfile, and <code>fly.toml<\/code>. Let&#8217;s take a look at each. We&#8217;ll start with the Dockerfile.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-comment\"># syntax = docker\/dockerfile:1<\/span>\n\n<span class=\"hljs-comment\"># Adjust NODE_VERSION as desired<\/span>\n<span class=\"hljs-keyword\">ARG<\/span> NODE_VERSION=<span class=\"hljs-number\">20.18<\/span>.<span class=\"hljs-number\">1<\/span>\n<span class=\"hljs-keyword\">FROM<\/span> node:${NODE_VERSION}-slim as base\n\n<span class=\"hljs-keyword\">LABEL<\/span><span class=\"bash\"> fly_launch_runtime=<span class=\"hljs-string\">\"Next.js\"<\/span><\/span>\n\n<span class=\"hljs-comment\"># Next.js app lives here<\/span>\n<span class=\"hljs-keyword\">WORKDIR<\/span><span class=\"bash\"> \/app<\/span>\n\n<span class=\"hljs-comment\"># Set production environment<\/span>\n<span class=\"hljs-keyword\">ENV<\/span> NODE_ENV=<span class=\"hljs-string\">\"production\"<\/span>\n\n<span class=\"hljs-comment\"># Throw-away build stage to reduce size of final image<\/span>\n<span class=\"hljs-keyword\">FROM<\/span> base as build\n\n<span class=\"hljs-comment\"># Install packages needed to build node modules<\/span>\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> apt-get update -qq &amp;&amp; \\\n    apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3<\/span>\n\n<span class=\"hljs-comment\"># Install node modules<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> package-lock.json package.json .\/<\/span>\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> npm ci --include=dev<\/span>\n\n<span class=\"hljs-comment\"># Copy application code<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> . .<\/span>\n\n<span class=\"hljs-comment\"># Build application<\/span>\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> npm run build<\/span>\n\n<span class=\"hljs-comment\"># Remove development dependencies<\/span>\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> npm prune --omit=dev<\/span>\n\n\n<span class=\"hljs-comment\"># Final stage for app image<\/span>\n<span class=\"hljs-keyword\">FROM<\/span> base\n\n<span class=\"hljs-comment\"># Copy built application<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> --from=build \/app \/app<\/span>\n\n<span class=\"hljs-comment\"># Start the server by default, this can be overwritten at runtime<\/span>\n<span class=\"hljs-keyword\">EXPOSE<\/span> <span class=\"hljs-number\">3000<\/span>\n<span class=\"hljs-keyword\">CMD<\/span><span class=\"bash\"> &#91; <span class=\"hljs-string\">\"npm\"<\/span>, <span class=\"hljs-string\">\"run\"<\/span>, <span class=\"hljs-string\">\"start\"<\/span> ]<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">A Quick Detour to Understand Docker<\/h3>\n\n\n\n<p>Docker is a book unto its own, but as an extremely quick intro: Docker allows us to package our app into an &#8220;image.&#8221; Containers allow you to start with an entire operating system (almost always a minimal Linux distro), and allow you to do whatever you want with it. Docker then packages whatever you create, and allows it to be run. The Docker image is completely self-contained. You choose the whatever goes into it, from the base operating system, down to whatever you install into the image. Again, they&#8217;re self-contained.<\/p>\n\n\n\n<p>Now let&#8217;s take a quick tour of the important pieces of our Dockerfile.<\/p>\n\n\n\n<p>After some comments and labels, we find what will always be present at the top of a Dockerfile: the <code>FROM<\/code> command.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-keyword\">FROM<\/span> node:${NODE_VERSION}-slim as base<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This tells us the base of the image. We could start with any random Linux distro, and then install Node and npm, but unsurprisingly there&#8217;s already an officially maintained Node image: there will almost always be officially maintained Docker images for almost any technology. In fact, t<a href=\"https:\/\/hub.docker.com\/_\/node\">here&#8217;s many different Node images to choose from<\/a>, many with different underlying base Linux distro&#8217;s.<\/p>\n\n\n\n<p>There&#8217;s a <code>LABEL<\/code> that&#8217;s added, likely for use with Fly. Then we set the working directory in our image.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-keyword\">WORKDIR<\/span><span class=\"bash\"> \/app<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We copy the <code>package.json<\/code> and lockfiles.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-comment\"># Install node modules<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> package-lock.json package.json .\/<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then run\u00a0<code>npm i<\/code>\u00a0(but <em>in<\/em> our Docker image):<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> npm ci --include=dev<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then we copy the rest of the application code:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-comment\"># Copy application code<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> . .<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Hopefully you get the point. We won&#8217;t go over every line, here. But hopefully the general idea is clear enough, and hopefully you&#8217;d feel comfortable tweaking this if you wanted to. Two last points though. See this part:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-comment\"># Install packages needed to build node modules<\/span>\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> apt-get update -qq &amp;&amp; \\\n    apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That tells the Linux package manager to install some things Fly thinks Next might need, but in actuality\u00a0<a href=\"https:\/\/x.com\/leeerob\/status\/1862312276985868783\">probably doesn&#8217;t<\/a>. Don&#8217;t be surprised if these lines are absent when you read this, and try for yourself.<\/p>\n\n\n\n<p>Lastly, if you were wondering why the <code>package.json<\/code> and lockfiles were copied, followed by\u00a0<code>npm install<\/code>\u00a0<em>and then<\/em>\u00a0followed by copying the rest of the application code, the reason is (Docker) performance. Briefly, each line in the Dockerfile creates a &#8220;layer.&#8221; These layers can be cached and re-used if nothing has changed. If anything\u00a0<em>has<\/em>\u00a0changed, that invalidates the cache for that layer,\u00a0<em>and also<\/em>\u00a0all layers after it. So you&#8217;ll want to push your likely-to-change work as low as possible. Your application code will almost always change between deployments; the dependencies in your <code>package.json<\/code> will change much less frequently. So we do that install first, by itself, so it will be more likely to be cached, and speed up our builds.<\/p>\n\n\n\n<p>I tried my best to provide the absolute minimal amount of a Docker intro to make this post make sense, without being overhwelming. I hope I&#8217;ve succeeded. If you&#8217;d like to learn more, there&#8217;s tons of books and YouTube videos, and even an entire course\u00a0<a href=\"https:\/\/frontendmasters.com\/courses\/complete-intro-containers-v2\/?utm_source=boost&amp;utm_medium=blog&amp;utm_campaign=boost\">here on Frontend Masters<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Fly.toml<\/h2>\n\n\n\n<p>Now let&#8217;s take a peek at the <code>fly.toml<\/code> file.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"TOML, also INI\" data-shcb-language-slug=\"ini\"><span><code class=\"hljs language-ini\"><span class=\"hljs-comment\"># fly.toml app configuration file generated for next-fly-test on 2024-11-28T19:04:19-06:00<\/span>\n<span class=\"hljs-comment\">#<\/span>\n<span class=\"hljs-comment\"># See https:\/\/fly.io\/docs\/reference\/configuration\/ for information about how to use this file.<\/span>\n<span class=\"hljs-comment\">#<\/span>\n\n<span class=\"hljs-attr\">app<\/span> = <span class=\"hljs-string\">'next-fly-test'<\/span>\n<span class=\"hljs-attr\">primary_region<\/span> = <span class=\"hljs-string\">'iad'<\/span>\n\n<span class=\"hljs-section\">&#91;build]<\/span>\n\n<span class=\"hljs-section\">&#91;http_service]<\/span>\n  internal_port = 3000\n  force_https = true\n  auto_stop_machines = 'stop'\n  auto_start_machines = true\n  min_machines_running = 0\n  processes = <span class=\"hljs-section\">&#91;'app']<\/span>\n\n<span class=\"hljs-section\">&#91;&#91;vm]]<\/span>\n  size = 'shared-cpu-1x'<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TOML, also INI<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">ini<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This is basically the config file for the Fly app. The options for this file are almost endless, and are documented\u00a0<a href=\"https:\/\/fly.io\/docs\/reference\/configuration\/\">here<\/a>. The three most important lines are the next three.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"TOML, also INI\" data-shcb-language-slug=\"ini\"><span><code class=\"hljs language-ini\"><span class=\"hljs-attr\">auto_stop_machines<\/span> = <span class=\"hljs-string\">'stop'<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TOML, also INI<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">ini<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This tells Fly to automatically kill machines when they&#8217;re not needed, when traffic is low on our app.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"TOML, also INI\" data-shcb-language-slug=\"ini\"><span><code class=\"hljs language-ini\"><span class=\"hljs-attr\">auto_start_machines<\/span> = <span class=\"hljs-literal\">true<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TOML, also INI<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">ini<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The line above tells Fly to automatically spin up new machines when it detects it needs to do so, given your traffic. Lastly, this line<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"TOML, also INI\" data-shcb-language-slug=\"ini\"><span><code class=\"hljs language-ini\"><span class=\"hljs-attr\">min_machines_running<\/span> = <span class=\"hljs-number\">0<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TOML, also INI<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">ini<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That line allows us to tell Fly to always keep a minimum number of machines running, no matter how minimal your current traffic is. Setting it to zero allows for no machines to be running, which means your next visitor will see a slow response as the first machine spins up.<\/p>\n\n\n\n<p>You may have noticed above that Fly spun up two machines initially, even though there was no traffic at all. It does this by default to give your app a higher availability, that is, in case anything happens to the one machine, the other will (hopefully) still be up and running. If you don&#8217;t want or need this, you can prevent it by passing\u00a0<code>--ha=false<\/code>\u00a0when you run\u00a0<code>fly launch<\/code>\u00a0or\u00a0<code>fly deploy<\/code>\u00a0(or you can just kill one of the machines in the dashboard &#8211; Fly will not re-create it on subsequent deploys).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Machines won&#8217;t bill you if they&#8217;re not running<\/h3>\n\n\n\n<p>When a machine is not running, you&#8217;ll be billed\u00a0<em>essentially<\/em>\u00a0zero for it. You&#8217;ll just pay $0.15 per GB, per month, per machine (machines will usually have only one GB).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Adding a database<\/h2>\n\n\n\n<div class=\"wp-block-group is-nowrap is-layout-flex wp-container-core-group-is-layout-ad2f72ca wp-block-group-is-layout-flex\">\n<p>You can launch a Fly app anytime with just a Dockerfile. You could absolutely find an official Postgres Docker image and deploy from that. But it turns out Fly has this built in. Let&#8217;s run&nbsp;<code>fly postgres create<\/code>&nbsp;in a terminal, and see what happens<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"381\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-10-fly-pg-created.png?resize=1024%2C381&#038;ssl=1\" alt=\"\" class=\"wp-image-4763\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-10-fly-pg-created.png?resize=1024%2C381&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-10-fly-pg-created.png?resize=300%2C112&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-10-fly-pg-created.png?resize=768%2C285&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-10-fly-pg-created.png?w=1528&amp;ssl=1 1528w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n<\/div>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"299\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-9-fly-postgres-create.png?resize=1024%2C299&#038;ssl=1\" alt=\"\" class=\"wp-image-4762\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-9-fly-postgres-create.png?resize=1024%2C299&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-9-fly-postgres-create.png?resize=300%2C87&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-9-fly-postgres-create.png?resize=768%2C224&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-9-fly-postgres-create.png?w=1132&amp;ssl=1 1132w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>It&#8217;ll ask you for a name and a region, and then how serious of a Postgres setup you want. Once it&#8217;s done, it&#8217;ll show you something like this.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/adam-rackis-blog-staging.fly.dev\/fly-io\/img-10-fly-pg-created.png?ssl=1\" alt=\"Fly postgres create\"\/><\/figure>\n\n\n\n<p>The connection string listed at the bottom can be used to connect to your db\u00a0<em>from within another Fly app<\/em>\u00a0(which you own). But to run database creation and migration scripts, and for local development you&#8217;ll need to connect to this db on your local machine. To do that, you can run this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">fly proxy 5432 -a <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">your<\/span> <span class=\"hljs-attr\">app<\/span> <span class=\"hljs-attr\">name<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"902\" height=\"122\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-11-fly-proxy.png?resize=902%2C122&#038;ssl=1\" alt=\"\" class=\"wp-image-4764\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-11-fly-proxy.png?w=902&amp;ssl=1 902w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-11-fly-proxy.png?resize=300%2C41&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/img-11-fly-proxy.png?resize=768%2C104&amp;ssl=1 768w\" sizes=\"auto, (max-width: 902px) 100vw, 902px\" \/><\/figure>\n\n\n\n<p><a href=\"https:\/\/fly.io\/docs\/postgres\/connecting\/\">Now you can connect<\/a> via the same connection string on your local machine, but on\u00a0<code>localhost:5432<\/code>\u00a0instead of\u00a0<code>flycast:5432<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Making your database publicly available<\/h3>\n\n\n\n<p>It&#8217;s not ideal, but if you want to <a href=\"https:\/\/fly.io\/docs\/postgres\/connecting\/connecting-external\/\">make your Fly pg box publicly available<\/a>, you can. You basically have to add a dedicated ipv4 address to it (at a cost of $2 per month), and then tweak your config.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Consider using a dedicated host for serious applications.<\/h3>\n\n\n\n<p>Fly&#8217;s\u00a0<a href=\"https:\/\/fly.io\/docs\/postgres\/\">built-in Postgres support<\/a>\u00a0is superb, but there&#8217;s some things you&#8217;ll have to\u00a0<a href=\"https:\/\/fly.io\/docs\/postgres\/getting-started\/what-you-should-know\/#heres-what-you-manage\">manage yourself<\/a>. If that&#8217;s not for you,\u00a0<a href=\"https:\/\/supabase.com\/\">Supabase<\/a>\u00a0is a fully managed pg host, and it&#8217;s also superb. Fly even has\u00a0<a href=\"https:\/\/fly.io\/docs\/supabase\/\">a service<\/a>\u00a0for creating Supabase db&#8217;s on Fly infra, for extra low latency. It&#8217;s currently only in public alpha, but it might be worth keeping an eye on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Interlude<\/h2>\n\n\n\n<p>If you just want a nice place to deploy your apps, what we&#8217;ve covered will suffice for the vast majority of use cases. I could stop this post here, but I&#8217;d be remiss if I didn&#8217;t show some of the cooler things you can do with Fly. Please don&#8217;t let what follows be indicative of the complexity you&#8217;ll normally deal with. We&#8217;ll be putting together a cron job for running Postgres backups. In practice, you&#8217;ll just use a mature DB provider like Supabase or PlanetScale, which will handle things like this for you.<\/p>\n\n\n\n<p>But sometimes it&#8217;s fun to tinker, especially for side projects. So let&#8217;s kick the tires a bit and see what we can come up with.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Having Fun<\/h2>\n\n\n\n<p>One of Fly&#8217;s greatest strengths is its flexibility. You give it a Dockerfile, and it&#8217;ll run it. To drive that point home, let&#8217;s conclude this post with a fun example.<\/p>\n\n\n\n<p>As much as I love Fly, it makes me a&nbsp;<em>little<\/em>&nbsp;uneasy that my database is running isolated in some VM under my account. Accidents happen, and I&#8217;d want automatic backups. Why don&#8217;t we build a Docker image to do just that?<\/p>\n\n\n\n<p>I&#8217;ll want to run a script, written in TypeScript, preferably without hating my life: Bun is ideal for this. I&#8217;ll also need to run the actual\u00a0<code>pg_dump<\/code>\u00a0command. So what should I build my Dockerfile from: the bun image, which would lack to pg utilities, or the pg base, which wouldn&#8217;t have bun installed. I could do either, and use the Linux package manager to install what I need. But really, there&#8217;s a simpler way: use a multi-stage Docker build. Let&#8217;s see the whole Dockerfile<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-keyword\">FROM<\/span> oven\/bun:latest AS BUILDER\n\n<span class=\"hljs-keyword\">WORKDIR<\/span><span class=\"bash\"> \/app<\/span>\n\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> . .<\/span>\n\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> &#91;<span class=\"hljs-string\">\"bun\"<\/span>, <span class=\"hljs-string\">\"install\"<\/span>]<\/span>\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> &#91;<span class=\"hljs-string\">\"bun\"<\/span>, <span class=\"hljs-string\">\"build\"<\/span>, <span class=\"hljs-string\">\"index.ts\"<\/span>, <span class=\"hljs-string\">\"--compile\"<\/span>, <span class=\"hljs-string\">\"--outfile\"<\/span>, <span class=\"hljs-string\">\"run-pg_dump\"<\/span>]<\/span>\n\n<span class=\"hljs-keyword\">FROM<\/span> postgres:<span class=\"hljs-number\">16.4<\/span>\n\n<span class=\"hljs-keyword\">WORKDIR<\/span><span class=\"bash\"> \/app<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> --from=BUILDER \/app\/run-pg_dump .<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> --from=BUILDER \/app\/run-backup.sh .<\/span>\n\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> chmod +x .\/run-backup.sh<\/span>\n\n<span class=\"hljs-keyword\">CMD<\/span><span class=\"bash\"> &#91;<span class=\"hljs-string\">\".\/run-backup.sh\"<\/span>]<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We start with a Bun image. We run a\u00a0<code>bun install<\/code>\u00a0to tell Bun to install what we need: aws sdk&#8217;s and such. Then we tell Bun to compile our script into a standalone executable: yes, Bun can do that, and yes: it&#8217;s that easy.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-keyword\">FROM<\/span> postgres:<span class=\"hljs-number\">16.4<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Tells Docker to start a new stage, from a new (Postgres) base.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-keyword\">WORKDIR<\/span><span class=\"bash\"> \/app<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> --from=BUILDER \/app\/run-pg_dump .<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> --from=BUILDER \/app\/run-backup.sh .<\/span>\n\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> chmod +x .\/run-backup.sh<\/span>\n\n<span class=\"hljs-keyword\">CMD<\/span><span class=\"bash\"> &#91;<span class=\"hljs-string\">\".\/run-backup.sh\"<\/span>]<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This drops into the <code>\/app<\/code> folder from the prior step, and copies over the\u00a0<code>run-pg_dump<\/code>\u00a0file, which Bun compiled for us, and also copies over\u00a0<code>run-backup.sh<\/code>. This is a shell script I wrote. It runs\u00a0<code>pg_dump<\/code>\u00a0a few times, to generate the files the Bun script (<code>run-pg_dump<\/code>) is expecting, and then calls it. Here&#8217;s what that file looks like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">&lt;strong&gt;<span class=\"hljs-comment\">#!\/bin\/sh&lt;\/strong&gt;<\/span>\n\nPG_URI_CLEANED=$(<span class=\"hljs-built_in\">echo<\/span> <span class=\"hljs-variable\">${PG_URI}<\/span> | sed -e <span class=\"hljs-string\">'s\/^\"\/\/'<\/span> -e <span class=\"hljs-string\">'s\/\"$\/\/'<\/span>)\n\npg_dump <span class=\"hljs-variable\">${PG_URI_CLEANED}<\/span> -Fc &gt; .\/backup.dump\n\npg_dump <span class=\"hljs-variable\">${PG_URI_CLEANED}<\/span> -f .\/backup.sql\n\n.\/run-pg_dump<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This unhinged line:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">PG_URI_CLEANED=$(<span class=\"hljs-built_in\">echo<\/span> <span class=\"hljs-variable\">${PG_URI}<\/span> | sed -e <span class=\"hljs-string\">'s\/^\"\/\/'<\/span> -e <span class=\"hljs-string\">'s\/\"$\/\/'<\/span>)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>is something ChatGPT helped me write, to strip the double quotes from my connection string environment variable.<\/p>\n\n\n\n<p>Lastly, if you&#8217;re curious about the <code>index.ts<\/code> file Bun compiled into a standalone executable, this is it:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> fs <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"fs\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> path <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"path\"<\/span>;\n\n<span class=\"hljs-keyword\">import<\/span> { S3Client, PutObjectCommand } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@aws-sdk\/client-s3\"<\/span>;\n\n<span class=\"hljs-keyword\">const<\/span> numToDisplay = <span class=\"hljs-function\">(<span class=\"hljs-params\">num: number<\/span>) =&gt;<\/span> num.toString().padStart(<span class=\"hljs-number\">2<\/span>, <span class=\"hljs-string\">\"0\"<\/span>);\n\n<span class=\"hljs-keyword\">const<\/span> today = <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>();\n<span class=\"hljs-keyword\">const<\/span> date = <span class=\"hljs-string\">`<span class=\"hljs-subst\">${today.getFullYear()}<\/span>\/<span class=\"hljs-subst\">${numToDisplay(today.getMonth() + <span class=\"hljs-number\">1<\/span>)}<\/span>\/<span class=\"hljs-subst\">${numToDisplay(today.getDate())}<\/span>`<\/span>;\n<span class=\"hljs-keyword\">const<\/span> time = <span class=\"hljs-string\">`<span class=\"hljs-subst\">${today.getHours()}<\/span>-<span class=\"hljs-subst\">${numToDisplay(today.getMinutes())}<\/span>-<span class=\"hljs-subst\">${numToDisplay(today.getSeconds())}<\/span>`<\/span>;\n<span class=\"hljs-keyword\">const<\/span> filename = <span class=\"hljs-string\">`<span class=\"hljs-subst\">${date}<\/span>\/<span class=\"hljs-subst\">${time}<\/span>`<\/span>;\n\n<span class=\"hljs-keyword\">const<\/span> REGION = <span class=\"hljs-string\">\"us-east-1\"<\/span>;\n<span class=\"hljs-keyword\">const<\/span> dumpParams = {\n  <span class=\"hljs-attr\">Bucket<\/span>: <span class=\"hljs-string\">\"my-library-backups\"<\/span>,\n  <span class=\"hljs-attr\">Key<\/span>: <span class=\"hljs-string\">`<span class=\"hljs-subst\">${filename}<\/span>.dump`<\/span>,\n  <span class=\"hljs-attr\">Body<\/span>: fs.readFileSync(path.resolve(__dirname, <span class=\"hljs-string\">\"backup.dump\"<\/span>)),\n};\n<span class=\"hljs-keyword\">const<\/span> sqlParams = {\n  <span class=\"hljs-attr\">Bucket<\/span>: <span class=\"hljs-string\">\"my-library-backups\"<\/span>,\n  <span class=\"hljs-attr\">Key<\/span>: <span class=\"hljs-string\">`<span class=\"hljs-subst\">${filename}<\/span>.sql`<\/span>,\n  <span class=\"hljs-attr\">Body<\/span>: fs.readFileSync(path.resolve(__dirname, <span class=\"hljs-string\">\"backup.sql\"<\/span>)),\n};\n\n<span class=\"hljs-keyword\">const<\/span> s3 = <span class=\"hljs-keyword\">new<\/span> S3Client({\n  <span class=\"hljs-attr\">region<\/span>: REGION,\n  <span class=\"hljs-attr\">credentials<\/span>: {\n    <span class=\"hljs-attr\">accessKeyId<\/span>: process.env.AWS_ID!,\n    <span class=\"hljs-attr\">secretAccessKey<\/span>: process.env.AWS_SECRET!,\n  },\n});\n\ns3.send(<span class=\"hljs-keyword\">new<\/span> PutObjectCommand(sqlParams))\n  .then(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"SQL Backup Uploaded!\"<\/span>);\n  })\n  .catch(<span class=\"hljs-function\"><span class=\"hljs-params\">err<\/span> =&gt;<\/span> {\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"Error: \"<\/span>, err);\n  });\n\ns3.send(<span class=\"hljs-keyword\">new<\/span> PutObjectCommand(dumpParams))\n  .then(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"Dump Backup Uploaded!\"<\/span>);\n  })\n  .catch(<span class=\"hljs-function\"><span class=\"hljs-params\">err<\/span> =&gt;<\/span> {\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"Error: \"<\/span>, err);\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>I&#8217;m sure someone who&#8217;s actually good with Docker could come up with something better, but this works well enough.<\/p>\n\n\n\n<p>To see this whole thing all together, in one place, you can <a href=\"https:\/\/github.com\/arackaf\/booklist\/tree\/master\/data\/my-library-pg-backup\">see it in my GitHub<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Scheduling a custom job<\/h3>\n\n\n\n<p>We have a working, valid Docker image. How do we tell Fly to run it on an interval? Fly has a command just for that:\u00a0<code><a href=\"https:\/\/fly.io\/docs\/flyctl\/machine-run\/\">fly machine run<\/a><\/code>. In fact, it can take a\u00a0<code>schedule<\/code>\u00a0argument, to have Fly run it on an interval. Unfortunately, the options are horribly limited: only hourly, daily, and monthly. But, as a workaround you can run this command at different times: this will set up executions at whatever interval you selected, scheduled off of when you ran the command.<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">fly machine run . --schedule=daily<\/code><\/span><\/pre>\n\n\n<p>If you ran that command at noon, that will schedule a daily task that runs at noon every day. If you run that command again at 5pm, it will schedule a\u00a0<em>second<\/em>\u00a0task to run daily, at 5pm (without interfering with the first). Each job will have a dedicated machine, but will be idle when not running, which means it will cost you almost nothing; you&#8217;ll pay the normal $0.15 per month, per GB on the machine.<\/p>\n\n\n\n<p>I hate this limitation in scheduling machines. In theory there&#8217;s <a href=\"https:\/\/github.com\/fly-apps\/cron-manager\">a true cron job template\u00a0here<\/a>, but it&#8217;s not the simplest thing to look through.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Odds and ends<\/h2>\n\n\n\n<p>That was a lot. Let&#8217;s lighten things up a bit with some happy odds and ends, before we wrap up.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Custom domains<\/h3>\n\n\n\n<p>Fly makes it easy to add a custom domain to your app. You&#8217;ll just need to add the right records. <a href=\"https:\/\/fly.io\/docs\/networking\/custom-domain\/\">Full instructions are\u00a0here<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Secrets<\/h3>\n\n\n\n<p>You&#8217;ll probably have some secrets you want run in your app, in production. If you&#8217;re thinking you could just bundle a <code>.env.prod<\/code> file into your Docker image, yes, you could. But that&#8217;s considered a bad idea. Instead, leverage <a href=\"https:\/\/fly.io\/docs\/js\/the-basics\/secrets\/\">Fly&#8217;s secret management<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Learning More<\/h2>\n\n\n\n<p>This post started brushing up against some full-stack topics. If this sparked your interest, be sure to check out the\u00a0<a href=\"https:\/\/frontendmasters.com\/courses\/fullstack-v3\/?utm_source=boost&amp;utm_medium=blog&amp;utm_campaign=boost\">entire course\u00a0on full-stack engineering<\/a> here on Frontend Masters.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping Up<\/h2>\n\n\n\n<p>The truth is we&#8217;ve truly, barely scratched the surface of Fly. For simple side projects what we&#8217;ve covered here is probably more than you&#8217;d need. But Fly also has power tools available for advanced use cases. The sky&#8217;s the limit!<\/p>\n\n\n\n<p>Fly.io is a wonderful platform. It&#8217;s fun to work with, will scale to your application&#8217;s changing load, and is incredibly flexible. I urge you to give it a try for your next project.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If it can go in a Docker, Fly can host it, and they&#8217;ll help you with that. Adam Rackis takes a look at the platform and shows off all the things he likes about it.<\/p>\n","protected":false},"author":21,"featured_media":4779,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[270,269],"class_list":["post-4742","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-fly-io","tag-hosting"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/fly-io.jpg?fit=1000%2C500&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4742","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=4742"}],"version-history":[{"count":15,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4742\/revisions"}],"predecessor-version":[{"id":4780,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4742\/revisions\/4780"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/4779"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=4742"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=4742"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=4742"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}