The JavaScript Journal The home for JavaScript-oriented content including Node.js, Deno, Bun, React.js and more. https://fly.io/javascript-journal/ 2024年06月25日T00:00:00+00:00 Fly Launching a Remix app with Postgres using Prisma https://fly.io/javascript-journal/remix-with-prisma-postgres/ 2024年06月25日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <div class="lead"><p>We’re Fly.io and we make it easy for developers to do hard things without breaking a sweat, like deploying applications close to your users and scaling to exactly what you need in the blink of an eye. Or, in the case of this article, launching applications with Postgres with incredible simplicity.</p> </div> <p>In 2021, we launched <a href='https://fly.io/docs/postgres/' title=''>Fly Postgres</a> to resounding fanfare - seriously, we&#39;e still cheering about it to this day. For folks who need a straightforward, familiar database storage solution and aren&rsquo;t too pressed about managing it themselves, it&rsquo;s an incredible and flexible storage option. In addition, launching an application on Fly with Postgres is so simple that it kind of feels like magic. </p> <p>Launching a new app with Postgres is trivial. I&rsquo;m not even exaggerating, and to prove it we&rsquo;ll be walking you through the process of launching a basic <a href='https://remix.run/' title=''>Remix</a> app on Fly that you can follow along with. </p> <p>This demo is meant to be a starting point for folks who are new to using Fly Postgres. You can use it as a playground for experimentation, whether you&rsquo;re familiar with Postgres but want a low-stakes, simple example to tool around with directly or are new to Postgres and/or relational databases as a whole. Feel free to turn the code completely upside down. </p> <p>The three primary steps we&rsquo;ll go through are:</p> <ul> <li>Launch our app </li><li>Tweak our database settings </li><li>Deploy changes </li></ul> <p>We can also use the <code>DATABASE_URL</code> environment variable directly within our code as soon as Postgres is up and running without any extra work, as it&rsquo;s automatically configured as a <a href='https://fly.io/docs/reference/secrets/' title=''>secret</a> during provisioning. </p> <h1 id='okay-so-what-does-this-look-like-in-action' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#okay-so-what-does-this-look-like-in-action' aria-label='Anchor'></a><span class='plain-code'>Okay, so what does this look like in action?</span></h1> <p>Want to experience this process but don&rsquo;t have anything you can actively test with? No problem! We created a simple Remix app which you can find in <a href='https://github.com/velspera/fly-postgres-demo' title=''>this repository</a>. </p> <p>Picture this: Your best friend&rsquo;s birthday is coming up and you want to gather &lsquo;happy birthday&rsquo; messages from their friends and family as a surprise. So, you&rsquo;ve created a simple online guestbook with Remix and have decided to use Postgres, leveraging the Prisma ORM, to store those messages.</p> <p>Clone the repository linked above, navigate to the directory in your terminal, and get ready to follow along with us.</p> <h2 id='our-application' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#our-application' aria-label='Anchor'></a><span class='plain-code'>Our Application</span></h2> <p>First, I&rsquo;ll provide a little foundational information about the project that should help you as you&rsquo;re looking through the files. The structure itself is simple:</p> <p><img alt="Diagram of app structure" src="/javascript-journal/remix-with-prisma-postgres/assets/diagrampg-sm.webp" /></p> <p>Our guestbook itself only uses a single page (fancied up with <a href='https://tailwindcss.com/' title=''>TailwindCSS</a>), so the only route we&rsquo;re concerned with is <code>_index.tsx</code>. The header of this page explains its purpose: to allow folks to post messages for our bestie&rsquo;s birthday. In our <code>&lt;main&gt;</code> element, we&rsquo;ve imported two components via our index route: <code>NewMessage.tsx</code> (a form that writes submitted messages to our database) and <code>MessageList.tsx</code> (a section of code that displays submitted messages on the page). </p> <p>We make use of <a href='https://www.prisma.io/docs' title=''>Prisma</a> in this application - an incredibly robust open-source ORM that simplifies the process of building projects with databases. In our demo app, we&rsquo;ve used it to inform the overall structure (essentially, what fields will house what kind of data and in what way and/or capacity) of the data we&rsquo;ll be storing in our database by creating a model in a <a href='https://www.prisma.io/docs/orm/prisma-schema' title=''>Prisma schema</a>, which you can find in <code>/prisma/schema.prisma</code>.</p> <p>In our case, we only need one table in our database so we only have one model in our schema. We&rsquo;ve named that table <strong class='font-semibold text-navy-950'>visitors</strong> and it contains fields for <strong class='font-semibold text-navy-950'>id</strong> (we use this to number our messages), a <strong class='font-semibold text-navy-950'>createdAt</strong> timestamp, <strong class='font-semibold text-navy-950'>name</strong>, <strong class='font-semibold text-navy-950'>email</strong>, message <strong class='font-semibold text-navy-950'>title</strong>, and message <strong class='font-semibold text-navy-950'>content</strong>. </p> <p><img alt="Diagram of database schema" src="/javascript-journal/remix-with-prisma-postgres/assets/dbdiagram-sm.webp" /></p> <h2 id='step-1-flyctl' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#step-1-flyctl' aria-label='Anchor'></a><span class='plain-code'>Step 1: Flyctl</span></h2> <p>Everything begins with <a href='https://fly.io/docs/hands-on/install-flyctl/' title=''>flyctl</a>, the command-line interface for Fly.io. You&rsquo;ll want to <a href='https://fly.io/docs/hands-on/install-flyctl/' title=''>install it</a> as a first step. Then, we&rsquo;ll kick things off with the <a href='https://fly.io/docs/apps/launch/' title=''>Fly Launch</a> command: </p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-4b7vadl" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-4b7vadl">fly launch </code></pre> </div> </div> <p>This creates a Dockerfile, a <a href='https://fly.io/docs/reference/configuration/' title=''>fly.toml</a> config file, and any other platform resources necessary to get your app up and running on a Machine. Flyctl is clever enough to recognize we&rsquo;ve used Prisma in our application, and so it understand it should set up Fly Postgres for us by default. </p> <p>If you want to tweak those settings, you can select <code>y</code> when prompted to be taken to the web interface where you can customize the name and configuration settlings for your database. Otherwise, we can select <code>n</code> to proceed with the settings that have been selected for us. With your settings confirmed, two apps will be created: one for your app and one for Postgres.</p> <p>Flyctl will handle provisioning and completing the first deployment of your application all in one fell swoop, so maybe enjoy a snack while it does its thing - but maybe a small one. It won&rsquo;t take as long as you think. </p> <p>Thanks to Prisma and flyctl, all the heavy lifting has been done for us. During the development process, we declared a <code>DATABASE_URL</code> in our <code>.env</code> file in order to connect to our Postgres database. However, since we&rsquo;ve already created our database schema with Prisma, Fly has everything it needs to move forward with building the database and configuring the <code>DATABASE_URL</code> as a secret for us.</p> <p>Flyctl will move forward with building your Fly App and getting it up and running, including running your database migration and building your table from the prisma schema. Once complete, the CLI will share a link to your new app, So, why not go add a message?</p> <h2 id='step-2-celebrate' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#step-2-celebrate' aria-label='Anchor'></a><span class='plain-code'>Step 2: Celebrate</span></h2> <p>Only two steps? Yep! Only two. Your application and its associated database are up and running and you didn&rsquo;t even break a sweat. </p> <p>Moving forward, you&rsquo;ll rely on the <code>fly deploy</code> command when <a href='https://fly.io/docs/apps/deploy/' title=''>deploying changes</a> to your app.</p> <p><img alt="Animated gif of guestbook submission" src="/javascript-journal/remix-with-prisma-postgres/assets/message.gif" /></p> <h1 id='but-what-if-i-want-managed-postgres' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#but-what-if-i-want-managed-postgres' aria-label='Anchor'></a><span class='plain-code'>But what if I want managed Postgres?</span></h1> <p>It&rsquo;s true Fly Postgres is <em>unmanaged</em>. If you&rsquo;re not comfortable with managing things like scaling for memory and storage or outage recovery yourself, then there are definitely managed database providers out there that can serve as great solutions like <a href='https://supabase.com/' title=''>Supabase</a>.</p> <p>The process of launching a Remix app using Supabase as a managed Postgres provider only involves a couple added steps, especially if you continue to use Prisma as your ORM to streamline things. We have <a href='https://fly.io/docs/reference/supabase/' title=''>detailed guidance for using Supabase on our infrastructure</a> but, simplified, the steps look like this:</p> <ol> <li>Run the <code>fly launch</code> command. Select <code>y</code> to tweak your options and set the database to <code>none</code> instead of using Fly Postgres. </li><li>Run the <code>fly ext supabase create</code> command. This will initiate Supabase by creating an account associated with your Fly.io account, set up your db, and set the environment variables for interfacing with it. </li><li>The command line will return a <code>DATABASE_URL</code> and <code>DATABASE_POOLER_URL</code>. We use the <code>DATABASE_URL</code> for this project. You&rsquo;ll want to set it as a secret in your app by using the <code>fly secrets set</code> command like this: <code>cmd fly secrets set DATABASE_URL=postgres://example.com/mydb </code> </li><li>Celebrate! </li></ol> Using WebSockets with Next.js on Fly.io https://fly.io/javascript-journal/websockets-with-nextjs/ 2024年06月05日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <div class="lead"><p>We’re Fly.io. We make hard things easy, including deploying your apps around the world, close to your users. This article illustrates how to use WebSockets in your Next.js app without the need for third-party services. If you’d like to learn more about deploying Next.js apps on Fly.io, check out our docs <a href="https://fly.io/docs/js/frameworks/nextjs/" title="">here</a>.</p> </div> <p>In this article I&rsquo;ll show you how to build a real-time chat app with WebSocket in a Next.js app. This solution works whether you&rsquo;re using the App Router or the Pages Router, as much of the magic comes from setting up a custom Next.js server. Let&rsquo;s dive in!</p> <h2 id='demystifying-websockets' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#demystifying-websockets' aria-label='Anchor'></a><span class='plain-code'>Demystifying WebSockets</span></h2> <p>To understand WebSockets, we should first compare them with something we&rsquo;re more familiar with: HTTP requests. In an HTTP request, a message is sent to the server. The server Does A Thing, and then sends back a response. Once the request-response cycle has been complete, the connection is closed. This is an oversimplifictation, but the pattern of <em>request followed by a response</em> is the key point.</p> <p>WebSockets work a bit differently, because the connection between the client and server is held open until one of the parties closes it. The difference between these two protocols is a bit like having a conversation over walkie-talkie (HTTP requests) vs talking on the telephone (WebSockets). With walkie-talkies, one person talks at a time, and thus the line is only ever open in one direction at a time. When talking over the phone, both parties can speak at anytime, and the connection remains open until someone hangs up.</p> <p>All WebSocket connections start out as normal HTTP requests with a special header. This header informs the server, &ldquo;Hey, FYI, I should be treated as a WebSocket connection; please keep the door open.&rdquo; Its then up to the server to &ldquo;upgrade&rdquo; the connection to WebSockets, and from there, the client and server are free to send information back and forth at will.</p> <h2 id='the-challenge-of-websockets-on-next-js' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-challenge-of-websockets-on-next-js' aria-label='Anchor'></a><span class='plain-code'>The Challenge of WebSockets on Next.js</span></h2> <p>If you&rsquo;re used to deploying your Next.js apps on serverless architecture, WebSockets don&rsquo;t work out of the box. Serverless functions are built to be spun up and spun down as fast as possible and don&rsquo;t accommodate long-running connections. It&rsquo;s possible to use third-party services to add WebSocket support, but that isn&rsquo;t your only option.</p> <p>When deploying your app on Fly.io, your app runs as a traditional server, and thus WebSocket implementation is very straightforward; <strong class='font-semibold text-navy-950'>no third-party tools necessary.</strong> Additionally, Fly Machines are actually just fast-booting micro virtual machines that are managed by Firecracker (the same virtualization engine behind AWS Lambda). And unlike traditional servers that stay running, even when not in use, Fly Machines can be configured to automatically start and stop based on demand. Yay for only paying for what you use!</p> <h2 id='building-a-next-js-app-with-websockets' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#building-a-next-js-app-with-websockets' aria-label='Anchor'></a><span class='plain-code'>Building a Next.js App with WebSockets</span></h2> <p>We&rsquo;re going to build a very simple chat application that uses WebSocket to allow multiple clients to send and receive messages in realtime. Since the purpose is just to illustrate <em>how</em> one could use WebSockets with Next.js, we&rsquo;ll be making this dead simple: no authentication, no users, just an input where you can send a message for all clients to see.</p> <p>Now, as we learned before, WebSockets need to be upgraded on the server to maintain the connection, and so we&rsquo;ll be building a custom server for Next.js.</p> <p>Let&rsquo;s start out with a fresh Next.js install. We won&rsquo;t be needing any styling for this mini-project, so skip Tailwind for now.</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-x7ru3ted" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-x7ru3ted">npx create-next-app@latest </code></pre> </div> </div> <p>Next, let&rsquo;s install the dependencies we&rsquo;ll need for our custom server:</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-oturp0tq" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-oturp0tq">npm i express ws </code></pre> </div> </div><h3 id='creating-the-custom-server' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#creating-the-custom-server' aria-label='Anchor'></a><span class='plain-code'>Creating the custom server</span></h3> <p>At the root of your application create a file called <code>server.js</code> with the following content:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-o0rcnujw" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-o0rcnujw"><span class="c1">// server.js</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">parse</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">url</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">express</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">express</span><span class="dl">"</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">next</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">next</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">WebSocket</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">ws</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">WebSocketServer</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">ws</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">3000</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">wss</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WebSocketServer</span><span class="p">({</span> <span class="na">noServer</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">nextApp</span> <span class="o">=</span> <span class="nx">next</span><span class="p">({</span> <span class="na">dev</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">production</span><span class="dl">"</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">clients</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Set</span><span class="p">();</span> <span class="nx">nextApp</span><span class="p">.</span><span class="nx">prepare</span><span class="p">().</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">((</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">nextApp</span><span class="p">.</span><span class="nx">getRequestHandler</span><span class="p">()(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">parse</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span> <span class="kc">true</span><span class="p">));</span> <span class="p">});</span> <span class="nx">wss</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">connection</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">ws</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">clients</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">ws</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">New client connected</span><span class="dl">'</span><span class="p">);</span> <span class="nx">ws</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">message</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">isBinary</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Message received: </span><span class="p">${</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="nx">clients</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">client</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">client</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="nx">WebSocket</span><span class="p">.</span><span class="nx">OPEN</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">toString</span><span class="p">()</span> <span class="o">!==</span> <span class="s2">`{"event":"ping"}`</span><span class="p">))</span> <span class="p">{</span> <span class="nx">client</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="p">{</span> <span class="na">binary</span><span class="p">:</span> <span class="nx">isBinary</span> <span class="p">});</span> <span class="p">}</span> <span class="p">});</span> <span class="p">});</span> <span class="nx">ws</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">close</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">clients</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">ws</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Client disconnected</span><span class="dl">'</span><span class="p">);</span> <span class="p">});</span> <span class="p">});</span> <span class="nx">server</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">upgrade</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">socket</span><span class="p">,</span> <span class="nx">head</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">pathname</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">parse</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">url</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span> <span class="c1">// Make sure we all for hot module reloading</span> <span class="k">if</span> <span class="p">(</span><span class="nx">pathname</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">/_next/webpack-hmr</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="nx">nextApp</span><span class="p">.</span><span class="nx">getUpgradeHandler</span><span class="p">()(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">socket</span><span class="p">,</span> <span class="nx">head</span><span class="p">);</span> <span class="p">}</span> <span class="c1">// Set the path we want to upgrade to WebSockets</span> <span class="k">if</span> <span class="p">(</span><span class="nx">pathname</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">/api/ws</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="nx">wss</span><span class="p">.</span><span class="nx">handleUpgrade</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">socket</span><span class="p">,</span> <span class="nx">head</span><span class="p">,</span> <span class="p">(</span><span class="nx">ws</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">wss</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="dl">'</span><span class="s1">connection</span><span class="dl">'</span><span class="p">,</span> <span class="nx">ws</span><span class="p">,</span> <span class="nx">req</span><span class="p">);</span> <span class="p">});</span> <span class="p">}</span> <span class="p">});</span> <span class="p">})</span> </code></pre> </div> </div> <p>Let&rsquo;s take a look at this section here, as this is what you&rsquo;d want to change to tweak the app behavior when a new message arrives via a WebSocket channel:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-pwpr7qga" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-pwpr7qga"><span class="nx">ws</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">message</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">isBinary</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Received message: </span><span class="p">${</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="nx">clients</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">client</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">client</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="nx">WebSocket</span><span class="p">.</span><span class="nx">OPEN</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">toString</span><span class="p">()</span> <span class="o">!==</span> <span class="s2">`{"event":"ping"}`</span><span class="p">))</span> <span class="p">{</span> <span class="nx">client</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="p">{</span> <span class="na">binary</span><span class="p">:</span> <span class="nx">isBinary</span> <span class="p">});</span> <span class="p">}</span> <span class="p">});</span> <span class="p">});</span> </code></pre> </div> </div> <p>We&rsquo;re building a realtime chat app, so the only thing we need our WebSocket server to do is send the message received back to each client, and that&rsquo;s what we&rsquo;re doing above.</p> <p>Another significant section of our custom server is this:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-5bjm2flw" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-5bjm2flw"><span class="k">if</span> <span class="p">(</span><span class="nx">pathname</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">/api/ws</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="nx">wss</span><span class="p">.</span><span class="nx">handleUpgrade</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">socket</span><span class="p">,</span> <span class="nx">head</span><span class="p">,</span> <span class="p">(</span><span class="nx">ws</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">wss</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="dl">'</span><span class="s1">connection</span><span class="dl">'</span><span class="p">,</span> <span class="nx">ws</span><span class="p">,</span> <span class="nx">req</span><span class="p">);</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> </div> <p>This is the path we will use in our client code to initiate a WebSocket connection. It doesn&rsquo;t matter what this route is, as we won&rsquo;t actually be creating a file-based route in Next.js like you normally would. In fact, this is all the code we need to make this route usable.</p> <p>Finally, in order to use our custom server, we&rsquo;ll need to update our <code>start</code> command from <code>next start</code> (or <code>next dev</code>) to something else:</p> <div class="highlight-wrapper group relative json"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-i6sr66z3" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-i6sr66z3"><span class="err">...</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node server.js"</span><span class="p">,</span><span class="w"> </span><span class="nl">"start"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node server.js"</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">}</span><span class="err">,</span><span class="w"> </span><span class="err">...</span><span class="w"> </span></code></pre> </div> </div><div class="callout"><p><strong class="font-semibold text-navy-950">What about TypeScript?</strong> It’s more common to see custom servers written without TypeScript (using CommonJS), since otherwise you’d need to transpile the code first, or use something like <code>ts-node</code>. If you’re using a runtime that supports TypeScript out of the box, such as Deno or Bun, this is less of a concern.</p> </div><h3 id='adding-the-client-side-code' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#adding-the-client-side-code' aria-label='Anchor'></a><span class='plain-code'>Adding the client-side code</span></h3> <p>Now that we have our Next.js app running on a custom server that handles WebSocket requests, let&rsquo;s finally build our chat app!</p> <p>Replace the homepage of your app with the following code (it doesn&rsquo;t matter if you&rsquo;re using the Pages or the App router). I&rsquo;ll be using the App router.</p> <div class="highlight-wrapper group relative tsx"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-8ld2txop" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-8ld2txop"><span class="c1">// app/page.tsx</span> <span class="c1">// "use client" only necessary for App Router</span> <span class="dl">"</span><span class="s2">use client</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span> <span class="kd">let</span> <span class="nx">webSocket</span><span class="p">:</span> <span class="nx">WebSocket</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nb">window</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">undefined</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">protocol</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">protocol</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">https:</span><span class="dl">"</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">wss:</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">ws:</span><span class="dl">"</span><span class="p">;</span> <span class="nx">webSocket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WebSocket</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">protocol</span><span class="p">}</span><span class="s2">//</span><span class="p">${</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">host</span><span class="p">}</span><span class="s2">/api/ws`</span><span class="p">);</span> <span class="nx">setInterval</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">webSocket</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">!==</span> <span class="nx">webSocket</span><span class="p">.</span><span class="nx">OPEN</span><span class="p">)</span> <span class="p">{</span> <span class="nx">webSocket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WebSocket</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">protocol</span><span class="p">}</span><span class="s2">//</span><span class="p">${</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">host</span><span class="p">}</span><span class="s2">/api/ws`</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="nx">webSocket</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="s2">`{"event":"ping"}`</span><span class="p">);</span> <span class="p">},</span> <span class="mi">29000</span><span class="p">);</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">Index</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">messages</span><span class="p">,</span> <span class="nx">setMessages</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">[]</span><span class="o">&gt;</span><span class="p">([]);</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">newMessage</span><span class="p">,</span> <span class="nx">setNewMessage</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="dl">''</span><span class="p">);</span> <span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">webSocket</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">data</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">connection established</span><span class="dl">"</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> <span class="nx">setMessages</span><span class="p">((</span><span class="nx">prevMessages</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">[...</span><span class="nx">prevMessages</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="p">]);</span> <span class="p">};</span> <span class="p">},</span> <span class="p">[]);</span> <span class="kd">const</span> <span class="nx">sendMessage</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">webSocket</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">newMessage</span><span class="p">);</span> <span class="nx">setNewMessage</span><span class="p">(</span><span class="dl">''</span><span class="p">);</span> <span class="p">};</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Real-Time Chat<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span> <span class="si">{</span><span class="nx">messages</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">message</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">index</span><span class="si">}</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">message</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">))</span><span class="si">}</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">className</span><span class="p">=</span><span class="s">"border border-gray-400 rounded p-2"</span> <span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">newMessage</span><span class="si">}</span> <span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">setNewMessage</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">)</span><span class="si">}</span> <span class="na">placeholder</span><span class="p">=</span><span class="s">"Type your message..."</span> <span class="p">/&gt;</span> <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">sendMessage</span><span class="si">}</span><span class="p">&gt;</span>Send<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">);</span> <span class="p">};</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">Index</span><span class="p">;</span> </code></pre> </div> </div> <p>Let&rsquo;s try running this code! Start your development server and then open <code>http://localhost:3000</code> in <strong class='font-semibold text-navy-950'>two tabs</strong>. You should see the following:</p> <p><img alt="Screenshot of chat app" src="/javascript-journal/websockets-with-nextjs/assets/chat-app.gif" /></p> <p>If you type a message and click &ldquo;Send&rdquo;, you should not only see your message inserted above, but it should be visible in <em>both tabs.</em> This is because each tab creates its own WebSocket connection and our server is configured to send back the messages to all clients.</p> <p>And there you have it! An extremely paired down chat application. Who needs Slack?</p> <h3 id='deploying-our-chat-app-to-fly-io' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#deploying-our-chat-app-to-fly-io' aria-label='Anchor'></a><span class='plain-code'>Deploying our chat app to Fly.io</span></h3> <p>As mentioned before, using WebSockets on Fly.io doesn&rsquo;t require any special work, so let&rsquo;s deploy our new chat app!</p> <p>We can do this with one command, which will also generate a Dockerfile for us as well as a <code>fly.toml</code>:</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-vmghm7x2" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-vmghm7x2">fly launch --name &lt;your-app-name&gt; </code></pre> </div> </div> <p>And that&rsquo;s it! If you run <code>fly open</code> you should see your live application at <code>https://&lt;your-app-name&gt;.fly.dev</code></p> <h2 id='conclusion' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#conclusion' aria-label='Anchor'></a><span class='plain-code'>Conclusion</span></h2> <p>The documentation for using WebSockets on Next.js is a bit lacking, but that doesn&rsquo;t mean it&rsquo;s hard to implement. After this article, you now have a primer to help build more elaborate realtime Next.js services on Fly.io.</p> Build a file sharing service without making your brain hurt https://fly.io/javascript-journal/soar-file-sharing/ 2024年05月23日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <div class="lead"><p>We’re Fly.io. We make hard things easy, including deploying your apps around the world, close to your users. This post is about scaling a file sharing app using Fly.io and Tigris.</p> </div> <p>I&rsquo;m a pretty online person, and I have friends all around the world. We tend to do things like share mod files and video replays pretty frequently, and this creates issues when the &gt;5 GB file someone just uploaded in Poland needs to be downloaded in Canada.</p> <p>Most if not all messaging services will have this problem, uploads go either to a local storage backend to the uploader, which makes reads slow anywhere else, or they go to the big bucket in Virginia and everything is slow for everyone.</p> <p>This sort of thing is annoying enough that I&rsquo;ve wanted a better solution for a while: a simple file sharing service that is fast for everyone, no matter who&rsquo;s sending what to who.</p> <p>I&rsquo;ve tried to solve this problem before. I&rsquo;ve looked at using off-the-shelf solutions, but I&rsquo;ve found them either <a href='https://docs.ceph.com/en/latest/cephadm/install/' title=''>too</a> <a href='https://min.io/docs/minio/linux/operations/install-deploy-manage/deploy-minio-multi-node-multi-drive.html' title=''>complex</a> or too simple, and so I had the itch to DIY it so I can control the implementation.</p> <p>The must-have list for the new service:</p> <ul> <li>Uploads and downloads to be fast regardless of who is sharing to who, this means handling files moving across oceans easily. </li><li>Some control over access, and the ability to add features easily. </li><li>Reasonable fault tolerance, we don&rsquo;t need enterprise HA but not someone&rsquo;s home server either </li></ul> <p>To achieve those goals, I&rsquo;ve resurrected this idea with the ensemble cast of Tigris, Fly.io and SvelteKit, which are some of my favourite tools that help me make complex stuff without turning my brain into goo.</p> <p>SvelteKit makes the frontend a breeze with easy HTML form handling and SSR out of the box, and Fly.io and Tigris give me global object storage on demand and compute to match.</p> <p>So, how can Fly.io, Tigris and SvelteKit make this hard thing easy?</p> <h2 id='building-soar' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#building-soar' aria-label='Anchor'></a><span class='plain-code'>Building Soar</span></h2> <p>I&rsquo;ve dubbed the file sharing service &ldquo;Soar&rdquo;, just to give it a bit of personality.</p> <p>Soar is a pretty tiny full-stack SvelteKit app, only about 100 lines of important code. It takes user uploads via a normal HTML form, processes them in a SvelteKit form action, and returns info about the uploaded file to the user.</p> <h3 id='step-0-setting-up-a-sveltekit-project' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#step-0-setting-up-a-sveltekit-project' aria-label='Anchor'></a><span class='plain-code'>Step 0: Setting up a SvelteKit project</span></h3> <p>I&rsquo;m using <a href='https://bun.sh' title=''>Bun</a> for this project&rsquo;s package manager, but you could swap in <code>npm</code>, <code>pnpm</code>, <code>yarn</code>, or whatever instead.</p> <div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-z6ce0ox5" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-z6ce0ox5"><span class="c"># Create the project</span> <span class="nv">$ </span>bun create svelte@latest my-app <span class="c"># Install dependencies</span> <span class="nv">$ </span><span class="nb">cd </span>my-app <span class="nv">$ </span>bun <span class="nb">install</span> <span class="c"># Run the dev server</span> <span class="nv">$ </span>bun <span class="nt">--bun</span> run dev </code></pre> </div> </div><h3 id='step-1-uploading-files-to-tigris' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#step-1-uploading-files-to-tigris' aria-label='Anchor'></a><span class='plain-code'>Step 1: Uploading files to Tigris</span></h3> <p>The first half of a file sharing service is getting stuff from users&rsquo; computers into the storage backend.</p> <p>There&rsquo;s a few approaches we could take here, but we&rsquo;re going to go with running the file uploads through the SvelteKit server, which keeps the client lean and means it works pre-hydration (it uses normal browser forms).</p> <p>We can start by adding storage to our app, which we can do right from the flyctl CLI.</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-aa2iex32" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-aa2iex32">fly storage create </code></pre> </div> </div> <p>Name this something like &ldquo;soar-dev&rdquo; since we&rsquo;re going to use a different bucket for our deployment later.</p> <p>And we put our credentials into a file called <code>.env.development</code> in our project root.</p> <div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-xmpta03n" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-xmpta03n"><span class="nv">AWS_REGION</span><span class="o">=</span><span class="s2">"auto"</span> <span class="nv">AWS_ENDPOINT_URL_S3</span><span class="o">=</span><span class="s2">"https://fly.storage.tigris.dev"</span> <span class="c"># or a local S3 server</span> <span class="nv">AWS_ACCESS_KEY_ID</span><span class="o">=</span><span class="s2">"..."</span> <span class="nv">AWS_SECRET_ACCESS_KEY</span><span class="o">=</span><span class="s2">"..."</span> <span class="nv">BUCKET_NAME</span><span class="o">=</span><span class="s2">"soar"</span> <span class="c"># This is the name of our Tigris storage bucket</span> </code></pre> </div> </div> <p>Now, let&rsquo;s get to building.</p> <h4 id='s3-client' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#s3-client' aria-label='Anchor'></a><span class='plain-code'>S3 Client</span></h4> <p>We need to pull in the AWS S3 SDK to use the S3 API from our code:</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-fi6qqjdl" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-fi6qqjdl"># Install the aws-sdk packages we're going to use $ bun add --dev @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner nanoid </code></pre> </div> </div> <p>Then we can use our credentials to create an S3 client for our server code to use later:</p> <p><code>/src/lib/storage.ts</code></p> <div class="highlight-wrapper group relative ts"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-tazh2wyu" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-tazh2wyu"><span class="k">import</span> <span class="p">{</span> <span class="nx">env</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">$env/dynamic/private</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">S3Client</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@aws-sdk/client-s3</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">S3</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">S3Client</span><span class="p">({</span> <span class="na">region</span><span class="p">:</span> <span class="nx">env</span><span class="p">.</span><span class="nx">AWS_REGION</span><span class="p">,</span> <span class="na">endpoint</span><span class="p">:</span> <span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ENDPOINT_URL_S3</span><span class="p">,</span> <span class="na">credentials</span><span class="p">:</span> <span class="p">{</span> <span class="na">accessKeyId</span><span class="p">:</span> <span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">,</span> <span class="na">secretAccessKey</span><span class="p">:</span> <span class="nx">env</span><span class="p">.</span><span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">,</span> <span class="p">},</span> <span class="na">forcePathStyle</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> </div><h4 id='upload-ui' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#upload-ui' aria-label='Anchor'></a><span class='plain-code'>Upload UI</span></h4> <p>This is made easy by Svelte&rsquo;s similarity to HTML, we just plop down an HTML form with some props set to make it all work once we go to the server side.</p> <ul> <li><code>action=&quot;?/upload&quot;</code> is a bit of SvelteKit magic that routes our form submit to a particular handler. </li><li><code>enctype=&quot;multipart/form-data&quot;</code> is HTML spec magic speak for &ldquo;upload the whole file instead of just the name&rdquo; </li><li><code>use:enhance</code> is a Svelte action that makes the form submit smoother (no page reload) when JS is available. </li></ul> <p><code>/src/routes/+page.svelte</code></p> <div class="highlight-wrapper group relative html"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-mfbmf5zo" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-mfbmf5zo"><span class="nt">&lt;script </span><span class="na">lang=</span><span class="s">"ts"</span><span class="nt">&gt;</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">form</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">$props</span><span class="p">();</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">enhance</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">$app/forms</span><span class="dl">"</span><span class="p">;</span> <span class="kd">let</span> <span class="nx">loading</span> <span class="o">=</span> <span class="nx">$state</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span> <span class="kd">function</span> <span class="nx">submit</span><span class="p">()</span> <span class="p">{</span> <span class="nx">loading</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="k">return</span> <span class="k">async</span> <span class="p">({</span> <span class="nx">update</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">await</span> <span class="nx">update</span><span class="p">();</span> <span class="nx">loading</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="p">};</span> <span class="p">}</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;h1&gt;</span>Welcome to Soar<span class="nt">&lt;/h1&gt;</span> <span class="nt">&lt;form</span> <span class="na">action=</span><span class="s">"?/upload"</span> <span class="na">method=</span><span class="s">"post"</span> <span class="na">enctype=</span><span class="s">"multipart/form-data"</span> <span class="na">use:enhance=</span><span class="s">{submit}</span> <span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"file"</span> <span class="na">name=</span><span class="s">"file"</span> <span class="nt">/&gt;</span> <span class="nt">&lt;div&gt;</span> {#if loading} uploading... {:else if form?.error} <span class="nt">&lt;span&gt;</span>An error occured: {form.message}<span class="nt">&lt;/span&gt;</span> {/if} <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;button</span> <span class="na">disabled=</span><span class="s">"{loading}"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">&gt;</span>submit<span class="nt">&lt;/button&gt;</span> <span class="nt">&lt;/form&gt;</span> </code></pre> </div> </div><h4 id='and-on-the-server' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#and-on-the-server' aria-label='Anchor'></a><span class='plain-code'>And on the server&hellip;</span></h4> <p>This is a little more complex, but all we&rsquo;re doing here is using the special SvelteKit export <code>actions</code> to handle the aforementioned <code>?/upload</code> action.</p> <div class="highlight-wrapper group relative ts"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-k1znip3z" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-k1znip3z"><span class="k">export</span> <span class="kd">const</span> <span class="nx">actions</span><span class="p">:</span> <span class="nx">Actions</span> <span class="o">=</span> <span class="p">{</span> <span class="k">async</span> <span class="nx">upload</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="p">};</span> </code></pre> </div> </div> <p>We pull the file out of the form data:</p> <div class="highlight-wrapper group relative ts"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-8yiwroxt" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-8yiwroxt"><span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">e</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">formData</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">file</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">file</span><span class="dl">"</span><span class="p">)</span> <span class="k">as</span> <span class="nx">File</span><span class="p">;</span> </code></pre> </div> </div> <p>Do a little check that it&rsquo;s actually there:</p> <div class="highlight-wrapper group relative ts"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-umel08if" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-umel08if"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">file</span><span class="p">.</span><span class="nx">name</span> <span class="o">||</span> <span class="nx">file</span><span class="p">.</span><span class="nx">name</span> <span class="o">==</span> <span class="dl">"</span><span class="s2">undefined</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">fail</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">message</span><span class="p">:</span> <span class="dl">"</span><span class="s2">You must provide a file</span><span class="dl">"</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> </div> <p>Then upload it to S3 using the client we created earlier.</p> <div class="highlight-wrapper group relative ts"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-kah7qsbs" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-kah7qsbs"><span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">nanoid</span><span class="p">()</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">_</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">file</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">upload</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Upload</span><span class="p">({</span> <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">Bucket</span><span class="p">:</span> <span class="nx">env</span><span class="p">.</span><span class="nx">BUCKET_NAME</span><span class="p">,</span> <span class="na">Key</span><span class="p">:</span> <span class="nx">key</span><span class="p">,</span> <span class="na">Body</span><span class="p">:</span> <span class="nx">file</span><span class="p">,</span> <span class="na">ContentType</span><span class="p">:</span> <span class="nx">file</span><span class="p">.</span><span class="kd">type</span><span class="p">,</span> <span class="p">},</span> <span class="na">client</span><span class="p">:</span> <span class="nx">S3</span><span class="p">,</span> <span class="c1">// Imported from $lib/server/storage</span> <span class="p">});</span> <span class="k">await</span> <span class="nx">upload</span><span class="p">.</span><span class="nx">done</span><span class="p">();</span> </code></pre> </div> </div> <p>And finally, redirect the user to the uploaded file.</p> <div class="highlight-wrapper group relative ts"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-e5gppx8r" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-e5gppx8r"><span class="nx">redirect</span><span class="p">(</span><span class="mi">303</span><span class="p">,</span> <span class="s2">`/</span><span class="p">${</span><span class="nx">key</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> </code></pre> </div> </div> <p>And, uploading works!</p> <p>The magic Tigris provides us here is when we scale our SvelteKit app to new regions, Tigris is already there, ready to go. We don&rsquo;t have to pay more or do anything at all. We automatically get our uploads going from user -&gt; our app -&gt; Tigris in the <a href='https://www.tigrisdata.com/docs/concepts/regions/' title=''>nearest region</a>. This lowers latency, improves bandwidth and reliability by eliminating possible bottlenecks.</p> <div class="callout"><p>We <em>could</em> make the uploads even more direct by using <a href="https://www.tigrisdata.com/docs/objects/presigned/" title="">presigned URLs</a> but it complicates the code by requiring a two-step upload process.</p> </div><h3 id='step-2-retrieving-files-from-tigris-and-displaying-them-to-the-user' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#step-2-retrieving-files-from-tigris-and-displaying-them-to-the-user' aria-label='Anchor'></a><span class='plain-code'>Step 2: Retrieving files from Tigris and displaying them to the user</span></h3> <p>Now that we&rsquo;ve uploaded a bunch of cat photos, mod files and database credentials to our service, we need a way to retrieve uploaded objects.</p> <p>This is similarly easy, and we can do most of the work server-side with a load function inside <code>+page.server.ts</code>:</p> <div class="highlight-wrapper group relative ts"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-uraggc51" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-uraggc51"><span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">load</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">metadata</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">S3</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span> <span class="k">new</span> <span class="nx">HeadObjectCommand</span><span class="p">({</span> <span class="na">Bucket</span><span class="p">:</span> <span class="nx">env</span><span class="p">.</span><span class="nx">BUCKET_NAME</span><span class="p">,</span> <span class="na">Key</span><span class="p">:</span> <span class="nx">e</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">file</span> <span class="p">})</span> <span class="p">);</span> <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getSignedUrl</span><span class="p">(</span> <span class="nx">S3</span><span class="p">,</span> <span class="k">new</span> <span class="nx">GetObjectCommand</span><span class="p">({</span> <span class="na">Bucket</span><span class="p">:</span> <span class="nx">env</span><span class="p">.</span><span class="nx">BUCKET_NAME</span><span class="p">,</span> <span class="na">Key</span><span class="p">:</span> <span class="nx">e</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">file</span> <span class="p">}),</span> <span class="p">{</span> <span class="na">expiresIn</span><span class="p">:</span> <span class="mi">3600</span><span class="p">,</span> <span class="p">}</span> <span class="p">);</span> <span class="k">return</span> <span class="p">{</span> <span class="nx">url</span><span class="p">,</span> <span class="nx">metadata</span> <span class="p">};</span> <span class="p">}</span> </code></pre> </div> </div> <p>We grab the metadata of the object, create a pre-signed URL for accessing the file contents, and send it off to our future selves in frontend-land.</p> <p>This all runs on the server, which means we can implement authorization checks or rate limits or whatever else here. Unlike uploads, we use pre-signed URLs here so users are always pulling the file from the closest Tigris region.</p> <div class="callout"><p>Alternatively, we could proxy the body of the object through our app. This would offer stronger authorization guarantees, but would mean users would retrieve the potentially large file through the closest instance of our app, not necessarily the closest Tigris region.</p> </div> <p>A little sprinkle of frontend, and we have working file retrieval!</p> <div class="highlight-wrapper group relative html"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-4xs9brjq" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-4xs9brjq"><span class="nt">&lt;script </span><span class="na">lang=</span><span class="s">"ts"</span><span class="nt">&gt;</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">data</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">$props</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">embedType</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">metadata</span><span class="p">.</span><span class="nx">ContentType</span><span class="p">?.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span> <span class="nt">&lt;/script&gt;</span> {#if embedType == 'image'} <span class="nt">&lt;img</span> <span class="na">class=</span><span class="s">"block"</span> <span class="na">src=</span><span class="s">{data.url}"</span> <span class="nt">/&gt;</span> {:else if embedType == 'video'} <span class="nt">&lt;video</span> <span class="na">class=</span><span class="s">"block"</span> <span class="na">src=</span><span class="s">{data.url}</span><span class="nt">&gt;&lt;/video&gt;</span> {/if} <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">{data.url}</span><span class="nt">&gt;</span>raw<span class="nt">&lt;/a&gt;</span> </code></pre> </div> </div> <p>This is a happy middle ground between making the bucket public and proxying uploads through the SvelteKit app. It gives us the benefits of serving the files directly from Tigris, while retaining most of the control over who can access what.</p> <p>When users request a file, Tigris will do the right thing automatically. If an object is requested in a different region than the one it was uploaded to, the file will transparently be copied to that region when it is requested, making subsequent access as fast as the region where it was initially uploaded.</p> <div class="callout"><p>This is sometimes referred to as "Pull-through" caching. <a href="https://www.tigrisdata.com/docs/objects/caching/" title="">Tigris actually supports caching on put and caching on list as well</a>.</p> </div><h3 id='step-3-wheres-the-rest-of-the-code' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#step-3-wheres-the-rest-of-the-code' aria-label='Anchor'></a><span class='plain-code'>Step 3: Where&rsquo;s the rest of the code?</span></h3> <p>When I got to this point, I was left with a bit of wonder at how quickly everything comes together with today&rsquo;s &ldquo;magic&rdquo; tooling. SvelteKit handles the frontend, data loading, uploading and storing with no fuss, Tigris just does the right thing without us telling it to, and getting it ready to deploy on Fly.io happens pretty much automatically.</p> <p>Ship it!</p> <div class="callout"><p>Currently, you have to swap out <code>adapter-auto</code> for <a href="https://kit.svelte.dev/docs/adapter-node" title=""><code>adapter-node</code></a> in <code>svelte.config.js</code> manually before you can deploy to Fly.io. We’re working on making this easier in the future.</p> </div><div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-5birrtix" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-5birrtix"><span class="nv">$ </span>fly launch <span class="nt">--no-deploy</span> <span class="nv">$ </span>fly storage create <span class="c"># Production bucket</span> <span class="nv">$ </span>fly deploy </code></pre> </div> </div> <p>Scale it&hellip;</p> <div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-rvzlh4ny" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-rvzlh4ny"><span class="nv">$ </span>fly scale count 3 <span class="nt">--region</span> ewr,waw,lhr </code></pre> </div> </div><h2 id='what-comes-next' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-comes-next' aria-label='Anchor'></a><span class='plain-code'>What comes next</span></h2> <p>For me, not much. This project exists only as a KISS file uploader for me and a few of my friends. For you, there&rsquo;s a few directions we could take this:</p> <ul> <li><strong class='font-semibold text-navy-950'>Add an accounts system to namespace and track previous uploads</strong><br> We could add this functionality if you wanted to offer a sort of &ldquo;index&rdquo; for a given uploader, maybe with public / private files. </li><li><strong class='font-semibold text-navy-950'>Build an API for uploading objects</strong><br> We could use this to integrate with image/file sharing applications like ShareX, or build a CLI uploader. </li></ul> <p>For more interesting things to do with Tigris and Fly, check out these articles:</p> <ul> <li><a href='https://fly.io/blog/tigris-public-beta/' title=''>Globally Distributed Object Storage with Tigris</a> </li><li><a href='https://fly.io/javascript-journal/single-tenant-sqlite-in-tigris/' title=''>Multi-tenant apps with single-tenant SQLite databases in global Tigris buckets</a> </li><li><a href='https://fly.io/phoenix-files/what-if-s3-could-be-a-fast-globally-synced-key-value-database-that-s-tigris/' title=''>What if S3 could be a fast, globally synced, Key Value Database? That&rsquo;s Tigris</a> </li></ul> Multi-tenant apps with single-tenant SQLite databases in global Tigris buckets https://fly.io/javascript-journal/single-tenant-sqlite-in-tigris/ 2024年04月17日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <div class="lead"><p>We’re Fly.io, where apps run on Fly Machines—fast-starting VMs with a simple API—in 30+ regions around the world. This article is about Fly Machines storing their SQLite databases in a globally distributed <a href="https://www.tigrisdata.com/" title="">Tigris</a> bucket. If you haven’t tried Machines, take them for a spin! <a href="https://fly.io/docs/speedrun/" title="">It’s fast to get started.</a></p> </div> <p>I got nerd-sniped by one of the ideas Jason brainstormed in <a href='https://fly.io/phoenix-files/what-if-s3-could-be-a-fast-globally-synced-key-value-database-that-s-tigris/' title=''>his Phoenix Files article about Tigris</a>, an S3-compatible globally synced object store. <a href='https://www.tigrisdata.com/' title=''>Tigris</a> is, not coincidentally, built on Fly.io. (If you haven&rsquo;t read Jason&rsquo;s article, <a href='https://fly.io/phoenix-files/what-if-s3-could-be-a-fast-globally-synced-key-value-database-that-s-tigris/' title=''>go read it</a>! You&rsquo;ll be fine! There&rsquo;s hardly any Elixir in it.)</p> <p>Here&rsquo;s the idea I&rsquo;m here to explore: multi-tenant applications with <a href='https://fly.io/phoenix-files/what-if-s3-could-be-a-fast-globally-synced-key-value-database-that-s-tigris/#single-tenancy' title=''>single-tenant SQLite databases</a> stored in Tigris and run on single-tenant Fly Machines:</p> <div class='group relative min-w-0 bg-white shadow-md shadow-navy-500/10 rounded-xl mb-7 ring-1 ring-navy-300/40'><button type='button' class='bubble-wrap z-20 absolute right-2.5 top-2.5 text-transparent group-hover:text-navy-950 hocus:text-violet-600 bg-transparent group-hover:bg-white hocus:bg-violet-200/40 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none' data-wrap-target='#table-e29ewggo' data-wrap-type='nowrap'><svg class='w-5 h-5 pointer-events-none' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><g buffered-rendering='static'><path d='M11.912 10.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.314 2.314 0 00-2.315-2.31H4.959M15.187 14.5H4.959M8.802 10H4.959' /><path d='M13.081 8.466l-1.548 1.571 1.548 1.571' /></g></svg><span class='bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950'>Wrap text</span></button><div class='min-w-0 overflow-x-auto rounded-xl'><table class='table-stripe table-stretch table-pad text-sm whitespace-nowrap m-0' id='table-e29ewggo'><thead class='text-navy-950 text-left'><tr> <th style="text-align: center"><img alt="Diagram with 3 boxes: 2 for Tenants running Fly Machines and 1 for Tigris with 2 SQLite icons inside for the Tenants." src="/javascript-journal/single-tenant-sqlite-in-tigris/assets/./single-tenant-sqlite-in-tigris-diagram-1.png" /></th> </tr> </thead><tbody><tr> <td style="text-align: center">The multi-tenant architecture with single-tenant SQLite databases.</td> </tr> </tbody></table></div></div> <p>If we&rsquo;re to handle a tenant request within this architecture, we&rsquo;d route the request to its Machine, ensure its database is there, serve the request, update database file in the bucket&hellip; and potentially auto-stop the Machine if that was just a one-off request! (And yes, we can also wake it up if another request comes in.)</p> <div class='group relative min-w-0 bg-white shadow-md shadow-navy-500/10 rounded-xl mb-7 ring-1 ring-navy-300/40'><button type='button' class='bubble-wrap z-20 absolute right-2.5 top-2.5 text-transparent group-hover:text-navy-950 hocus:text-violet-600 bg-transparent group-hover:bg-white hocus:bg-violet-200/40 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none' data-wrap-target='#table-iitzgx07' data-wrap-type='nowrap'><svg class='w-5 h-5 pointer-events-none' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><g buffered-rendering='static'><path d='M11.912 10.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.314 2.314 0 00-2.315-2.31H4.959M15.187 14.5H4.959M8.802 10H4.959' /><path d='M13.081 8.466l-1.548 1.571 1.548 1.571' /></g></svg><span class='bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950'>Wrap text</span></button><div class='min-w-0 overflow-x-auto rounded-xl'><table class='table-stripe table-stretch table-pad text-sm whitespace-nowrap m-0' id='table-iitzgx07'><thead class='text-navy-950 text-left'><tr> <th style="text-align: center"><img alt="Sequence diagram explaining the request flow described above" src="/javascript-journal/single-tenant-sqlite-in-tigris/assets/./single-tenant-sqlite-in-tigris-diagram-3.png" /></th> </tr> </thead><tbody><tr> <td style="text-align: center">The sequence diagram</td> </tr> </tbody></table></div></div><div class="callout"><p>To learn more about Tigris, go <a href="https://www.tigrisdata.com/docs/overview/" title="">here</a>. To learn about Fly Machines, go <a href="https://fly.io/docs/machines/overview/" title="">here</a>.</p> </div> <p>However, there&rsquo;s a caveat to this approach: if we allow more than one Machine concurrently access the tenants&rsquo; SQLite database, the Machine updating the database file in the bucket last would overwrite changes done by the other ones. Also, these Machines would work on snapshots of the data without knowledge of the changes applied in parallel.</p> <p>That&rsquo;s enough rambling about theory! Let&rsquo;s bring this idea to life. In this post, we&rsquo;ll build a Vanilla JavaScript app that keeps a counter in a SQLite database and increments it every time a user visits a web page. It&rsquo;ll fetch the database file from Tigris on the app start-up and send it back on shutdown. At the end, we&rsquo;ll make the app customizable so that an instance can be run per tenant and deploy a multi-tenant setup at Fly.io.</p> <p>Actually, I&rsquo;ve already build the app at: <a href="https://github.com/fly-apps/js-sqlite-in-tigris">https://github.com/fly-apps/js-sqlite-in-tigris</a>&hellip; let&rsquo;s go through it step by step together!</p> <div class="callout"><p>If you want to run the examples, make sure you’ve got a <a href="https://fly.io/app/sign-up" title="">Fly.io</a> account.</p> </div><h2 id='tigris-setup' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#tigris-setup' aria-label='Anchor'></a><span class='plain-code'>Tigris setup</span></h2> <p>Issue <code>fly storage create</code> to create a Tigris project. This will output a bunch of environment variables that have to be available in the shell to run the following examples. The environment variables are important. If they’re not set, the following examples won’t work. </p> <p>The simplest way to set them up is to issue a set of export statements in your shell:</p> <div class="highlight-wrapper group relative shell"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-ve3theid" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-ve3theid"><span class="nb">export </span><span class="nv">AWS_ACCESS_KEY_ID</span><span class="o">=</span>&lt;you-access-key-id&gt; <span class="nb">export </span><span class="nv">AWS_ENDPOINT_URL_S3</span><span class="o">=</span>https://fly.storage.tigris.dev <span class="nb">export </span><span class="nv">AWS_REGION</span><span class="o">=</span>auto <span class="nb">export </span><span class="nv">AWS_SECRET_ACCESS_KEY</span><span class="o">=</span>&lt;your-secret-access-key&gt; <span class="nb">export </span><span class="nv">BUCKET_NAME</span><span class="o">=</span>&lt;your-bucket-name&gt; </code></pre> </div> </div><div class="callout"><p>To keep the variables handy, install a tool like <a href="https://direnv.net/" title="">dotenv</a> and put the exports into a <code>.envrc</code> file. <code>dotenv</code> will make them available for the shell commands run inside the current directory.</p> </div> <p>Get the feel of Tigris with the <a href='https://aws.amazon.com/cli/' title=''>AWS CLI</a>:</p> <div class="highlight-wrapper group relative shell"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-x6gti2ut" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-x6gti2ut">aws s3api list-buckets <span class="c"># you should see the bucket you've created on setup </span> aws s3api put-object <span class="nt">--bucket</span> <span class="nv">$BUCKET_NAME</span> <span class="nt">--key</span> mykey <span class="nt">--body</span> sampe-file <span class="c"># put a file into a bucket</span> aws s3api list-objects-v2 <span class="nt">--bucket</span> <span class="nv">$BUCKET_NAME</span> <span class="c"># list objects in the bucket</span> aws s3api get-object <span class="nt">--bucket</span> <span class="nv">$BUCKET_NAME</span> <span class="nt">--key</span> mykey sample-file <span class="c"># download the file</span> fly storage dashboard <span class="nv">$BUCKET_NAME</span> <span class="c"># see the bucket in the Tigris console</span> </code></pre> </div> </div> <p>Now, let&rsquo;s switch gears and focus on the JS app and SQLite for a bit.</p> <h2 id='node-js-app-with-sqlite' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#node-js-app-with-sqlite' aria-label='Anchor'></a><span class='plain-code'>Node.js app with SQLite</span></h2><div class="callout"><p>The code corresponding with this step is available at <a href="https://github.com/fly-apps/js-sqlite-in-tigris/commit/7303da9" title="">7303da9</a>.</p> </div> <p>It&rsquo;s pretty easy to start building with our Node generator. You can install it with <code>npx --yes @flydotio/node-demo@latest</code> (more on it <a href='https://fly.io/javascript-journal/vanilla-candy-sprinkles/' title=''>here</a>).</p> <p>With that installed, if you run <code>npx node-demo --esm --ejs --express --sqlite3</code> and make <a href='https://github.com/fly-apps/js-sqlite-in-tigris/commit/da59370d63c3b9dfa85144386d6e76a67aaa4d2f' title=''>a few tweaks</a> for our use case, you&rsquo;ll get an example Vanilla JavaScript app running Express server with SQLite. To test it locally, start the server: <code>npm install &amp; npm run start</code></p> <p>If you visit <a href="http://localhost:3000/">http://localhost:3000/</a> and start refreshing the page, you&rsquo;ll see a counter incrementing. If you restart the app, the counter should pick up where it left off - we do persist our state to the database, great!</p> <p>Nothing fancy here code-wise:</p> <ul> <li>we create and set up an SQLite database: </li></ul> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-7udfw4ij" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-7udfw4ij"><span class="c1">// server.js</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DATABASE_PATH</span> <span class="o">||=</span> <span class="dl">'</span><span class="s1">./db.sqlite3</span><span class="dl">'</span> <span class="kd">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">sqlite3</span><span class="p">.</span><span class="nx">Database</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DATABASE_PATH</span><span class="p">)</span> <span class="p">...</span> <span class="nx">db</span><span class="p">.</span><span class="nx">run</span><span class="p">(</span><span class="dl">'</span><span class="s1">CREATE TABLE IF NOT EXISTS "welcome" ( "count" INTEGER )</span><span class="dl">'</span><span class="p">)</span> </code></pre> </div> </div> <ul> <li>configure the Express server to listen at port 3000: </li></ul> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-drbi0i9s" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-drbi0i9s"><span class="c1">// server.js</span> <span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">()</span> <span class="p">...</span> <span class="c1">// set up static content and ejs views</span> <span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">express</span><span class="p">.</span><span class="kd">static</span><span class="p">(</span><span class="dl">'</span><span class="s1">public</span><span class="dl">'</span><span class="p">))</span> <span class="nx">app</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">view engine</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">ejs</span><span class="dl">'</span><span class="p">)</span> <span class="p">...</span> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">3000</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="p">...</span> <span class="p">})</span> </code></pre> </div> </div> <ul> <li>and expose a page to increment the counter in the database: </li></ul> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-d0197gxk" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-d0197gxk"><span class="c1">// server.js</span> <span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span><span class="p">(</span><span class="nx">_request</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// increment count, creating table row if necessary</span> <span class="k">await</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">db</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">SELECT "count" from "welcome"</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">row</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">let</span> <span class="nx">query</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">UPDATE "welcome" SET "count" = ?</span><span class="dl">'</span> <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="k">return</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">row</span><span class="p">)</span> <span class="p">{</span> <span class="nx">count</span> <span class="o">=</span> <span class="nx">row</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nx">count</span> <span class="o">=</span> <span class="mi">1</span> <span class="nx">query</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">INSERT INTO "welcome" VALUES(?)</span><span class="dl">'</span> <span class="p">}</span> <span class="nx">db</span><span class="p">.</span><span class="nx">run</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="p">[</span><span class="nx">count</span><span class="p">],</span> <span class="nx">err</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">err</span> <span class="p">?</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">:</span> <span class="nx">resolve</span><span class="p">()</span> <span class="p">})</span> <span class="p">})</span> <span class="p">})</span> <span class="c1">// render HTML response</span> <span class="nx">response</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="dl">'</span><span class="s1">index</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">count</span> <span class="p">});</span> <span class="p">})</span> </code></pre> </div> </div> <p>And that&rsquo;s the essence of the functionality our app will be providing to our users. Let&rsquo;s see how we can store the database file in Tigris.</p> <h2 id='store-the-tenant-database-file-in-tigris' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#store-the-tenant-database-file-in-tigris' aria-label='Anchor'></a><span class='plain-code'>Store the tenant database file in Tigris!</span></h2><div class="callout"><p>The code corresponding with this step is available at <a href="https://github.com/fly-apps/js-sqlite-in-tigris/commit/d8496f6" title="">d8496f6</a>.</p> </div> <p>If you played with Tigris in one of the previous sections, you probably know what we&rsquo;re going to do now: just grab the <code>db.sqlite3</code> file and store it in our bucket! Not only that, but we&rsquo;ll also parametrize the app so that it knows what tenant it is supposed to serve.</p> <p>To make the latter work, we make the app read the <code>CUSTOMER_ID</code> environment variable and parametrize the route path with it:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-paldp1vn" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-paldp1vn"><span class="c1">// server.js</span> <span class="kd">const</span> <span class="nx">customerId</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CUSTOMER_ID</span> <span class="o">||</span> <span class="mi">0</span> <span class="p">...</span> <span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`/customers/</span><span class="p">${</span><span class="nx">customerId</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// the counter logic</span> <span class="p">})</span> <span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`/customers/:customerId`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">403</span><span class="p">).</span><span class="nx">send</span><span class="p">();</span> <span class="p">})</span> </code></pre> </div> </div> <p>If we get a request to a <code>CUSTOMER_ID</code> that we&rsquo;re not serving, <code>403</code> will be returned.</p> <p>To talk to Tigris, we&rsquo;ll use the <a href='https://www.npmjs.com/package/@aws-sdk/client-s3' title=''>@aws-sdk/client-s3 library</a> - as with any other S3-compatible service. For this to work, we need to set up our S3 client to point at the Tigris endpoint:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-m3xxuw4y" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-m3xxuw4y"><span class="c1">// server.js</span> <span class="kd">const</span> <span class="nx">S3</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">S3Client</span><span class="p">({</span> <span class="na">region</span><span class="p">:</span> <span class="dl">"</span><span class="s2">auto</span><span class="dl">"</span><span class="p">,</span> <span class="na">endpoint</span><span class="p">:</span> <span class="s2">`https://fly.storage.tigris.dev`</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> </div> <p>We also need to read our bucket name and construct the object key under which we&rsquo;ll store a customer&rsquo;s database file:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-vsy4q0g6" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-vsy4q0g6"><span class="c1">// server.js</span> <span class="kd">const</span> <span class="nx">bucketName</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">BUCKET_NAME</span> <span class="kd">const</span> <span class="nx">databaseKey</span> <span class="o">=</span> <span class="s2">`/customer/</span><span class="p">${</span><span class="nx">customerId</span><span class="p">}</span><span class="s2">/db.sqlite3`</span><span class="p">;</span> </code></pre> </div> </div> <p>On the server startup, we retrieve the database file from the bucket, or we&rsquo;ll use the pre-created one:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-sii2ya05" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-sii2ya05"><span class="c1">// server.js</span> <span class="kd">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">sqlite3</span><span class="p">.</span><span class="nx">Database</span><span class="p">(</span><span class="nx">databasePath</span><span class="p">)</span> <span class="c1">// that file gets overwritten</span> <span class="p">...</span> <span class="kd">const</span> <span class="nx">checkDbInS3</span> <span class="o">=</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">Body</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">S3</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="k">new</span> <span class="nx">GetObjectCommand</span><span class="p">({</span> <span class="na">Bucket</span><span class="p">:</span> <span class="nx">bucketName</span><span class="p">,</span> <span class="na">Key</span><span class="p">:</span> <span class="nx">databaseKey</span> <span class="p">}));</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">writeFileSync</span><span class="p">(</span><span class="nx">databasePath</span><span class="p">,</span> <span class="k">await</span> <span class="nx">Body</span><span class="p">.</span><span class="nx">transformToByteArray</span><span class="p">());</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// error handling</span> <span class="p">}</span> <span class="p">};</span> <span class="p">...</span> <span class="nx">checkDbInS3</span><span class="p">();</span> </code></pre> </div> </div> <p>On the server shutdown, we&rsquo;ll be uploading the database file to the bucket:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-5exjg73y" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-5exjg73y"><span class="kd">const</span> <span class="nx">sendDbToS3</span> <span class="o">=</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">fileContent</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span><span class="p">(</span><span class="nx">databasePath</span><span class="p">);</span> <span class="k">await</span> <span class="nx">S3</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="k">new</span> <span class="nx">PutObjectCommand</span><span class="p">({</span> <span class="na">Bucket</span><span class="p">:</span> <span class="nx">bucketName</span><span class="p">,</span> <span class="na">Key</span><span class="p">:</span> <span class="nx">databaseKey</span><span class="p">,</span> <span class="na">Body</span><span class="p">:</span> <span class="nx">fileContent</span> <span class="p">}));</span> <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="p">}</span> <span class="p">};</span> <span class="p">...</span> <span class="nx">process</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">SIGINT</span><span class="dl">"</span><span class="p">,</span> <span class="nx">sendDbToS3</span><span class="p">);</span> <span class="nx">process</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">SIGTERM</span><span class="dl">"</span><span class="p">,</span> <span class="nx">sendDbToS3</span><span class="p">);</span> </code></pre> </div> </div> <p>And that&rsquo;s it for storing our tenant&rsquo;s database file in Tigris!</p> <div class="callout"><p>Little reminder: to be able to run the examples in this post, the environment variables mentioned in the <a href="#tigris-setup" title="">Tigris setup</a> section must be exported in the shell you run the examples in.</p> </div> <p>If you want to make your hands dirty, give it a try: <code>npm install &amp;&amp; CUSTOMER_ID=10 npm start run</code></p> <p>Jump to <a href="http://localhost:3000/customers/10">http://localhost:3000/customers/10</a> to see it in action. The server logs should be telling us we fetch/store data from/in Tigris:</p> <div class="highlight-wrapper group relative shell"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-gep391g0" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-gep391g0"><span class="o">&gt;</span> start <span class="o">&gt;</span> node server.js run Server is listening on port 3000, serving customer_id: 10 Successfully downloaded the db file from S3. <span class="o">[</span><span class="nv">customer_id</span><span class="o">=</span>0] Received request <span class="k">for </span>customer: 10 <span class="o">[</span><span class="nv">customer_id</span><span class="o">=</span>0] Received request <span class="k">for </span>customer: 10 <span class="o">[</span><span class="nv">customer_id</span><span class="o">=</span>0] Received request <span class="k">for </span>customer: 10 ^CSuccessfully sent the db file to S3. <span class="c"># NOTE that I'm sending Ctrl+C here</span> </code></pre> </div> </div> <p>You can always <code>fly storage dashboard $BUCKET_NAME</code> to see the bucket on the Tigris console.</p> <p>That&rsquo;s great! Try to run the app as a different customer, that should start counting from the beginning; remember to remove the <code>db.sqlite3</code> file: <code>rm db.sqlite3 &amp;&amp; CUSTOMER_ID=300 npm start run</code>.</p> <p>OK, you&rsquo;ve got pretty far already... ready for a <strong class='font-semibold text-navy-950'>big thing</strong>?</p> <h2 id='the-big-thing-1-deploy-to-fly-io' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-big-thing-1-deploy-to-fly-io' aria-label='Anchor'></a><span class='plain-code'>The big thing #1: deploy to Fly.io</span></h2> <p>Actually&hellip;I should have said #2 since the #1 big thing was to set up Tigris and use it! But I guess it was easy enough not to notice! But let&rsquo;s reflect on it: your SQLite database can now be served from servers close to your users since it will be automatically cached in the regions it will be accessed from.</p> <p>Anyhow, let&rsquo;s jump straight to the deployment part. If you want to follow the easy path, check out <a href='https://github.com/fly-apps/js-sqlite-in-tigris/commit/87ab970' title=''>commit 87ab970</a> and:</p> <ol> <li><p>Supply the app name in the <code>fly.toml</code>: <code>app = &#39;&lt;app-name&gt;&#39;</code></p> </li><li><p>Create the app at Fly.io: <code>fly apps create &lt;app-name&gt;</code> (an app is just a bag of resources on our platform)</p> </li><li><p>Make the secrets available in the app’s Machines (this assumes you have the $AWS_* variables exported as described in the <a href='#tigris-setup' title=''>Tigris setup</a> section):</p> <div class="highlight-wrapper group relative shell"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-uzdhocok" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-uzdhocok">fly secrets <span class="nb">set </span><span class="nv">AWS_ACCESS_KEY_ID</span><span class="o">=</span><span class="nv">$AWS_ACCESS_KEY_ID</span> fly secrets <span class="nb">set </span><span class="nv">AWS_SECRET_ACCESS_KEY</span><span class="o">=</span><span class="nv">$AWS_SECRET_ACCESS_KEY</span> </code></pre> </div> </div></li><li><p>Allocate an IP address for the app: <code>fly ips allocate-v4 --shared</code></p> </li><li><p>Run a machine for a customer (single tenant):</p> <div class="highlight-wrapper group relative shell"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-lto0zh4u" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-lto0zh4u">fly machine run <span class="nb">.</span> <span class="nt">--name</span> customer10 <span class="nt">--port</span> 443:3000/tcp:http:tls <span class="se">\</span> <span class="nt">--env</span> <span class="nv">CUSTOMER_ID</span><span class="o">=</span>10 <span class="se">\</span> <span class="nt">--env</span> <span class="nv">AWS_REGION</span><span class="o">=</span><span class="nv">$AWS_REGION</span> <span class="se">\</span> <span class="nt">--env</span> <span class="nv">AWS_ENDPOINT_URL_S3</span><span class="o">=</span><span class="nv">$AWS_ENDPOINT_URL_S3</span> <span class="se">\</span> <span class="nt">--env</span> <span class="nv">BUCKET_NAME</span><span class="o">=</span><span class="nv">$BUCKET_NAME</span> </code></pre> </div> </div></li><li><p>Fire up logs to see as things are happening: <code>fly logs</code></p> </li><li><p>Visit the <code>https://&lt;app-name&gt;.fly.dev/customers/10</code> and celebrate 🎉</p> </li><li><p>Kill the Machine with <code>fly machine destroy --force</code></p> </li></ol> <p>The slightly harder path involves generating the <code>Dockerfile</code> and <code>fly.toml</code> on your own. We&rsquo;ll use <code>fly launch</code> for that and customize them a little. To the point:</p> <ol> <li>Check out the repo at <a href='https://github.com/fly-apps/js-sqlite-in-tigris/commit/d8496f6' title=''>commit d8496f6</a> </li><li>Issue <code>fly launch --build-only --no-deploy --name &lt;app-name&gt;</code> and decline tweaking the settings </li><li>Clean up the generated files so that they reflect what&rsquo;s in the repo at this <a href='https://github.com/fly-apps/js-sqlite-in-tigris/tree/87ab970' title=''>commit</a> </li></ol> <p>Then follow the easy path 3-7 steps and&hellip; Wow! We&rsquo;re live now.</p> <div class='group relative min-w-0 bg-white shadow-md shadow-navy-500/10 rounded-xl mb-7 ring-1 ring-navy-300/40'><button type='button' class='bubble-wrap z-20 absolute right-2.5 top-2.5 text-transparent group-hover:text-navy-950 hocus:text-violet-600 bg-transparent group-hover:bg-white hocus:bg-violet-200/40 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none' data-wrap-target='#table-g8tyibjj' data-wrap-type='nowrap'><svg class='w-5 h-5 pointer-events-none' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><g buffered-rendering='static'><path d='M11.912 10.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.314 2.314 0 00-2.315-2.31H4.959M15.187 14.5H4.959M8.802 10H4.959' /><path d='M13.081 8.466l-1.548 1.571 1.548 1.571' /></g></svg><span class='bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950'>Wrap text</span></button><div class='min-w-0 overflow-x-auto rounded-xl'><table class='table-stripe table-stretch table-pad text-sm whitespace-nowrap m-0' id='table-g8tyibjj'><thead class='text-navy-950 text-left'><tr> <th style="text-align: center"><img alt="A counter increments on a page served by the deployed app." src="/javascript-journal/single-tenant-sqlite-in-tigris/assets/./single-tenant-sqlite-in-tigris-live.gif" /></th> </tr> </thead><tbody><tr> <td style="text-align: center">Refreshing the <code>https://&lt;app-name&gt;.fly.dev/customers/10</code></td> </tr> </tbody></table></div></div> <p>Couldn&rsquo;t we just go multi-tenant by spinning up multiple machines for our customers and call it a day? If you give it a try you&rsquo;ll quickly bump into a routing/port assigning mess - and this is the big thing #2 we&rsquo;ll tackle!</p> <h2 id='the-big-thing-2-multi-tenancy-with-fly-replay' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-big-thing-2-multi-tenancy-with-fly-replay' aria-label='Anchor'></a><span class='plain-code'>The big thing #2: multi-tenancy with <code>fly-replay</code></span></h2> <p>For this to work, we&rsquo;ll need:</p> <ul> <li><a href='https://fly.io/docs/networking/dynamic-request-routing/#' title=''>Dynamic Request Routing</a> - to route requests to the tenants&rsquo; machines, </li><li><a href='https://fly.io/docs/networking/private-networking/#fly-io-internal-addresses' title=''>Fly.io internal addressing</a> - to lookup a Machine ID we want to route a request to, </li><li><a href='https://fly.io/docs/machines/flyctl/fly-machine-run/#add-metadata-to-the-machine' title=''>Machine&rsquo;s metadata</a> - to mark a machine with its <code>CUSTOMER_ID</code> so we can easily discover them using our tenants&rsquo; IDs. </li></ul> <p>Wow, that was probably the most buzzword-intensive statement in this post, but bear with me - it will work, it will make sense, and life will be beautiful again ❤️. Anyhow, if you are curious, see our <a href='https://fly.io/blog/globally-distributed-postgres/' title=''>Globally Distributed Postgres</a> post to see how powerful are the features we&rsquo;re dealing with here.</p> <p>If there&rsquo;s more than one Machine exposing the same <a href='https://fly.io/docs/machines/flyctl/fly-machine-run/#define-a-fly-proxy-network-service' title=''>service</a>, the Fly Proxy will load balance requests to our app across all of them (this is how our proxy works by default). This is not what we want since a request for a given customer must reach its machine.</p> <p>The way to achieve this is to make each machine execute business logic and act as a router for our app and replay requests that are not destined for it. For example, if a machine for customer <code>100</code> gets a request for customer <code>200</code>, it will route the request to the customer&rsquo;s <code>200</code> machine. If there&rsquo;s no machine for a given customer, <code>404</code> will be returned. The following diagram depicts a request flow in which a Machine acts as a router:</p> <div class='group relative min-w-0 bg-white shadow-md shadow-navy-500/10 rounded-xl mb-7 ring-1 ring-navy-300/40'><button type='button' class='bubble-wrap z-20 absolute right-2.5 top-2.5 text-transparent group-hover:text-navy-950 hocus:text-violet-600 bg-transparent group-hover:bg-white hocus:bg-violet-200/40 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none' data-wrap-target='#table-bkboqkdo' data-wrap-type='nowrap'><svg class='w-5 h-5 pointer-events-none' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><g buffered-rendering='static'><path d='M11.912 10.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.314 2.314 0 00-2.315-2.31H4.959M15.187 14.5H4.959M8.802 10H4.959' /><path d='M13.081 8.466l-1.548 1.571 1.548 1.571' /></g></svg><span class='bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950'>Wrap text</span></button><div class='min-w-0 overflow-x-auto rounded-xl'><table class='table-stripe table-stretch table-pad text-sm whitespace-nowrap m-0' id='table-bkboqkdo'><thead class='text-navy-950 text-left'><tr> <th style="text-align: center"><img alt="A diagram explaining the request flow described above" src="/javascript-journal/single-tenant-sqlite-in-tigris/assets/./single-tenant-sqlite-in-tigris-diagram-2.png" /></th> </tr> </thead><tbody><tr> <td style="text-align: center">Tenant-1 user gets load-balanced to a Tenant-2 Machine (2), which replays the request to Tenant-1 (3-4) machine, where it gets served.</td> </tr> </tbody></table></div></div> <p>At the high level, we&rsquo;ll have 2 Express routes: one for the <code>CUSTOMER_ID</code> the Machine is running for and the other one for the routing:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-ym8ro7kr" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-ym8ro7kr"><span class="c1">// server.js</span> <span class="kd">const</span> <span class="nx">customerId</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CUSTOMER_ID</span> <span class="o">||</span> <span class="mi">0</span> <span class="p">...</span> <span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`/customers/</span><span class="p">${</span><span class="nx">customerId</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// business logic (the counter) for the CUSTOMER_ID this machine IS serving</span> <span class="p">});</span> <span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`/customers/:customerId`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// routing logic for CUSTOMER_IDs this machine IS NOT serving</span> <span class="kd">let</span> <span class="nx">machineId</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">get_machine_id</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">customerId</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">machineId</span><span class="p">)</span> <span class="p">{</span> <span class="nx">response</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">fly-replay</span><span class="dl">'</span><span class="p">,</span> <span class="s2">`instance=</span><span class="p">${</span><span class="nx">machineId</span><span class="p">}</span><span class="s2">`</span><span class="p">).</span><span class="nx">send</span><span class="p">();</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">404</span><span class="p">).</span><span class="nx">send</span><span class="p">(</span><span class="dl">"</span><span class="s2">Not found</span><span class="dl">"</span><span class="p">);</span> <span class="p">}</span> <span class="p">})</span> </code></pre> </div> </div> <p>If the <code>get_machine_id(request.params.customerId)</code> returns a valid <code>machineId</code>, we set the <code>fly-replay</code> HTTP header with that customer&rsquo;s <code>machineId</code> and send a response back. Because of the header, the proxy won&rsquo;t return the response to the caller but will replay the request to the given machine. That way, each tenant hits its instance.</p> <p>Now, how does the <code>get_machine_id(request.params.customerId)</code> work? It relies on each machine having a <code>CUSTOMER_ID</code> metadata key being set. That allows us to make a DNS lookup to fetch a customer&rsquo;s machine IPv6 via a special address: <code>dig aaaa +short &lt;CUSTOMER_ID&gt;.customer_id.kv._metadata.&lt;app-name&gt;.internal.</code> Having the IP address, we can do one more lookup (a reverse one) to fetch the machine ID that we need for the <code>fly-replay</code> header: <code>dig +short -x fdaa:6:32a:a7b:23c8:3d9b:fe49:2</code> (the last argument is an example IPv6 address of a Machine).</p> <p>The <code>get_machine_id(customerId)</code> function uses the <a href='https://www.npmjs.com/package/node-dig-dns' title=''>node-dig-dns</a> package to implement these queries in JS:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-8y8vdubm" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-8y8vdubm"><span class="c1">// server.js</span> <span class="kd">const</span> <span class="nx">appName</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">FLY_APP_NAME</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">get_machine_id</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">customerId</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// return a sample ID the app is not deployed on Fly.io</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">appName</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="dl">"</span><span class="s2">abcd1234</span><span class="dl">"</span> <span class="p">};</span> <span class="k">try</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">ip</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">dig</span><span class="p">([</span><span class="s2">`</span><span class="p">${</span><span class="nx">customerId</span><span class="p">}</span><span class="s2">.customer_id.kv._metadata.</span><span class="p">${</span><span class="nx">appName</span><span class="p">}</span><span class="s2">.internal`</span><span class="p">,</span> <span class="dl">'</span><span class="s1">aaaa</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">+short</span><span class="dl">'</span><span class="p">])</span> <span class="kd">const</span> <span class="nx">addr</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">dig</span><span class="p">([</span><span class="dl">'</span><span class="s1">+short</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">-x</span><span class="dl">'</span><span class="p">,</span> <span class="nx">ip</span><span class="p">]);</span> <span class="k">return</span> <span class="nx">addr</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">.</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// error handling</span> <span class="p">}</span> <span class="p">};</span> </code></pre> </div> </div><div class="callout"><p><code>node-dig-dns</code> requires the <code>dnsutils</code> OS package, thus we install it in the <a href="https://github.com/fly-apps/js-sqlite-in-tigris/blob/52bf41433db66677b213c42c612f40e2a0099f97/Dockerfile#L35-L36" title="">Dockerfile</a>.</p> </div> <p>You may have spotted the <code>addr.split(&#39;.&#39;)[0]</code>. This is to extract only the 1st segment of the returned string since the reverse lookup (the one with <code>-x</code>) returns an FQDN address, e.g.: <code>3d8dd15c1ed778.vm.jts.internal.</code>.</p> <p>Finally, how do we set that metadata? There are a few ways to do that, like with an API, but we will do that at the machine startup as <code>fly machine run ... --metadata customer_id=100</code>.</p> <p>Lastly, let&rsquo;s put the cherry on the cake and deploy the whole thing for 3 customers. Take it easy, check out the <a href='https://github.com/fly-apps/js-sqlite-in-tigris' title=''>main branch</a>, and providing you&rsquo;ve set up the <code>&lt;app-name&gt;</code>, secrets, and the IPv4 address as described in <a href='#the-big-thing-1-deploy-to-fly-io' title=''>The big thing #1</a>, then just execute the following:</p> <div class="highlight-wrapper group relative shell"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-mo7zyodg" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-mo7zyodg"><span class="k">for </span>i <span class="k">in</span> <span class="sb">`</span><span class="nb">seq </span>1 3<span class="sb">`</span><span class="p">;</span> <span class="k">do </span>fly machine run <span class="nb">.</span> <span class="nt">--name</span> customer<span class="k">${</span><span class="nv">i</span><span class="k">}</span>00 <span class="nt">--port</span> 443:3000/tcp:http:tls <span class="se">\</span> <span class="nt">--env</span> <span class="nv">CUSTOMER_ID</span><span class="o">=</span><span class="k">${</span><span class="nv">i</span><span class="k">}</span>00 <span class="se">\</span> <span class="nt">--env</span> <span class="nv">AWS_REGION</span><span class="o">=</span><span class="nv">$AWS_REGION</span> <span class="se">\</span> <span class="nt">--env</span> <span class="nv">AWS_ENDPOINT_URL_S3</span><span class="o">=</span><span class="nv">$AWS_ENDPOINT_URL_S3</span> <span class="se">\</span> <span class="nt">--env</span> <span class="nv">BUCKET_NAME</span><span class="o">=</span><span class="nv">$BUCKET_NAME</span> <span class="se">\</span> <span class="nt">--metadata</span> <span class="nv">customer_id</span><span class="o">=</span><span class="k">${</span><span class="nv">i</span><span class="k">}</span>00 <span class="k">done</span> </code></pre> </div> </div> <p>That will spin up Machines for customers with IDs <code>100</code>, <code>200</code> and <code>300</code> and make your app available at <code>https://&lt;app-name&gt;.fly.io/customer/&lt;CUSTOMER_ID&gt;</code>.</p> <p>That&rsquo;s is it! We&rsquo;ve made it - we have a demo multi-tenant application with exclusive computing with Machines and data storage with SQLite database hosted at Tigris.</p> <h2 id='conclusion' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#conclusion' aria-label='Anchor'></a><span class='plain-code'>Conclusion</span></h2> <p>There are a number of things that we could elaborate on here, but let me focus on just two things: automating tenant provisioning and scalability aspects of the app we&rsquo;ve built.</p> <p>I will start with the former because it&rsquo;s simpler: we have all the tooling to automate tenant provisioning and its lifecycle management. The <a href='https://fly.io/docs/machines/api/machines-resource/' title=''>Machines API</a> makes it possible to programmatically create and manage the lifecycle of the machines. Also, by tweaking the machines config appropriately, we can instruct the proxy on things like <a href='https://fly.io/docs/machines/flyctl/fly-machine-run/#set-fly-proxy-auto-start-and-auto-stop' title=''>automatic shutdown or wake-up</a>. That&rsquo;s probably a big topic for another post, so I will just stop here.</p> <p>When it comes to scalability, our approach is far from perfect since if we had more than one customer Machine (i.e., if we attempted to scale horizontally), we&rsquo;d end up mutating the SQLite database in parallel, which would result in overwrites. One possible solution to this problem would be to transactionally create a <code>machine.lock</code> file in a customer bucket containing the Machine&rsquo;s ID that performs the DB mutations at the current moment. That would possibly allow a Machine failing to acquire the lock to <code>fly-replay</code> the request to the one holding it. Once the &ldquo;mutator&rdquo; is done, the lock will be released by removing the file.</p> <p>Tigris allows for <a href='https://www.tigrisdata.com/docs/objects/conditionals/' title=''>Conditional Operations</a>, which would help ensure that only one request creates the lock file and all the other concurrent ones would fail.</p> <p>Happy multi-tenanting! </p> Building a Remix app locally with Docker https://fly.io/javascript-journal/building-remix-app-locally-with-docker/ 2024年04月15日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <div class="lead"><p>Fly.io makes it easy to deploy containerized apps in seconds. The more comfortable you are with Docker, the smoother and more reliable your deployments can be. Let’s learn how to leverage Docker when developing your app locally!</p> </div> <p>Docker can give developers a high degree of confidence that their applications will run exactly as expected in production. However, many people still run into failed deployments or unanticipated problems because when developing locally, the aren&rsquo;t using Docker. In this article, we&rsquo;re going to explore how to setup your Remix local environment to use Docker in development.</p> <p>In the end, we&rsquo;re going to be able to run our application locally as a Docker image that will <strong class='font-semibold text-navy-950'>live reload</strong> as we make changes.</p> <h2 id='create-a-new-remix-app' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#create-a-new-remix-app' aria-label='Anchor'></a><span class='plain-code'>Create a new Remix app</span></h2> <p>Let&rsquo;s start by instantiating a new Remix app with the default template:</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-esv2dksa" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-esv2dksa">npx create-remix@latest </code></pre> </div> </div> <p>As of right now, the default template for Remix uses Vite. In order the allow the Vite server to be exposed to Docker later on, we need to make a change to our <code>dev</code> script in our <code>package.json</code> by adding <code>--host</code> :</p> <div class="highlight-wrapper group relative json"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-wp062vlp" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-wp062vlp"><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"remix vite:dev --host"</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> </div> <p>If you happen to be using a version of Remix that does <em>not</em> use Vite, this step isn&rsquo;t necessary. With our Remix app initiated, let&rsquo;s try running it locally:</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-lahw4epg" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-lahw4epg">npm run dev </code></pre> </div> </div> <p>With this running, we can visit <code>localhost:5173</code> in our browser (or port <code>3000</code> if not using Vite) and see our sample app up and running:</p> <p><img src="/javascript-journal/building-remix-app-locally-with-docker/assets/welcome-to-remix-screenshot.png" /></p> <p>Hooray!</p> <p>Before we set up our local environment to use Docker, let&rsquo;s deploy our app as-is to Fly.io. This will set us up with the foundational Dockerfile that we&rsquo;ll rely on in configuring Docker Compose for local development.</p> <h2 id='deploy-to-fly-io' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#deploy-to-fly-io' aria-label='Anchor'></a><span class='plain-code'>Deploy to Fly.io</span></h2> <p>If this is your first time deploying to Fly.io, you&rsquo;ll want to start by downloading the CLI <a href='https://fly.io/docs/hands-on/install-flyctl/' title=''>flyctl</a>, as this is the primary method you&rsquo;ll use to interact with the platform.</p> <p>Once installed, all you need to do is run two commands:</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-3frqkt8w" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-3frqkt8w">fly launch </code></pre> </div> </div> <p>This auto-generates a <code>fly.toml</code> (required config for Fly Apps) and a Dockerfile. The generated Dockerfile is specific to Remix, so no need to change a thing. 😃</p> <p>Next, deploy you application to Fly.io:</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-mgt2g4lv" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-mgt2g4lv">fly deploy </code></pre> </div> </div> <p>Once completed, your app should be available at <code>https://&lt;YOUR-APP-NAME&gt;.fly.dev</code></p> <p>Now that we have our app running on Fly.io we can get to work setting up our local environment. To do so, we&rsquo;ll be relying on <strong class='font-semibold text-navy-950'>Docker Compose</strong>.</p> <h2 id='what-is-docker-compose-and-why-do-i-need-it' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-is-docker-compose-and-why-do-i-need-it' aria-label='Anchor'></a><span class='plain-code'>What is Docker Compose and why do I need it?</span></h2> <p>Working with Docker involves two steps:</p> <ul> <li><strong class='font-semibold text-navy-950'>Building</strong> the Docker image </li><li><strong class='font-semibold text-navy-950'>Running</strong> the Docker image in a container </li></ul> <p>These both require separate commands, often with a slew of extra parameters to set things like exposed ports, volumes, bind mounts, environment variables, and more. These commands can get quite long and tedious to remember.</p> <p><strong class='font-semibold text-navy-950'>Docker Compose</strong> is a way of defining params you&rsquo;d normally pass to your <code>build</code> and <code>run</code> commands into a single <code>docker-compose.yml</code> file. This way, all you need to do to get your app running in a Docker container is:</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-4o89793w" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-4o89793w">docker-compose up -d --build </code></pre> </div> </div> <p>Docker Compose is an invaluable tool for local development, as you&rsquo;ll soon see. Let&rsquo;s try using it with our Remix app.</p> <h2 id='docker-compose-for-local-development' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#docker-compose-for-local-development' aria-label='Anchor'></a><span class='plain-code'>Docker Compose for local development</span></h2> <p>First, we need to make a small change to our Dockerfile so our app runs as expected in development mode. Search for this line, which removes all <code>devDependencies</code>:</p> <div class="highlight-wrapper group relative docker"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-jxmrwlcl" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-jxmrwlcl"><span class="k">RUN </span>npm prune <span class="nt">--omit</span><span class="o">=</span>dev </code></pre> </div> </div> <p>Replace this with the following, so the command only runs when our app is in production:</p> <div class="highlight-wrapper group relative docker"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-yhvcuffl" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-yhvcuffl"><span class="k">RUN if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$NODE_ENV</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"production"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> <span class="se">\ </span> npm prune <span class="nt">--omit-dev</span><span class="p">;</span> <span class="se">\ </span> <span class="k">fi</span> </code></pre> </div> </div> <p>Next, create a file called <code>docker-compose.yml</code> at the root of your project:</p> <div class="highlight-wrapper group relative yaml"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-8z15sr78" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-8z15sr78"><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.8"</span> <span class="c1"># this the version of Docker Compose</span> <span class="na">services</span><span class="pi">:</span> <span class="na">app</span><span class="pi">:</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">./</span> <span class="na">command</span><span class="pi">:</span> <span class="s">npm run dev</span> <span class="na">environment</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">NODE_ENV=development</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">5173:5173'</span> </code></pre> </div> </div> <p>A number of these settings allow us to override things defined in our Dockerfile, things like environment variables and the command to start our application. In this case, we&rsquo;re overriding the <code>NODE_ENV</code> variable to <code>development</code>, as well as the start script to <code>npm run dev</code> . We can now run our app in a container with the command:</p> <div class="highlight-wrapper group relative cmd"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-vdg1irvh" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight cmd'><code id="code-vdg1irvh">docker-compose up -d --build </code></pre> </div> </div> <p>Once completed, you&rsquo;ll be able to access your app at <code>localhost:5173</code>. However, any changes that you make to your code won&rsquo;t be reflected unless you were to rebuild the image! That&rsquo;s no good for local development, so let&rsquo;s fix that.</p> <h2 id='making-local-changes-available' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#making-local-changes-available' aria-label='Anchor'></a><span class='plain-code'>Making local changes available</span></h2> <p>We&rsquo;ll be using a feature of Docker called <strong class='font-semibold text-navy-950'>bind mounts</strong> to allow our code changes to pass through to the container. To do this, we&rsquo;ll be setting the top-level <code>volumes</code> key in our Docker Compose settings like so:</p> <div class="highlight-wrapper group relative yaml"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-th3mpwmw" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-th3mpwmw"><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.8"</span> <span class="c1"># this the version of Docker Compose</span> <span class="na">services</span><span class="pi">:</span> <span class="na">app</span><span class="pi">:</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">./</span> <span class="na">command</span><span class="pi">:</span> <span class="s">npm run dev</span> <span class="na">environment</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">NODE_ENV=development</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">5173:5173'</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">${APP_DIR}:/app</span> <span class="pi">-</span> <span class="s">/app/node_modules</span> </code></pre> </div> </div> <p>This <code>volumes</code> key in Docker Compose configures both <strong class='font-semibold text-navy-950'>bind mounts</strong> and <strong class='font-semibold text-navy-950'>volumes.</strong></p> <p>Bind mounts follow the pattern <code>&lt;/local/path&gt;:&lt;/path/in/docker/image&gt;</code>, which tells Docker to use the files from the underlying filesystem <em>instead</em> of what&rsquo;s bundled in the image.</p> <p>You can think of bind mounts as a way to &ldquo;cut a hole&rdquo; in the Docker image to allow the underlying files on your computer to pass through.</p> <p>In our case, <code>${APP_DIR}:/app</code> allows us to make changes to our app code that will be reflected in our running container. You can set <code>APP_DIR</code> in a <code>.env</code> file, and it should be the <em>absolute path</em> to your application directory.</p> <h3 id='using-an-anonymous-volume-for-node_modules' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#using-an-anonymous-volume-for-node_modules' aria-label='Anchor'></a><span class='plain-code'>Using an anonymous volume for <code>node_modules</code></span></h3> <p>By using a bind mount, we&rsquo;re now letting our whole application codebase pass through to the container, and this includes <code>node_modules</code> , and that&rsquo;s a problem, but it might not be obvious.</p> <p>Some npm packages have <em>platform-specific</em> versions. For example, on macOS, <code>esbuild</code> will install the package <code>@esbuild/darwin-arm64</code>, but on Linux systems, <code>@esbuild/linux-arm64</code> will be required. For this reason, <strong class='font-semibold text-navy-950'>we should not rely on the underlying filesystem for <code>node_modules</code>.</strong></p> <p>So, how do we tell Docker to exclude <em>only</em> this folder from our bind mount? Enter <strong class='font-semibold text-navy-950'>anonymous volumes</strong>.</p> <p>Let&rsquo;s look again at this section of our <code>docker-compose.yml</code>:</p> <div class="highlight-wrapper group relative yaml"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-24i5pzl5" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-24i5pzl5"> <span class="na">volumes</span><span class="pi">:</span> <span class="c1"># bind mount 👇</span> <span class="pi">-</span> <span class="s">${APP_DIR}:/app</span> <span class="c1"># anonymous volume 👇</span> <span class="pi">-</span> <span class="s">/app/node_modules</span> </code></pre> </div> </div> <p>While bind mounts are controlled by the <em>host machine</em>, volumes are controlled by <em>Docker</em>. In both cases, a parallel folder exists in the host filesystem, but in the case of volumes, instead of the <em>host</em> dictating what goes in it, <em>Docker</em> controls what goes in.</p> <p>If we look in our Dockerfile, we&rsquo;ll see that it runs <code>npm ci --include-dev</code> (basically <code>npm install</code>), and those dependencies get placed inside the directory <code>/app/node_modules</code> in our image . By adding <code>- /app/node_modules</code> as a volume, we&rsquo;re letting Docker override the contents of our local <code>node_modules</code> folder in the image, thus avoiding the problem. In other words, we&rsquo;re saying &ldquo;Use the underlying host files for everything <em>except</em> <code>node_modules</code>.&rdquo;</p> <p>With our bind mount and volume set up, we can now run <code>docker-compose up -d --build</code>, and any changes to our code will be reflected in our container.</p> <h3 id='live-reload-changes-on-non-vite-remix-apps' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#live-reload-changes-on-non-vite-remix-apps' aria-label='Anchor'></a><span class='plain-code'>Live reload changes on non-Vite Remix apps</span></h3> <p>As stated earlier, the default templates for Remix now use Vite, which don&rsquo;t require any extra setup for live reloading to kick in. However, if <em>not</em> using Vite, and your application normally runs on port <code>3000</code>, you can add a second <code>port</code> entry to include <code>3001</code> like so:</p> <div class="highlight-wrapper group relative yaml"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-7it5107b" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-7it5107b"> <span class="c1"># ...</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">3000:3000'</span> <span class="pi">-</span> <span class="s1">'</span><span class="s">3001:3001'</span> <span class="c1">#...</span> </code></pre> </div> </div> <p>For non-Vite Remix apps, live reloading operates through a web socket connection on port <code>3001</code>. With this extra port exposed in our configuration, your changes will be available instantly in your container.</p> <h2 id='conclusion' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#conclusion' aria-label='Anchor'></a><span class='plain-code'>Conclusion</span></h2> <p>Leveraging Docker Compose for local development can give you the confidence that what you build locally will be reflected in production when you deploy your app. Docker gives you the most control over the environment in which your app runs, and Fly.io makes deploying containerized applications easy.</p> FLAME for JavaScript: Rethinking Serverless https://fly.io/javascript-journal/flame-for-javascript-rethinking-serverless/ 2024年02月26日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <div class="lead"><p>We’re Fly.io. We run apps for our users on hardware we host around the world. This post is about fast launching a machine to run a JavaScript function as an alternative to a Lambda service. It’s easy to <a href="/docs/js/" title="">get started</a>! with your JS app on Fly.io!</p> </div> <p>A couple of months ago, Chris McCord, the creator of the Phoenix framework, published an article about a new pattern called <a href='https://fly.io/blog/rethinking-serverless-with-flame/' title=''>FLAME as an alternative to serverless computing</a>. The original article focused on the implementation in Elixir, which has some unique clustering capabilities that make it exceptionally well-suited for the FLAME pattern, and this had some folks asking – what would FLAME look like in other languages?</p> <p>Today we will explore some possible approaches to FLAME in JavaScript. These solutions are experimental and, as you&rsquo;ll see, not without trade-offs, but let&rsquo;s see how far we get!</p> <h2 id='what-is-flame' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-is-flame' aria-label='Anchor'></a><span class='plain-code'>What is FLAME?</span></h2> <p>As a quick recap, FLAME stands for Fleeting Lambda Application for Modular Execution. It auto-scales tasks simply by <strong class='font-semibold text-navy-950'>wrapping any existing code in a function and having that block of code run in a temporary copy of the app.</strong></p> <p>In JavaScript, it might look something like this:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-limux8b6" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-limux8b6"><span class="kd">function</span> <span class="nx">doSomeWork</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="c1">// run `doSomeWork` on another machine</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">flame</span><span class="p">(</span><span class="nx">doSomeWork</span><span class="p">,</span> <span class="p">...)</span> </code></pre> </div> </div> <p>FLAME removes the <a href='https://fly.io/blog/rethinking-serverless-with-flame/#solving-a-problem-vs-removing-the-problem' title=''>FaaS labyrinth of complexity</a> <em>and</em> is cloud-provider agnostic. While the Elixir library that Chris wrote is designed specifically for spinning up Machines on Fly.io, FLAME is a pattern that can be used on any cloud that provides an API for spinning up instances of your application.</p> <p>For a more thorough rundown of FLAME, be sure to read Chris&rsquo;s full article here: <a href='https://fly.io/blog/rethinking-serverless-with-flame/' title=''>FLAME: Rethinking Serverless</a></p> <h2 id='what-are-machines-on-fly-io' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-are-machines-on-fly-io' aria-label='Anchor'></a><span class='plain-code'>What are Machines on Fly.io?</span></h2> <p>Fly Machines are the engine of the Fly.io platform: fast-launching VMs that can be started and stopped at subsecond speeds. Fly Machines themselves are not containers, but they do run your application code inside containers. You can read more about them <a href='https://fly.io/docs/machines/overview/' title=''>here</a>, but for this article, all you need to know is that Machines are just instances of your application code.</p> <h2 id='flame-in-vanilla-no-build-javascript' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#flame-in-vanilla-no-build-javascript' aria-label='Anchor'></a><span class='plain-code'>FLAME in vanilla, no-build JavaScript</span></h2> <p>As mentioned earlier, Elixir is uniquely suited to FLAME because of it&rsquo;s clustering abilities. The main question for implementing this pattern in other languages is this: <strong class='font-semibold text-navy-950'>how do you run specific code blocks on an entirely different instance of your app?</strong></p> <p>In this post, we&rsquo;re pulling back the curtain on the inner workings so you, as the developer, can feel more confident about using it in your app, but once you have a working FLAME library, you should just be able to drop it into your project and start making FLAME calls.</p> <h3 id='overview' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#overview' aria-label='Anchor'></a><span class='plain-code'>Overview</span></h3> <p><img alt="Diagram of how the FLAME pattern works in JavaScript, as described below" src="/javascript-journal/flame-for-javascript-rethinking-serverless/assets/./flame-for-js-diagram.png" /></p> <p>My teammate, Lubien, wrote a sample JavaScript app that implements FLAME a little differently than how it&rsquo;s done in Elixir. You can find the complete files at <a href='https://github.com/fly-apps/fly-run-this-function-on-another-machine' title=''>https://github.com/fly-apps/fly-run-this-function-on-another-machine</a></p> <p>So, how does it work?</p> <p>Whenever we want to run a task on another Machine, we pass the code as a parameter to our FLAME function, which punts it off to be run on the other Machine, which we will call our <code>runner</code> Machine.</p> <p>An important thing to understand is that this <strong class='font-semibold text-navy-950'>FLAME call is always executed twice</strong>, and it will return <strong class='font-semibold text-navy-950'>one of two functions</strong>:</p> <p>The <strong class='font-semibold text-navy-950'>first time</strong> it&rsquo;s run on our normal app instance and returns an <em>anonymous</em> function that...</p> <ol> <li>First checks for existing <code>runner</code> Machines and spawns one if none are found. <code>runner</code> Machines are copies of our application code, but replace our starting process with a simple HTTP server. </li><li>Then, it makes a <code>POST</code> request to that <code>runner</code> Machine, with two parameters: <ol> <li><code>filepath</code> – the absolute path to the file containing our FLAME-wrapped code </li><li><code>args</code> – any arguments needed for the code inside your FLAME function </li></ol> </li></ol> <p>The <strong class='font-semibold text-navy-950'>second time</strong> it&rsquo;s run will be on our <code>runner</code> Machine, and this time our <em>original</em> code block will be returned, to be executed with the <code>args</code> we passed in via our <code>POST</code> request.</p> <p>With that overview out of the way, let&rsquo;s dive into Lubien&rsquo;s code.</p> <h3 id='the-files' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-files' aria-label='Anchor'></a><span class='plain-code'>The Files</span></h3> <p>In Lubien&rsquo;s app, there are three files involved in pulling off our FLAME implementation:</p> <ul> <li><code>index.mjs</code> – This is where our application starts, and it calls the function containing our FLAME-wrapped code, <code>runMath</code> </li><li><code>runMath.mjs</code> – This is where we import <code>runMath</code> from, and exports the returned value of our FLAME function. Unlike the Elixir implementation, <strong class='font-semibold text-navy-950'>all FLAME-wrapped tasks need to be exported as the <code>default</code> module in a given file.</strong> </li><li><code>flame.mjs</code> – This is our FLAME library. This code is specific to spinning up Fly Machines, but again, this could just as easily be swapped for another cloud provider API. </li></ul> <p>Let&rsquo;s open up each file and see how the magic works. ✨</p> <p><strong class='font-semibold text-navy-950'><code>index.mjs</code></strong></p> <p>Lubien&rsquo;s FLAME app starts in <code>./index.mjs</code>, which runs a single function, <code>runMath</code>:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-ebgqlswm" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-ebgqlswm"><span class="c1">// index.mjs</span> <span class="k">import</span> <span class="nx">runMath</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./runMath.mjs</span><span class="dl">'</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">math</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">runMath</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">20</span><span class="p">)</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">math</span><span class="p">)</span> <span class="p">}</span> <span class="nx">main</span><span class="p">()</span> </code></pre> </div> </div> <p><strong class='font-semibold text-navy-950'><code>runMath.mjs</code></strong></p> <p>Inside of<code>./runMath.mjs</code> we find the following:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-juwvqoyd" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-juwvqoyd"><span class="c1">// runMath.mjs</span> <span class="k">import</span> <span class="nx">flame</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./flame.mjs</span><span class="dl">"</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">flame</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span> <span class="p">},</span> <span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span> <span class="na">guest</span><span class="p">:</span> <span class="p">{</span> <span class="na">cpu_kind</span><span class="p">:</span> <span class="dl">"</span><span class="s2">shared</span><span class="dl">"</span><span class="p">,</span> <span class="na">cpus</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="na">memory_mb</span><span class="p">:</span> <span class="mi">1024</span> <span class="p">}</span> <span class="p">})</span> </code></pre> </div> </div> <p>This file doesn&rsquo;t actually contain a function called <code>runMath</code>, but instead exports whatever <code>flame</code> returns. <strong class='font-semibold text-navy-950'><code>flame</code></strong> is roughly the equivalent of the Elixir <code>FLAME.call</code> method, and it returns a function.</p> <p><code>flame</code> accepts the following parameters:</p> <ul> <li><code>originalFunc</code> : The function you want to execute on another Machine; In this example, we&rsquo;ve passed it as an anonymous function. </li><li><code>config</code>: <ul> <li><code>path</code>: the path to the current file. This is what we&rsquo;ll pass to our <code>runner</code> server as the <code>filepath</code> </li><li><code>guest</code>: the specs for the Machine you want to spin up </li></ul> </li></ul> <p><strong class='font-semibold text-navy-950'><code>flame.mjs</code></strong></p> <p>Next, if we look inside <code>./flame.mjs</code> we find a much longer file. As stated before, this is our FLAME library. This does a number of things, including code that...</p> <ul> <li>Checks to see if there are any existing <code>runner</code> Machines </li><li>Spawns a new <code>runner</code> Machine if none are found </li><li>Boots up our simple HTTP server on our <code>runner</code> Machines </li><li>Schedules timeouts for our <code>runner</code> Machines to shut down </li><li>In a <code>POST</code> request, imports &amp; runs our FLAME code block </li></ul> <p>Rather than step through every line in this file, we&rsquo;re just going to focus on the <code>flame</code> function, but you can check out the commented documentation here: <a href='https://github.com/fly-apps/fly-run-this-function-on-another-machine/blob/main/flame.mjs' title=''>https://github.com/fly-apps/fly-run-this-function-on-another-machine/blob/main/flame.mjs</a></p> <p>Let&rsquo;s skip down to where <code>flame</code> is actually defined:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-50ut7r7x" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-50ut7r7x"><span class="c1">// ./flame.mjs</span> <span class="p">...</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">flame</span><span class="p">(</span><span class="nx">originalFunc</span><span class="p">,</span> <span class="nx">config</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">IS_RUNNER</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">originalFunc</span> <span class="p">}</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">meta</span><span class="p">,</span> <span class="nx">guest</span> <span class="o">=</span> <span class="p">{</span> <span class="na">cpu_kind</span><span class="p">:</span> <span class="dl">"</span><span class="s2">shared</span><span class="dl">"</span><span class="p">,</span> <span class="na">cpus</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">memory_mb</span><span class="p">:</span> <span class="mi">256</span> <span class="p">}</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">config</span> <span class="kd">const</span> <span class="nx">filename</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">fileURLToPath</span><span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">path</span><span class="p">);</span> <span class="k">return</span> <span class="k">async</span> <span class="kd">function</span> <span class="p">(...</span><span class="nx">args</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="k">await</span> <span class="nx">checkIfThereAreWorkers</span><span class="p">()))</span> <span class="p">{</span> <span class="k">await</span> <span class="nx">spawnAnotherMachine</span><span class="p">(</span><span class="nx">guest</span><span class="p">)</span> <span class="p">}</span> <span class="k">return</span> <span class="k">await</span> <span class="nx">execOnMachine</span><span class="p">(</span><span class="nx">filepath</span><span class="p">,</span> <span class="nx">args</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">...</span> </code></pre> </div> </div> <p>As mentioned before, our <code>flame</code> function is always called twice. Above we can see that it will either return our original function that we passed in, OR it will return an anonymous function that checks for <code>runner</code> Machines and sends a request to one.</p> <p>Further up the file, we define how to handle POST requests on our <code>runner</code> Machine. This is where we import the module from the file that contains our FLAME call. <strong class='font-semibold text-navy-950'>This is the second time</strong> <code>flame</code> is called, and this time it returns our original function, which gets executed with the <code>args</code> passed to it.</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-wey0ucnh" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-wey0ucnh"><span class="c1">// ./flame.mjs</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span><span class="nx">IS_RUNNER</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="c1">// get the request params `filepath` and `args`</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">filename</span><span class="p">,</span> <span class="nx">args</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">)</span> <span class="c1">// import the default module inside `filepath`</span> <span class="c1">// in this case, filepath = "./runMath.mjs"</span> <span class="kd">const</span> <span class="nx">mod</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="nx">filename</span><span class="p">)</span> <span class="c1">// run the default module (a function) and pass in `args`</span> <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">mod</span><span class="p">.</span><span class="k">default</span><span class="p">(...</span><span class="nx">args</span><span class="p">)</span> <span class="p">...</span> <span class="p">}</span> <span class="p">...</span> </code></pre> </div> </div> <p>And that&rsquo;s it! FLAME implemented in vanilla JavaScript.</p> <p>Understanding how this FLAME pattern works is very helpful, and once you have your FLAME library working for your preferred cloud provider (Fly.io or others), you can start abstracting any functions that you&rsquo;d like to auto-scale with a FLAME wrapper, like so:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-ahu7d7vb" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-ahu7d7vb"><span class="k">export</span> <span class="k">default</span> <span class="nx">flame</span><span class="p">(</span><span class="nx">doSomeWork</span><span class="p">,</span> <span class="p">...)</span> </code></pre> </div> </div><h2 id='limitations-of-flame-in-javascript' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#limitations-of-flame-in-javascript' aria-label='Anchor'></a><span class='plain-code'>Limitations of FLAME in JavaScript</span></h2> <p>The biggest limitation in the flow defined above comes down to the assumption that running our FLAME function is as simple as importing the file that contains it, and running the default module. This only works when you...</p> <ul> <li>Have your FLAME-wrapped code block as the <code>default</code> export of a file </li><li>Are not mixing CommonJS and ES Module syntax </li><li>Are NOT using TypeScript 😢 </li></ul> <p>It&rsquo;s especially tricky with any transpiled code like TypeScript since it means you can&rsquo;t simply import the file as is unless your HTTP server was using something like <code>ts-node</code> instead of <code>node</code>. For better performance, you could transpile all of your FLAME TypeScript code, but this would add extra files to your project that you may not want. If your application is using Bun or Deno, however, all these TypeScript problems are a non-issue, since those runtimes support it out of the box!</p> <h3 id='using-flame-in-a-javascript-framework' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#using-flame-in-a-javascript-framework' aria-label='Anchor'></a><span class='plain-code'>Using FLAME in a JavaScript framework</span></h3> <p>It&rsquo;s entirely possible to drop the FLAME library into any JavaScript framework, such as Next.js, and have it work, provided that the above-mentioned prerequisites are satisfied. However, you&rsquo;d have to ditch the TypeScript.</p> <p>An alternative pattern could be running an <em>exact</em> copy of your web application, instead of using your app image and overriding the start command. In other words, instead of using a slimmed-down HTTP server on your <code>runner</code> Machines, you could stick with your standard <code>npm run start</code> (or whatever your start command is). This would likely require some extra configuration in your framework to handle the FLAME <code>POST</code> requests, however, so it&rsquo;s not a perfect turn-key solution.</p> <h2 id='cold-starts-amp-pooling' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#cold-starts-amp-pooling' aria-label='Anchor'></a><span class='plain-code'>Cold starts &amp; pooling</span></h2> <p>While Fly Machine cold starts are extremely fast, it still takes a few hundred milliseconds, so it&rsquo;s still worth weighing the impact it has on performance. The code we&rsquo;ve shown today will boot up a new <code>runner</code> Machine if none are found, which does incur cold start time. Luckily, it&rsquo;s currently free (and soon, cheap) to have stopped Machines ready for near-instant start ups. For this reason, you may want to consider maintaining a pool of <code>runner</code> Machines that can quickly be started and stopped.</p> <h2 id='autoscaling-for-request-queuing' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#autoscaling-for-request-queuing' aria-label='Anchor'></a><span class='plain-code'>Autoscaling for request queuing</span></h2> <p>Now, even if we have a pool of <code>runner</code> Machines available at our beck and call, these could still become inundated with FLAME requests. A more production-ready implementation would need to have a way of scaling up <code>runner</code> Machines based on a given metric, such as response time.</p> <h2 id='conclusion' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#conclusion' aria-label='Anchor'></a><span class='plain-code'>Conclusion</span></h2> <p>Deploying on Fly.io makes this process even simpler by providing a convenient Machines API for scaling instances of your app. And while initial work of building a cloud-specific FLAME library requires a bit of work upfront, once done, you have an incredibly simple way of auto-scaling select tasks in your application.</p> Demystifying Docker for JavaScript https://fly.io/javascript-journal/demystify-docker-js/ 2023年12月12日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <p>Dockerfiles for JavaScript applications can range quite a bit – from two lines to fifty. What gives? This complexity can drive some developers away from really understanding this powerful tool, so today, I&rsquo;d like to demystify Docker by examining a sample Dockerfile for a JavaScript application. This should be a useful resource, regardless of what JS framework you work in.</p> <p>While this article will not be a comprehensive education on the subject, by the end of this article, you&rsquo;ll feel much more confident in your ability to write and modify any Dockerfile to suit the needs of your app.</p> <h2 id='a-quick-primer-on-docker-vocabulary' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-quick-primer-on-docker-vocabulary' aria-label='Anchor'></a><span class='plain-code'>A quick primer on Docker vocabulary</span></h2> <p>Some vocabulary:</p> <ul> <li><strong class='font-semibold text-navy-950'>Image:</strong> This contains your application code and everything needed to run it. For the most part, Docker images (or, more specifically, <a href='https://opencontainers.org/' title=''>Open Container Initiative (OCI)</a> images) are just a stack of filesystem layers. You can build a new image using <code>docker build</code> or with other tools like <a href='https://podman.io/' title=''>Podman</a>. To learn more, check out the section on OCI Images in our blog post <a href='https://fly.io/blog/docker-without-docker/#whats-an-oci-image' title=''>Docker without Docker</a>. </li><li><strong class='font-semibold text-navy-950'>Container:</strong> This is what actually runs your image and makes your app go zoom. You can run an existing Docker image using <code>docker run</code>. Setting up your container is typically where you would define things like environment variables, which ports to expose, which protocols to allow, etc. </li><li><strong class='font-semibold text-navy-950'>Instructions:</strong> These are the bits in ALLCAPS at the start of each line, followed by any number of parameters. You might hear people call them commands or statements, but they are officially called instructions. </li><li><strong class='font-semibold text-navy-950'>Layers:</strong> Almost every instruction line in a Dockerfile gets turned into a <em>layer</em>. These layers are the tarballs that make up the Docker image. Also, the order of these layers matters<em>,</em> especially for build optimization. </li></ul> <h2 id='make-your-life-easy-use-the-dockerfile-generator' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#make-your-life-easy-use-the-dockerfile-generator' aria-label='Anchor'></a><span class='plain-code'>Make your life easy: use the Dockerfile generator</span></h2> <p>I&rsquo;m going to cheat a little and tell you upfront that while having a stronger understanding of Dockerfile syntax is incredibly useful, <em>there is an easier way</em> (at least for JavaScript devs!), and that&rsquo;s using <a href='https://github.com/fly-apps/dockerfile-node' title=''>Fly.io&rsquo;s Dockerfile generator</a>.</p> <p>Using it is as simple as this:</p> <div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-r1at9vt0" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-r1at9vt0"><span class="nv">$ </span>npx @flydotio/dockerfile@latest <span class="nv">$ </span>npx dockerfile </code></pre> </div> </div> <p>The package can be used with both <code>npm</code> and <a href='https://bun.sh/' title=''><code>bun</code></a> (you can use <code>bunx</code> instead of <code>npx</code> to run the script with <code>bun</code>). There are extra parameters for tweaking the Dockerfile as needed; be sure to check out the README for more details.</p> <p><img alt="Screenshot of the main webpage for the Dockerfile generator package from npmjs.com" src="/javascript-journal/demystify-docker-js/assets/./dockerfile-package.png" /></p> <h2 id='the-anatomy-of-a-dockerfile' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-anatomy-of-a-dockerfile' aria-label='Anchor'></a><span class='plain-code'>The Anatomy of a Dockerfile</span></h2> <p>Today we&rsquo;ll be dissecting the Dockerfile for a <a href='https://nextjs.org/' title=''>Next.JS</a> app as it covers all of the different ways you can use Dockerfiles productively. This will help explain the different parts of a Dockerfile even if you aren&rsquo;t working on a Next.js app.</p> <p>Here&rsquo;s the Dockerfile we&rsquo;ll be stepping through.</p> <div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-km2l8dub" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-km2l8dub"><span class="c"># syntax = docker/dockerfile:1</span> <span class="c"># Adjust NODE_VERSION as desired</span> <span class="k">ARG</span><span class="s"> NODE_VERSION=20.9.0</span> <span class="k">FROM</span><span class="w"> </span><span class="s">node:${NODE_VERSION}-slim</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span> <span class="k">LABEL</span><span class="s"> fly_launch_runtime="Next.js"</span> <span class="c"># Next.js app lives here</span> <span class="k">WORKDIR</span><span class="s"> /app</span> <span class="c"># Set production environment</span> <span class="k">ENV</span><span class="s"> NODE_ENV="production"</span> <span class="c"># Throw-away build stage to reduce size of final image</span> <span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span> <span class="c"># Install packages needed to build node modules</span> <span class="k">RUN </span>apt-get update <span class="nt">-qq</span> <span class="o">&amp;&amp;</span> <span class="se">\ </span> apt-get <span class="nb">install</span> <span class="nt">-y</span> build-essential pkg-config python-is-python3 <span class="c"># Install node modules</span> <span class="k">COPY</span><span class="s"> --link package-lock.json package.json ./</span> <span class="k">RUN </span>npm ci <span class="nt">--include</span><span class="o">=</span>dev <span class="c"># Copy application code</span> <span class="k">COPY</span><span class="s"> --link . .</span> <span class="c"># Build application</span> <span class="k">RUN </span>npm run build <span class="c"># Remove development dependencies</span> <span class="k">RUN </span>npm prune <span class="nt">--omit</span><span class="o">=</span>dev <span class="c"># Final stage for app image</span> <span class="k">FROM</span><span class="s"> base</span> <span class="c"># Copy built application</span> <span class="k">COPY</span><span class="s"> --from=build /app /app</span> <span class="c"># Start the server by default, this can be overwritten at runtime</span> <span class="k">EXPOSE</span><span class="s"> 3000</span> <span class="k">CMD</span><span class="s"> [ "npm", "run", "start" ]</span> </code></pre> </div> </div> <p><strong class='font-semibold text-navy-950'>This might seem like a lot of steps.</strong> But there are reasons for every line, and by the end of this post, you&rsquo;ll understand what each one does and why it&rsquo;s used.</p> <p>Let&rsquo;s start by taking a high-level overview. Our Dockerfile can be broken into three stages:</p> <ol> <li>Set up our base image (spoilers: it&rsquo;s just Node) </li><li>Build the application </li><li>Add our app code to our original base image </li></ol> <p><img alt="A diagram highlighting the three stages of the dockerfile, the base stage where Node is set up, the build stage where dependencies are installed and the application is built, and the runner stage where the built code is made ready for production. The rest of the article text will explain what goes in these stages and why." src="/javascript-journal/demystify-docker-js/assets/./multistage-build-docker.png" /></p> <p>You&rsquo;ll notice that each of these stages starts with a <code>FROM</code> statement, which sets a base image. FROM statements are used to set a base image, and there are a few things to note about them:</p> <ul> <li>Every Dockerfile must contain at least <em>one</em> <code>FROM</code> instruction </li><li>It&rsquo;s possible to have more than one <code>FROM</code> instruction </li><li><strong class='font-semibold text-navy-950'>The last <code>FROM</code> instruction always wins</strong>* – And by wins, I mean used in the final image. Everything before it gets tossed out </li></ul> <p>The reason you might want to have more than one <code>FROM</code> statement is to optimize the size of your final image. This brings us to an important topic for Dockerfile construction: <strong class='font-semibold text-navy-950'>multi-stage builds.</strong></p> <p>*This behavior can be overridden when deploying your app with <code>fly deploy —build-target=&lt;specific target&gt;</code></p> <h2 id='multi-stage-builds-what-they-are-and-why-we-use-them' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#multi-stage-builds-what-they-are-and-why-we-use-them' aria-label='Anchor'></a><span class='plain-code'>Multi-stage builds: what they are and why we use them</span></h2> <p>Using multiple <code>FROM</code> statements in our Dockerfiles allows us to break up our build process into chunks and ultimately keep our final image as small as possible.</p> <p>To give an analogy, let&rsquo;s pretend you&rsquo;re making a batch of vegetable stock. It&rsquo;s dead simple: simmer a bunch of veggie scraps in water for an hour or so, and voila! You have stock. Now, as you might expect, you have to strain out the vegetables at the end; otherwise, you just made weird soup. But! Just because the end product doesn&rsquo;t contain any vegetables doesn&rsquo;t mean they weren&rsquo;t vital to the process.</p> <p><img alt="An illustration of the Docker whale and the JavaScript character making veggie stock together" src="/javascript-journal/demystify-docker-js/assets/./veggie-stock.webp" /></p> <p>That&rsquo;s the main advantage of multi-stage Docker builds. You borrow what you need from different images to do important work and use the result in your final image, tossing out the rest. The smaller your image, the faster your app starts.</p> <p>Now that we have a high-level overview of the overall flow of the Docker build, here&rsquo;s what happens at each stage.</p> <h2 id='stage-1-base' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#stage-1-base' aria-label='Anchor'></a><span class='plain-code'>Stage 1: <code>base</code></span></h2><h3 id='arg' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#arg' aria-label='Anchor'></a><span class='plain-code'>ARG</span></h3><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-gj5y96vh" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-gj5y96vh"><span class="k">ARG</span><span class="s"> NODE_VERSION=20.9.0</span> </code></pre> </div> </div> <p>We start our file by defining which version of Node (or Bun! Or anything else) we&rsquo;d like to use. Simple enough, but what exactly is that <code>ARG</code> command?</p> <p>In Dockerfiles, there are two ways of setting variables<em>:</em></p> <ol> <li><code>ARG</code> - These are used to set <strong class='font-semibold text-navy-950'>build time variables</strong>. As you might expect, these variables are available at build time (but NOT runtime) </li><li><code>ENV</code> - These variables become available to your app at both <em>build</em> and <em>runtime</em> </li></ol> <p><strong class='font-semibold text-navy-950'>Do not store sensitive data in your Dockerfile.</strong> They are safe to use for things like <code>NODE_VERSION</code> or <code>NODE_ENV</code>, but things like tokens, database URLs, or other secrets should be handled differently.</p> <p>For handling <strong class='font-semibold text-navy-950'>build-time <em>secrets</em></strong>, you&rsquo;ll want to <a href='https://fly.io/docs/reference/build-secrets/#mounting-secrets' title=''>mount them </a>using the <code>RUN</code> instruction. You can do this using the <code>@flydotio/dockerfile</code> package mentioned earlier, and then, when you deploy to Fly.io, set your build secret&rsquo;s value (second command):</p> <div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-gyrllbj7" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-gyrllbj7"><span class="nv">$ </span>npx dockerfile <span class="nt">--mount-secret</span><span class="o">=</span>MY_SECRET <span class="nv">$ </span>fly deploy <span class="nt">--build-secret</span> <span class="nv">MY_SECRET</span><span class="o">=</span>&lt;value&gt; </code></pre> </div> </div> <p>For handling <strong class='font-semibold text-navy-950'>runtime secrets</strong>, these should be kept <em>out</em> of your Dockerfile and instead set with the following command:</p> <div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-jvxbfrqz" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-jvxbfrqz"><span class="nv">$ </span>fly secrets <span class="nb">set </span><span class="nv">SECRET_PASSWORD</span><span class="o">=</span>&lt;value&gt; </code></pre> </div> </div> <p>These secrets are <a href='https://fly.io/docs/reference/secrets/#set-secrets' title=''>exposed as environment variables</a> when your application runs in production.</p> <hr> <h3 id='from' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#from' aria-label='Anchor'></a><span class='plain-code'>FROM</span></h3><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-alxogazo" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-alxogazo"><span class="k">FROM</span><span class="w"> </span><span class="s">node:${NODE_VERSION}-slim</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s">base</span> </code></pre> </div> </div> <p>The <code>FROM</code> instruction sets the <em>base image</em> on top of which all subsequent instructions are run. In our case, <code>node</code> is the <strong class='font-semibold text-navy-950'>name of the image</strong> and <code>:&lt;version&gt;-slim</code> is a <strong class='font-semibold text-navy-950'>tag</strong> that&rsquo;s used to denote a specific <em>version</em> of the base image.</p> <p>When it comes to tags, here&rsquo;s what the official documentation for our <code>node</code> image has to say:</p> <blockquote> <p>Some of these tags may have names like bookworm, bullseye, or buster in them. These are the suite code names for releases of <a href='https://wiki.debian.org/DebianReleases' title=''>Debian</a> and indicate which release the image is based on.</p> <p>SOURCE: <a href='https://hub.docker.com/_/node/' title=''>https://hub.docker.com/_/node/</a></p> </blockquote> <p><strong class='font-semibold text-navy-950'>How do I know what Debian release my app needs to use?</strong></p> <p>For most applications using Node or Bun, <strong class='font-semibold text-navy-950'>we suggest starting with the <code>-slim</code> variant.</strong> This takes the most recent version of Debian and strips out everything that you don&rsquo;t need. If you actually do need something that was stripped out, you can add it back later to your build stage using <code>apt-get</code> (something we&rsquo;ll cover in the build stage section).</p> <hr> <h3 id='label' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#label' aria-label='Anchor'></a><span class='plain-code'>LABEL</span></h3><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-6n24ifzs" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-6n24ifzs"><span class="k">LABEL</span><span class="s"> fly_launch_runtime="Next.js"</span> </code></pre> </div> </div> <p><code>LABEL</code> lets you set arbitrary key-value metadata for your Docker images and containers. It allows you to annotate whatever you want should that information be useful for your automation. This is a Fly.io specific label used by our frameworks team to track commonly used frameworks so we know what to prioritize when working on new features. It&rsquo;s technically optional, but it really helps us cater to the needs of your preferred framework! 😄</p> <hr> <h3 id='workdir' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#workdir' aria-label='Anchor'></a><span class='plain-code'>WORKDIR</span></h3><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-qxqda716" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-qxqda716"><span class="k">WORKDIR</span><span class="s"> /app</span> </code></pre> </div> </div> <p>The <code>WORKDIR</code> instruction sets the current working directory of any subsequent <code>RUN</code>, <code>COPY</code>, and <code>ADD</code> statements. This is where your application code will be built, and it is also the folder that gets deployed into production.</p> <h3 id='env' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#env' aria-label='Anchor'></a><span class='plain-code'>ENV</span></h3><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-1o573wyj" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-1o573wyj"><span class="k">ENV</span><span class="s"> NODE_ENV="production"</span> </code></pre> </div> </div> <p>The <code>ENV</code> instruction sets environment variables that are available during <em>both build time and runtime.</em></p> <p><strong class='font-semibold text-navy-950'>Remember</strong>, your Dockerfile is source code. Source code must never contain secret information. If you need to set secret information as an environment variable, use Fly.io secrets:</p> <div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-fcgo75gi" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-fcgo75gi"><span class="nv">$ </span>fly secrets <span class="nb">set </span><span class="nv">SECRET_KEY</span><span class="o">=</span>&lt;value&gt; </code></pre> </div> </div> <p>As stated previously in the section above about <code>ARG</code>s, the <code>ENV</code>` instruction is used to set environment variables that will be available during both the build <em>and</em> runtime. For setting sensitive data or secrets, please instead set your environment variables like so:</p> <div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-xkeywxni" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-xkeywxni"><span class="nv">$ </span>fly secrets <span class="nb">set </span><span class="nv">SECRET_KEY</span><span class="o">=</span>&lt;value&gt; </code></pre> </div> </div> <hr> <h2 id='stage-2-build' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#stage-2-build' aria-label='Anchor'></a><span class='plain-code'>Stage 2: <code>build</code></span></h2> <p>As we discussed earlier, every time you encounter a <code>FROM</code> statement, you know you&rsquo;ve reached a new stage in a Docker build. Let&rsquo;s take a look at our Dockerfile&rsquo;s second stage.</p> <h3 id='from-___-as-____' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#from-___-as-____' aria-label='Anchor'></a><span class='plain-code'>FROM ___ AS ____</span></h3><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-xclnd5a1" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-xclnd5a1"><span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span> </code></pre> </div> </div> <p>This <code>FROM...AS...</code> signifies the start of our second stage. If you remember, our first stage began with <code>node:&lt;version&gt;-slim</code> which we <em>named</em> <code>base</code>. Now in stage 2, we&rsquo;re making a copy of <code>base</code> and naming it <code>build</code>. From here on out, anything done to <code>build</code> <em>will not affect the original <code>base</code>.</em> You&rsquo;ll see later that this allows us to cherry-pick only the bits we want to keep from <code>build</code> and toss the rest.</p> <hr> <h3 id='run' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#run' aria-label='Anchor'></a><span class='plain-code'>RUN</span></h3><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-vpdot02i" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-vpdot02i"><span class="k">RUN </span>apt-get update <span class="nt">-qq</span> <span class="se">\ </span> <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> build-essential pkg-config python-is-python3 </code></pre> </div> </div> <p>Now that we&rsquo;ve established a copy of <code>base</code> as <code>build</code>, we&rsquo;re going to start doing some <em>actual</em> build work. First, let&rsquo;s understand what the <code>RUN</code> instruction is used for, and then we&rsquo;ll talk about this <code>apt-get</code> command.</p> <p>The RUN statement is used to run commands. Shocker, I know. But it&rsquo;s worth noting that it&rsquo;s not the <em>only</em> way to run a shell command. There are actually <em>three</em> common instructions used for such tasks:</p> <ol> <li><code>RUN</code><strong class='font-semibold text-navy-950'>:</strong> Always creates a new layer, thus it&rsquo;s best to chain these commands into a single instruction, just as we&rsquo;ve done above with multiple <code>apt-get</code> commands. Generally speaking, RUN is great for the <em>setup</em> of application code. </li><li><code>ENTRYPOINT</code><strong class='font-semibold text-navy-950'>:</strong> This sets the process that is first run inside your container. This is generally NOT your web server. The default entry point is <code>/bin/sh -c</code>, which starts up a shell process, but can be customized with the <code>ENTRYPOINT</code> instruction. Anything you set with the CMD instruction will then get passed as a parameter to that shell process (such as the command for starting your server). </li><li><code>CMD</code><strong class='font-semibold text-navy-950'>:</strong> This instruction sets the default command that&rsquo;s passed to your entrypoint. This is typically where you would write the command to start your web server, for example, such as <code>CMD [&quot;npm&quot;, &quot;run&quot;, &quot;start&quot;]</code>. </li></ol> <p><some diagram about docker images being onions, maybe make a Shrek reference in the diagram somehow, have fun with it, etc.></p> <p>You might be wondering, what&rsquo;s the difference between...</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-hytx2y7d" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-hytx2y7d">&lt;INSTRUCTION&gt; npm install </code></pre> </div> </div> <p>And using an array of strings, like so:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-3ju7jb6f" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-3ju7jb6f">&lt;INSTRUCTION&gt; ["npm", "install"] </code></pre> </div> </div> <p>The difference is that when you use a string argument, Docker will run your command inside a shell (more specifically, inside <code>/bin/sh -c</code>). If you use a string array, it&rsquo;ll run the program directly without wrapping it in a shell. In many cases, this doesn&rsquo;t matter, but sometimes it can matter in very uncommon use cases, such as when you don&rsquo;t have an OS in your container image or when every kilobyte of RAM matters.</p> <h3 id='what-is-apt-get-and-why-do-i-need-it' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-is-apt-get-and-why-do-i-need-it' aria-label='Anchor'></a><span class='plain-code'>What is apt-get, and why do I need it?</span></h3> <p>The tool <code>apt-get</code> is used for installing and managing Debian-based Linux packages (think NPM, but for OS packages; it&rsquo;s similar to <code>homebrew</code> on macOS). The packages we&rsquo;ve included (<code>build-essential</code>, <code>pkg-config</code>, and <code>python-is-python3</code>) are common requirements for many JavaScript packages. Experimentation may be required to find the exact set of dependencies your application requires, but even if you think your application won&rsquo;t need them, it&rsquo;s safe to keep this line for future development without worrying about bloat, as these won&rsquo;t be included in your final image.</p> <hr> <h3 id='copy' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#copy' aria-label='Anchor'></a><span class='plain-code'>COPY</span></h3><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-6evucn3b" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-6evucn3b"><span class="k">COPY</span><span class="s"> --link package-lock.json package.json ./</span> </code></pre> </div> </div> <p>After installing any Linux package requirements, we can finally start copying over parts of our application code. The <code>COPY</code> instruction copies a local file in your repository to a location on your Docker image.</p> <p><strong class='font-semibold text-navy-950'>What is <code>--link</code>?</strong></p> <p>Typically, if there are new changes to the layers preceding a <code>COPY</code>, that <code>COPY</code> statement will need to be re-run. Remember, for nearly every Docker instruction, a new <strong class='font-semibold text-navy-950'>layer</strong> is created, and layers are the tarballs that make up our Docker image. Because these layers need to be, well, <em>layered</em>, a diff is run to see if there are any changes to the previous layers that might affect the current one. If there <em>are</em> changes to earlier layers, it will invalidate the subsequent ones.</p> <p>However, by including <code>--link</code>, <strong class='font-semibold text-navy-950'>we create a new layer</strong> that does not get invalidated when changes to the preceding image are made, allowing us to cache our <code>--link</code> layers.</p> <p>To learn more, check out <a href='https://www.docker.com/blog/image-rebase-and-improved-remote-cache-support-in-new-buildkit/' title=''>this article</a>, which includes a super helpful infographic to illustrate the difference between <code>COPY</code> and <code>COPY --link</code>.</p> <hr> <p>The last lines of our <code>build</code> step should now feel more familiar. At this point, we&rsquo;re simply installing the rest of our dependencies, copying over our application code, and shucking out all of our <code>devDependencies</code> so our code is ready for production.</p> <div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-5to2atkt" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-5to2atkt"><span class="k">RUN </span>npm ci <span class="nt">--include</span><span class="o">=</span>dev <span class="c"># Copy application code</span> <span class="k">COPY</span><span class="s"> --link . .</span> <span class="c"># Build application</span> <span class="k">RUN </span>npm run build <span class="c"># Remove development dependencies</span> <span class="k">RUN </span>npm prune <span class="nt">--omit</span><span class="o">=</span>dev </code></pre> </div> </div> <p>Which now brings us to the final stage...</p> <h2 id='stage-3-base-build' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#stage-3-base-build' aria-label='Anchor'></a><span class='plain-code'>Stage 3: <code>base</code> + <code>build</code></span></h2><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-itwyour1" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-itwyour1"><span class="c"># Final stage for app image</span> <span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">runner</span> <span class="c"># Copy built application</span> <span class="k">COPY</span><span class="s"> --from=build /app /app</span> </code></pre> </div> </div> <p>We&rsquo;re in the home stretch! Since we&rsquo;ve reached the final <code>FROM</code> in our file, we know that <code>base</code> is going to be the target for our final image.</p> <h3 id='copy-from-lt-target-gt' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#copy-from-lt-target-gt' aria-label='Anchor'></a><span class='plain-code'>COPY <code>--from=&lt;target&gt;</code></span></h3> <p>You&rsquo;ll notice our <code>COPY</code> statement includes <code>--from=build</code>. <strong class='font-semibold text-navy-950'>This is the magic of using multi-stage builds.</strong> Here, we are cherry-picking the parts from our <code>build</code> target and leaving anything that we don&rsquo;t need for runtime behind. This keeps our final image as small as possible!</p> <hr> <h3 id='expose' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#expose' aria-label='Anchor'></a><span class='plain-code'>EXPOSE</span></h3><div class="highlight-wrapper group relative dockerfile"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-dzn9go7p" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-dzn9go7p"><span class="k">EXPOSE</span><span class="s"> 3000</span> <span class="k">CMD</span><span class="s"> [ "npm", "run", "start" ]</span> </code></pre> </div> </div> <p>We&rsquo;ve made it to the end of our Dockerfile! We talked briefly about <code>CMD</code> statements in the sections about <code>RUN</code> statements, and now we can see it in action. As mentioned previously, when starting a web service, <code>CMD</code> is typically the instruction used to specify our start process.</p> <p>Just before our start command, however, we specify the internal port that we want our Docker container to expose using the <code>EXPOSE</code> instruction. The key word here is <em>internal</em> – for web services, the specified port gets mapped to external port 80 or 443 for accepting HTTP(S) requests. </p> <p>A note when deploying to Fly.io: this internal port is also set in your <code>fly.toml</code>. Any port specified here will overwrite the port you set in your <code>EXPOSE</code> instruction:</p> <div class="highlight-wrapper group relative toml"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-ce7b25b1" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-ce7b25b1"><span class="nn">[http_service]</span> <span class="py">internal_port</span> <span class="p">=</span> <span class="mi">8080</span> </code></pre> </div> </div><h2 id='conclusion' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#conclusion' aria-label='Anchor'></a><span class='plain-code'>Conclusion</span></h2> <p>Hopefully, by now, you&rsquo;re starting to feel more comfortable with your understanding of Dockerfiles.</p> <p>Now, I did mention our Dockerfile generator node package at the beginning of the article, but there&rsquo;s an <strong class='font-semibold text-navy-950'>even easier method:</strong></p> <div class="highlight-wrapper group relative bash"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-kpa17fws" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-kpa17fws"><span class="nv">$ </span>fly launch </code></pre> </div> </div> <p>When deploying on Fly.io with <code>fly launch</code>, if a Dockerfile doesn&rsquo;t already exist, we generate one for you! This guide we walked through today should make understanding and tweaking the generated Dockerfile more approachable. If you get stuck, please do reach out to us on <a href='https://community.fly.io/' title=''>our community forum</a> so we can help.</p> <p>Congratulations, you&rsquo;ve now passed Docker on Fly.io 101. Go forth and build cool things!</p> Ciabatta with Garlic & Basil https://fly.io/javascript-journal/ciabatta/ 2023年09月18日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <p>Previously, <a href='../vanilla-candy-sprinkles' title=''>Vanilla with Candy Sprinkles</a> covered how you could select your own &ldquo;vanilla&rdquo; JS demo and sprinkle in as many options as you like. But that blog post focused on Node.js, and now that <a href='https://bun.sh/blog/bun-v1.0' title=''>Bun 1.0</a> has been released it is time for an update.</p> <p>We could have updated the previous article in place with a series of &ldquo;for Node.js do this, for Bun do that&rdquo; choices, but that would have made for a much more cumbersome experience. So instead we went with a mostly parallel article with the choices already applied.</p> <p>Overall the differences are:</p> <ul> <li>Bun has built in file system, web server, and websocket features that enable you to create meaningful demos with zero <code>require</code> or <code>import</code> statements. </li><li>Bun has built in support for sqlite3 that can be used without installing any additional modules </li><li>Typescript is supported without any build step needed. </li><li>Bun supports ES import statements by default, and while it also supports cjs require statements, it discourages their use. </li></ul> <p>The Bun demos that you can select show off these features. You are encouraged try both the Bun and node.js demos side-by-side and compare the source code. If you are impatient, you can find selected <a href='https://github.com/fly-apps/node-demo/tree/main/test/results' title=''>test results</a> online.</p> <p>Additionally, Bun aims to be faster, and these demos do seem to load faster with Bun. But beyond that, these demos aren&rsquo;t computationally intensive in a way that would show off performance improvements.</p> <p>Let&rsquo;s get started!</p> <h2 id='baseline-requirements' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#baseline-requirements' aria-label='Anchor'></a><span class='plain-code'>Baseline requirements</span></h2> <p>What we are looking for is a cross between <a href='https://en.wikipedia.org/wiki/%22Hello,_World!%22_program' title=''>Hello, World!</a> and <a href='https://rosettacode.org/wiki/Rosetta_Code' title=''>Rosetta Code</a>, but for a full stack application. For our purposes, the baseline is a stateful web server. Ideally one that can be deployed around the globe, and can deliver real time updates. But for now we will start small and before you know it we will have grown into the full application.</p> <p>A simple application that meets these requirements is one that shows a visitors counter. A counter that starts at one, and increments each time you refresh the page, return to the page, or even open the page in another tab, window, browser, or on another machine. It looks something like this:</p> <p><img alt="welcome counter" src="/javascript-journal/ciabatta/assets/welcome-counter.webp" /></p> <p>As <a href='https://fly.io/blog/flydotio-heart-js/#package-json-enters-the-chat' title=''>previously discussed</a>, key to deployment is a <code>package.json</code> file that lists all of your dependencies, optional build instructions, and how to start your application. We are going to start very simple, with no dependencies and no build process, so the <code>package.json</code> file will start out looking like the following:</p> <div class="highlight-wrapper group relative json"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-fqkmv2mv" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-fqkmv2mv"><span class="p">{</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"start"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bun server.js"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> </div> <p>Now to complete this we are going to need not only a <code>server.js</code> file, but also HTML, CSS, and image(s). As with some of the cooking shows you see on the television, we are going to skip ahead and pull a completed meal out of the oven. Run the following commands on a machine that has bun installed:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-assr8tuj" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-assr8tuj">mkdir demo cd demo bunx @flydotio/bun-demo@latest </code></pre> </div> </div> <p>Once this command completes, you can launch the application with <code>bun start</code>. If you have authenticated and have <code>flyctl</code> installed, you can launch this application with <code>fly launch</code> followed by <code>fly deploy</code>. Should you decide to run <code>fly launch</code>, consider saying <em>yes</em> to deploying a postgres and redis database as we will be using them later.</p> <figure class="post-cta"> <figcaption> <h1>You can play with this right now.</h1> <p>Don&rsquo;t have Bun installed or a fly.io login? Deploy using <a href='https://fly.io/terminal'>Fly.io terminal</a> or see our <a href='https://fly.io/docs/hands-on/'>Hands-on</a>) guide that will walk you through the steps.</p> <a class="btn btn-lg" href="https://fly.io/docs/about/pricing/#free-allowances"> Try Fly for free <span class='opacity-50'>→</span> </a> </figcaption> <div class="image-container"> <img src="/static/images/cta-turtle.webp" srcset="/static/images/cta-turtle@2x.webp 2x" alt=""> </div> </figure> <p>If you are running it locally, open <code>http://localhost:3000/</code> in your browser. If you have deployed it on fly.io, try <code>fly open</code>. If you are running in a fly.io terminal, there is a handy link you can use on the left hand pane.</p> <p>Now take a look at <code>server.js</code>. It is all of 72 lines, including blank lines and comments. In subsequent sections we show how to make it smaller using available libraries, and how to add features. But before we proceed, lets save time and keystrokes by installing the node-demo package, which we will use repeatedly to generate variations on this application:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-6yzav76s" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-6yzav76s">bun add @flydotio/bun-demo --dev </code></pre> </div> </div><h2 id='using-a-real-template' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#using-a-real-template' aria-label='Anchor'></a><span class='plain-code'>Using a real template</span></h2> <p>Inside the application you can see that the HTML response is produced by reading a template file and replacing a placeholder string with the current count:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-9rqv36di" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-9rqv36di"><span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">@@COUNT@@</span><span class="dl">'</span><span class="p">,</span> <span class="nx">count</span><span class="p">.</span><span class="nx">toString</span><span class="p">())</span> </code></pre> </div> </div> <p>While this is fine for this example, larger projects would be better served with a real template. <code>bun-demo</code> supports two such templating engines at the moment: <a href='https://ejs.co/' title=''>ejs</a> and <a href='https://mustache.github.io/' title=''>mustache</a>. Select your favorite, or switch back and forth:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-kjubbv5a" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-kjubbv5a">bunx bun-demo --ejs </code></pre> </div> </div> <p>and</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-mjgi000z" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-mjgi000z">bunx bun-demo --mustache </code></pre> </div> </div> <p>In either case, this script will detect what changes need to be made, give you the option to show a diff of the changes, and to accept or reject the changes. This leads us to the second option: <code>--force</code> that will automatically apply the changes without prompting:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-7b4fce88" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-7b4fce88">bunx bun-demo --ejs --force </code></pre> </div> </div> <p>Relaunch your application locally using <code>bun start</code> or redeploy it remotely using <code>fly deploy</code>.</p> <h2 id='a-more-substantial-change' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-more-substantial-change' aria-label='Anchor'></a><span class='plain-code'>A more substantial change</span></h2> <p>While Bun.server provides the means for you to create a capable HTTP server, <a href='https://expressjs.com/' title=''>express</a> makes it easy to incrementally add routes, can be configured to automatically serve static files, and integration with template engines. To convert the demo to use express, run:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-5efknhf3" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-5efknhf3">bunx bun-demo --express </code></pre> </div> </div> <p>Both ejs and mustache have integrations with express. Try switching between the two to see how they differ.</p> <h2 id='a-real-database' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-real-database' aria-label='Anchor'></a><span class='plain-code'>A real database</span></h2> <p>Maintaining a counter in a text file is good enough for a demo, but not suitable for production. Sqlite3 and PostgreSQL are better alternatives:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-937zlban" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-937zlban">bunx bun-demo --sqlite3 </code></pre> </div> </div> <p>and</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-xgygef91" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-xgygef91">bunx bun-demo --postgresql </code></pre> </div> </div> <p>Sqlite3 is great for development, and when used with <a href='https://fly.io/docs/litefs/' title=''>litefs</a> is great for deployment. PostgreSQL can be used in development, and currently is the best choice for production.</p> <p>To run with PostgreSQL locally, you need to install and start the server and create a database. For MacOS:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-e86tis0v" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-e86tis0v">brew install postgresql brew services start postgresql psql -U postgres -c "drop database if exists $USER;" psql -U postgres -c "create database $USER;" export DATABASE_URL=postgresql://$USER:$USER@localhost:5432/$USER </code></pre> </div> </div><h2 id='be-as-weird-as-you-want-to-be' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#be-as-weird-as-you-want-to-be' aria-label='Anchor'></a><span class='plain-code'>Be as weird as you want to be</span></h2> <p>The JavaScript ecosystem can be <a href='../js-ecosystem-delightfully-wierd' title=''>delightfully weird</a>.</p> <p>The next two options are frankly polarizing. People either love them or hate them. We won&rsquo;t judge you.</p> <p>First <a href='https://tailwindcss.com/' title=''>tailwindcss</a> is a CSS builder that works based on parsing your class attributes in your HTML:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-2rkze8m2" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-2rkze8m2">bunx bun-demo --tailwindcss </code></pre> </div> </div> <p>Next is <a href='https://www.typescriptlang.org/' title=''>typescript</a> which adds type annotations:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-icvu1twd" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-icvu1twd">bunx bun-demo --typescript </code></pre> </div> </div> <p>TypeScript should work with all of the options on this page, in many cases making use of development only <a href='https://www.npmjs.com/search?q=%40types' title=''>@types</a>. All of this should be handled automatically by node-demo.</p> <p>Tailwind requires a build step, which can be run via <code>bun run build</code>. A change to the Dockerfile used to deploy is also required, which can be made using:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-lefbg3xr" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-lefbg3xr">bunx dockerfile </code></pre> </div> </div> <p><a href='https://www.npmjs.com/package/@flydotio/dockerfile' title=''>@flydotio/dockerfile</a> is actually a separate project with its own options for you to explore.</p> <h3 id='object-relational-mappers-orms' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#object-relational-mappers-orms' aria-label='Anchor'></a><span class='plain-code'>Object Relational Mappers (ORMs)</span></h3> <p>Adding databases was the first change that we&rsquo;ve seen that actually makes the demo application noticeably larger, particularly with PostgreSQL once the code that handles reconnecting to the database after network failures is included. This can be handled by including still more libraries, this time Object Relational Managers (ORMs). Three popular ones:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-tjt4lqzi" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-tjt4lqzi">bunx bun-demo --drizzle </code></pre> </div> </div> <p>and</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-6orxqrc" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-6orxqrc">bunx bun-demo --knex </code></pre> </div> </div> <p>and</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-ri6cp5jq" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-ri6cp5jq">bunx bun-demo --prisma </code></pre> </div> </div> <p>Knex runs just fine with vanilla JavaScript. Prisma can run with vanilla JavaScript, but works better with TypeScript. Drizzle requires TypeScript.</p> <p>Prisma and Drizzle also require a build step.</p> <p>A final note: if you switch back and forth between Sqlite3 and PostgreSQL, you may get into a state where the migrations generated are for the wrong database. Simply delete the <code>prisma</code> or <code>src/db/migrations</code> directory and rerun the <code>bunx bun-demo</code> command to regenerate the migrations.</p> <h2 id='real-time-updates' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#real-time-updates' aria-label='Anchor'></a><span class='plain-code'>Real Time Updates</span></h2> <p>If you open more than one browser window or tab, each will show a different number. This can be addressed by introducing websockets:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-k2aslhv2" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-k2aslhv2">bunx bun-demo --websocket </code></pre> </div> </div> <p>The server side of web sockets will be different based on whether or not you are using express. For the first time we are providing a client side script which is responsible for establishing (and reestablishing) the connection, and updating the <a href='https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction' title=''>DOM</a> when messages are received. This is a chore, and <a href='https://htmx.org/' title=''>htmx</a> is one of the many libraries that can be used to handle this chore:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-s1qf4wfh" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-s1qf4wfh">bunx bun-demo --htmx </code></pre> </div> </div> <p>The next problem is that if you are running multiple servers, each will manage their own pool of WebSockets so that only clients in the same pool will be notified of updates. This can be addressed by using redis:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-hzkpcziz" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-hzkpcziz">bunx bun-demo --redis </code></pre> </div> </div> <p>At this point, if you are using fly.io, postgres, and redis, you can go global:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-bf7cez1v" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-bf7cez1v">fly scale count 8 --region ams,syd,nrt,dfw </code></pre> </div> </div><h2 id='future-explorations' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#future-explorations' aria-label='Anchor'></a><span class='plain-code'>Future explorations</span></h2> <p>While we have explored alternative implementation of the previous Node demo, this only scratches the surface. Bun includes much more:</p> <ul> <li>Bun has a <a href='https://bun.sh/docs/bundler' title=''>native bundler</a> built in that can be sued to package up client side scripts. </li><li>Bun has built in <a href='https://react.dev/' title=''>React</a>/JSX support. This can be run both server side and client side. </li><li>Bun has a built in <a href='https://bun.sh/docs/cli/test' title=''>Jest-compatible test runner</a> </li></ul> <p><a href='https://github.com/fly-apps/node-demo' title=''>node-demo</a> is open source, and contains both the Node.js and Bun demos. <a href='https://github.com/fly-apps/node-demo/issues' title=''>issues</a>, <a href='https://github.com/fly-apps/node-demo/pulls' title=''>pull requests</a>, and <a href='https://github.com/fly-apps/node-demo/discussions' title=''>discussions</a> are always welcome!</p> <p>I hope you have found this blog post to be informative, and perhaps some of you will use this information to start your next application &ldquo;vanilla&rdquo; with your personal selection of toppings. Yummy!</p> Fly.io ❤️ Bun https://fly.io/javascript-journal/flydotio-heart-bun/ 2023年07月11日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <p><a href='https://lu.ma/cqk31rvl' title=''>Bun 1.0 comes out September 7th</a>. Fly.io is making preparations.</p> <p>Previously, we stated that <a href='https://fly.io/blog/flydotio-heart-js/' title=''>Fly.io ❤️ JS</a>, and we understandably started with Node.js. While that work is ongoing, it makes sense to start expanding to other runtimes.</p> <p>Bun is the obvious next choice given it <a href='https://bun.sh/docs/runtime/nodejs-apis' title=''>aims for complete Node.js API compatibility</a>.</p> <p>Starting with <a href='https://fly.io/docs/hands-on/install-flyctl/' title=''>flyctl</a> version 0.1.54 and <a href='https://www.npmjs.com/package/@flydotio/dockerfile' title=''>@flydotio/dockerfile</a> version 0.3.3, you can launch and deploy bun applications using <code>fly launch</code> and <code>fly deploy</code>, provided:</p> <ul> <li>You&rsquo;ve installed bun version 0.5.3 or later </li><li>You have a <code>package.json</code> that meets at least one of the following conditions: <ul> <li>It has a <code>start</code> entry in the <code>scripts</code> section. </li><li>It has a <code>module</code> entry and specified <code>module</code> as the <code>type</code>. </li><li>If has a <code>main</code> entry. </li></ul> </li></ul> <p>Basically, if you can run <a href='https://bun.sh/docs/quickstart' title=''>Bun&rsquo;s Quickstart</a> and <a href='https://fly.io/docs/hands-on/' title=''>Fly&rsquo;s hands-on walk-through</a>, you have all you need to deploy your application on fly.io.</p> <p>We also have a <a href='https://github.com/fly-apps/bun/' title=''>sample</a> that you can deploy.</p> <p>Be forewarned that everything is beta at this point. Some issues we encountered while preparing this support:</p> <ul> <li><a href='https://github.com/oven-sh/bun/issues/3605' title=''><code>bun install</code> has no <code>--prune</code> option</a>. Our Dockerfiles use this to remove development dependencies after running <code>build</code>. Of course with bun you are less likely to need a build step as TS and JSX are built in. </li><li><a href='https://github.com/oven-sh/bun/issues/1579' title=''><code>throwIfNoEntry</code> is not supported in <code>fs.statSync</code></a>. <a href='https://github.com/fly-apps/node-demo' title=''><code>fly-apps/node-demo</code></a> uses that. </li><li>Programs that used <a href='https://nodejs.org/api/readline.html' title=''>readline</a> <a href='https://github.com/oven-sh/bun/issues/3604' title=''>never exit</a>. Switching to <a href='https://bun.sh/docs/api/globals' title=''>global</a>.<a href='https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt' title=''>prompt</a> resolved this issue for <code>@flydotio/dockerfile</code>. </li></ul> <p>Undoubtedly there will be bugs in fly&rsquo;s dockerfile generator too. But as Node.js and Bun share the same generator, fixes that are made for either framework will generally benefit both.</p> <p>If you see a problem, <a href='https://community.fly.io/' title=''>start a discussion</a>, <a href='https://github.com/fly-apps/dockerfile-node' title=''>open an issue</a>, or <a href='https://github.com/fly-apps/dockerfile-node/pulls' title=''>create a pull request</a>.</p> Vanilla with Candy Sprinkles https://fly.io/javascript-journal/vanilla-candy-sprinkles/ 2023年05月18日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <p>Recapping where we are to date:</p> <ul> <li>There are <a href='https://fly.io/blog/flydotio-heart-js/' title=''>plenty of JavaScript frameworks to choose from</a>, and fly.io loves them all. </li><li>Pretty much <strong class='font-semibold text-navy-950'>all</strong> of the big name frameworks are <a href='https://fly.io/blog/js-ecosystem-delightfully-wierd/' title=''>delightfully weird</a>. </li></ul> <p>Picking up where we left off, this blog post will describe literally dozens (and that&rsquo;s actually an understatement as you will soon see) of considerably more, dare I say it, <a href='http://vanilla-js.com/' title=''>vanilla</a> frameworks that you can assemble on your own and deploy to fly.io and elsewhere.</p> <p>This can be overwhelming, so to make things easier we are going to define a baseline application that will be reimplemented to take advantage of various tools. The result will be:</p> <ul> <li>Educational. Seeing a bite sized working example is a great way to learn how a tool works. </li><li>Useful starting point. Whereas large frameworks make a number of choices for you, being able to selectively include only the tools you need can provide you with a preconfigured configuration to build upon. </li><li>Debugging aid. When a large system doesn&rsquo;t behave the way you want it to, being able to reproduce and debug the problems on a smaller base not only can help you quickly narrow down the problem, and also can be used as a test case for a bug report. </li></ul> <p>Let&rsquo;s get started!</p> <h2 id='baseline-requirements' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#baseline-requirements' aria-label='Anchor'></a><span class='plain-code'>Baseline requirements</span></h2> <p>What we are looking for is a cross between <a href='https://en.wikipedia.org/wiki/%22Hello,_World!%22_program' title=''>Hello, World!</a> and <a href='https://rosettacode.org/wiki/Rosetta_Code' title=''>Rosetta Code</a>, but for a full stack application. For our purposes, the baseline is a stateful web server. Ideally one that can be deployed around the globe, and can deliver real time updates. But for now we will start small and before you know it we will have grown into the full application.</p> <p>A simple application that meets these requirements is one that shows a visitors counter. A counter that starts at one, and increments each time you refresh the page, return to the page, or even open the page in another tab, window, browser, or on another machine. It looks something like this:</p> <p><img alt="welcome counter" src="/javascript-journal/vanilla-candy-sprinkles/assets/welcome-counter.webp" /></p> <p>As <a href='https://fly.io/blog/flydotio-heart-js/#package-json-enters-the-chat' title=''>previously discussed</a>, key to deployment is a <code>package.json</code> file that lists all of your dependencies, optional build instructions, and how to start your application. We are going to start very simple, with no dependencies and no build process, so the <code>package.json</code> file will start out looking like the following:</p> <div class="highlight-wrapper group relative json"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-uvr63zcz" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-uvr63zcz"><span class="p">{</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"start"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node server.js"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> </div> <p>Now to complete this we are going to need not only a <code>server.js</code> file, but also HTML, CSS, and image(s). As with some of the cooking shows you see on the television, we are going to skip ahead and pull a completed meal out of the oven. Run the following commands on a machine that has node.js &gt;= 16 installed:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-3lqpdiwg" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-3lqpdiwg">mkdir demo cd demo npx --yes @flydotio/node-demo@latest </code></pre> </div> </div> <p>Once this command completes, you can launch the application with <code>npm run start</code>. If you have authenticated and have flyctl version 0.1.6 or later installed, you can launch this application with <code>fly launch</code> followed by <code>fly deploy</code>. When you run <code>fly launch</code>, consider saying <em>yes</em> to deploying a postgres and redis database as we will be using them later.</p> <figure class="post-cta"> <figcaption> <h1>You can play with this right now.</h1> <p>Don&rsquo;t have node installed or a fly.io login? Deploy using [Fly.io terminal](https://fly.io/terminal) or see our [Hands-on](https://fly.io/docs/hands-on/) guide that will walk you through the steps.</p> <a class="btn btn-lg" href="https://fly.io/docs/about/pricing/#free-allowances"> Try Fly for free <span class='opacity-50'>→</span> </a> </figcaption> <div class="image-container"> <img src="/static/images/cta-rabbit.webp" srcset="/static/images/cta-rabbit@2x.webp 2x" alt=""> </div> </figure> <p>If you are running it locally, open <code>http://localhost:3000/</code> in your browser. If you have deployed it on fly.io, try <code>fly open</code>. If you are running in a fly.io terminal, there is a handy link you can use on the left hand pane.</p> <p>Now take a look at <code>server.js</code>. It is all of 72 lines, including blank lines and comments. In subsequent sections we show how to make it smaller using available libraries, and how to add features. But before we proceed, lets save time and keystrokes by installing the node-demo package, which we will use repeatedly to generate variations on this application:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-d8ya3qd7" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-d8ya3qd7">npm install @flydotio/node-demo --save-dev </code></pre> </div> </div><h2 id='starting-small' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#starting-small' aria-label='Anchor'></a><span class='plain-code'>Starting small</span></h2> <p>If you look at the top of the <code>server.js</code> file you will see a number of calls to <code>require()</code>. This is Nodes <a href='https://nodejs.org/api/modules.html#modules-commonjs-modules' title=''>CommonJS</a> modules. Node also supports <a href='https://nodejs.org/api/esm.html#modules-ecmascript-modules' title=''>EMCAScript</a> modules, which is what all the cool kids are using these days.</p> <p>This requires opting in. You can let <code>node-demo</code> make the changes for you by running the following command:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-ns9rsf5l" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-ns9rsf5l">npx node-demo --esm </code></pre> </div> </div> <p>This script will detect what changes need to be made, give you the option to show a diff of the changes, and to accept or reject the changes. This leads us to the second option: <code>--force</code> that will automatically apply the changes without prompting:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-l8hfbf70" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-l8hfbf70">npx node-demo --esm --force </code></pre> </div> </div> <p>Relaunch your application locally using <code>npm run start</code> or redeploy it remotely using <code>fly deploy</code>.</p> <h2 id='using-a-real-template' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#using-a-real-template' aria-label='Anchor'></a><span class='plain-code'>Using a real template</span></h2> <p>Inside the application you can see that the HTML response is produced by reading a template file and replacing a placeholder string with the current count:</p> <div class="highlight-wrapper group relative javascript"> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-1lhy48xm" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-1lhy48xm"><span class="nx">contents</span> <span class="o">=</span> <span class="nx">contents</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">@@COUNT@@</span><span class="dl">'</span><span class="p">,</span> <span class="nx">count</span><span class="p">.</span><span class="nx">toString</span><span class="p">());</span> </code></pre> </div> </div> <p>While this is fine for this example, larger projects would be better served with a real template. <code>node-demo</code> supports two such templating engines at the moment: <a href='https://ejs.co/' title=''>ejs</a> and <a href='https://mustache.github.io/' title=''>mustache</a>. Select your favorite, or switch back and forth:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-xesi2chs" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-xesi2chs">npx node-demo --ejs </code></pre> </div> </div> <p>and</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-h85bq8ax" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-h85bq8ax">npx node-demo --mustache </code></pre> </div> </div> <p>Be sure to add <code>--esm</code> if you want to continue to use <code>import</code> statements.</p> <h2 id='a-more-substantial-change' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-more-substantial-change' aria-label='Anchor'></a><span class='plain-code'>A more substantial change</span></h2> <p>While <code>node:http</code> provides the means for you to create a capable HTTP server, it requires you to be responsible for status codes, mime types, headers, and other protocol details. <a href='https://expressjs.com/' title=''>express</a> will take care of all of this for you:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-3sccyra7" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-3sccyra7">npx node-demo --express </code></pre> </div> </div> <p>Both ejs and mustache have integrations with express. Try switching between the two to see how they differ.</p> <h2 id='a-real-database' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#a-real-database' aria-label='Anchor'></a><span class='plain-code'>A real database</span></h2> <p>Maintaining a counter in a text file is good enough for a demo, but not suitable for production. Sqlite3 and PostgreSQL are better alternatives:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-2og0zhzk" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-2og0zhzk">npx node-demo --sqlite3 </code></pre> </div> </div> <p>and</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-9i9rd67g" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-9i9rd67g">npx node-demo --postgresql </code></pre> </div> </div> <p>Sqlite3 is great for development, and when used with <a href='https://fly.io/docs/litefs/' title=''>litefs</a> is great for deployment. PostgreSQL can be used in development, and currently is the best choice for production.</p> <p>To run with PostgreSQL locally, you need to install and start the server and create a database. For MacOS:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-uh7bwdj1" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-uh7bwdj1">brew install postgresql brew services start postgresql psql -U postgres -c "drop database if exists $USER;" psql -U postgres -c "create database $USER;" export DATABASE_URL=postgresql://$USER:$USER@localhost:5432/$USER </code></pre> </div> </div><h2 id='be-as-weird-as-you-want-to-be' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#be-as-weird-as-you-want-to-be' aria-label='Anchor'></a><span class='plain-code'>Be as weird as you want to be</span></h2> <p>The next two options are frankly polarizing. People either love them or hate them. We won&rsquo;t judge you.</p> <p>First <a href='https://tailwindcss.com/' title=''>tailwindcss</a> is a CSS builder that works based on parsing your class attributes in your HTML:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-ppkc1gor" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-ppkc1gor">npx node-demo --tailwindcss </code></pre> </div> </div> <p>Next is <a href='https://www.typescriptlang.org/' title=''>typescript</a> which adds type annotations:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-z65tjdg0" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-z65tjdg0">npx node-demo --typescript </code></pre> </div> </div> <p>TypeScript should work with all of the options on this page, in many cases making use of development only <a href='https://www.npmjs.com/search?q=%40types' title=''>@types</a>. All of this should be handled automatically by node-demo.</p> <p>Both of these require a build step, which can be run via <code>npm run build</code>. A change to the Dockerfile used to deploy is also required, which can be made using:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-r585ahki" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-r585ahki">npx dockerfile </code></pre> </div> </div> <p><a href='https://github.com/fly-apps/dockerfile-node#overview' title=''>dockerfile-node</a> is actually a separate project with its own options for you to explore.</p> <h3 id='object-relational-mappers-orms' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#object-relational-mappers-orms' aria-label='Anchor'></a><span class='plain-code'>Object Relational Mappers (ORMs)</span></h3> <p>Adding databases was the first change that we&rsquo;ve seen that actually makes the demo application noticeably larger, particularly with PostgreSQL once the code that handles reconnecting to the database after network failures is included. This can be handled by including still more libraries, this time Object Relational Managers (ORMs). Three popular ones:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-eir6orlq" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-eir6orlq">npx node-demo --drizzle </code></pre> </div> </div> <p>and</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-owzfvjep" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-owzfvjep">npx node-demo --knex </code></pre> </div> </div> <p>and</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-j3ef4hfs" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-j3ef4hfs">npx node-demo --prisma </code></pre> </div> </div> <p>Knex runs just fine with vanilla JavaScript. Prisma can run with vanilla JavaScript, but works better with TypeScript. Drizzle requires TypeScript.</p> <p>Prisma and Drizzle also require a build step.</p> <p>A final note: if you switch back and forth between Sqlite3 and PostgreSQL, you may get into a state where the migrations generated are for the wrong database. Simply delete the <code>prisma</code> or <code>src/db/migrations</code> directory and rerun the <code>npx demo</code> command to regenerate the migrations.</p> <h2 id='real-time-updates' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#real-time-updates' aria-label='Anchor'></a><span class='plain-code'>Real Time Updates</span></h2> <p>If you open more than one browser window or tab, each will show a different number. This can be addressed by introducing websockets:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-btuva7jk" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-btuva7jk">npx node-demo --websocket </code></pre> </div> </div> <p>The server side of web sockets will be different based on whether or not you are using express. For the first time we are providing a client side script which is responsible for establishing (and reestablishing) the connection, and updating the <a href='https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction' title=''>DOM</a> when messages are received. This is a chore, and <a href='https://htmx.org/' title=''>htmx</a> is one of the many libraries that can be used to handle this chore:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-byrjstkm" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-byrjstkm">npx node-demo --htmx </code></pre> </div> </div> <p>The next problem is that if you are running multiple servers, each will manage their own pool of WebSockets so that only clients in the same pool will be notified of updates. This can be addressed by using redis:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-2qs1mnov" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-2qs1mnov">npx node-demo --redis </code></pre> </div> </div> <p>At this point, if you are using fly.io, postgres, and redis, you can go global:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-xgb1s1b5" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-xgb1s1b5">fly scale count 8 --region ams,syd,nrt,dfw </code></pre> </div> </div><h2 id='packaging-alternatives' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#packaging-alternatives' aria-label='Anchor'></a><span class='plain-code'>Packaging alternatives</span></h2> <p>So far, we have been using <code>npm</code>, but <code>yarn</code> and <code>pnpm</code> are alternatives that may be better for some use cases:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-gsrs7tgb" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-gsrs7tgb">npx node-demo --yarn </code></pre> </div> </div> <p>and</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-lru108d9" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-lru108d9">npx node-demo --pnpm </code></pre> </div> </div> <p>Each package manager organizes the <code>node_modules</code> directory a bit differently, so for best results when switching, remove the <code>node_modules</code> directory before switching:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-57dhviy4" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-57dhviy4">rm -rf node_modules </code></pre> </div> </div> <p>Windows Powershell users will want to use the following command instead:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-gl1gj1yr" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-gl1gj1yr">rm -r -fo node_modules </code></pre> </div> </div><h2 id='future-explorations' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#future-explorations' aria-label='Anchor'></a><span class='plain-code'>Future explorations</span></h2> <p>While we have explored many options, this only scratches the surface. There are many alternatives to the libraries above, and many more things to explore. Examples:</p> <ul> <li><a href='https://react.dev/' title=''>React</a> can be run server side in a number of different ways, and can be run client side using a <a href='https://en.wikipedia.org/wiki/Content_delivery_network' title=''>CDN</a> or self hosted scripts. </li><li>In addition to React, there are a number of client side libraries like <a href='https://angularjs.org/' title=''>Angular</a>, <a href='https://lit.dev/' title=''>Lit</a>, <a href='https://www.solidjs.com/' title=''>SolidJS</a>, <a href='https://svelte.dev/' title=''>Svelte</a>, and <a href='https://vuejs.org/' title=''>Vue</a>. Coupling this with bundlers like <a href='https://esbuild.github.io/' title=''>esbuild</a> and <a href='https://rollupjs.org/' title=''>rollup</a>, perhaps in a <a href='https://en.wikipedia.org/wiki/Monorepo' title=''>monorepo</a> using <a href='https://docs.npmjs.com/cli/using-npm/workspaces' title=''>workspaces</a> would make good starting points for larger projects. </li><li>I welcome alternate implementations of this demo, perhaps using decidedly non-vanilla frameworks as a starting point. I&rsquo;m particularly interested in implementations that support real time updates and globally distributed replications. If we get enough, perhaps we can maintain a catalog of pointers to these implementations. </li><li>While this blog post has focused on local development and deployment on fly.io, there is no lock in here. Maintaining a catalog of pointers to blog posts that describe how to deploy this application elsewhere would be welcomed too. Again, bonus points for geographic distribution and real-time updates. </li></ul> <p><a href='https://github.com/fly-apps/node-demo' title=''>node-demo</a> is open source, so <a href='https://github.com/fly-apps/node-demo/issues' title=''>issues</a>, <a href='https://github.com/fly-apps/node-demo/pulls' title=''>pull requests</a>, and <a href='https://github.com/fly-apps/node-demo/discussions' title=''>discussions</a> are always welcome!</p> <p>I hope you have found this blog post to be informative, and perhaps some of you will use this information to start your next application &ldquo;vanilla&rdquo; with your personal selection of toppings. Yummy!</p> The JavaScript Ecosystem is Delightfully Weird https://fly.io/javascript-journal/js-ecosystem-delightfully-wierd/ 2023年05月11日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <p>Note, I&rsquo;m not saying that JavaScript is weird, though it <a href='https://wtfjs.com/' title=''>definitely is weird</a>. But that&rsquo;s not the point of this blog post.</p> <p>Bear with me, instead of starting with <em>how</em> JavaScript ecosystem is weird, I&rsquo;m going to start with <em>why</em> the JavaScript ecosystem is weird.</p> <h2 id='historical-background' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#historical-background' aria-label='Anchor'></a><span class='plain-code'>Historical Background</span></h2> <p>Less that 10 years ago, JavaScript sucked bad. It had no imports, no classes, no async, no arrow functions, no template literals, no destructuring assignment, no default parameters, no rest/spread parameters. And the environment it predominately ran in, namely the browser&rsquo;s DOM, sucked too. <a href='https://jquery.com/' title=''>JQuery</a> made it suck less. It still sucked, but was — at that point in time — relatively sane.</p> <p>Bundling JS to run in the browser was the first sign of weirdness. In that process you would also want to both minimize and tree shake the source, and perhaps even code split. In general the process involved reading a number of JavaScript sources as input and then producing one or more JavaScript sources as output. This meant that the code you were executing wasn&rsquo;t the code you wrote. <a href='https://sourcemaps.info/spec.html' title=''>Sourcemaps</a> helped.</p> <p>Then <a href='https://coffeescript.org/' title=''>CoffeeScript</a> came along. Instead of writing in JavaScript, you would write in a language which was compiled into JavaScript. This is a bit different than languages like <a href='https://elixir-lang.org/' title=''>Elixir</a> and <a href='https://kotlinlang.org/' title=''>Kotlin</a> which compile into the same byte codes as another language, CoffeeScript actually compiles into the other language. C++ started out this way.</p> <p>Then came ECMAScript 6 in 2015. JavaScript improved rapidly in the next few years. This eventually mostly displaced CoffeeScript, but presented a different problem: for a while the implementations were not keeping up so <em>transpilers</em> like <a href='https://babeljs.io/' title=''>Babel</a> came along that compiled current and future versions of JavaScript into older versions of JavaScript that ran on supported environments. Currently <a href='https://esbuild.github.io/' title=''>esbuild</a> is rapidly rising in popularity as a Javascript bundler/transpiler.</p> <p>Along the way, <a href='https://emscripten.org/' title=''>emscripten</a> came along which compiled actual machine code into a subset of JavaScript, though these days the new target for this tool is generally <a href='https://webassembly.org/' title=''>Wasm.</a></p> <p>Lately the pace of innovation in JavaScript has slowed, and JavaScript implementations are doing a better job of keeping up, so you would think that the need for transpilers would be waning, particularly on the server side where there is no need for bundlers. But that&rsquo;s not happening. And the reason why is an interesting story.</p> <h2 id='nobody-writes-javascript-any-more' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#nobody-writes-javascript-any-more' aria-label='Anchor'></a><span class='plain-code'>Nobody Writes JavaScript Any More</span></h2> <p>OK, the title above is clearly hyperbole, but I&rsquo;ll describe a number of the many ways that people aren&rsquo;t writing JavaScript any more.</p> <p>If you write a Rails application, you write it in Ruby. If you write a Django application, you write it in Python. Phoenix, Elixir. Lavavel, PHP. Rails gets a lot of flack for doing magic using meta-programming, and Elixir has macros, but all of the above stay within the boundaries of what can be done by the language.</p> <p>JavaScript, however, is different. While it nominally is standardized by <a href='https://tc39.es/' title=''>EMCA TC39</a>, if you are using a popular framework like <a href='https://nextjs.org/' title=''>Next.JS</a>, <a href='http://remix.run/' title=''>Remix</a>, or <a href='https://svelte.dev/' title=''>Svelte</a> you are <strong class='font-semibold text-navy-950'>not</strong> coding in <a href='https://www.ecma-international.org/publications-and-standards/standards/ecma-262/' title=''>ECMAScript</a> as standardized by ECMA TC39. Four examples:</p> <ul> <li>Once upon a time, nearly 20 years ago, the ECMA committee standardized <a href='https://www-archive.mozilla.org/js/language/ECMA-357.pdf' title=''>E4X</a> that enabled XML to be treated as a data type. This lost favor, got deprecated and archived. Years later what once was Facebook (now Meta) had a similar need and invented <a href='https://facebook.github.io/jsx/' title=''>JSX</a>. It differs from E4X in that it compiles into JS. </li><li>One thing that ECMA TC39 has never standardized is type annotations. Undeterred, Microsoft did it anyway with <a href='https://www.typescriptlang.org/' title=''>TypeScript</a>. It, too, compiles into JS. </li><li>Svelte has their own <a href='https://www.npmjs.com/package/svelte' title=''>compiler</a> that even <a href='https://svelte.dev/docs#component-format-script-3-$-marks-a-statement-as-reactive' title=''>deliberately misuses the JavaScript label syntax</a> to enable marking a statement as reactive. </li><li>It doesn&rsquo;t stop there. When a bundler/transpiler encounters an import statement, they don&rsquo;t necessarily presume that the file being imported is JavaScript or even any of the variants mentioned above. If configured properly and you want to import a CSS or PNG file, it will <a href='https://esbuild.github.io/content-types/' title=''>happily do so for you</a>. </li></ul> <p>I mentioned earlier that Rails gets a lot of flack for its use of meta programming. Nobody bats an eye at any of the &ldquo;abuses&rdquo; of the JavaScript language mentioned above. The JavaScript ecosystem is a Big Tent party.</p> <h2 id='quot-use-server-quot' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#quot-use-server-quot' aria-label='Anchor'></a><span class='plain-code'>&ldquo;use server&rdquo;;</span></h2> <p>The latest <a href='https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#adoption-strategy' title=''>abuse of the bundler</a> is by <a href='https://legacy.reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html' title=''>React Server Components (RSC)</a>. First <a href='https://github.com/reactjs/server-components-demo' title=''>demoed with express</a>, it is now adopted by <a href='https://nextjs.org/docs/getting-started/react-essentials#server-components' title=''>Next.js</a>.</p> <p>What &ldquo;<code>use server&quot;</code> and <code>&quot;use client&quot;</code> do, other than being a valid JavaScript statements that do absolutely nothing, is change the meaning of the code that follows them. This has gotten mixed reviews, but in my mind is very much in the spirit of <code>&quot;use strict&quot;</code>which also changes the meaning of the code that follows.</p> <p>While JSX often compiles to JS, the <a href='https://react.dev/reference/react-dom/server' title=''>Server React DOM APIs</a> enable compilation to HTML. RSC goes a different way, and compiles into a <a href='https://www.plasmic.app/blog/how-react-server-components-work' title=''>stream of tagged JSON</a>. This is all very transparent to you, but what it does enable is a <a href='https://twitter.com/levelsio/status/1654053489004417026' title=''>different style of programming. One that many are comparing to PHP</a> and even Rails:</p> <p><a style="display: inline" href="https://twitter.com/wobsoriano/status/1654181584357019649"><img style="display: inline; width: 200px; margin: 0.5em" src="https://pbs.twimg.com/media/FvTVW3taUAEc1PM?format=jpg&name=medium"></a> <a style="display: inline" href="https://twitter.com/jaredpalmer/status/1654178077356720164"><img style="display: inline; width: 200px; margin: 0.5em" src="https://pbs.twimg.com/media/FvTSI9TacAAQ0rg?format=png&name=medium"></a> <a style="display: inline" href="https://twitter.com/jeremyopendata/status/1654297213781131266"><img style="display: inline; width: 200px; margin: 0.5em" src="https://pbs.twimg.com/media/FvU-T0iWwAIj6i5?format=jpg&name=medium"></a></p> <p>It is not clear to me whether these comparisons are meant in a positive way, but I will say that from my perspective it is a very good thing.</p> <p>From a fly.io perspective, RSC enabling an <a href='https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#update-refetch-sequence' title=''>Update (Refetch) Sequence</a> is very much of interest. We&rsquo;ve always been especially shiny for frameworks that benefit from geographic distribution, like Elixir&rsquo;s <a href='https://fly.io/blog/how-we-got-to-liveview/' title=''>LiveView</a>, Laravel&rsquo;s <a href='https://laravel-livewire.com/' title=''>Livewire</a> and Ruby on Rail&rsquo;s <a href='https://hotwired.dev/' title=''>Hotwire</a>. We want those kinds of frameworks to succeed, because the better they do, the more valuable we are. Now we can add React&rsquo;s RSC to that list.</p> <p>Returning to the topic at hand, the fact that such a feature is only made possible through cooperation with bundlers — a statement tantamount to saying a change to the JavaScript language itself — is profound and, dare I say it, delightful.</p> <h2 id='another-dimension' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#another-dimension' aria-label='Anchor'></a><span class='plain-code'>Another Dimension</span></h2> <p>Dan Abramov gave a talk at RemixConf entitled <u>React from Another Dimension</u>:</p> <p><p><div class="youtube-container"></p> <div class="youtube-video"><iframe width="100%" height="100%" src="https://www.youtube.com/embed/wobP9yhrmhQ?start=21085&amp;end=23591" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""> </iframe> </div> <p>In Dan&rsquo;s talk he imagines an alternate universe in which React was first implemented in the late 90s on the server and still managed to converge to where it is today. During the talk he launches a Windows 95 emulator and runs Internet Explorer (specifically, IE6) with React. He even manages to get nine out of ten steps working using that operating system and browser combination.</p> <p>The mind bending parts of this presentation are where he first utilizes <code>use server</code> to implement a client side form action, and then later launches a client side alert from the server using <code>use client</code>. </p> <p>And he closes by saying that this requires new generation routers and new generation bundlers.</p> <p>And to think all of this is made possible by the fact that the JavaScript you write not only isn&rsquo;t the JavaScript you run, but under closer examination isn&rsquo;t even JavaScript at all.</p> Fly.io ❤️ JS https://fly.io/javascript-journal/flydotio-heart-js/ 2023年04月24日T00:00:00+00:00 2025年05月20日T16:26:25+00:00 <p>Fly.io is a great place to run fullstack applications. For most programming languages, there is a defacto default fullstack framework. For Ruby, there is Rails. For Elixir, there is Phoenix. For PHP there is Laravel. For Python, there is Django.</p> <p>If you don&rsquo;t know where to look, Node.js appears to be a mess. For starters there are <a href='https://stackdiary.com/node-js-frameworks/' title=''>plenty of js frameworks to choose from</a>. Then there are three different package managers. Not to mention that Typescript as an alternative to JavaScript. And if that is not bad enough Bun and Deno are providing alternatives to Node itself.</p> <p>The result is predictable. Fly.io has a number of community contributed templates for a small number of Node frameworks. Some have had more attention than others.</p> <p>It is time to clean up the mess.</p> <h2 id='package-json-enters-the-chat' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#package-json-enters-the-chat' aria-label='Anchor'></a><span class='plain-code'><code>package.json</code> enters the chat</span></h2> <p>The key sentence in the preceding section starts with <em>If you don&rsquo;t know where to look</em>. The right place to start is <code>package.json</code>. It tells you what dependencies need to be installed. For most frameworks, it tells you how to start the web server. And if there is a build step. And if there are any development dependencies that may be needed to run the build, and removed prior to deployment.</p> <p>Given this knowledge, a baseline Dockerfile can be built for any framework that follows these conventions. Handling different package managers can be accomplished by looking for <code>yarn.lock</code> and <code>pnpm-lock.yaml</code> files. TypeScript is a devDependency and handled by the build step. While Deno projects don&rsquo;t typically have <code>package.json</code> files, some bun projects do.</p> <p>The <a href='https://github.com/fly-apps/dockerfile-node' title=''>dockerfile-node</a> project endeavors to do exactly that:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-18y1fhm5" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-18y1fhm5">npx @flydotio/dockerfile </code></pre> </div> </div> <p>This will create (or replace!) your existing Dockerfile, as well as ensure that you have a <code>.dockerignore</code> file, and optionally may create a <code>docker-entrypoint</code> script. You can run with this Dockerfile locally, or use it to deploy on your favorite cloud provider. For Fly.io, you would get started by running:</p> <div class="highlight-wrapper group relative "> <button type="button" class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-wrap-target="#code-j2q1qkqt" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><g buffered-rendering="static"><path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /><path d="M11.081 6.466L9.533 8.037l1.548 1.571" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"> Wrap text </span> </button> <button type="button" class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none" data-copy-target="sibling" > <svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"><g buffered-rendering="static"><path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /><path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /></g></svg> <span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"> Copy to clipboard </span> </button> <div class='highlight relative group'> <pre class='highlight '><code id="code-j2q1qkqt">fly launch --dockerfile Dockerfile </code></pre> </div> </div> <p>The <code>--dockerfile</code> parameter is needed to tell <code>fly launch</code> to use your Dockerfile rather than trying to generate a new one.</p> <p>Of course, if you prefer to run your application on Google Cloud Run, Amazon ECS, MRSK, or even locally, you are welcome to do so.</p> <figure class="post-cta"> <figcaption> <h1>You can play with this right now.</h1> <p>Deploy using [Fly.io terminal](https://fly.io/terminal) or see our [Hands-on](https://fly.io/docs/hands-on/) guide that will walk you through the steps.</p> <a class="btn btn-lg" href="https://fly.io/docs/rails/"> Try Fly for free <span class='opacity-50'>→</span> </a> </figcaption> <div class="image-container"> <img src="/static/images/cta-rabbit.webp" srcset="/static/images/cta-rabbit@2x.webp 2x" alt=""> </div> </figure> <h2 id='devils-in-the-details' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#devils-in-the-details' aria-label='Anchor'></a><span class='plain-code'>Devils in the details</span></h2> <p>Not all frameworks are alike.</p> <p>Some will, by default, start servers that only process requests that come from the localhost. That, of course, is entirely unsatisfactory.</p> <p>Some require extra steps, for example applications that make use of Prisma.</p> <p>One (and I won&rsquo;t mention the name) actually lists the package needed to run the production server as a development only dependency.</p> <p>Fortunately, <a href='https://ejs.co/' title=''>ejs</a> templates can include <code>if</code> statements and/or make use of computed variables that customize the Dockerfiles produced.</p> <p>As a starter set, I&rsquo;ve got templates working for the following frameworks: <a href='https://expressjs.com/' title=''>Express</a>, <a href='https://www.fastify.io/' title=''>Fastify</a>, <a href='https://www.gatsbyjs.com/' title=''>Gatsby</a>, <a href='https://nestjs.com/' title=''>Nest</a>, <a href='https://nextjs.org/' title=''>Next</a>, <a href='https://nuxtjs.org/' title=''>Nuxt</a>, and <a href='https://remix.run/' title=''>Remix</a>. At the moment, I&rsquo;ve been focusing on breadth vs depth, so what I have working may not be able to handle much more than the splash screen, but my experience is that getting that far is often the hardest part, after that point you have all the scaffolding in place and can focus on any specific issue that may come up.</p> <p>Those are the successes so far. Here&rsquo;s a list of frameworks that are still being worked on, along with the current blocking issue:</p> <ul> <li><a href='https://trpc.io/' title=''>tRPC</a>: Access to the Postgres database is required during the build step. Worst case, we do the build step during the deployment of the server, but that is suboptimal for cases where multiple servers are started. </li><li><a href='https://strapi.io/' title=''>Strapi</a>: Needs to set secrets for JWT, session. This isn&rsquo;t a problem, and already is solved for Remix deployment for fly, but at the moment goes beyond what a Dockerfile generator can do by itself. </li><li><a href='https://redwoodjs.com/' title=''>RedwoodJS</a>: No scripts, recommends nginx. Fly.io already has a template for Redwood, so it presumably is just a matter of work to figure out how to fit the steps required into the general purpose template. It may make sense to either encourage Redwood to add scripts to their <code>package.json</code>, or to add them during the dockerfile generation. If not, <code>if</code> statements can be used to generate Redwood-specific steps rather than generic ones. </li><li><a href='https://kit.svelte.dev/' title=''>SvelteKit</a>: attempting to deploy results in <code>Could not detect a supported production environment</code>. Again, just appears to be a matter of work to add a new production environment. </li><li><a href='https://keystonejs.com/' title=''>KeystoneJS</a>: at build time, I&rsquo;m seeing <code>✘ [ERROR] Could not resolve &quot;./keystone&quot;</code>. Works fine on development machine, so I probably just missed a step. </li></ul> <p>In the fullness of time, these will be picked off one by one. This code is all open source, so everybody with an interest in a particular framework can contribute via issues and pull requests. Interest and participation will definitely affect prioritization of this work.</p> <h2 id='futures' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#futures' aria-label='Anchor'></a><span class='plain-code'>Futures</span></h2> <p>Once this script has a little bit of exposure to real world usage, it will replace the existing flyctl scanners, much in the way that <a href='https://github.com/rubys/dockerfile-rails' title=''>dockerfile-rails</a> is the basis for the Dockerfiles produced for Rails applications with Fly.io. At which point, usage will be as simple as <code>fly launch</code>.</p> <p>Integration with fly launch will also enable thing like setting of secrets, defining volumes, launching of databases, and defining health checks as part of the workflow.</p> <p>This package will also be designed to be re-run and accept arguments which will customize the Dockerfile produced. Peruse the <a href='https://github.com/rubys/dockerfile-rails#usage' title=''>usage</a> for dockerfile-rails to see examples of the types of customizations possible. Some highlights:</p> <ul> <li><code>--cache</code> - use build caching to speed up builds </li><li><code>--swap=n</code> - allocate swap space enabling running of larger applications on memory constrained VMs. </li><li><code>--compose</code> - generate a <code>docker-compose.yml</code> file </li></ul> <p>The scanner will also be able to do things like detect the inclusion of <code>puppeteer</code> and automatically install and configure Chrome/Chromium. This is already being done for Rails applications today.</p> <p>Another thing already being done for Rails applications is to run the web server as a non-root user for security reasons. Repeating this for Node.js will require knowledge of what files the application is expected to write to and which are expected to be read-only. This knowledge is necessarily framework specific, and may not be possible for minimal and general purpose frameworks like express.</p> <h2 id='got-feedback' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'><a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#got-feedback' aria-label='Anchor'></a><span class='plain-code'>Got Feedback?</span></h2> <p>If you have questions, comments, or concerns, let us know!</p> <p>If they are even vaguely Fly.io related, feel free to use our <a href='https://community.fly.io/' title=''>community forum</a>. Otherwise, start a <a href='https://github.com/fly-apps/dockerfile-node/discussions' title=''>discussion</a> on GitHub.</p> <p>And to those that wish to contribute, perhaps to make support for their favorite framework(s) better&hellip;. let&rsquo;s do this!</p>

AltStyle によって変換されたページ (->オリジナル) /