<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Light Cloud</title>
    <description>The latest articles on DEV Community by Light Cloud (lightcloud).</description>
    <link>https://dev.to/lightcloud</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14938%2Fe488b677-4276-480d-bddd-4095227d5d5e.png</url>
      <title>DEV Community: Light Cloud</title>
      <link>https://dev.to/lightcloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lightcloud"/>
    <language>en</language>
    <item>
      <title>Run SQL in the Browser: Queries, Joins and Quick Fixes on a Live Database</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Fri, 30 Oct 2026 07:02:13 +0000</pubDate>
      <link>https://dev.to/lightcloud/run-sql-in-the-browser-queries-joins-and-quick-fixes-on-a-live-database-5gdf</link>
      <guid>https://dev.to/lightcloud/run-sql-in-the-browser-queries-joins-and-quick-fixes-on-a-live-database-5gdf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff3dkyqlqonft3a0x3zrf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff3dkyqlqonft3a0x3zrf.webp" alt="Run SQL in the Browser: Queries, Joins and Quick Fixes on a Live Database" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To run SQL on a Light Cloud PostgreSQL database from the browser, open the database's &lt;strong&gt;Data&lt;/strong&gt; tab, type the query in the box above the table, and click &lt;strong&gt;Run query&lt;/strong&gt;. The box is read-only by default, so a stray &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; is rejected. To change data, tick &lt;strong&gt;Allow writes&lt;/strong&gt; first.&lt;/p&gt;

&lt;p&gt;This is part 3 of the database series. It uses the &lt;code&gt;bookshop-db&lt;/code&gt; database from &lt;a href="https://blog.light-cloud.com/tutorials/create-a-postgres-database" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; and the &lt;code&gt;reviews&lt;/code&gt; table from &lt;a href="https://blog.light-cloud.com/tutorials/database-data-explorer" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run a first query with a join and totals.&lt;/li&gt;
&lt;li&gt;Join three tables.&lt;/li&gt;
&lt;li&gt;See a write being rejected, then make a quick fix safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;bookshop-db&lt;/code&gt; database from &lt;a href="https://blog.light-cloud.com/tutorials/create-a-postgres-database" rel="noopener noreferrer"&gt;part 1&lt;/a&gt;, with the changes from &lt;a href="https://blog.light-cloud.com/tutorials/database-data-explorer" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A Light Cloud account on the &lt;strong&gt;Lite&lt;/strong&gt; plan or higher.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Run a first query
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;a href="https://console.light-cloud.com" rel="noopener noreferrer"&gt;Light Cloud console&lt;/a&gt;, open &lt;strong&gt;bookshop-db&lt;/strong&gt; and its &lt;strong&gt;Data&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Click into the box that says &lt;code&gt;SELECT * FROM my_table LIMIT 10;&lt;/code&gt; and paste this query. It shows how many books each author has, and what their stock is worth:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_stock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stock_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;stock_value&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Run query&lt;/strong&gt; , or press &lt;strong&gt;Cmd+Enter&lt;/strong&gt; ( &lt;strong&gt;Ctrl+Enter&lt;/strong&gt; on Windows and Linux).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkl4l7bgqczh3hq9s2skv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkl4l7bgqczh3hq9s2skv.webp" alt="The query box with the stock value query, the Run query button highlighted, and the result: Terry Pratchett 3 books 332.63, Ursula K. Le Guin 3 237.88, Octavia E. Butler 2 134.25, Stanislaw Lem 2 69.00" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The result appears under &lt;strong&gt;Query result&lt;/strong&gt; , with the number of rows and how long the database took. Terry Pratchett has 3 books because of &lt;em&gt;Mort&lt;/em&gt;, the book you added in part 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Join three tables
&lt;/h2&gt;

&lt;p&gt;Reviews point at books, and books point at authors. To see each review with its book and author, join all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reviews&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result has one row: &lt;em&gt;Mort&lt;/em&gt;, Terry Pratchett, rating 5, "Funny, kind and about Death."&lt;/p&gt;

&lt;p&gt;Queries like this are what the box is best at: quick questions about live data, without setting up a client. Another useful one, the books that are running low:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_stock&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;in_stock&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;in_stock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns &lt;em&gt;The Cyberiad&lt;/em&gt; (2), &lt;em&gt;Parable of the Sower&lt;/em&gt; (3) and &lt;em&gt;The Left Hand of Darkness&lt;/em&gt; (4).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Try a write
&lt;/h2&gt;

&lt;p&gt;The shop raises the price of every Terry Pratchett book to 9.49. Paste this and click &lt;strong&gt;Run query&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;49&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;author_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fails:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fskhcrcni9c1mtqplmxnm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fskhcrcni9c1mtqplmxnm.webp" alt="The UPDATE statement in the query box and the result cannot execute UPDATE in a read-only transaction, highlighted" width="799" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the safety net: the note under the box says &lt;strong&gt;Read-only session&lt;/strong&gt; , and every statement that would change data is rejected. Nothing was changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Make the fix with Allow writes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Tick &lt;strong&gt;Allow writes&lt;/strong&gt;. The note turns orange: &lt;strong&gt;Writes enabled - statements run with full access to this database.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Run query&lt;/strong&gt; again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkyap1xe7izvqb7vbq8ib.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkyap1xe7izvqb7vbq8ib.webp" alt="Allow writes ticked and highlighted, the warning that statements run with full access, and the result listing Guards! Guards!, Small Gods and Mort, each with price 9.49" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RETURNING id, title, price&lt;/code&gt; makes the statement list the rows it changed: three books, each now 9.49. Without &lt;code&gt;RETURNING&lt;/code&gt;, the result says &lt;strong&gt;0 rows&lt;/strong&gt; , even though the update worked, because the statement itself returns no rows.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Untick &lt;strong&gt;Allow writes&lt;/strong&gt; when you are done, so the next query is read-only again.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Before you write to a production database
&lt;/h2&gt;

&lt;p&gt;A few habits that keep quick fixes quick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run the &lt;code&gt;SELECT&lt;/code&gt; first.&lt;/strong&gt; Write the &lt;code&gt;WHERE&lt;/code&gt; clause as a &lt;code&gt;SELECT&lt;/code&gt; (&lt;code&gt;SELECT id, title, price FROM books WHERE author_id = 2;&lt;/code&gt;), check the rows, then turn it into the &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always use &lt;code&gt;WHERE&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;UPDATE books SET price = 9.49&lt;/code&gt; without it changes every book.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;RETURNING&lt;/code&gt;.&lt;/strong&gt; You see exactly what changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back up before big changes.&lt;/strong&gt; The &lt;strong&gt;Backup&lt;/strong&gt; tab downloads the whole database; &lt;a href="https://blog.light-cloud.com/tutorials/export-a-database-backup" rel="noopener noreferrer"&gt;part 4&lt;/a&gt; shows how.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave schema changes to migrations.&lt;/strong&gt; &lt;code&gt;CREATE&lt;/code&gt;, &lt;code&gt;ALTER&lt;/code&gt; and &lt;code&gt;DROP&lt;/code&gt; belong in your app's migrations, so every environment gets them. Part 7 covers migrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A query stops after &lt;strong&gt;10 seconds&lt;/strong&gt; , with &lt;code&gt;canceling statement due to statement timeout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The result shows at most &lt;strong&gt;500 rows&lt;/strong&gt; , marked &lt;strong&gt;truncated - refine the query to see more&lt;/strong&gt;. Add a &lt;code&gt;WHERE&lt;/code&gt; or &lt;code&gt;LIMIT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A query can be up to &lt;strong&gt;20,000 characters&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Several statements separated by semicolons all run; only the last one's result is shown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For long jobs or large exports, connect with &lt;code&gt;psql&lt;/code&gt; as in &lt;a href="https://blog.light-cloud.com/tutorials/create-a-postgres-database" rel="noopener noreferrer"&gt;part 1&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  cannot execute UPDATE in a read-only transaction
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Allow writes&lt;/strong&gt; is not ticked. The same applies to &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;CREATE&lt;/code&gt; and every other statement that changes something.&lt;/p&gt;

&lt;h3&gt;
  
  
  canceling statement due to statement timeout
&lt;/h3&gt;

&lt;p&gt;The query ran for more than 10 seconds. Narrow it with &lt;code&gt;WHERE&lt;/code&gt; or &lt;code&gt;LIMIT&lt;/code&gt;, or run it with &lt;code&gt;psql&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  relation "..." does not exist
&lt;/h3&gt;

&lt;p&gt;The table name is misspelt, or the table is in another schema. The table list on the left shows the exact names.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>tutorial</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Free REST API Hosting</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Thu, 29 Oct 2026 17:31:02 +0000</pubDate>
      <link>https://dev.to/lightcloud/free-rest-api-hosting-mh3</link>
      <guid>https://dev.to/lightcloud/free-rest-api-hosting-mh3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdes5rfuzjqzu3k61yapn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdes5rfuzjqzu3k61yapn.webp" alt="Free REST API Hosting" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to host a small Express or FastAPI app for free, with no card and no surprise bill, Light Cloud's Free plan is the only one of the eight below that passes every check: it runs the app as it is, wakes in a few seconds and never bills you. It does that up to about 4,800 requests a day. Cloudflare Workers comes second and gives you far more room, 100,000 requests a day, but only for code written for its runtime. Vercel, Render, Google Cloud Run, Netlify, AWS Lambda and Railway are all free in some form, each with a catch the table names.&lt;/p&gt;

&lt;p&gt;Disclosure: I make Light Cloud. The checks are written down below, so you can score the eight yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we ranked
&lt;/h2&gt;

&lt;p&gt;"Free API hosting" means different things on different pricing pages, so I fixed the API first. It's an Express or FastAPI app serving 3,000 requests a day, each taking 0.2 seconds, which is the "steady" API on Light Cloud's own pricing page. No database for now; that comes at the end.&lt;/p&gt;

&lt;p&gt;Each free plan then got a point for each of five checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it costs $0 for that API;&lt;/li&gt;
&lt;li&gt;the vendor's current pages say no card is needed to sign up;&lt;/li&gt;
&lt;li&gt;the app runs as it is, without being rewritten into a function format;&lt;/li&gt;
&lt;li&gt;nothing documented makes the first request after a quiet spell wait about a minute, or fail;&lt;/li&gt;
&lt;li&gt;the free plan has no end date and can't put a charge on a card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ties go to the plan whose vendor restricts less what you may run on it, such as production or commercial use, and then to the faster first request. Fly.io, Heroku, Koyeb and DigitalOcean aren't scored: Fly has no free tier for new accounts, Heroku dropped its free dynos in 2022, Koyeb has asked new users to choose a paid plan since February 2026, and DigitalOcean's free tier covers static sites only. Everything was checked on 2026-10-13.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ranking
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;th&gt;Reference API a month&lt;/th&gt;
&lt;th&gt;Card at sign-up&lt;/th&gt;
&lt;th&gt;After an idle spell&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Light Cloud&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;$0, uses $0.64 of the $1 included&lt;/td&gt;
&lt;td&gt;No card&lt;/td&gt;
&lt;td&gt;Answers in 3.1 to 3.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;$0, up to 100,000 requests a day&lt;/td&gt;
&lt;td&gt;No card&lt;/td&gt;
&lt;td&gt;No sleep documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;$0 on Hobby, non-commercial only&lt;/td&gt;
&lt;td&gt;Not stated&lt;/td&gt;
&lt;td&gt;Functions, no sleep documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;$0, one free web service&lt;/td&gt;
&lt;td&gt;No card&lt;/td&gt;
&lt;td&gt;Spins down; about a minute to wake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Google Cloud Run&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0 inside the free tier&lt;/td&gt;
&lt;td&gt;Billing account&lt;/td&gt;
&lt;td&gt;Scales to zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0, about 70 of 300 credits&lt;/td&gt;
&lt;td&gt;Not stated on current pages&lt;/td&gt;
&lt;td&gt;Functions, no sleep documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;AWS Lambda&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0 inside the free tier&lt;/td&gt;
&lt;td&gt;Not needed for most new accounts&lt;/td&gt;
&lt;td&gt;Functions, no sleep documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$1 credit, may not cover a month&lt;/td&gt;
&lt;td&gt;No card&lt;/td&gt;
&lt;td&gt;Sleep is opt-in; first request may 502&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Vendors' own pricing, limits and docs pages, checked 2026-10-13. Reference API: 3,000 requests a day at 0.2 s each, no database. "Not stated" means the vendor's current pages don't say either way, so the point isn't given.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Light Cloud
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; a small API that should stay free, need no card and never send a bill.&lt;/p&gt;

&lt;p&gt;Five points. The Free plan includes $1 of usage a month, 3 server apps and no card (&lt;a href="https://light-cloud.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). Our pricing page works out this exact API: 3,000 requests a day at 0.2 s is 5 instance-hours a month at $0.12, plus two deploys, $0.64 in all. You push the repository and it runs; there are &lt;a href="https://blog.light-cloud.com/tutorials/deploy-an-express-api" rel="noopener noreferrer"&gt;Express&lt;/a&gt; and &lt;a href="https://blog.light-cloud.com/tutorials/deploy-a-fastapi-app" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt; walkthroughs, and the same works to &lt;a href="https://light-cloud.com/frameworks" rel="noopener noreferrer"&gt;deploy Express, FastAPI, Go and Spring Boot APIs&lt;/a&gt; without a Dockerfile, with &lt;a href="https://light-cloud.com/deployments" rel="noopener noreferrer"&gt;deploys on every git push&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Free never bills. If the $1 runs out, the API pauses until next month and nothing reaches your card, because there isn't one. On the same formula the $1 lasts up to about 4,800 requests a day; past that, Lite is $5 a month with $5 of usage. A container that has scaled to zero answers its first request in &lt;a href="https://blog.light-cloud.com/tutorials/microservices-on-light-cloud-part-4-cold-starts" rel="noopener noreferrer"&gt;3.1 to 3.7 seconds&lt;/a&gt;, then fast.&lt;/p&gt;

&lt;p&gt;The limits, plainly: Free apps live on a light-cloud.io address, with no custom domain, and Free has no database at all. Your own domain and Postgres start on Lite, which is also where you get &lt;a href="https://light-cloud.com/domains" rel="noopener noreferrer"&gt;your own domain with free SSL&lt;/a&gt;. There's no cron and no private network between services.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cloudflare Workers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; APIs written for the Workers runtime that need far more free requests than anyone else gives.&lt;/p&gt;

&lt;p&gt;Four points. Workers Free allows 100,000 requests a day, reset at midnight UTC (&lt;a href="https://developers.cloudflare.com/workers/platform/limits/" rel="noopener noreferrer"&gt;limits&lt;/a&gt;), and Cloudflare's &lt;a href="https://workers.cloudflare.com/plans" rel="noopener noreferrer"&gt;plans page&lt;/a&gt; says no credit card is required. Go over and requests fail with Error 1027; nothing is billed. Nobody else here comes close on volume: that's 33 times the reference API.&lt;/p&gt;

&lt;p&gt;The point it loses is the runtime. A Worker isn't a Node server or a container; you write for Cloudflare's runtime in JavaScript, TypeScript, Python or Rust (&lt;a href="https://developers.cloudflare.com/workers/languages/" rel="noopener noreferrer"&gt;languages&lt;/a&gt;), and each invocation gets 10 ms of CPU on Free. Cloudflare's docs include a FastAPI guide for Python Workers, so a small FastAPI app may port with little work. An Express app built around a long-running server needs more.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Vercel
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; personal projects whose API lives next to a Vercel frontend.&lt;/p&gt;

&lt;p&gt;Four points. Hobby includes 1 million function invocations and 4 hours of active CPU a month and can't buy more, so it can't bill you; go over and the feature pauses (&lt;a href="https://vercel.com/docs/plans/hobby" rel="noopener noreferrer"&gt;Hobby&lt;/a&gt;). A Docker image can run as a function on every plan, in beta, so the app needn't change much.&lt;/p&gt;

&lt;p&gt;The catch is the use, not the money. Vercel's &lt;a href="https://vercel.com/docs/limits/fair-use-guidelines" rel="noopener noreferrer"&gt;fair use guidelines&lt;/a&gt; limit Hobby to non-commercial personal use, and a paid developer or taking payments counts as commercial. I also couldn't find a current Vercel page that says whether Hobby needs a card, so that point isn't given.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Render
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; running an unchanged app for free when a slow first request is fine.&lt;/p&gt;

&lt;p&gt;Four points. A free web service runs any Docker or native app as it is, no card is needed, and a workspace gets 750 free instance hours a month, enough for one service around the clock (&lt;a href="https://render.com/docs/free" rel="noopener noreferrer"&gt;free tier&lt;/a&gt;). Without a card on file, going over the free bandwidth suspends free services instead of billing.&lt;/p&gt;

&lt;p&gt;It loses the wake-up point. A free web service spins down after 15 minutes without traffic, and Render says waking takes about one minute. That's fine for a portfolio demo and painful for a mobile app's login call. Render also says plainly that free instances are not for production.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Google Cloud Run
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; the biggest free container allowance, if a billing account is fine.&lt;/p&gt;

&lt;p&gt;Three points. The free tier covers 2 million requests and 180,000 vCPU-seconds a month (&lt;a href="https://cloud.google.com/run/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;), and the reference API uses 90,000 requests and about 18,000 vCPU-seconds. It runs your container unchanged and scales to zero.&lt;/p&gt;

&lt;p&gt;But the free tier needs a Google Cloud billing account, and usage beyond it is billed at standard rates, not paused. Spend caps that pause Cloud Run at a budget became generally available on 2026-10-12, which helps. You'll also wire up a custom domain yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Netlify
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; small function-based APIs next to a Netlify site.&lt;/p&gt;

&lt;p&gt;Three points. The Free plan has 300 credits a month with a hard limit and is never billed (&lt;a href="https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/" rel="noopener noreferrer"&gt;plans&lt;/a&gt;). By its rates the reference API uses about 18 credits for requests and 50 for function compute at the default 1 GB, around 70, plus 15 per production deploy. It also includes Netlify Database, a Postgres that sleeps after 5 idle minutes, which matters for an API.&lt;/p&gt;

&lt;p&gt;Two points go missing: the API becomes Netlify Functions rather than your Express server, and the current pricing docs don't say whether a card is needed. When credits run out, every project on the team pauses, the site included.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. AWS Lambda
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams heading for AWS anyway.&lt;/p&gt;

&lt;p&gt;Three points. Lambda's free tier is one million requests and 400,000 GB-seconds a month (&lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;); the reference API at 512 MB uses about 9,000. A function URL gives it an HTTPS endpoint, and most new customers sign up without a payment method.&lt;/p&gt;

&lt;p&gt;The app needs adapting: a container image, for example, must include Lambda's runtime interface client. And AWS's Free plan &lt;a href="https://aws.amazon.com/free/" rel="noopener noreferrer"&gt;closes the account after six months&lt;/a&gt; or when its credits run out, unless you move to the Paid plan, which bills usage beyond the free limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Railway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; trying an API on the platform you'll pay $5 for later.&lt;/p&gt;

&lt;p&gt;Two points. After a 30-day trial, the Free plan gives $1 of credit a month, needs no card and runs your app as it is (&lt;a href="https://docs.railway.com/pricing/free-trial" rel="noopener noreferrer"&gt;free plan&lt;/a&gt;). But Railway bills RAM at $10 per GB a month for what your process holds, so an API holding 100 MB around the clock uses the whole dollar before any CPU. Turn on sleep to save it and Railway warns the first request may return a 502. Railway's docs don't say what happens when the free credit runs out, so the last point isn't given either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the others beat us
&lt;/h2&gt;

&lt;p&gt;On volume, Cloudflare Workers wins without contest: 100,000 free requests a day against our roughly 4,800. If your API is JavaScript or Python and you're happy to write it for Workers, that's the better free home. Need a free database too? We have none on Free; Netlify includes one, and a free Neon or Supabase database works next to any row here. A custom domain on a free API is something we only offer from Lite. And if your API is part of a bigger AWS or Google Cloud setup, Lambda and Cloud Run sit closer to everything else you run.&lt;/p&gt;

&lt;p&gt;What we do differently is the shape of the promise. No card, an unchanged Express or FastAPI app, a first request in seconds rather than a minute, and a plan that pauses instead of billing. If that's the &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;free REST API hosting&lt;/a&gt; you were looking for, it's &lt;a href="https://light-cloud.com/pricing" rel="noopener noreferrer"&gt;free hosting with no credit card&lt;/a&gt; up to the point where your API has real users, and then $5.&lt;/p&gt;

&lt;p&gt;How many requests a day does your API actually get?&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>Browse and Edit Your Data Without a SQL Client: the Data Explorer</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Thu, 29 Oct 2026 09:12:02 +0000</pubDate>
      <link>https://dev.to/lightcloud/browse-and-edit-your-data-without-a-sql-client-the-data-explorer-1i0b</link>
      <guid>https://dev.to/lightcloud/browse-and-edit-your-data-without-a-sql-client-the-data-explorer-1i0b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0nlheswwucmv6i7kjz0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0nlheswwucmv6i7kjz0.webp" alt="Browse and Edit Your Data Without a SQL Client: the Data Explorer" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To browse and edit a PostgreSQL database on Light Cloud without a SQL client, open the database in the console and click the &lt;strong&gt;Data&lt;/strong&gt; tab. It lists every table. Click a table to see its rows, click a row to edit it, and use &lt;strong&gt;Insert row&lt;/strong&gt; , &lt;strong&gt;Columns&lt;/strong&gt; and &lt;strong&gt;New&lt;/strong&gt; to add rows, columns and tables.&lt;/p&gt;

&lt;p&gt;This is part 2 of the database series. It uses the &lt;code&gt;bookshop-db&lt;/code&gt; database and sample data from &lt;a href="https://blog.light-cloud.com/tutorials/create-a-postgres-database" rel="noopener noreferrer"&gt;part 1&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will do
&lt;/h2&gt;

&lt;p&gt;In the bookshop database, you will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;change the stock of one book,&lt;/li&gt;
&lt;li&gt;add a new book,&lt;/li&gt;
&lt;li&gt;add an &lt;code&gt;isbn&lt;/code&gt; column to &lt;code&gt;books&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;create a &lt;code&gt;reviews&lt;/code&gt; table and add reviews,&lt;/li&gt;
&lt;li&gt;delete a row safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;bookshop-db&lt;/code&gt; database with the sample data, from &lt;a href="https://blog.light-cloud.com/tutorials/create-a-postgres-database" rel="noopener noreferrer"&gt;part 1&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A Light Cloud account on the &lt;strong&gt;Lite&lt;/strong&gt; plan or higher.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing to install: everything happens in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Open a table
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;a href="https://console.light-cloud.com" rel="noopener noreferrer"&gt;Light Cloud console&lt;/a&gt;, click &lt;strong&gt;bookshop-db&lt;/strong&gt; in the sidebar.&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;Data&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;books&lt;/strong&gt; in the table list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The table shows 50 rows per page. Click a column name to sort by it. Nothing you do while browsing changes the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Edit a row
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The Dispossessed&lt;/em&gt; is back in stock. Click its row. The &lt;strong&gt;Edit row&lt;/strong&gt; window shows every column with its type, and whether it may be empty (&lt;code&gt;not null&lt;/code&gt; means it may not):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change &lt;strong&gt;in_stock&lt;/strong&gt; from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;6&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F277a1vnased7viisy9dv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F277a1vnased7viisy9dv.webp" alt="The Edit row window for The Dispossessed, with in_stock changed to 6 and the Save changes button highlighted" width="672" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only the fields you changed are saved, so you cannot overwrite a column you did not touch. The console confirms with &lt;strong&gt;Row updated&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Insert a row
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Insert row&lt;/strong&gt; above the table and fill in a new book:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;leave empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;title&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mort&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;author_id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;year&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1987&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;price&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8.99&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;in_stock&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F33ft7mkus29ieoqdb4ve.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F33ft7mkus29ieoqdb4ve.webp" alt="The Insert row window with id showing default, title Mort, author_id 2, year 1987, price 8.99, in_stock 10, and the Insert button highlighted" width="672" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Insert&lt;/strong&gt;. Empty fields get the column's default, so PostgreSQL picks the next &lt;code&gt;id&lt;/code&gt;, 10. &lt;code&gt;author_id&lt;/code&gt; 2 is Terry Pratchett in the &lt;code&gt;authors&lt;/code&gt; table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add a column
&lt;/h2&gt;

&lt;p&gt;The shop wants to store ISBNs. Click &lt;strong&gt;Columns&lt;/strong&gt; above the table, then &lt;strong&gt;Add column&lt;/strong&gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Name it &lt;code&gt;isbn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Choose the type &lt;strong&gt;text&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Leave &lt;strong&gt;NULL&lt;/strong&gt; ticked, so existing books may have no ISBN yet.&lt;/li&gt;
&lt;li&gt;Click the green tick.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07dgltp6npx7tjp8utro.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07dgltp6npx7tjp8utro.webp" alt="The Manage columns window for books with a new column isbn of type text, NULL ticked, and the confirm tick highlighted" width="672" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Changes in this window apply at once. The pencil next to a column renames it, and the bin drops it. Click &lt;strong&gt;Done&lt;/strong&gt;. Every book now has an &lt;code&gt;isbn&lt;/code&gt; column showing &lt;code&gt;NULL&lt;/code&gt;, which means empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Create a table
&lt;/h2&gt;

&lt;p&gt;Readers want to leave reviews. Click &lt;strong&gt;New&lt;/strong&gt; next to &lt;strong&gt;Tables&lt;/strong&gt; and fill in the &lt;strong&gt;New table&lt;/strong&gt; window:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Table name&lt;/strong&gt; : &lt;code&gt;reviews&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The first column is already there: &lt;code&gt;id&lt;/code&gt;, type &lt;strong&gt;serial&lt;/strong&gt; , &lt;strong&gt;PK&lt;/strong&gt; ticked. &lt;strong&gt;serial&lt;/strong&gt; numbers new rows automatically, and &lt;strong&gt;PK&lt;/strong&gt; makes &lt;code&gt;id&lt;/code&gt; the primary key.&lt;/li&gt;
&lt;li&gt;Second column: &lt;code&gt;book_id&lt;/code&gt;, type &lt;strong&gt;integer&lt;/strong&gt; , &lt;strong&gt;NULL&lt;/strong&gt; not ticked.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add column&lt;/strong&gt;. Third column: &lt;code&gt;rating&lt;/code&gt;, type &lt;strong&gt;integer&lt;/strong&gt; , &lt;strong&gt;NULL&lt;/strong&gt; not ticked.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add column&lt;/strong&gt;. Fourth column: &lt;code&gt;body&lt;/code&gt;, type &lt;strong&gt;text&lt;/strong&gt; , &lt;strong&gt;NULL&lt;/strong&gt; ticked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faf4tmtbrd6erzvtu4v8g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faf4tmtbrd6erzvtu4v8g.webp" alt="The New table window with table name reviews and the columns id serial PK, book_id integer, rating integer and body text with NULL ticked, and the Create table button highlighted" width="672" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create table&lt;/strong&gt;. &lt;code&gt;reviews&lt;/code&gt; appears in the table list, empty. Add two reviews with &lt;strong&gt;Insert row&lt;/strong&gt; , leaving &lt;code&gt;id&lt;/code&gt; empty each time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;book_id&lt;/code&gt; &lt;code&gt;10&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt; &lt;code&gt;5&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt; &lt;code&gt;Funny, kind and about Death.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;book_id&lt;/code&gt; &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt; &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt; &lt;code&gt;test&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep a primary key on every table you create here: the data explorer can only edit rows of tables that have one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Delete a row safely
&lt;/h2&gt;

&lt;p&gt;The second review was a test. Tick its checkbox, then click &lt;strong&gt;Delete selected&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0mjhd55q5byhv4ybb80.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0mjhd55q5byhv4ybb80.webp" alt="The reviews table with the test row ticked, 1 selected, and the Delete selected button highlighted" width="798" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The console asks first, and says how many rows will go:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdr3cgfknhhzrwpklxzk8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdr3cgfknhhzrwpklxzk8.webp" alt="The Delete 1 row confirmation saying the selected rows will be permanently deleted and the action cannot be undone, with the Delete row button highlighted" width="448" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check the number, then click &lt;strong&gt;Delete row&lt;/strong&gt;. To delete a single row, you can also open it and click &lt;strong&gt;Delete row...&lt;/strong&gt; in the &lt;strong&gt;Edit row&lt;/strong&gt; window.&lt;/p&gt;

&lt;p&gt;Deleting cannot be undone, and neither can dropping a column or a table. Before a big cleanup, download a backup from the &lt;strong&gt;Backup&lt;/strong&gt; tab; &lt;a href="https://blog.light-cloud.com/tutorials/export-a-database-backup" rel="noopener noreferrer"&gt;part 4&lt;/a&gt; shows how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the result
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;books&lt;/strong&gt; again and click &lt;strong&gt;id&lt;/strong&gt; to sort. The Dispossessed has 6 in stock, Mort is book 10, and every book has an empty &lt;code&gt;isbn&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftx6ccj4cx6p4v2ty0qmk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftx6ccj4cx6p4v2ty0qmk.webp" alt="The books table sorted by id, showing 10 rows including Mort, and the new isbn column with NULL in every row" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same changes are visible to anything that connects to the database, such as &lt;code&gt;psql&lt;/code&gt; or your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Clicking a row does nothing
&lt;/h3&gt;

&lt;p&gt;The table has no primary key, so the explorer cannot tell rows apart; the header above the rows says &lt;strong&gt;read-only - no primary key&lt;/strong&gt;. Add a primary key with SQL, or edit such tables with SQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Could not save the row
&lt;/h3&gt;

&lt;p&gt;The window shows the database's reason. The most common: a &lt;code&gt;not null&lt;/code&gt; field left empty, or text in a number field.&lt;/p&gt;

&lt;h3&gt;
  
  
  Could not add the column
&lt;/h3&gt;

&lt;p&gt;Adding a column that may not be empty fails on a table that already has rows, because the existing rows would have no value. Leave &lt;strong&gt;NULL&lt;/strong&gt; ticked, fill in the values, and tighten the rule later with SQL.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>tutorial</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Create a PostgreSQL Database in One Minute and Connect From Your Laptop</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:28:52 +0000</pubDate>
      <link>https://dev.to/lightcloud/create-a-postgresql-database-in-one-minute-and-connect-from-your-laptop-1akb</link>
      <guid>https://dev.to/lightcloud/create-a-postgresql-database-in-one-minute-and-connect-from-your-laptop-1akb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6fcqn11ol9jnc0gwyacr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6fcqn11ol9jnc0gwyacr.webp" alt="Create a PostgreSQL Database in One Minute and Connect From Your Laptop" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To create a PostgreSQL database on Light Cloud, click &lt;strong&gt;New...&lt;/strong&gt; , &lt;strong&gt;New Database&lt;/strong&gt; in the console and then &lt;strong&gt;Create database&lt;/strong&gt;. A Dev database is ready in about ten seconds. To connect from your laptop, copy the connection string from the &lt;strong&gt;Credentials&lt;/strong&gt; tab, add &lt;code&gt;?sslmode=require&lt;/code&gt; to the end, and pass it to &lt;code&gt;psql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is part 1 of a series on databases. Here you create the database, load a small bookshop into it, look at it in the console, and connect from Node.js and Python. Later parts use the same database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;A PostgreSQL database named &lt;code&gt;bookshop-db&lt;/code&gt; with two tables, &lt;code&gt;authors&lt;/code&gt; and &lt;code&gt;books&lt;/code&gt;, that you can reach from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;psql&lt;/code&gt; in your terminal,&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;Data&lt;/strong&gt; tab in the console,&lt;/li&gt;
&lt;li&gt;a Node.js script and a Python script.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source code and sample data: &lt;a href="https://github.com/light-cloud-com/tutorial-database-basics" rel="noopener noreferrer"&gt;github.com/light-cloud-com/tutorial-database-basics&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Light Cloud account on the &lt;strong&gt;Lite&lt;/strong&gt; plan or higher: the Free plan has no databases.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/downloads" rel="noopener noreferrer"&gt;Git&lt;/a&gt; and a terminal. On Windows, use &lt;a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows" rel="noopener noreferrer"&gt;PowerShell 7&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For the last step, &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; 20 or later and &lt;a href="https://www.python.org/downloads/" rel="noopener noreferrer"&gt;Python&lt;/a&gt; 3.10 or later. I used Node.js 22 and Python 3.13.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Install psql
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;psql&lt;/code&gt; is the PostgreSQL command-line client. You only need the client, not a PostgreSQL server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;libpq
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export PATH="/opt/homebrew/opt/libpq/bin:$PATH"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc


&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;postgresql-client

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS this uses &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt;. Homebrew does not put &lt;code&gt;libpq&lt;/code&gt; on your &lt;code&gt;PATH&lt;/code&gt; by itself, hence the second line. On Fedora and other Linux distributions, the package is also called &lt;code&gt;postgresql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On Windows, run the &lt;a href="https://www.enterprisedb.com/downloads/postgres-postgresql-downloads" rel="noopener noreferrer"&gt;PostgreSQL installer&lt;/a&gt; and select only &lt;strong&gt;Command Line Tools&lt;/strong&gt;. Then add its &lt;code&gt;bin&lt;/code&gt; folder, for example &lt;code&gt;C:\Program Files\PostgreSQL\17\bin&lt;/code&gt;, to your &lt;code&gt;PATH&lt;/code&gt; and open a new PowerShell window.&lt;/p&gt;

&lt;p&gt;Check that it works. Any recent version is fine; I used 17.5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ psql --version
psql (PostgreSQL) 17.5


PS&amp;gt; psql --version
psql (PostgreSQL) 17.5

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Create the database
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;a href="https://console.light-cloud.com" rel="noopener noreferrer"&gt;Light Cloud console&lt;/a&gt;, click &lt;strong&gt;New...&lt;/strong&gt; and then &lt;strong&gt;New Database&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;PostgreSQL&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;bookshop-db&lt;/code&gt; and keep &lt;strong&gt;Size&lt;/strong&gt; at &lt;strong&gt;Dev&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create database&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bv0rbwjjqmer2mq9yem.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bv0rbwjjqmer2mq9yem.webp" alt="The Create page with Database selected, Name bookshop-db, Size Dev, Storage 1 GB and Region Belgium, and the Create database button highlighted" width="768" height="784"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my run the database was &lt;strong&gt;Ready&lt;/strong&gt; ten seconds later.&lt;/p&gt;

&lt;p&gt;Which size to pick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dev&lt;/strong&gt; runs on a shared instance, is ready in seconds, and allows 10 connections per database user. It is the right choice for development, side projects and these tutorials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starter&lt;/strong&gt; (0.6 GB RAM) and &lt;strong&gt;Pro&lt;/strong&gt; (1.7 GB RAM) get an instance of their own, take 10 to 20 minutes to create, and need the Starter plan or higher.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Copy the connection string
&lt;/h2&gt;

&lt;p&gt;Open the &lt;strong&gt;Credentials&lt;/strong&gt; tab. It shows the host, port, database name, user and password, and the whole connection string. Click &lt;strong&gt;Copy&lt;/strong&gt; next to the connection string:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnvkek4e1psfqb5nfwlht.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnvkek4e1psfqb5nfwlht.webp" alt="The Credentials tab with host, port and masked database name, user and password, and the Copy button of the connection string highlighted" width="799" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;?sslmode=require&lt;/code&gt; to the end, and save it as &lt;code&gt;DATABASE_URL&lt;/code&gt; in your terminal. The database only accepts encrypted connections, and this setting asks for one. Paste your own string instead of the stars:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'postgresql://u_ ******:******** @bookshop-db-yourworkspace.db.light-cloud.io:5432/db ******?sslmode=require'&lt;/span&gt;&lt;span class="w"&gt;


&lt;/span&gt;&lt;span class="n"&gt;PS&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'postgresql://u_ ******:******** @bookshop-db-yourworkspace.db.light-cloud.io:5432/db ******?sslmode=require'&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the single quotes: they stop the shell from changing special characters in the password. The variable lasts until you close the terminal. It contains the password, so keep it out of Git and screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Connect with psql
&lt;/h2&gt;

&lt;p&gt;Check the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'\conninfo'&lt;/span&gt;
&lt;span class="go"&gt;You are connected to database "db ******" as user "u_******" on host "bookshop-db-yourworkspace.db.light-cloud.io" (address "***") at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: none)


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$env&lt;/span&gt;&lt;span class="s2"&gt;:DATABASE_URL"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'\conninfo'&lt;/span&gt;
&lt;span class="go"&gt;You are connected to database "db ******" as user "u_******" on host "bookshop-db-yourworkspace.db.light-cloud.io" (address "***") at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: none)

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SSL connection&lt;/code&gt; confirms the connection is encrypted. To work interactively, run &lt;code&gt;psql "$DATABASE_URL"&lt;/code&gt; (on Windows, &lt;code&gt;psql "$env:DATABASE_URL"&lt;/code&gt;) without &lt;code&gt;-c&lt;/code&gt;, and type &lt;code&gt;\q&lt;/code&gt; to leave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Load the sample data
&lt;/h2&gt;

&lt;p&gt;Clone the repository and run its &lt;code&gt;seed.sql&lt;/code&gt; against the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/light-cloud-com/tutorial-database-basics.git
&lt;span class="go"&gt;Cloning into 'tutorial-database-basics'...
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;tutorial-database-basics
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; seed.sql
&lt;span class="go"&gt;psql:seed.sql:4: NOTICE: table "books" does not exist, skipping
DROP TABLE
psql:seed.sql:5: NOTICE: table "authors" does not exist, skipping
DROP TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 4
INSERT 0 9


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/light-cloud-com/tutorial-database-basics.git
&lt;span class="go"&gt;Cloning into 'tutorial-database-basics'...
&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;tutorial-database-basics
&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$env&lt;/span&gt;&lt;span class="s2"&gt;:DATABASE_URL"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; seed.sql
&lt;span class="go"&gt;psql:seed.sql:4: NOTICE: table "books" does not exist, skipping
DROP TABLE
psql:seed.sql:5: NOTICE: table "authors" does not exist, skipping
DROP TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 4
INSERT 0 9

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two notices are expected on a new database: the script first drops its tables so you can run it again to reset the data. You now have 4 authors and 9 books. Ask for the five oldest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT title, year FROM books ORDER BY year LIMIT 5;"&lt;/span&gt;
&lt;span class="go"&gt;           title | year
---------------------------+------
 Solaris | 1961
 The Cyberiad | 1965
 A Wizard of Earthsea | 1968
 The Left Hand of Darkness | 1969
 The Dispossessed | 1974
(5 rows)


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$env&lt;/span&gt;&lt;span class="s2"&gt;:DATABASE_URL"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT title, year FROM books ORDER BY year LIMIT 5;"&lt;/span&gt;
&lt;span class="go"&gt;           title | year
---------------------------+------
 Solaris | 1961
 The Cyberiad | 1965
 A Wizard of Earthsea | 1968
 The Left Hand of Darkness | 1969
 The Dispossessed | 1974
(5 rows)

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: See the data in the console
&lt;/h2&gt;

&lt;p&gt;Back in the console, open the &lt;strong&gt;Data&lt;/strong&gt; tab and click &lt;strong&gt;books&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1et68mn1p0f7fj0fkzch.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1et68mn1p0f7fj0fkzch.webp" alt="The Data tab with the tables authors and books, books selected, showing 9 rows with id, title, author_id, year, price and in_stock" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the data explorer: every table in the database, its rows, and a SQL box above them. The session is read-only unless you tick &lt;strong&gt;Allow writes&lt;/strong&gt; , so browsing cannot change anything by accident. &lt;a href="https://blog.light-cloud.com/tutorials/database-data-explorer" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt; covers editing rows and tables here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Connect from Node.js and Python
&lt;/h2&gt;

&lt;p&gt;The repository has one small script per language. Each connects with &lt;code&gt;DATABASE_URL&lt;/code&gt; and counts the books.&lt;/p&gt;

&lt;p&gt;Python, with &lt;a href="https://www.psycopg.org/psycopg3/" rel="noopener noreferrer"&gt;psycopg&lt;/a&gt;, uses the string exactly as you saved it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 -m venv .venv
$ .venv/bin/pip install -r requirements.txt
$ .venv/bin/python connect/python.py
Connected from Python: 9 books


PS&amp;gt; python -m venv .venv
PS&amp;gt; .venv\Scripts\pip install -r requirements.txt
PS&amp;gt; .venv\Scripts\python connect\python.py
Connected from Python: 9 books

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node.js, with &lt;a href="https://node-postgres.com/" rel="noopener noreferrer"&gt;node-postgres&lt;/a&gt;, needs one more setting at the end, &lt;code&gt;&amp;amp;uselibpqcompat=true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npm install
$ DATABASE_URL="$DATABASE_URL&amp;amp;uselibpqcompat=true" node connect/node.mjs
Connected from Node.js: 9 books


PS&amp;gt; npm install
PS&amp;gt; $env:DATABASE_URL = "$env:DATABASE_URL&amp;amp;uselibpqcompat=true"
PS&amp;gt; node connect/node.mjs
Connected from Node.js: 9 books

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt; also prints how many packages it added. On Windows, the variable keeps the Node.js ending from now on, so run the Python script before the Node.js one.&lt;/p&gt;

&lt;p&gt;The ending each client needs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Add to the connection string&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;psql, Python (psycopg), Prisma, GUI tools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?sslmode=require&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js (node-postgres, &lt;code&gt;pg&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?sslmode=require&amp;amp;uselibpqcompat=true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In node-postgres 8, &lt;code&gt;sslmode=require&lt;/code&gt; also checks the server's certificate against the certificate authorities Node.js trusts, and fails. &lt;code&gt;uselibpqcompat=true&lt;/code&gt; gives &lt;code&gt;require&lt;/code&gt; the meaning it has in every other PostgreSQL client: encrypt, without that check. Only node-postgres knows this setting; the others reject it.&lt;/p&gt;

&lt;p&gt;To connect a GUI tool such as &lt;a href="https://tableplus.com/" rel="noopener noreferrer"&gt;TablePlus&lt;/a&gt;, &lt;a href="https://dbeaver.io/" rel="noopener noreferrer"&gt;DBeaver&lt;/a&gt; or &lt;a href="https://www.pgadmin.org/" rel="noopener noreferrer"&gt;pgAdmin&lt;/a&gt;, copy the host, port, database name, user and password from the &lt;strong&gt;Credentials&lt;/strong&gt; tab into a new PostgreSQL connection, and set &lt;strong&gt;SSL mode&lt;/strong&gt; to &lt;strong&gt;require&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  pg_hba.conf rejects connection ... no encryption
&lt;/h3&gt;

&lt;p&gt;The client connected without encryption. Add &lt;code&gt;?sslmode=require&lt;/code&gt; to the end of the connection string.&lt;/p&gt;

&lt;h3&gt;
  
  
  unable to verify the first certificate
&lt;/h3&gt;

&lt;p&gt;node-postgres with &lt;code&gt;?sslmode=require&lt;/code&gt; only. Add &lt;code&gt;&amp;amp;uselibpqcompat=true&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  invalid URI query parameter: "uselibpqcompat"
&lt;/h3&gt;

&lt;p&gt;psql or psycopg got the Node.js ending. Remove &lt;code&gt;&amp;amp;uselibpqcompat=true&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  psql: command not found
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;psql&lt;/code&gt; is not on your &lt;code&gt;PATH&lt;/code&gt;. On macOS, run the &lt;code&gt;echo&lt;/code&gt; and &lt;code&gt;source&lt;/code&gt; lines from Step 1. On Windows, add the PostgreSQL &lt;code&gt;bin&lt;/code&gt; folder to &lt;code&gt;PATH&lt;/code&gt; and open a new window.&lt;/p&gt;

&lt;h3&gt;
  
  
  password authentication failed
&lt;/h3&gt;

&lt;p&gt;The password in the string is wrong or was changed. Copy the connection string again from the &lt;strong&gt;Credentials&lt;/strong&gt; tab.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>tutorial</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Password Protect a Website for Free</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Tue, 27 Oct 2026 16:36:39 +0000</pubDate>
      <link>https://dev.to/lightcloud/password-protect-a-website-for-free-3hhp</link>
      <guid>https://dev.to/lightcloud/password-protect-a-website-for-free-3hhp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuz7ln7c323s9qs3wtbz0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuz7ln7c323s9qs3wtbz0.webp" alt="Password Protect a Website for Free" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three platforms let you password protect a website for free. Light Cloud does it with one shared password, set in four clicks on every plan, Free included. Cloudflare Access is free for teams under 50 people and makes each visitor sign in with a code sent to their email, which is safer and takes about a dozen steps to set up. Render has no setting for it, but its blog shows how to encrypt a static page at build time with PageCrypt. Everywhere else the password sits behind a paid plan: $20 a month on Netlify, $21 per user on GitHub, $29 on Koyeb and $40 on Vercel for one developer and one project.&lt;/p&gt;

&lt;p&gt;We make Light Cloud, so it's our row at the top. Below is how the order was decided, and every row links to the vendor's own page so you can check it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The September price change that started this
&lt;/h2&gt;

&lt;p&gt;On 2026-09-09 Vercel moved Password Protection from a $150-a-month team add-on to &lt;a href="https://vercel.com/changelog/password-protection-now-costs-20-per-project-per-month-on-pro" rel="noopener noreferrer"&gt;$20 per protected project a month&lt;/a&gt; on Pro. For a team with one project, that's a price cut. An agency with ten client projects now pays $200 a month, more than the old add-on. The same day, Vercel let its account-based gate, Vercel Authentication, protect production deployments too, at no cost on every plan.&lt;/p&gt;

&lt;p&gt;Answers to "how do I lock a staging site for free" tend to mix three different things: a shared password, a login with a personal account, and code you write yourself. They're priced very differently, so this table keeps them apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we ranked
&lt;/h2&gt;

&lt;p&gt;The scenario is the one I meet most: a commercial project has a staging site, and a client who has no account anywhere needs to open it, while search engines and strangers must not.&lt;/p&gt;

&lt;p&gt;Each platform is priced on the cheapest plan where its own protection works, for one developer and one site, using list prices checked on 2026-10-13. "Commercial" rules out plans that forbid it, such as Vercel's Hobby. Ties on price are broken by the number of steps, which I counted from each vendor's own instructions, starting at the settings page and leaving out sign-up.&lt;/p&gt;

&lt;p&gt;The table also says what the visitor has to type. A shared password is the least work for the client. Personal logins are better security, and more work for everyone. Neither is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ranking
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Password on one site a month&lt;/th&gt;
&lt;th&gt;What the visitor enters&lt;/th&gt;
&lt;th&gt;Steps in the vendor's docs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Light Cloud&lt;/td&gt;
&lt;td&gt;$0, every plan&lt;/td&gt;
&lt;td&gt;One shared password&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Cloudflare&lt;/td&gt;
&lt;td&gt;$0, Access Free under 50 users&lt;/td&gt;
&lt;td&gt;Emailed code or company login&lt;/td&gt;
&lt;td&gt;1 for preview links, about 12 for a hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;$0, with PageCrypt&lt;/td&gt;
&lt;td&gt;Shared password, decrypted in the browser&lt;/td&gt;
&lt;td&gt;3, plus inlining CSS, JavaScript and images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;td&gt;$20, Pro&lt;/td&gt;
&lt;td&gt;One shared password&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;GitHub Pages&lt;/td&gt;
&lt;td&gt;$21 per user, Enterprise Cloud&lt;/td&gt;
&lt;td&gt;GitHub sign-in with read access to the repo&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Koyeb&lt;/td&gt;
&lt;td&gt;$29, Pro&lt;/td&gt;
&lt;td&gt;Username and password (HTTP basic auth)&lt;/td&gt;
&lt;td&gt;1 CLI flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;$40: $20 Pro seat plus $20 per project&lt;/td&gt;
&lt;td&gt;One shared password&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;$5,000 committed-spend tier&lt;/td&gt;
&lt;td&gt;Railway account sign-in&lt;/td&gt;
&lt;td&gt;Not counted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prices and steps from each vendor's docs and pricing pages, checked 2026-10-13, US dollars. Light Cloud steps from our own tutorial. Sign-up, plan upgrades and DNS changes not counted as steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Light Cloud
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; a staging site that one shared password should cover.&lt;/p&gt;

&lt;p&gt;Password protection works on every plan, and &lt;a href="https://blog.light-cloud.com/tutorials/password-protect-a-staging-site" rel="noopener noreferrer"&gt;the tutorial&lt;/a&gt; runs on Free: open the environment's Security tab, click Set password, type it twice, click Save password. It applies from the next request, with no redeploy. The check happens at the edge before the request reaches your app, so images and JavaScript are covered too, and the password page answers with a 401 and &lt;code&gt;X-Robots-Tag: noindex, nofollow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few details I like. The password belongs to one environment, so production stays public while the &lt;code&gt;staging&lt;/code&gt; branch is locked; &lt;a href="https://blog.light-cloud.com/tutorials/preview-url-for-every-branch" rel="noopener noreferrer"&gt;the branch preview tutorial&lt;/a&gt; shows how a branch gets that environment in the first place. A visitor stays signed in for 12 hours. After 8 wrong guesses from one address within 15 minutes, further attempts are refused for a while. Clicking Change signs everyone out, which is what you want when a review ends. That's the whole of how we &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;password protect a website for free&lt;/a&gt;: it comes with &lt;a href="https://light-cloud.com/pricing" rel="noopener noreferrer"&gt;free hosting with no credit card&lt;/a&gt;, for static sites and containers alike.&lt;/p&gt;

&lt;p&gt;Now the limits. Everyone shares one password, so you can't remove one person without changing it for all. There's no single sign-on. On Free the site lives on a light-cloud.io address with a small "by Light Cloud" link; &lt;a href="https://light-cloud.com/domains" rel="noopener noreferrer"&gt;a custom domain with automatic SSL&lt;/a&gt; starts on Lite at $5 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cloudflare
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that want every visitor to sign in as themselves.&lt;/p&gt;

&lt;p&gt;Cloudflare doesn't do shared passwords. It does Cloudflare Access, which is $0 for teams under 50 users and $7 per user a month, paid annually, above that (&lt;a href="https://www.cloudflare.com/sase/products/access/" rel="noopener noreferrer"&gt;Access pricing&lt;/a&gt;). For preview links on Pages it's one setting, Settings &amp;gt; General &amp;gt; Enable access policy, which limits them to your Cloudflare account until you add people. That setting &lt;a href="https://developers.cloudflare.com/pages/configuration/preview-deployments/" rel="noopener noreferrer"&gt;doesn't cover the pages.dev domain or a custom domain&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;staging.example.com&lt;/code&gt; you create a self-hosted Access application, and the domain has to be active on Cloudflare. The &lt;a href="https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/self-hosted-public-app/" rel="noopener noreferrer"&gt;docs list nine actions&lt;/a&gt; before the optional settings. A client without a company login signs in with a &lt;a href="https://developers.cloudflare.com/cloudflare-one/identity/one-time-pin/" rel="noopener noreferrer"&gt;one-time PIN sent by email&lt;/a&gt;, which new organizations add as an identity provider in three more steps. So about a dozen, once. In return you can let in one email address and later remove exactly that one.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Render
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; a small static site where a password in the page is good enough.&lt;/p&gt;

&lt;p&gt;Render has no password setting. Its blog suggests &lt;a href="https://render.com/blog/static-site-auth-pagecrypt" rel="noopener noreferrer"&gt;PageCrypt&lt;/a&gt;, an open source library that encrypts an HTML file at build time; the browser decrypts it when the visitor types the password. Setup is three steps: add the dependency, change the build command, and set the password as an environment variable. Render's static sites are free, so the cost is $0.&lt;/p&gt;

&lt;p&gt;Render is upfront about the catch. PageCrypt encrypts a single HTML file by default, so CSS, JavaScript and images have to be inlined into it to be protected, and there's only one shared password. It's a static-site trick; an API behind it needs its own authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Netlify
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; agencies that want a password on every project for one flat price.&lt;/p&gt;

&lt;p&gt;On the credit-based plans, the shared password comes with Pro, $20 a month with unlimited team members, and it's not available on Free or Personal (&lt;a href="https://docs.netlify.com/manage/security/secure-access-to-sites/password-protection/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). Free and Personal can make a project private instead, but then only the Team Owner can see it, which doesn't help with a client.&lt;/p&gt;

&lt;p&gt;Five steps under Project configuration &amp;gt; Visitor access &amp;gt; Project visibility: Edit visibility, choose Password, type it, choose Production and previews or Previews only, Save. There's no team-wide default password, so each project gets its own. The flat price is the real draw: the password adds no per-project fee, so twenty client sites don't multiply it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. GitHub Pages
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; internal docs for people who already work in the repository.&lt;/p&gt;

&lt;p&gt;A GitHub Pages site can be published privately only if the organization uses GitHub Enterprise Cloud, which starts at $21 per user a month (&lt;a href="https://github.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). Then only people with read access to the repository can open it, and only project sites from private or internal organization repositories qualify (&lt;a href="https://docs.github.com/en/pages/getting-started-with-github-pages/changing-the-visibility-of-your-github-pages-site" rel="noopener noreferrer"&gt;docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The switch itself is three clicks: Settings, Pages, the visibility dropdown. It's a fine answer for an engineering handbook. For a client outside your GitHub organization, it isn't one.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Koyeb
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; an API or app that should answer only to a username and password.&lt;/p&gt;

&lt;p&gt;Koyeb's CLI can put HTTP basic auth on every route of a service with one flag, &lt;code&gt;--auth USERNAME:PASSWORD&lt;/code&gt;, or require an API key instead (&lt;a href="https://www.koyeb.com/docs/build-and-deploy/cli/reference" rel="noopener noreferrer"&gt;CLI reference&lt;/a&gt;). That's the shortest setup in this table if you live in a terminal, and it protects an API as well as a website.&lt;/p&gt;

&lt;p&gt;The price comes from the plan, not the feature. Since Koyeb &lt;a href="https://www.koyeb.com/blog/koyeb-is-joining-mistral-ai-to-build-the-future-of-ai-infrastructure" rel="noopener noreferrer"&gt;announced it is joining Mistral AI&lt;/a&gt;, new users must pick a paid plan, and Pro is $29 a month with $10 of compute included. The browser shows its own plain sign-in box, which looks dated to a client but works everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Vercel
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams already on Pro that need a password on one or two projects.&lt;/p&gt;

&lt;p&gt;Password Protection is &lt;a href="https://vercel.com/docs/deployment-protection/methods-to-protect-deployments/password-protection" rel="noopener noreferrer"&gt;$20 per protected project a month on Pro&lt;/a&gt; and isn't available on Hobby. Pro itself is $20 per deploying seat, so one developer with one protected project pays $40. The docs list six actions: pick the project, open Deployment Protection, flip the toggle, pick the environment, type the password, Save.&lt;/p&gt;

&lt;p&gt;The free alternative is Vercel Authentication, on every plan. Visitors log in with a Vercel account and request access, which you approve, and &lt;a href="https://vercel.com/docs/deployment-protection/methods-to-protect-deployments/vercel-authentication" rel="noopener noreferrer"&gt;Hobby allows one external user&lt;/a&gt;. For a team reviewing its own previews it's excellent; for a client it means asking them to create a Vercel account.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Railway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; large teams that want staging services behind Railway accounts.&lt;/p&gt;

&lt;p&gt;Railway's gate, &lt;a href="https://blog.railway.com/p/railway-authentication-identity-aware-proxy" rel="noopener noreferrer"&gt;announced on 2026-10-08&lt;/a&gt;, puts services such as staging previews and internal tools behind a Railway login. It requires the $5,000-a-month committed-spend tier. Below that tier, you'd put authentication into the app itself, which is a fine answer, just not a setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the others beat us
&lt;/h2&gt;

&lt;p&gt;On security, Cloudflare Access wins outright. Each visitor proves who they are, you can remove one person without touching anyone else, and you can plug in a company login. Our shared password can't do any of that, and if it leaks, the fix is to change it and send the new one around. Vercel Authentication and GitHub's private Pages do the same job for teams whose reviewers already have accounts there.&lt;/p&gt;

&lt;p&gt;For an agency with many client sites, Netlify's flat $20 covers a password on every project, and its team login can be tied to single sign-on through an identity provider. If you need production itself behind company sign-in, that's what these products are built for.&lt;/p&gt;

&lt;p&gt;Where we're ahead is the plain case: a client, a staging link, a password in a message. It's four clicks on any &lt;a href="https://light-cloud.com/previews" rel="noopener noreferrer"&gt;branch preview URL&lt;/a&gt;, on any plan, and the &lt;a href="https://light-cloud.com/deployments" rel="noopener noreferrer"&gt;git push deploys&lt;/a&gt; keep redeploying the protected branch without anyone touching the setting again. If you're choosing a host for the whole app, &lt;a href="https://blog.light-cloud.com/comparisons/where-to-host-a-saas" rel="noopener noreferrer"&gt;where to host a SaaS app&lt;/a&gt; prices the rest of it.&lt;/p&gt;

&lt;p&gt;How many people outside your team need to open your staging site this month?&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>passwordprotection</category>
      <category>security</category>
    </item>
    <item>
      <title>Kubernetes Alternatives for Small Teams</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Sun, 25 Oct 2026 17:34:00 +0000</pubDate>
      <link>https://dev.to/lightcloud/kubernetes-alternatives-for-small-teams-2eig</link>
      <guid>https://dev.to/lightcloud/kubernetes-alternatives-for-small-teams-2eig</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9k0fhy5q8t6bigprkb8i.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9k0fhy5q8t6bigprkb8i.webp" alt="Kubernetes Alternatives for Small Teams" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a small team that wants containers without Kubernetes, the options with the least to learn are Light Cloud, Railway and Render: you connect a repository and get HTTPS URLs, with no cluster, no registry and no config file to master. Fly.io and Coolify each add one new thing, Google Cloud Run and AWS ECS add cloud permissions and plumbing, and Kamal, Docker Swarm and Nomad hand you the servers. Light Cloud is first because, of the three, it is also the cheapest way to keep a few quiet services online.&lt;/p&gt;

&lt;p&gt;I run Light Cloud, so weigh that. The scoring below is simple enough to redo yourself from the vendors' own docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we ranked
&lt;/h2&gt;

&lt;p&gt;Kubernetes is hard for a small team for a boring reason: the list of things you have to understand before anything runs. Nodes, a registry, ingress, certificates, YAML for each object. &lt;a href="https://blog.light-cloud.com/kubernetes/kubernetes-won-developers-lost" rel="noopener noreferrer"&gt;The longer argument&lt;/a&gt; ran on this blog earlier this month, so this post stays practical.&lt;/p&gt;

&lt;p&gt;Every option below got one point for each of these six things it makes you learn or run yourself before your first deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;machines you rent, patch and keep alive;&lt;/li&gt;
&lt;li&gt;a config file in the tool's own format;&lt;/li&gt;
&lt;li&gt;a container registry to set up and log in to;&lt;/li&gt;
&lt;li&gt;HTTPS and load balancing you configure;&lt;/li&gt;
&lt;li&gt;cloud permissions, such as IAM roles;&lt;/li&gt;
&lt;li&gt;extra tools that must run next to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writing a Dockerfile doesn't count, since every option accepts one. Fewer points rank higher. Where two options tie, the one where someone else runs the machines goes first, and after that the lower bill for a reference system: three small services that are quiet most of the day. Facts come from the vendors' own docs and pricing pages, checked on 2026-10-13. Self-hosted tools get "your servers" in the price column, because I couldn't check a server price on a page I trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ranking
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;th&gt;Who runs the machines&lt;/th&gt;
&lt;th&gt;What you write&lt;/th&gt;
&lt;th&gt;Three quiet services a month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Light Cloud&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Light Cloud&lt;/td&gt;
&lt;td&gt;Nothing; a Dockerfile is optional&lt;/td&gt;
&lt;td&gt;$0 on Free, $5 with your own domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;Nothing required&lt;/td&gt;
&lt;td&gt;$5 plus usage over $5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;Nothing; render.yaml optional&lt;/td&gt;
&lt;td&gt;$21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Fly.io&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Fly.io&lt;/td&gt;
&lt;td&gt;fly.toml, generated for you&lt;/td&gt;
&lt;td&gt;up to $6.57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Coolify&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Nothing; it has a dashboard&lt;/td&gt;
&lt;td&gt;Your servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Google Cloud Run&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;IAM, plus a load balancer for your domain&lt;/td&gt;
&lt;td&gt;$0 inside the free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;AWS ECS Express Mode&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;AWS (Fargate)&lt;/td&gt;
&lt;td&gt;IAM roles, an image in a registry&lt;/td&gt;
&lt;td&gt;about $43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Kamal&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;config/deploy.yml&lt;/td&gt;
&lt;td&gt;Your servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Docker Swarm&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Service definitions, an outside load balancer&lt;/td&gt;
&lt;td&gt;Your servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Nomad&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Job files, plus Consul and Vault&lt;/td&gt;
&lt;td&gt;Your servers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scored from each vendor's own docs, checked 2026-10-13. Prices in US dollars from the vendors' pricing pages; "your servers" means the tool is free and you pay whoever hosts the machines. Kamal scores 4 once you run more than one server.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Light Cloud
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that want push-to-URL with nothing to configure and services that cost nothing while idle.&lt;/p&gt;

&lt;p&gt;Zero points. You connect a GitHub repository and each push builds and deploys; with a Dockerfile at the root it builds exactly that, and without one it detects the framework and writes the Dockerfile for you, so you can &lt;a href="https://light-cloud.com/frameworks" rel="noopener noreferrer"&gt;deploy Node.js, Python, Go and Java without a Dockerfile&lt;/a&gt;. Certificates are issued and renewed automatically. There is no registry to log in to and no IAM. Those &lt;a href="https://light-cloud.com/deployments" rel="noopener noreferrer"&gt;git push deploys&lt;/a&gt; come with a preview URL per branch, and &lt;a href="https://light-cloud.com/observability" rel="noopener noreferrer"&gt;logs and metrics&lt;/a&gt; sit next to the deploy history.&lt;/p&gt;

&lt;p&gt;Three quiet services fit the Free plan. The pricing page puts a quiet server app at about $0.10 a month of usage, so three use $0.30 of Free's $1, with no card (&lt;a href="https://light-cloud.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). Your own domain and a database start on Lite at $5. That's &lt;a href="https://light-cloud.com/pricing" rel="noopener noreferrer"&gt;pay-per-use container hosting&lt;/a&gt; in practice: every container scales to zero by default and bills by the hour while it runs.&lt;/p&gt;

&lt;p&gt;Now the part a Kubernetes refugee should read twice. Services talk to each other over public HTTPS, not a private network; &lt;a href="https://blog.light-cloud.com/tutorials/microservices-on-light-cloud-part-2-connect-services" rel="noopener noreferrer"&gt;the microservices series&lt;/a&gt; shows the CORS and shared-secret setup that takes. There is no cron and no background worker type. An app scales to 3 instances on Free and Lite, 10 on Starter and 50 on Business, and a warm instance that never sleeps costs about $52 a month and needs Starter. A cold service takes &lt;a href="https://blog.light-cloud.com/tutorials/microservices-on-light-cloud-part-4-cold-starts" rel="noopener noreferrer"&gt;3.1 to 3.7 seconds to answer&lt;/a&gt;. And settings live in the console, not in a manifest in your repository (the CLI's small &lt;code&gt;.lightcloud&lt;/code&gt; file only records which app a folder belongs to). That's the point of it, but it also means no infrastructure diff to review in a pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Railway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; small systems that need private networking and cron between services from day one.&lt;/p&gt;

&lt;p&gt;Also zero points. Railway builds from your repository, and services in a project reach each other over a private WireGuard network with internal DNS, which is exactly what Light Cloud lacks (&lt;a href="https://docs.railway.com/networking/private-networking" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). Cron jobs and pull request environments come on every paid plan.&lt;/p&gt;

&lt;p&gt;Hobby is $5 a month with $5 of usage included, billed by the RAM and CPU your services actually use (&lt;a href="https://docs.railway.com/pricing/plans" rel="noopener noreferrer"&gt;plans&lt;/a&gt;). Three quiet services may stay inside that, but I can't promise it: Railway has no fixed sizes, so it depends on how much memory your processes hold. Sleep is opt-in, and Railway warns that the first request to a sleeping service may get a 502.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Render
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; fixed prices per service, and a whole system in one optional file.&lt;/p&gt;

&lt;p&gt;Zero points, because nothing beyond a repository is required. When you want it, a Blueprint describes services, databases and environment groups in &lt;a href="https://render.com/docs/infrastructure-as-code" rel="noopener noreferrer"&gt;a single render.yaml&lt;/a&gt;, which is the closest thing here to the manifests you'd keep in a Kubernetes repo. Private networking, background workers and cron jobs are all built in.&lt;/p&gt;

&lt;p&gt;It ranks third on the tiebreak. A paid service is a fixed instance, the smallest $7 a month (&lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;), so three services are $21 whether anyone calls them or not. The free instances spin down after 15 minutes and Render says not to use them in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Fly.io
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; the same containers in many regions, with a private network between them.&lt;/p&gt;

&lt;p&gt;One point: &lt;code&gt;fly launch&lt;/code&gt; writes a fly.toml for your app's Fly-specific settings (&lt;a href="https://fly.io/docs/launch/create/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;), and you'll edit it sooner or later. In exchange, every organization gets a private WireGuard network across regions, and machines can stop when idle so you pay only for their disk.&lt;/p&gt;

&lt;p&gt;The smallest always-on machine is $2.19 a month (&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;), so three are at most $6.57, less if they stop. There is no free tier for new accounts, a card is required after the trial, and Fly says it doesn't support billing alerts yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Coolify
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; a Heroku-style dashboard on servers you already pay for.&lt;/p&gt;

&lt;p&gt;One point, but it's the big one: you run the machines. Everything else is handled from a dashboard. Coolify builds with Nixpacks, a Dockerfile or Docker Compose, issues Let's Encrypt certificates, starts databases from a list and deploys pull requests separately (&lt;a href="https://coolify.io/" rel="noopener noreferrer"&gt;features&lt;/a&gt;). It also runs cron jobs inside your apps, which several managed rows can't.&lt;/p&gt;

&lt;p&gt;Self-hosted Coolify is free with every feature; Coolify Cloud, where they run the dashboard and you bring the servers, is $5 a month for two servers plus $3 per extra one (&lt;a href="https://coolify.io/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). The Coolify server needs 2 CPU cores, 2 GB of RAM and 10 GB of disk on its own (&lt;a href="https://coolify.io/docs/get-started/installation" rel="noopener noreferrer"&gt;installation&lt;/a&gt;). Patching, backups to your own S3 bucket, and what happens when the box dies are yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Google Cloud Run
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams already on Google Cloud that want scale to zero at raw cloud prices.&lt;/p&gt;

&lt;p&gt;Two points. &lt;code&gt;gcloud run deploy --source&lt;/code&gt; builds from a Dockerfile or buildpacks and creates the registry for you (&lt;a href="https://cloud.google.com/run/docs/deploying-source-code" rel="noopener noreferrer"&gt;docs&lt;/a&gt;), which is kind. But you set up a billing account and IAM, and your own domain needs a load balancer or Firebase Hosting, because Cloud Run's built-in domain mapping is a preview that Google calls &lt;a href="https://cloud.google.com/run/docs/mapping-custom-domains" rel="noopener noreferrer"&gt;not production-ready&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Three quiet services cost $0 inside the free tier of 180,000 vCPU-seconds and 2 million requests a month (&lt;a href="https://cloud.google.com/run/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). Light Cloud runs on Cloud Run, so this is the same engine with the setup handed back to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. AWS ECS Express Mode
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; AWS shops that want Fargate with the load balancer and TLS done for them.&lt;/p&gt;

&lt;p&gt;Two points. AWS says an Express Mode service needs only three things: &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/express-service-overview.html" rel="noopener noreferrer"&gt;a container image, a task execution role and an infrastructure role&lt;/a&gt;. It then creates the Fargate service, a load balancer with TLS, auto scaling and monitoring. The image has to live in a registry first, and the roles are IAM.&lt;/p&gt;

&lt;p&gt;Express Mode is free; Fargate isn't. The smallest task, 0.25 vCPU with 0.5 GB, is about $8.89 a month around the clock in us-east-1 (&lt;a href="https://aws.amazon.com/fargate/pricing/" rel="noopener noreferrer"&gt;Fargate pricing&lt;/a&gt;), and a load balancer adds about $16.20 before usage charges. Three services sharing one load balancer come to about $43, before the load balancer's usage charges.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Kamal
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Linux-comfortable teams putting containers on cheap servers.&lt;/p&gt;

&lt;p&gt;Three points on one server, four on more. You run the servers, you write config/deploy.yml, and you need a registry, because &lt;code&gt;kamal setup&lt;/code&gt; builds the image, pushes it, and pulls it on each host (&lt;a href="https://kamal-deploy.org/docs/installation/" rel="noopener noreferrer"&gt;installation&lt;/a&gt;). On a single server kamal-proxy gets Let's Encrypt certificates for you; with more than one, you put a load balancer in front yourself.&lt;/p&gt;

&lt;p&gt;What you get back is a lot: zero-downtime deploys to any Ubuntu box with an SSH key, under the MIT license, from the team at 37signals that uses it for their own apps. Kamal's own homepage is candid that you're &lt;a href="https://kamal-deploy.org/" rel="noopener noreferrer"&gt;probably better off with a fully managed service&lt;/a&gt; if basic Linux or Docker is still difficult.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Docker Swarm
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; a real multi-machine cluster for people who already know Docker Compose.&lt;/p&gt;

&lt;p&gt;Three points: machines, service definitions, and a load balancer for outside traffic. Swarm is &lt;a href="https://docs.docker.com/engine/swarm/" rel="noopener noreferrer"&gt;built into Docker Engine&lt;/a&gt;, so there is no extra orchestrator to install, and it gives you service discovery by DNS name, rolling updates and rollbacks. Docker's tutorial uses three Linux hosts with ports 2377, 7946 and 4789 open between them.&lt;/p&gt;

&lt;p&gt;It is the gentlest way to learn what a cluster is. It's also a cluster, with managers, workers and reconciliation, and Docker's own docs call it an advanced feature and suggest plain Compose if you don't plan to run Swarm in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Nomad
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; mixed fleets that schedule containers, Java apps and VMs side by side.&lt;/p&gt;

&lt;p&gt;Three points: machines, job files, and the tools Nomad expects next to it. HashiCorp describes Nomad as a single binary that focuses on scheduling and &lt;a href="https://developer.hashicorp.com/nomad/docs/what-is-nomad" rel="noopener noreferrer"&gt;composes with Consul for service discovery and Vault for secrets&lt;/a&gt;. It schedules more than containers, which no other row here does.&lt;/p&gt;

&lt;p&gt;The license matters for some teams: Nomad Community Edition is under the Business Source License, per &lt;a href="https://developer.hashicorp.com/nomad/docs/ce-license-support" rel="noopener noreferrer"&gt;HashiCorp's own page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the others beat us
&lt;/h2&gt;

&lt;p&gt;Plenty of places, and they're the reasons people pick Kubernetes in the first place. If your services must talk privately, Railway, Render and Fly give you a private network today and we don't. Cron jobs are built into Railway, Render and Coolify, and Render sells background workers as a service type. Once your services are busy around the clock, a server running Kamal or Coolify is far cheaper than our always-on price, and Render's fixed $7 beats our $52. Teams that want the whole system in a reviewed file will prefer Render's Blueprint or a Kamal config to our console. And Nomad runs workloads that aren't containers at all.&lt;/p&gt;

&lt;p&gt;What we do better is the first step and the quiet months. There is nothing to install, no file to learn and no registry, and a service nobody calls costs nothing. For three small services that's $0 on Free, and the &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;Kubernetes alternative for small teams&lt;/a&gt; I'd pick for a first product is the one where you can &lt;a href="https://blog.light-cloud.com/tutorials/deploy-any-app-with-a-dockerfile" rel="noopener noreferrer"&gt;bring your own Dockerfile&lt;/a&gt; and be live before lunch.&lt;/p&gt;

&lt;p&gt;Which of the six points would you happily take on, and which made you leave Kubernetes in the first place?&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>kubernetes</category>
      <category>docker</category>
    </item>
    <item>
      <title>Heroku Alternatives in 2026</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:25:00 +0000</pubDate>
      <link>https://dev.to/lightcloud/heroku-alternatives-in-2026-2f55</link>
      <guid>https://dev.to/lightcloud/heroku-alternatives-in-2026-2f55</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcynzkar6ijmdqwwxmzyy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcynzkar6ijmdqwwxmzyy.webp" alt="Heroku Alternatives in 2026" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your team opens a review app for every pull request, the cheapest &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;Heroku alternative&lt;/a&gt; for a small app is Light Cloud at $5 a month, review apps included, against $16.80 for the same app and four review apps on Heroku. Railway starts at the same $5 and copies the database into each pull request environment, which we don't. If you rely on worker dynos and Heroku Scheduler, look at Render first: it feels the most like Heroku and lands at $16.10.&lt;/p&gt;

&lt;p&gt;We make Light Cloud, the first row below. The method comes before the table so you can check every number on the vendors' own pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why move off Heroku now
&lt;/h2&gt;

&lt;p&gt;Nothing broke. On 2026-02-06 Heroku said it is &lt;a href="https://www.heroku.com/blog/an-update-on-heroku/" rel="noopener noreferrer"&gt;transitioning to a sustaining engineering model&lt;/a&gt; "focused on stability, security, reliability, and support", and that Enterprise contracts will no longer be offered to new customers. Apps keep running, and prices for card-paying customers did not change. What changed is the direction: the platform you deploy to has stopped adding things.&lt;/p&gt;

&lt;p&gt;The money side is older news. Free dynos and free Postgres &lt;a href="https://devcenter.heroku.com/changelog-items/2502" rel="noopener noreferrer"&gt;ended on 2022-11-28&lt;/a&gt;, and you need a verified card before you can create an app. Any dyno scaled above zero bills &lt;a href="https://devcenter.heroku.com/articles/usage-and-billing" rel="noopener noreferrer"&gt;whether or not it gets traffic&lt;/a&gt;; only Eco dynos sleep, and Eco is for personal apps, not team apps. For teams that live on review apps, that means paying for a dyno and a database per pull request while the reviewer is at lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we ranked
&lt;/h2&gt;

&lt;p&gt;The reference app is the one from &lt;a href="https://blog.light-cloud.com/comparisons/where-to-host-a-saas" rel="noopener noreferrer"&gt;where to host a SaaS app&lt;/a&gt;: a React frontend, one Node.js API, a small PostgreSQL database and a custom domain, at about 1,000 API requests a day. On top of it I added the Heroku habit this post is about: four pull requests a month, each with a preview of the frontend and API that stays open for three days.&lt;/p&gt;

&lt;p&gt;On Heroku that costs $12 for the app on a Basic dyno with Essential-0 Postgres (&lt;a href="https://www.heroku.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;), plus four review apps. Review apps are &lt;a href="https://devcenter.heroku.com/articles/github-integration-review-apps" rel="noopener noreferrer"&gt;billed like normal apps&lt;/a&gt;, so a Basic dyno and Essential-0 for three days is about $1.20 each. Total: $16.80.&lt;/p&gt;

&lt;p&gt;For every other platform I priced the cheapest way to get a preview of the frontend and API per pull request, with list prices from the vendor's own pages, checked on 2026-10-13, in US dollars. Whether that preview also gets its own database is in the table, because Heroku review apps can carry one and not every alternative copies it. The second column covers the other two Heroku habits: worker dynos and the Scheduler add-on.&lt;/p&gt;

&lt;p&gt;Kept running all month, with no scale to zero, the same app gets the Always on prices in the table. Light Cloud comes last there at about $56, because an always-on instance needs the $19 Starter plan and a Micro costs about $52 a month. Every other platform here already runs the API around the clock at the price above, so their bills don't move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ranking
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;App + review apps a month&lt;/th&gt;
&lt;th&gt;Always on, a month&lt;/th&gt;
&lt;th&gt;Review apps&lt;/th&gt;
&lt;th&gt;Scheduler and workers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Light Cloud&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;about $56 (Starter, always-on Micro)&lt;/td&gt;
&lt;td&gt;Every branch, shares the production database&lt;/td&gt;
&lt;td&gt;Neither yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;$5 plus usage over $5&lt;/td&gt;
&lt;td&gt;$5 plus usage over $5&lt;/td&gt;
&lt;td&gt;Every pull request, own database&lt;/td&gt;
&lt;td&gt;Cron jobs built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;DigitalOcean App Platform&lt;/td&gt;
&lt;td&gt;$12 plus preview apps&lt;/td&gt;
&lt;td&gt;$12 plus preview apps&lt;/td&gt;
&lt;td&gt;Through GitHub Actions&lt;/td&gt;
&lt;td&gt;Scheduled jobs and workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;$16.10, or $43.50 with full-stack previews&lt;/td&gt;
&lt;td&gt;$16.10&lt;/td&gt;
&lt;td&gt;Per service on the free workspace; full stack on Pro&lt;/td&gt;
&lt;td&gt;Cron jobs and workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Koyeb&lt;/td&gt;
&lt;td&gt;$29 plus usage over $10&lt;/td&gt;
&lt;td&gt;$29 plus usage over $10&lt;/td&gt;
&lt;td&gt;Through GitHub Actions&lt;/td&gt;
&lt;td&gt;Workers; no scheduler documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Fly.io&lt;/td&gt;
&lt;td&gt;about $41&lt;/td&gt;
&lt;td&gt;about $41&lt;/td&gt;
&lt;td&gt;GitHub Action, one app, no database&lt;/td&gt;
&lt;td&gt;Workers; cron is an app you run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;List prices from each vendor's pricing and docs pages, checked 2026-10-13. Four pull requests a month, each previewed for three days. For comparison, the same app with four review apps is $16.80 on Heroku. Taxes and bandwidth beyond included allowances excluded.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Light Cloud
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that open a review app for every pull request and are tired of paying for idle dynos.&lt;/p&gt;

&lt;p&gt;Lite is $5 a month with $5 of usage included, a custom domain and one shared PostgreSQL database (&lt;a href="https://light-cloud.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). The reference app uses about $4.33 of that: $4 for the database, around 20 cents for the API, a few cents of builds. Four previews add about eight cents, because a preview only costs its two builds and then sits at $0 until somebody opens it. That's the trade in &lt;a href="https://light-cloud.com/pricing" rel="noopener noreferrer"&gt;pay-per-use hosting from $5 a month&lt;/a&gt;: no dyno waits for a reviewer.&lt;/p&gt;

&lt;p&gt;You get &lt;a href="https://light-cloud.com/previews" rel="noopener noreferrer"&gt;a review app for every branch&lt;/a&gt;, frontend and API, on every plan including Free, and a pull request comment with the link. Deploys come from GitHub, so &lt;code&gt;git push heroku main&lt;/code&gt; becomes a plain push to your repository and &lt;a href="https://light-cloud.com/deployments" rel="noopener noreferrer"&gt;deploys on every git push&lt;/a&gt; take it from there. The database is &lt;a href="https://light-cloud.com/databases" rel="noopener noreferrer"&gt;managed Postgres in the same console&lt;/a&gt;; moving yours over is a plain-SQL &lt;code&gt;pg_dump&lt;/code&gt; (the import doesn't take the custom format) and an upload of up to 512 MB.&lt;/p&gt;

&lt;p&gt;The gaps a Heroku team will hit first: there is no worker app type and no scheduler, so a &lt;code&gt;worker:&lt;/code&gt; line in your Procfile has nowhere to go. A branch preview uses the production database until you point it at another one, which is not what a Heroku review app with its own Postgres does; &lt;a href="https://blog.light-cloud.com/tutorials/preview-url-for-every-branch" rel="noopener noreferrer"&gt;the preview tutorial&lt;/a&gt; shows the setup. Shared databases run in Belgium only. Containers scale to zero, so a quiet API answers its first request in &lt;a href="https://blog.light-cloud.com/tutorials/microservices-on-light-cloud-part-4-cold-starts" rel="noopener noreferrer"&gt;3.1 to 3.7 seconds&lt;/a&gt;, and an always-on instance needs Starter and costs about $52 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Railway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; review apps that copy the whole app, database included.&lt;/p&gt;

&lt;p&gt;Hobby is $5 a month with $5 of usage included (&lt;a href="https://docs.railway.com/pricing/plans" rel="noopener noreferrer"&gt;plans&lt;/a&gt;), and usage is metered by the resource: $10 per GB of RAM a month and $20 per vCPU a month. Pull request environments &lt;a href="https://docs.railway.com/pricing/faqs" rel="noopener noreferrer"&gt;mirror the production workloads&lt;/a&gt;, Postgres included, and are billed as normal usage while they exist. If the app and its previews stay under about half a GB of RAM on average, the bill stays at $5; busier review apps push it over.&lt;/p&gt;

&lt;p&gt;Railway also has cron jobs (at least 5 minutes apart) and private networking on every plan, and a hard usage limit you can set from $10 (&lt;a href="https://docs.railway.com/pricing/cost-control" rel="noopener noreferrer"&gt;cost control&lt;/a&gt;). It recommends Pro, $20 a month, for commercial apps. Of everything here, it's the closest to Heroku's review apps at our price.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. DigitalOcean App Platform
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; fixed container prices with workers and scheduled jobs.&lt;/p&gt;

&lt;p&gt;A 512 MiB container is $5 a month and a development database $7 (&lt;a href="https://www.digitalocean.com/pricing/app-platform" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;), so the app is $12 with the frontend on the free static tier. Worker components are priced like services, and scheduled jobs are &lt;a href="https://docs.digitalocean.com/products/app-platform/how-to/manage-jobs/" rel="noopener noreferrer"&gt;billed only while they run&lt;/a&gt;, which covers both Heroku habits.&lt;/p&gt;

&lt;p&gt;There is no review app switch. DigitalOcean documents &lt;a href="https://docs.digitalocean.com/products/app-platform/how-to/deploy-from-github-actions/" rel="noopener noreferrer"&gt;a GitHub Actions workflow&lt;/a&gt; that deploys a separate app per pull request and deletes it on merge; each one is an ordinary app billed by the second. The development database has no backups by default, so a real product moves to managed Postgres from $15, and a payment method is needed before you create anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Render
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; the closest feel to Heroku, workers and cron included.&lt;/p&gt;

&lt;p&gt;A Starter web service is $7 a month and the smallest Postgres $6 plus $0.30 per GB (&lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;): $13.30, with the frontend as a free static site. On the free Hobby workspace each service gets its own &lt;a href="https://render.com/docs/service-previews" rel="noopener noreferrer"&gt;pull request previews&lt;/a&gt;, billed at the service's rate, so four API previews for three days add $2.80. That's $16.10, about Heroku's bill, but those previews come without a database of their own. &lt;a href="https://render.com/docs/preview-environments" rel="noopener noreferrer"&gt;Full-stack preview environments&lt;/a&gt; that copy the database need the $25 Pro workspace, which makes it $43.50.&lt;/p&gt;

&lt;p&gt;Render is where a Heroku app maps over with the least rethinking: web services, background workers, cron jobs (minimum $1 a month each), a private network and fixed monthly prices. What its docs don't describe is scaling a paid service to zero while it's idle, or a visitor password for a staging site.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Koyeb
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; worker services on a private network, on one paid plan.&lt;/p&gt;

&lt;p&gt;New users have needed a paid plan and a card since Koyeb &lt;a href="https://www.koyeb.com/blog/koyeb-is-joining-mistral-ai-to-build-the-future-of-ai-infrastructure" rel="noopener noreferrer"&gt;announced it is joining Mistral AI&lt;/a&gt; on 2026-02-17. Pro is $29 a month with $10 of compute included (&lt;a href="https://www.koyeb.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;), so the reference app is $29 plus whatever passes $10. Workers are a service type, services talk over a private mesh, and scale to zero is in public preview with a 1 to 5 second cold start.&lt;/p&gt;

&lt;p&gt;Previews per pull request come from a &lt;a href="https://www.koyeb.com/tutorials/deploy-preview-environments-on-koyeb-for-github-pull-requests" rel="noopener noreferrer"&gt;2023 GitHub Actions tutorial&lt;/a&gt;, not a setting. I found no scheduled-job feature in the docs, and Koyeb says spending limits are still in the works.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Fly.io
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; apps that should run close to users in several regions.&lt;/p&gt;

&lt;p&gt;Machines are cheap, from $2.19 a month always on (&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;), and can stop when idle. Postgres is the expensive part: Managed Postgres starts at $38 plus $0.28 per GB (&lt;a href="https://docs.fly.io/postgres" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). With four preview machines for three days each, about $41.&lt;/p&gt;

&lt;p&gt;Fly's review apps come from a GitHub Action that deploys &lt;a href="https://docs.fly.io/blueprints/review-apps-guide" rel="noopener noreferrer"&gt;one app without other resources&lt;/a&gt;, so a database per pull request is your own workflow to write. Workers are just Machines; scheduled jobs run through Cron Manager, which you &lt;a href="https://docs.fly.io/blueprints/task-scheduling" rel="noopener noreferrer"&gt;deploy as its own Fly app&lt;/a&gt;. No free tier for new accounts, and no billing alerts yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the others beat us
&lt;/h2&gt;

&lt;p&gt;Worker dynos and Heroku Scheduler are the big ones. If your Procfile has a &lt;code&gt;worker:&lt;/code&gt; line or your app depends on a nightly job, Render, Railway and DigitalOcean run those today and we can't. A review app with its own copy of Postgres is a Railway feature at our price, and a Render one on Pro. If your users are on several continents, Fly is the better home. And if your company buys software through procurement, Upsun is worth a call: &lt;a href="https://upsun.com/pricing/" rel="noopener noreferrer"&gt;its pricing page&lt;/a&gt; lists full-stack preview environments for every branch, a per-project fee and per-user licenses. I left it out of the table because I couldn't find its smallest app size next to the prices, so I can't price the reference app there.&lt;/p&gt;

&lt;p&gt;Where we come out ahead is the bill for waiting. On Heroku a review app is a dyno and a database that run for as long as the pull request is open, and most of that time nobody is looking. Here the preview of every branch costs its build and then nothing, the API scales to zero when traffic stops, and a five-dollar plan covers the app, the database and the domain. If your Heroku invoice is mostly review apps, that's the comparison to run.&lt;/p&gt;

&lt;p&gt;What's the first thing in your Procfile you'd have to change?&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>heroku</category>
      <category>hosting</category>
    </item>
    <item>
      <title>Free Hosting for Startups</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:11:11 +0000</pubDate>
      <link>https://dev.to/lightcloud/free-hosting-for-startups-55gc</link>
      <guid>https://dev.to/lightcloud/free-hosting-for-startups-55gc</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5lup6qpfc3hz6ge8qxzq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5lup6qpfc3hz6ge8qxzq.webp" alt="Free Hosting for Startups" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cloudflare stays free the longest for a startup whose app fits in Workers and static assets: custom domains, previews and extra people cost nothing, and the first bill is usually a $5 Postgres database. Light Cloud's first bill is also $5, and that $5 covers your own domain, the database and an API in any language, all in. Railway's step is $5 plus usage, Netlify's $9, and Vercel's is $20 per founder the day you take a payment.&lt;/p&gt;

&lt;p&gt;We make Light Cloud, the second row below. It isn't first here, and the method explains why.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we ranked
&lt;/h2&gt;

&lt;p&gt;Every free plan ends the same way: the startup needs something the plan doesn't include. So this ranking asks two questions per platform. What forces the first bill, and how big is it?&lt;/p&gt;

&lt;p&gt;The reference startup is small and ordinary: a frontend, one API, a PostgreSQL database and its own domain, with one founder deploying and about 1,000 API requests a day. Each price is the first monthly bill once the free plan no longer covers that stack, from each vendor's own pricing and docs pages, checked on 2026-10-13, in US dollars. Ties go to the platform whose free plan lasts longer before the bill arrives.&lt;/p&gt;

&lt;p&gt;A third column covers the moment a co-founder starts deploying, because that is the second bill most startups meet. Commercial use matters too: one platform's free plan bans it outright, and it changes the answer for that row.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ranking
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;First bill a month&lt;/th&gt;
&lt;th&gt;What forces the first bill&lt;/th&gt;
&lt;th&gt;Second founder&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Cloudflare&lt;/td&gt;
&lt;td&gt;$5, Postgres from PlanetScale&lt;/td&gt;
&lt;td&gt;Postgres, or 100,000 requests a day&lt;/td&gt;
&lt;td&gt;Free, no seat fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Light Cloud&lt;/td&gt;
&lt;td&gt;$5, all in&lt;/td&gt;
&lt;td&gt;Your own domain or a database&lt;/td&gt;
&lt;td&gt;Starter, $19 a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;$5 plus usage over $5&lt;/td&gt;
&lt;td&gt;Your own domain, or a second project&lt;/td&gt;
&lt;td&gt;Pro, $20 a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Google Cloud Run&lt;/td&gt;
&lt;td&gt;about $8, Cloud SQL&lt;/td&gt;
&lt;td&gt;A database; card from day one&lt;/td&gt;
&lt;td&gt;IAM, no seat fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;td&gt;$9, Personal plan&lt;/td&gt;
&lt;td&gt;300 credits, about 20 production deploys&lt;/td&gt;
&lt;td&gt;Pro, $20 a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;DigitalOcean App Platform&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;td&gt;Any backend; card for anything&lt;/td&gt;
&lt;td&gt;No seat fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;$13.30&lt;/td&gt;
&lt;td&gt;Free Postgres expires after 30 days&lt;/td&gt;
&lt;td&gt;Pro, $25 a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;$20 per deploying seat&lt;/td&gt;
&lt;td&gt;Commercial use, such as taking payments&lt;/td&gt;
&lt;td&gt;$20 per deploying seat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;List prices from each vendor's pricing and docs pages, checked 2026-10-13. Reference startup: frontend, one API, PostgreSQL, its own domain, one founder deploying, about 1,000 API requests a day. Taxes and usage beyond included allowances excluded.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cloudflare
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; startups whose app fits in Workers and static assets.&lt;/p&gt;

&lt;p&gt;Cloudflare's free plan is the most generous here if your code runs as a Worker. Workers Free allows 100,000 requests a day, static asset requests are &lt;a href="https://developers.cloudflare.com/workers/platform/pricing/" rel="noopener noreferrer"&gt;free and unlimited&lt;/a&gt;, and a Pages project gets 100 custom domains and unlimited preview deployments on the free plan (&lt;a href="https://developers.cloudflare.com/pages/platform/limits/" rel="noopener noreferrer"&gt;limits&lt;/a&gt;). Cloudflare also says a Pages site can be managed by an unlimited number of users, so the co-founder costs nothing.&lt;/p&gt;

&lt;p&gt;The first bill usually comes from the database. Cloudflare's own D1 is SQLite, not Postgres; since 2026-06-18 you can create a &lt;a href="https://developers.cloudflare.com/changelog/post/2026-06-18-planetscale-databases-cloudflare-billing/" rel="noopener noreferrer"&gt;PlanetScale Postgres database billed on your Cloudflare invoice&lt;/a&gt;, and PlanetScale's single node &lt;a href="https://planetscale.com/pricing" rel="noopener noreferrer"&gt;starts at $5 a month&lt;/a&gt;. If D1 is enough for you, the bill stays at $0 for a long time.&lt;/p&gt;

&lt;p&gt;The limits are real, though. A Worker gets 10 ms of CPU per request on the free plan, and past 100,000 requests a day requests fail with Error 1027 until midnight UTC (&lt;a href="https://developers.cloudflare.com/workers/platform/limits/" rel="noopener noreferrer"&gt;limits&lt;/a&gt;). Running a container needs Workers Paid, a $5 minimum. And Cloudflare's budget alerts only notify; they never cap spend.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Light Cloud
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; the smallest first bill that covers a container API in any language, Postgres and your domain.&lt;/p&gt;

&lt;p&gt;Free costs $0 with no card: unlimited static sites, 3 server apps and a preview URL for every branch, with $1 of usage a month (&lt;a href="https://light-cloud.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). It never bills you. When the $1 runs out, server apps pause until next month and static sites keep serving. The catch for a startup is plain: Free has no custom domain and no databases, and free pages carry a small "by Light Cloud" link.&lt;/p&gt;

&lt;p&gt;So the first bill comes early, the day you point your own domain at the app or create a database, and it is Lite at $5 a month. That $5 includes $5 of usage, a &lt;a href="https://light-cloud.com/domains" rel="noopener noreferrer"&gt;custom domain with automatic SSL&lt;/a&gt; and one shared PostgreSQL database. The reference startup uses about $4.25 of it: $4 for the database, about 20 cents for the API and a few cents of builds. Your API can be Node, Python, Go, Java or .NET, from &lt;a href="https://light-cloud.com/deployments" rel="noopener noreferrer"&gt;git push deploys&lt;/a&gt; or a Dockerfile.&lt;/p&gt;

&lt;p&gt;Where it falls behind in this ranking: the second bill. Lite has one team member, so a co-founder who deploys means Starter at $19 a month. There are no cron jobs, workers or private networking, the shared database runs in Belgium only, and a scaled-to-zero API takes &lt;a href="https://blog.light-cloud.com/tutorials/microservices-on-light-cloud-part-4-cold-starts" rel="noopener noreferrer"&gt;3.1 to 3.7 seconds&lt;/a&gt; to answer its first request after a quiet spell.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Railway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; a $5 first step with cron jobs and private networking built in.&lt;/p&gt;

&lt;p&gt;After a 30-day trial with $5 of credit, Railway's Free plan gives $1 of credit a month, one project, three services and no custom domains (&lt;a href="https://docs.railway.com/pricing/free-trial" rel="noopener noreferrer"&gt;free trial docs&lt;/a&gt;). No card is needed. The first bill comes with your own domain or a second project: Hobby at $5 a month, with $5 of usage included (&lt;a href="https://docs.railway.com/pricing/plans" rel="noopener noreferrer"&gt;plans&lt;/a&gt;). Postgres is billed as usage like any other service.&lt;/p&gt;

&lt;p&gt;Railway has extras we don't: cron jobs, private networking and pull request environments on every plan. Against it: Hobby is a single-developer workspace, and Railway &lt;a href="https://docs.railway.com/pricing/faqs" rel="noopener noreferrer"&gt;recommends Pro, $20 a month, for commercial applications&lt;/a&gt;, which a startup with paying customers is.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Google Cloud Run
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams already in Google Cloud.&lt;/p&gt;

&lt;p&gt;The compute is free for a long time. Cloud Run's free tier covers 180,000 vCPU-seconds and 2 million requests a month (&lt;a href="https://cloud.google.com/run/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;), far more than the reference API needs. But you need a billing account with a card to use it, and anything over the free tier is billed, not paused.&lt;/p&gt;

&lt;p&gt;The database is the first bill: Cloud SQL's smallest shared-core instance is $0.0105 an hour, about $8 a month before storage (&lt;a href="https://cloud.google.com/sql/pricing" rel="noopener noreferrer"&gt;Cloud SQL pricing&lt;/a&gt;). Your own domain takes setup, because Cloud Run's &lt;a href="https://cloud.google.com/run/docs/mapping-custom-domains" rel="noopener noreferrer"&gt;domain mapping is in preview&lt;/a&gt; and Google calls it not production-ready. Since 2026-10-12, a spend cap can pause Cloud Run at a budget. People are managed through IAM, with no seat fee.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Netlify
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; frontend-heavy startups.&lt;/p&gt;

&lt;p&gt;Netlify's free plan covers a lot of the reference startup: custom domains with SSL, functions, unlimited deploy previews and Netlify Database, a managed Postgres (&lt;a href="https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/" rel="noopener noreferrer"&gt;plans&lt;/a&gt;). It is never billed. But it runs on 300 credits a month, and each production deploy costs 15 of them, so about 20 deploys use the lot. When credits run out, every project on the team pauses until next month.&lt;/p&gt;

&lt;p&gt;The first bill is Personal at $9 for 1,000 credits. A co-founder can't join Free or Personal at all; Netlify's pricing page says those plans &lt;a href="https://www.netlify.com/pricing/" rel="noopener noreferrer"&gt;cannot add team member seats&lt;/a&gt;, so the second step is Pro at $20 a month with unlimited members. Password protection is a Pro feature too.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. DigitalOcean App Platform
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; fixed monthly prices from the first bill.&lt;/p&gt;

&lt;p&gt;The free tier is three static sites (&lt;a href="https://www.digitalocean.com/pricing/app-platform" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). Any backend is paid, and you need a payment method to create anything at all, even a free static site. The reference startup is a $5 container (512 MiB) plus a $7 development database: $12. There is no seat fee as the team grows.&lt;/p&gt;

&lt;p&gt;The development database has no backups by default and is deleted with the app, so a real product moves to managed Postgres at about $15 soon after.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Render
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; predictable per-service prices.&lt;/p&gt;

&lt;p&gt;Render's free tier needs no card and includes free web services and a free Postgres database (&lt;a href="https://render.com/docs/free" rel="noopener noreferrer"&gt;free tier docs&lt;/a&gt;). Two things force the first bill. Free web services spin down after 15 minutes without traffic and take about a minute to wake, and the free database expires 30 days after it is created. A Starter web service is $7 and the smallest Postgres $6 plus $0.30 per GB: $13.30.&lt;/p&gt;

&lt;p&gt;The free workspace has one team member. A co-founder means the Pro workspace, $25 a month flat with unlimited seats, plus compute.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Vercel
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Next.js startups that will pay per founder from the first payment.&lt;/p&gt;

&lt;p&gt;Vercel's free Hobby plan is generous and includes custom domains, but its &lt;a href="https://vercel.com/docs/limits/fair-use-guidelines" rel="noopener noreferrer"&gt;fair use guidelines&lt;/a&gt; restrict it to non-commercial personal use, and count a paid developer or taking payments as commercial. For a startup that is the trigger, whatever the traffic. Pro is $20 a month per deploying seat with $20 of usage included (&lt;a href="https://vercel.com/docs/plans/pro-plan" rel="noopener noreferrer"&gt;Pro plan&lt;/a&gt;), and Postgres comes from a Marketplace partner, billed by them. Two founders deploying is $40.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the others beat us
&lt;/h2&gt;

&lt;p&gt;Cloudflare beats us outright on staying free: your own domain, previews and a co-founder all cost nothing there, while our Free plan has no custom domain at all. If your app is JavaScript and fits in a Worker, start there. Netlify's free plan includes a custom domain and a database, which ours doesn't. Railway's $5 buys cron jobs and private networking we don't have. And DigitalOcean, Cloud Run and Cloudflare never charge for a second person, while our Lite plan stops at one.&lt;/p&gt;

&lt;p&gt;Where we come out ahead is the shape of the first step. It is $5, it covers a container in whatever language the API is written in, a database and your domain on one invoice, and nothing on the free plan can ever bill you. If that's your startup's stack, our &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;free hosting for startups&lt;/a&gt; turns into a $5 bill the day you need more, with &lt;a href="https://light-cloud.com/databases" rel="noopener noreferrer"&gt;a Postgres database ready in seconds&lt;/a&gt; included. A password on staging stays free on every plan; &lt;a href="https://blog.light-cloud.com/tutorials/password-protect-a-staging-site" rel="noopener noreferrer"&gt;the staging tutorial&lt;/a&gt; takes five minutes. For the full monthly picture after launch, see &lt;a href="https://blog.light-cloud.com/comparisons/where-to-host-a-saas" rel="noopener noreferrer"&gt;where to host a SaaS app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which limit hit your startup first: the domain, the database or the second person?&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>startup</category>
      <category>hosting</category>
    </item>
    <item>
      <title>Bring Your Own Dockerfile: Deploy Anything That Runs in a Container</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Thu, 15 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/lightcloud/bring-your-own-dockerfile-deploy-anything-that-runs-in-a-container-4k3l</link>
      <guid>https://dev.to/lightcloud/bring-your-own-dockerfile-deploy-anything-that-runs-in-a-container-4k3l</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0cngdlknaxk9d177cczu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0cngdlknaxk9d177cczu.webp" alt="Bring Your Own Dockerfile: Deploy Anything That Runs in a Container" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To deploy any app on Light Cloud with your own Dockerfile, put the &lt;code&gt;Dockerfile&lt;/code&gt; at the root of the repository, &lt;code&gt;EXPOSE&lt;/code&gt; the port your app listens on, read that port from the &lt;code&gt;PORT&lt;/code&gt; environment variable, push to GitHub and click &lt;strong&gt;Deploy&lt;/strong&gt;. Light Cloud builds your Dockerfile as it is, with a layer cache that covers every stage, and runs the image. Anything that runs in a Linux container can be deployed this way.&lt;/p&gt;

&lt;p&gt;In my run a Rust service was live 3 minutes after the first click, and a new instance answered 0.13 seconds after it started.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;A small word-counting API written in Rust with &lt;a href="https://github.com/tokio-rs/axum" rel="noopener noreferrer"&gt;axum&lt;/a&gt;, a language Light Cloud does not build on its own. The Dockerfile makes it deployable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /count&lt;/code&gt; returns the characters, words and lines of a text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /health&lt;/code&gt; answers &lt;code&gt;{"status":"ok"}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A two-stage Dockerfile compiles in a large Rust image and ships only the binary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/light-cloud-com/tutorial-dockerfile-app" rel="noopener noreferrer"&gt;github.com/light-cloud-com/tutorial-dockerfile-app&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to bring your own Dockerfile
&lt;/h2&gt;

&lt;p&gt;Light Cloud builds Node.js, Python, Go, Java, .NET, PHP, Ruby and static sites without one. Bring your own when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your language is not on that list (Rust, Elixir, Deno, C++, ...);&lt;/li&gt;
&lt;li&gt;you need system packages, such as &lt;code&gt;ffmpeg&lt;/code&gt; or a PDF renderer;&lt;/li&gt;
&lt;li&gt;you already have a Dockerfile that works and want exactly that image in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The four rules
&lt;/h2&gt;

&lt;p&gt;Whatever is inside, a Dockerfile that works on Light Cloud follows four rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listen on &lt;code&gt;PORT&lt;/code&gt;, on &lt;code&gt;0.0.0.0&lt;/code&gt;.&lt;/strong&gt; Light Cloud sets &lt;code&gt;PORT&lt;/code&gt;; &lt;code&gt;localhost&lt;/code&gt; inside a container is not reachable from outside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPOSE&lt;/code&gt; that port.&lt;/strong&gt; Light Cloud reads it to know where to send traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop cleanly on &lt;code&gt;SIGTERM&lt;/code&gt;.&lt;/strong&gt; That is how an old instance is told to go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log to standard output.&lt;/strong&gt; One JSON object per line with a &lt;code&gt;severity&lt;/code&gt; gets the right level in the &lt;strong&gt;Logs&lt;/strong&gt; tab.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rustup.rs/" rel="noopener noreferrer"&gt;Rust&lt;/a&gt; 1.85 or later, for the 2024 edition; I used 1.97.1. Only needed to run the app on your machine: the build happens in Docker.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/downloads" rel="noopener noreferrer"&gt;Git&lt;/a&gt; and a &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; account.&lt;/li&gt;
&lt;li&gt;A free Light Cloud account at &lt;a href="https://console.light-cloud.com" rel="noopener noreferrer"&gt;console.light-cloud.com&lt;/a&gt; with GitHub connected.&lt;/li&gt;
&lt;li&gt;A terminal with &lt;a href="https://curl.se/" rel="noopener noreferrer"&gt;curl&lt;/a&gt;. On Windows, use &lt;a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows" rel="noopener noreferrer"&gt;PowerShell 7&lt;/a&gt; and &lt;a href="https://curl.se/windows/" rel="noopener noreferrer"&gt;&lt;code&gt;curl.exe&lt;/code&gt;&lt;/a&gt;, which ships with Windows 10 and 11.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the app
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo new tutorial-dockerfile-app
&lt;span class="go"&gt;    Creating binary (application) `tutorial-dockerfile-app` package
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;tutorial-dockerfile-app
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo add axum tokio &lt;span class="nt"&gt;--features&lt;/span&gt; tokio/full
&lt;span class="go"&gt;      Adding axum v0.8.9 to dependencies
      Adding tokio v1.53.1 to dependencies
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo add serde &lt;span class="nt"&gt;--features&lt;/span&gt; derive
&lt;span class="go"&gt;      Adding serde v1.0.229 to dependencies
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo add serde_json
&lt;span class="go"&gt;      Adding serde_json v1.0.151 to dependencies


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo new tutorial-dockerfile-app
&lt;span class="go"&gt;    Creating binary (application) `tutorial-dockerfile-app` package
&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;tutorial-dockerfile-app
&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo add axum tokio &lt;span class="nt"&gt;--features&lt;/span&gt; tokio/full
&lt;span class="go"&gt;      Adding axum v0.8.9 to dependencies
      Adding tokio v1.53.1 to dependencies
&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo add serde &lt;span class="nt"&gt;--features&lt;/span&gt; derive
&lt;span class="go"&gt;      Adding serde v1.0.229 to dependencies
&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo add serde_json
&lt;span class="go"&gt;      Adding serde_json v1.0.151 to dependencies

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is trimmed. &lt;code&gt;cargo new&lt;/code&gt; also creates a Git repository with a &lt;code&gt;.gitignore&lt;/code&gt; for &lt;code&gt;/target&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Write the service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;axum&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;http&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;}};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;unix&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;SignalKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;CountRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Serialize)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;CountResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// One JSON object per line; the Logs tab reads "severity" and "message".&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"word counter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"try"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"POST /count with {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"ok"&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CountRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CountResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="py"&gt;.text&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PAYLOAD_TOO_LARGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"text is longer than 100000 bytes"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CountResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="py"&gt;.text&lt;/span&gt;&lt;span class="nf"&gt;.chars&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="py"&gt;.text&lt;/span&gt;&lt;span class="nf"&gt;.split_whitespace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="py"&gt;.text&lt;/span&gt;&lt;span class="nf"&gt;.lines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Light Cloud passes the port in PORT; 8080 matches EXPOSE in the Dockerfile.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PORT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or_else&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"8080"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;health&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;net&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;TcpListener&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0.0.0.0:{port}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"listening on port {port}"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Finish running requests when Light Cloud stops the instance with SIGTERM.&lt;/span&gt;
    &lt;span class="nn"&gt;axum&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.with_graceful_shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SignalKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SIGTERM received, shutting down"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules 1, 3 and 4 are in the code: it binds &lt;code&gt;0.0.0.0&lt;/code&gt; on &lt;code&gt;PORT&lt;/code&gt;, it waits for &lt;code&gt;SIGTERM&lt;/code&gt; to shut down, and it prints JSON log lines with a &lt;code&gt;severity&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write the Dockerfile
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: compile. The Rust toolchain is large, but it never reaches production.&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;rust:1.97-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Build the dependencies first, with an empty main.rs, so this slow layer is&lt;/span&gt;
&lt;span class="c"&gt;# cached and only rebuilt when Cargo.toml or Cargo.lock change.&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Cargo.toml Cargo.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;src &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fn main() {}"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; src/main.rs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; src

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src ./src&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;src/main.rs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;

&lt;span class="c"&gt;# Stage 2: run. Only the binary goes into a small Debian image.&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; debian:bookworm-slim&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; 10001 app
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/target/release/tutorial-dockerfile-app /usr/local/bin/app&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; app&lt;/span&gt;

&lt;span class="c"&gt;# Light Cloud reads EXPOSE to know which port to send traffic to.&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT=8080&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["app"]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three habits in there are worth copying into any Dockerfile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two stages.&lt;/strong&gt; The build stage has the compiler and every build tool; the final image has the binary and nothing else. The image you run is small and has less in it to attack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies before source.&lt;/strong&gt; Copying only &lt;code&gt;Cargo.toml&lt;/code&gt; and &lt;code&gt;Cargo.lock&lt;/code&gt; first, and building them on their own, puts the slow dependency build in a layer that survives code changes. The same pattern works as &lt;code&gt;package.json&lt;/code&gt; then &lt;code&gt;npm ci&lt;/code&gt;, or &lt;code&gt;requirements.txt&lt;/code&gt; then &lt;code&gt;pip install&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A non-root user.&lt;/strong&gt; &lt;code&gt;USER app&lt;/code&gt; means a bug in the app cannot write to the system. Port 8080 needs no special rights.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the build context small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target
.git

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Run it on your machine
&lt;/h2&gt;

&lt;p&gt;You do not need Docker for this: the same code runs with Cargo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cargo run --release
    Finished `release` profile [optimized] target(s) in 1.80s
     Running `target/release/tutorial-dockerfile-app`
{"message":"listening on port 8080","severity":"INFO"}


PS&amp;gt; wsl
$ cargo run --release
    Finished `release` profile [optimized] target(s) in 1.80s
     Running `target/release/tutorial-dockerfile-app`
{"message":"listening on port 8080","severity":"INFO"}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is trimmed; the first run also compiles every dependency. The &lt;code&gt;SIGTERM&lt;/code&gt; handler uses Unix signals, which is right for a container that runs on Linux, so on Windows run the app inside &lt;a href="https://learn.microsoft.com/en-us/windows/wsl/install" rel="noopener noreferrer"&gt;WSL&lt;/a&gt; (with Rust installed there) or only in Docker. Stopping it with Ctrl+C sends &lt;code&gt;SIGINT&lt;/code&gt;, not &lt;code&gt;SIGTERM&lt;/code&gt;, so you will not see the shutdown line locally. From a second terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -X POST http://localhost:8080/count -H "content-type: application/json" -d '{"text":"Bring your own Dockerfile.\nDeploy anything."}'
{"characters":43,"words":6,"lines":2}


PS&amp;gt; curl.exe -X POST http://localhost:8080/count -H "content-type: application/json" -d '{"text":"Bring your own Dockerfile.\nDeploy anything."}'
{"characters":43,"words":6,"lines":2}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have Docker, &lt;code&gt;docker build -t word-counter .&lt;/code&gt; and &lt;code&gt;docker run -p 8080:8080 word-counter&lt;/code&gt; run exactly what Light Cloud will run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Push and deploy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git add &lt;span class="nt"&gt;-A&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Rust word counter with a multi-stage Dockerfile"&lt;/span&gt;
&lt;span class="c"&gt;# Create an empty repository named tutorial-dockerfile-app on github.com/new first.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git remote add origin https://github.com/YOUR-USERNAME/tutorial-dockerfile-app.git
&lt;span class="nv"&gt;$ &lt;/span&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
To https://github.com/YOUR-USERNAME/tutorial-dockerfile-app.git
 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;new branch] main -&amp;gt; main
branch &lt;span class="s1"&gt;'main'&lt;/span&gt; &lt;span class="nb"&gt;set &lt;/span&gt;up to track &lt;span class="s1"&gt;'origin/main'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;


PS&amp;gt; git add &lt;span class="nt"&gt;-A&lt;/span&gt;
PS&amp;gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Rust word counter with a multi-stage Dockerfile"&lt;/span&gt;
&lt;span class="c"&gt;# Create an empty repository named tutorial-dockerfile-app on github.com/new first.&lt;/span&gt;
PS&amp;gt; git remote add origin https://github.com/YOUR-USERNAME/tutorial-dockerfile-app.git
PS&amp;gt; git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
To https://github.com/YOUR-USERNAME/tutorial-dockerfile-app.git
 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;new branch] main -&amp;gt; main
branch &lt;span class="s1"&gt;'main'&lt;/span&gt; &lt;span class="nb"&gt;set &lt;/span&gt;up to track &lt;span class="s1"&gt;'origin/main'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;a href="https://console.light-cloud.com" rel="noopener noreferrer"&gt;Light Cloud console&lt;/a&gt;, click &lt;strong&gt;New...&lt;/strong&gt; , then &lt;strong&gt;Deploy from GitHub&lt;/strong&gt; , and pick the repository. Light Cloud shows &lt;strong&gt;Dockerfile / Backend&lt;/strong&gt; and &lt;strong&gt;Runs on a server&lt;/strong&gt; : it found no framework it builds itself, and a Dockerfile that says how to build this one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsoxcb7cu4x20qh5e99x1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsoxcb7cu4x20qh5e99x1.webp" alt="The detection banner Dockerfile Backend, based on Dockerfile, with Runs on a server highlighted" width="799" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port&lt;/strong&gt; under &lt;strong&gt;Advanced&lt;/strong&gt; is &lt;code&gt;8080&lt;/code&gt;, read from &lt;code&gt;EXPOSE&lt;/code&gt;. Click &lt;strong&gt;Deploy&lt;/strong&gt;. The first build compiles every dependency; in my run the service was live 3 minutes 17 seconds after the click.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F139dapx64c1qzfwn0w1u.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F139dapx64c1qzfwn0w1u.webp" alt="The Production overview of tutorial-dockerfile-app with the Deployed badge and the URL card highlighted" width="799" height="333"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://main-tutorial-dockerfile-app-yourworkspace.light-cloud.io/count &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"text":"Bring your own Dockerfile.\nDeploy anything."}'&lt;/span&gt;
&lt;span class="go"&gt;{"characters":43,"words":6,"lines":2}


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl.exe &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://main-tutorial-dockerfile-app-yourworkspace.light-cloud.io/count &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"text":"Bring your own Dockerfile.\nDeploy anything."}'&lt;/span&gt;
&lt;span class="go"&gt;{"characters":43,"words":6,"lines":2}

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: See the layer cache at work
&lt;/h2&gt;

&lt;p&gt;Change something in &lt;code&gt;src/main.rs&lt;/code&gt;, commit and push. &lt;strong&gt;Deploy on push&lt;/strong&gt; rebuilds the image, and this time the dependency layers come from the cache. From the build logs of two pushes in a row:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;Dependency stage&lt;/th&gt;
&lt;th&gt;App compile&lt;/th&gt;
&lt;th&gt;Whole build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First, empty cache&lt;/td&gt;
&lt;td&gt;51.2 s&lt;/td&gt;
&lt;td&gt;7.5 s&lt;/td&gt;
&lt;td&gt;1 min 55 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After a code change&lt;/td&gt;
&lt;td&gt;cached&lt;/td&gt;
&lt;td&gt;8.0 s&lt;/td&gt;
&lt;td&gt;1 min 12 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cache covers every stage of the Dockerfile, including the builder stage that never ships. That is why the order of &lt;code&gt;COPY&lt;/code&gt; lines matters: everything above the first changed file is reused.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Logs&lt;/strong&gt; tab the rollout is visible too: the new instance logs &lt;code&gt;listening on port 8080&lt;/code&gt; 0.13 seconds after it starts, and the old one logs &lt;code&gt;SIGTERM received, shutting down&lt;/code&gt; before it goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The deploy fails the health check: the container did not listen on the port
&lt;/h3&gt;

&lt;p&gt;The app listens on &lt;code&gt;127.0.0.1&lt;/code&gt; or on a fixed port. Bind &lt;code&gt;0.0.0.0&lt;/code&gt; and read &lt;code&gt;PORT&lt;/code&gt;. Check that &lt;code&gt;EXPOSE&lt;/code&gt; matches what the app uses when &lt;code&gt;PORT&lt;/code&gt; is not set.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>tutorial</category>
      <category>docker</category>
    </item>
    <item>
      <title>Serverless Was a Pricing Model, Not an Architecture</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:36:41 +0000</pubDate>
      <link>https://dev.to/lightcloud/serverless-was-a-pricing-model-not-an-architecture-5bme</link>
      <guid>https://dev.to/lightcloud/serverless-was-a-pricing-model-not-an-architecture-5bme</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftle7nsgwk4wp1kqdtr8g.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftle7nsgwk4wp1kqdtr8g.jpg" alt="Serverless Was a Pricing Model, Not an Architecture" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In November 2014, AWS &lt;a href="https://en.wikipedia.org/wiki/AWS_Lambda" rel="noopener noreferrer"&gt;introduced Lambda&lt;/a&gt; with a sentence developers had waited a decade to hear: run code without thinking about servers. The fine print arrived immediately after the applause. Your code had to be a stateless function. It had to finish inside five minutes, a cap that &lt;a href="https://www.amazonaws.cn/en/new/2018/aws-lambda-enables-functions-that-can-run-up-to-15-minutes/" rel="noopener noreferrer"&gt;stood until October 10, 2018&lt;/a&gt;, when it grew to a still-arbitrary fifteen. It woke up cold, sometimes slowly, at exactly the moments you were being watched. And it billed per request and per gigabyte-second, which was the one part of the announcement that was genuinely new.&lt;/p&gt;

&lt;p&gt;Here's the thesis, a decade on: serverless was never an architecture. It was a pricing model, the first mainstream compute bill that hit zero when idle, and the architecture everyone spent years learning was just the container that pricing shipped in. Once you see which part was the invention and which part was the packaging, the last ten years of system design read differently, and so does what comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing for the meter
&lt;/h2&gt;

&lt;p&gt;Watch what teams actually built, because the meter's fingerprints are on all of it. Functions couldn't hold state, so state moved into queues, tables, and event buses, and simple call chains became distributed choreography with a dashboard per hop. Functions couldn't run long, so workflow services existed to stitch fifteen-minute fragments into things that were, functionally, long-running processes wearing a disguise. Cold starts punished the request path, so teams paid for provisioned concurrency, which is a fee for making scale-to-zero not scale to zero. Every one of these patterns has a euphemistic name and a conference talk, and every one is a workaround for a constraint that existed to make the billing granularity possible, at 2014's technology, on 2014's virtualization.&lt;/p&gt;

&lt;p&gt;None of that was malice. It was the meter shaping the buildings, the way &lt;a href="https://blog.light-cloud.com/industry/cloud-pricing-is-a-business-model" rel="noopener noreferrer"&gt;pricing always shapes architecture&lt;/a&gt; when the two live together long enough, and it produced systems whose complexity was justified by a bill rather than by a requirement. "We went serverless" too often meant "we refactored a web app into forty functions to make our idle time free", which is a trade with a name in any other market: paying with labor for a discount.&lt;/p&gt;

&lt;p&gt;In fairness to 2014, the constraints began as physics rather than ideology. Billing per request required isolation cheap enough to spin up per request, and the virtualization of the day couldn't do that for arbitrary long-running processes, so the function shape was an honest engineering compromise. AWS then built Firecracker, its microVM layer, precisely to make that isolation nearly free, and the compromise quietly expired. The constraints outlived their reason, the way constraints do once a generation of best practices has calcified around them and a certification industry depends on their permanence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell
&lt;/h2&gt;

&lt;p&gt;The proof that the architecture was packaging arrived when the pricing escaped it. Billing by the second, scaling to zero, paying nothing while idle: all of that turned out to be perfectly possible for ordinary containers running ordinary processes with ordinary state and no duration caps, which is how &lt;a href="https://blog.light-cloud.com/dev-tools/anatomy-of-a-3-cent-preview-environment" rel="noopener noreferrer"&gt;a preview environment costs cents&lt;/a&gt; and how a &lt;a href="https://blog.light-cloud.com/cloud/same-app-four-platforms" rel="noopener noreferrer"&gt;normal full-stack app bills a few dollars&lt;/a&gt; without being rewritten into event confetti. The moment scale-to-zero economics existed without the function shape, the function shape stopped being the price of admission, and the interesting question became visible: if you could have had the bill without the contortions, what were the contortions for?&lt;/p&gt;

&lt;p&gt;That's what "serverless" should have meant from the beginning, and what we've built our platform around: the pricing revolution applied to ordinary software. Your app, your framework, your process model, billed like Lambda, shaped like nothing in particular. The revolution was in the meter. It was always in the meter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The steelman: the constraints were the point
&lt;/h2&gt;

&lt;p&gt;The strongest defense of classic serverless deserves a fair paragraph, because it's partly right. Event-driven functions are genuinely the correct architecture for a real class of work: webhooks, glue code, bursty parallel jobs, anything that's naturally a small reaction to an event, and forcing statelessness imposed a discipline that made a generation of systems more crash-tolerant than their authors would have chosen to build. Lambda also pioneered the operational bar, patching, capacity, isolation as someone else's problem, that everything since gets measured against. The people who loved it weren't wrong about what they loved.&lt;/p&gt;

&lt;p&gt;Conceded, and the concession has a boundary: a good pattern for reactive glue was sold as a destination architecture for everything, and the selling worked because the bill was irresistible and came bundled. The sin was never the function; it was teaching a decade of developers that the meter's constraints were best practices, so thoroughly that "can this be a Lambda" became a design review question, asked before "what is this program". Architectures should be chosen by the shape of the problem. Bills should reward you for choosing well, and the two decisions should never have been the same decision. Unbundling them is the actual legacy Lambda deserves: it proved the industry would rearchitect anything for a fair meter, and the fair meter no longer asks for the rearchitecture.&lt;/p&gt;

&lt;p&gt;So run the disentangling exercise on your own stack. List what's function-shaped because the problem is event-shaped, and what's function-shaped because in 2019 that was the only door to a fair bill. The first list is fine where it is. The second list is a refactoring you were paid to do once and are paying to maintain forever, and the fair bill comes without it now. How long is your second list?&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>cloud</category>
      <category>serverless</category>
    </item>
    <item>
      <title>FastAPI in Production: From main.py to a Public URL</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/lightcloud/fastapi-in-production-from-mainpy-to-a-public-url-4mme</link>
      <guid>https://dev.to/lightcloud/fastapi-in-production-from-mainpy-to-a-public-url-4mme</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffufqkxd6b0yvlbcay5dw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffufqkxd6b0yvlbcay5dw.webp" alt="FastAPI in Production: From main.py to a Public URL" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To deploy a FastAPI app on Light Cloud, put &lt;code&gt;fastapi[standard]&lt;/code&gt; in &lt;code&gt;requirements.txt&lt;/code&gt;, keep the app object as &lt;code&gt;app&lt;/code&gt; in &lt;code&gt;main.py&lt;/code&gt;, push to GitHub and click &lt;strong&gt;Deploy&lt;/strong&gt; in the console. Light Cloud detects FastAPI, installs the requirements and starts &lt;code&gt;uvicorn main:app&lt;/code&gt; on the container's port. No Dockerfile, no Procfile. The interactive docs at &lt;code&gt;/docs&lt;/code&gt; are online with the API.&lt;/p&gt;

&lt;p&gt;In my run the first deploy took 97 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;A small book API with validation by &lt;a href="https://docs.pydantic.dev/" rel="noopener noreferrer"&gt;Pydantic&lt;/a&gt;, live on a &lt;code&gt;light-cloud.io&lt;/code&gt; address, with its Swagger docs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdaoq6e23xvoevrxawmfv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdaoq6e23xvoevrxawmfv.webp" alt="The Swagger UI docs page titled Reading List API with the routes GET /, GET /health, GET /books, POST /books and GET /books/{book_id}" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The title, &lt;strong&gt;Reading List API&lt;/strong&gt; , comes from an environment variable set in the console.&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/light-cloud-com/tutorial-fastapi-api" rel="noopener noreferrer"&gt;github.com/light-cloud-com/tutorial-fastapi-api&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.python.org/downloads/" rel="noopener noreferrer"&gt;Python&lt;/a&gt; 3.10 or later (what FastAPI 0.141 requires); I used Python 3.13.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/downloads" rel="noopener noreferrer"&gt;Git&lt;/a&gt; and a &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; account.&lt;/li&gt;
&lt;li&gt;A free Light Cloud account at &lt;a href="https://console.light-cloud.com" rel="noopener noreferrer"&gt;console.light-cloud.com&lt;/a&gt; with GitHub connected.&lt;/li&gt;
&lt;li&gt;A terminal with &lt;a href="https://curl.se/" rel="noopener noreferrer"&gt;curl&lt;/a&gt;. On Windows, use &lt;a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows" rel="noopener noreferrer"&gt;PowerShell 7&lt;/a&gt; and &lt;a href="https://curl.se/windows/" rel="noopener noreferrer"&gt;&lt;code&gt;curl.exe&lt;/code&gt;&lt;/a&gt;, which ships with Windows 10 and 11.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the project and a virtual environment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mkdir tutorial-fastapi-api
$ cd tutorial-fastapi-api
$ python3 -m venv .venv
$ source .venv/bin/activate


PS&amp;gt; mkdir tutorial-fastapi-api
PS&amp;gt; cd tutorial-fastapi-api
PS&amp;gt; py -m venv .venv
PS&amp;gt; .venv\Scripts\Activate.ps1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, &lt;code&gt;mkdir&lt;/code&gt; also prints a short table describing the new folder. Your prompt shows &lt;code&gt;(.venv)&lt;/code&gt; once the virtual environment is active.&lt;/p&gt;

&lt;p&gt;List the dependencies. &lt;code&gt;fastapi[standard]&lt;/code&gt; brings uvicorn and the &lt;code&gt;fastapi&lt;/code&gt; command with it; the version pin makes the build install exactly what you tested:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fastapi[standard]==0.141.1


$ pip install -r requirements.txt
Successfully installed ... fastapi-0.141.1 ... pydantic-2.13.5 ... starlette-1.7.0 ... uvicorn-0.54.0 ...


PS&amp;gt; pip install -r requirements.txt
Successfully installed ... fastapi-0.141.1 ... pydantic-2.13.5 ... starlette-1.7.0 ... uvicorn-0.54.0 ...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is trimmed to the main packages.&lt;/p&gt;

&lt;p&gt;Keep the virtual environment out of Git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.venv/
__pycache__ /
.env

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Write the API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;

&lt;span class="c1"&gt;# The title shows at the top of the interactive docs at /docs.
# Set API_TITLE on Light Cloud to change it without touching the code.
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API_TITLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Book API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BookIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1450&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BookIn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="c1"&gt;# In memory, so the list starts empty again after every restart.
&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello from FastAPI&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/books&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_books&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/books&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BookIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/books/{book_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;book_id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Book not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;book_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter for the deploy. The file is called &lt;code&gt;main.py&lt;/code&gt; and the FastAPI object is called &lt;code&gt;app&lt;/code&gt;: that is what Light Cloud starts, as &lt;code&gt;main:app&lt;/code&gt;. And the code never picks a port; the server that runs it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run it on your machine
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fastapi dev main.py
&lt;span class="go"&gt; 🌐 Server started at http://127.0.0.1:8000
    Documentation at http://127.0.0.1:8000/docs


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fastapi dev main.py
&lt;span class="go"&gt; 🌐 Server started at http://127.0.0.1:8000
    Documentation at http://127.0.0.1:8000/docs

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is trimmed. In a second terminal, add a book and send one that breaks the rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8000/books &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  -d '{"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999}'
{"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999,"id":1}
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8000/books &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  -d '{"title":"","author":"Nobody","year":3000}'
{"detail":[{"type":"string_too_short","loc":["body","title"],"msg":"String should have at least 1 character","input":"","ctx":{"min_length":1}},{"type":"less_than_equal","loc":["body","year"],"msg":"Input should be less than or equal to 2100","input":3000,"ctx":{"le":2100}}]}


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl.exe &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8000/books &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="go"&gt;  -d '{"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999}'
{"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999,"id":1}
&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl.exe &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8000/books &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="go"&gt;  -d '{"title":"","author":"Nobody","year":3000}'
{"detail":[{"type":"string_too_short","loc":["body","title"],"msg":"String should have at least 1 character","input":"","ctx":{"min_length":1}},{"type":"less_than_equal","loc":["body","year"],"msg":"Input should be less than or equal to 2100","input":3000,"ctx":{"le":2100}}]}

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second request gets a &lt;code&gt;422&lt;/code&gt; with both problems listed. Pydantic checked the body against &lt;code&gt;BookIn&lt;/code&gt; before your function ran. Stop the server with Ctrl+C.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Push it to GitHub
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git init &lt;span class="nt"&gt;-b&lt;/span&gt; main
&lt;span class="nv"&gt;$ &lt;/span&gt;git add &lt;span class="nt"&gt;-A&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"FastAPI book API"&lt;/span&gt;
&lt;span class="c"&gt;# Create an empty repository named tutorial-fastapi-api on github.com/new first.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git remote add origin https://github.com/YOUR-USERNAME/tutorial-fastapi-api.git
&lt;span class="nv"&gt;$ &lt;/span&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
To https://github.com/YOUR-USERNAME/tutorial-fastapi-api.git
 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;new branch] main -&amp;gt; main
branch &lt;span class="s1"&gt;'main'&lt;/span&gt; &lt;span class="nb"&gt;set &lt;/span&gt;up to track &lt;span class="s1"&gt;'origin/main'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;


PS&amp;gt; git init &lt;span class="nt"&gt;-b&lt;/span&gt; main
PS&amp;gt; git add &lt;span class="nt"&gt;-A&lt;/span&gt;
PS&amp;gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"FastAPI book API"&lt;/span&gt;
&lt;span class="c"&gt;# Create an empty repository named tutorial-fastapi-api on github.com/new first.&lt;/span&gt;
PS&amp;gt; git remote add origin https://github.com/YOUR-USERNAME/tutorial-fastapi-api.git
PS&amp;gt; git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
To https://github.com/YOUR-USERNAME/tutorial-fastapi-api.git
 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;new branch] main -&amp;gt; main
branch &lt;span class="s1"&gt;'main'&lt;/span&gt; &lt;span class="nb"&gt;set &lt;/span&gt;up to track &lt;span class="s1"&gt;'origin/main'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that &lt;code&gt;git status&lt;/code&gt; did not list &lt;code&gt;.venv&lt;/code&gt;: the build installs the packages itself from &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Deploy it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;a href="https://console.light-cloud.com" rel="noopener noreferrer"&gt;Light Cloud console&lt;/a&gt;, click &lt;strong&gt;New...&lt;/strong&gt; , then &lt;strong&gt;Deploy from GitHub&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;tutorial-fastapi-api&lt;/code&gt; and click it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Light Cloud shows &lt;strong&gt;FastAPI / Backend&lt;/strong&gt; , based on &lt;code&gt;requirements.txt&lt;/code&gt;, and &lt;strong&gt;Runs on a server&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ulhnqtlom9z8vkxarp1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ulhnqtlom9z8vkxarp1.webp" alt="The detection banner FastAPI Backend based on requirements.txt, with Runs on a server highlighted" width="799" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Advanced - build settings, environment variables, domain, scaling&lt;/strong&gt;. &lt;strong&gt;Port&lt;/strong&gt; is already &lt;code&gt;8000&lt;/code&gt;, the port uvicorn is started on. Leave it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnz1l33gr09pefs2p80d5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnz1l33gr09pefs2p80d5.webp" alt="The Advanced section with Port 8000 highlighted" width="720" height="109"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Under &lt;strong&gt;Environment variables&lt;/strong&gt; , click &lt;strong&gt;Add variable&lt;/strong&gt; and add &lt;code&gt;API_TITLE&lt;/code&gt; with the value &lt;code&gt;Reading List API&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F334vsv6xx1f3u04t274j.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F334vsv6xx1f3u04t274j.webp" alt="The Environment variables section with API_TITLE set to Reading List API" width="720" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy&lt;/strong&gt; and wait for &lt;strong&gt;Deployed&lt;/strong&gt; (97 seconds in my run: the first build installs every package). The &lt;strong&gt;URL&lt;/strong&gt; card has the address:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhsktolhh899dhn6ccpc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhsktolhh899dhn6ccpc.webp" alt="The Production overview of tutorial-fastapi-api with the Deployed badge and the URL card highlighted" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Use the live API and its docs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl https://main-tutorial-fastapi-api-yourworkspace.light-cloud.io/
{"message":"Hello from FastAPI","docs":"/docs"}
$ curl -X POST https://main-tutorial-fastapi-api-yourworkspace.light-cloud.io/books -H "content-type: application/json" \
  -d '{"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999}'
{"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999,"id":1}


PS&amp;gt; curl.exe https://main-tutorial-fastapi-api-yourworkspace.light-cloud.io/
{"message":"Hello from FastAPI","docs":"/docs"}
PS&amp;gt; curl.exe -X POST https://main-tutorial-fastapi-api-yourworkspace.light-cloud.io/books -H "content-type: application/json" `
  -d '{"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999}'
{"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999,"id":1}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first request after a quiet period takes a few seconds (3 seconds in my run) while an instance starts.&lt;/p&gt;

&lt;p&gt;Now open &lt;code&gt;/docs&lt;/code&gt; on your address. Swagger UI lists every route with the title from &lt;code&gt;API_TITLE&lt;/code&gt;. Expand &lt;strong&gt;POST /books&lt;/strong&gt; , click &lt;strong&gt;Try it out&lt;/strong&gt; , edit the example and click &lt;strong&gt;Execute&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ef4yl5oqvgyhzsil6kk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ef4yl5oqvgyhzsil6kk.webp" alt="POST /books in Swagger UI with a Clean Code request body and the server response 201 with the created book" width="800" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;id&lt;/code&gt; may differ. In my run it was &lt;code&gt;3&lt;/code&gt;, although I had added only one book before, from a different terminal. That is the in-memory list at work: each instance keeps its own copy, and the next section explains why that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where data lives
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;books&lt;/code&gt; dictionary lives in the memory of one running instance. On Light Cloud, as on any cloud platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new deploy replaces the instances, so the list starts empty;&lt;/li&gt;
&lt;li&gt;with &lt;strong&gt;Min instances&lt;/strong&gt; at 0, the API stops when idle and starts empty on the next request;&lt;/li&gt;
&lt;li&gt;under load several instances run at once, each with its own list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is fine for a demo and wrong for real data. Store it in a database: &lt;a href="https://blog.light-cloud.com/tutorials/microservices-on-light-cloud-part-1-deploy" rel="noopener noreferrer"&gt;a frontend, two APIs and a database&lt;/a&gt; shows a FastAPI service with PostgreSQL through &lt;code&gt;psycopg&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The deploy fails because the app cannot be found
&lt;/h3&gt;

&lt;p&gt;Light Cloud starts &lt;code&gt;uvicorn main:app&lt;/code&gt;, or &lt;code&gt;app:app&lt;/code&gt; if the file is &lt;code&gt;app.py&lt;/code&gt;. If your app is in &lt;code&gt;src/api.py&lt;/code&gt; or the object has another name, uvicorn cannot find it. Move the entry point to &lt;code&gt;main.py&lt;/code&gt; at the root with &lt;code&gt;app = FastAPI()&lt;/code&gt;, or set &lt;strong&gt;Root directory&lt;/strong&gt; to the folder that contains &lt;code&gt;main.py&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  FastAPI is not detected
&lt;/h3&gt;

&lt;p&gt;Light Cloud looks for &lt;code&gt;fastapi&lt;/code&gt; in &lt;code&gt;requirements.txt&lt;/code&gt; or &lt;code&gt;pyproject.toml&lt;/code&gt; at the root (or in the &lt;strong&gt;Root directory&lt;/strong&gt; ). A project that installs FastAPI only through another tool's lock file is not detected; add a &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
    <item>
      <title>Put a Node.js Express API Online: Port, Environment Variables and Logs</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Sun, 04 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/lightcloud/put-a-nodejs-express-api-online-port-environment-variables-and-logs-nki</link>
      <guid>https://dev.to/lightcloud/put-a-nodejs-express-api-online-port-environment-variables-and-logs-nki</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0vdow6gmspx8cay7cy0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0vdow6gmspx8cay7cy0.webp" alt="Put a Node.js Express API Online: Port, Environment Variables and Logs" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To put an Express API online with Light Cloud, make it listen on the port in &lt;code&gt;process.env.PORT&lt;/code&gt;, read its settings from environment variables, push it to GitHub and choose &lt;strong&gt;Deploy from GitHub&lt;/strong&gt; in the console. Light Cloud detects Express, runs &lt;code&gt;npm ci&lt;/code&gt; and &lt;code&gt;npm start&lt;/code&gt; in a container and gives the API an HTTPS address. No Dockerfile is needed. If every log line is JSON with a &lt;code&gt;severity&lt;/code&gt; field, the &lt;strong&gt;Logs&lt;/strong&gt; tab shows each request with its level.&lt;/p&gt;

&lt;p&gt;In my run the first deploy took 72 seconds, and a changed environment variable was live 59 seconds after clicking &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;A small Express 5 API with three routes, live on a &lt;code&gt;light-cloud.io&lt;/code&gt; address:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /&lt;/code&gt; returns a greeting and the Node.js version.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /hello/:name&lt;/code&gt; greets by name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /health&lt;/code&gt; answers &lt;code&gt;{"status":"ok"}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the way it picks up three habits every Node.js API needs in the cloud: it listens on the port it is given, it reads settings from environment variables, and it writes one JSON log line per request.&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/light-cloud-com/tutorial-express-api" rel="noopener noreferrer"&gt;github.com/light-cloud-com/tutorial-express-api&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; 18 or later (Express 5 requires 18); I used Node.js 22.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/downloads" rel="noopener noreferrer"&gt;Git&lt;/a&gt; and a &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; account.&lt;/li&gt;
&lt;li&gt;A free Light Cloud account at &lt;a href="https://console.light-cloud.com" rel="noopener noreferrer"&gt;console.light-cloud.com&lt;/a&gt; with GitHub connected. The free Hobby plan includes one service, which is enough for this API.&lt;/li&gt;
&lt;li&gt;A terminal with &lt;a href="https://curl.se/" rel="noopener noreferrer"&gt;curl&lt;/a&gt;. On Windows, use &lt;a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows" rel="noopener noreferrer"&gt;PowerShell 7&lt;/a&gt; and &lt;a href="https://curl.se/windows/" rel="noopener noreferrer"&gt;&lt;code&gt;curl.exe&lt;/code&gt;&lt;/a&gt;, which ships with Windows 10 and 11.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;tutorial-express-api
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;tutorial-express-api
&lt;span class="nv"&gt;$ &lt;/span&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express
added 68 packages, and audited 69 packages &lt;span class="k"&gt;in &lt;/span&gt;692ms

found 0 vulnerabilities
&lt;span class="nv"&gt;$ &lt;/span&gt;npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module scripts.start&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"node server.js"&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;server.js


PS&amp;gt; &lt;span class="nb"&gt;mkdir &lt;/span&gt;tutorial-express-api
PS&amp;gt; &lt;span class="nb"&gt;cd &lt;/span&gt;tutorial-express-api
PS&amp;gt; npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
PS&amp;gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;express
added 68 packages, and audited 69 packages &lt;span class="k"&gt;in &lt;/span&gt;692ms

found 0 vulnerabilities
PS&amp;gt; npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module scripts.start&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"node server.js"&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;server.js

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, &lt;code&gt;mkdir&lt;/code&gt; also prints a short table describing the new folder. The last command sets up &lt;code&gt;package.json&lt;/code&gt;: &lt;code&gt;type=module&lt;/code&gt; lets you use &lt;code&gt;import&lt;/code&gt;, and the &lt;code&gt;start&lt;/code&gt; script is how Light Cloud starts the app. Output is trimmed.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;node_modules&lt;/code&gt; out of Git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
.env

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Write the API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Light Cloud tells the app which port to listen on through PORT.&lt;/span&gt;
&lt;span class="c1"&gt;// 8080 is the fallback for running it on your own machine.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Settings come from environment variables, so the same code runs&lt;/span&gt;
&lt;span class="c1"&gt;// locally and in production with different values.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GREETING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GREETING&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// One JSON object per line. The Logs tab reads "severity" for the level.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ERROR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WARNING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INFO&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GREETING&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; from Express`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/hello/:name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GREETING&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ERROR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INFO&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`listening on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Light Cloud stops old instances with SIGTERM after a new version is live.&lt;/span&gt;
&lt;span class="c1"&gt;// Finish the requests in flight, then exit.&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INFO&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGTERM received, shutting down&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three habits, in the code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The port comes from &lt;code&gt;PORT&lt;/code&gt;.&lt;/strong&gt; In the cloud, the platform decides the port. A hard-coded port is the most common reason a Node.js app deploys but never answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings come from environment variables.&lt;/strong&gt; &lt;code&gt;GREETING&lt;/code&gt; is a stand-in for real settings such as an API key or a database address: the value lives on the platform, not in the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs are JSON with a &lt;code&gt;severity&lt;/code&gt;.&lt;/strong&gt; Each request is logged when it finishes, as &lt;code&gt;INFO&lt;/code&gt;, &lt;code&gt;WARNING&lt;/code&gt; for 4xx or &lt;code&gt;ERROR&lt;/code&gt; for 5xx.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;SIGTERM&lt;/code&gt; handler is a small extra: when a new version goes live, the old instance gets a few seconds to finish what it is doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run it on your machine
&lt;/h2&gt;

&lt;p&gt;Start the API in one terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm start
&lt;span class="go"&gt;{"severity":"INFO","message":"listening on port 8080"}


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm start
&lt;span class="go"&gt;{"severity":"INFO","message":"listening on port 8080"}

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it from a second terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl http://localhost:8080/hello/Ada
{"message":"Hello, Ada!"}
$ curl http://localhost:8080/nope
{"error":"Not found"}


PS&amp;gt; curl.exe http://localhost:8080/hello/Ada
{"message":"Hello, Ada!"}
PS&amp;gt; curl.exe http://localhost:8080/nope
{"error":"Not found"}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first terminal now shows one log line per request, the second as a warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"INFO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/hello/Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"WARNING"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/nope"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop the API with Ctrl+C.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Push it to GitHub
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git init &lt;span class="nt"&gt;-b&lt;/span&gt; main
&lt;span class="nv"&gt;$ &lt;/span&gt;git add &lt;span class="nt"&gt;-A&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Express API with PORT, env vars and JSON logs"&lt;/span&gt;
&lt;span class="c"&gt;# Create an empty repository named tutorial-express-api on github.com/new first.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git remote add origin https://github.com/YOUR-USERNAME/tutorial-express-api.git
&lt;span class="nv"&gt;$ &lt;/span&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
To https://github.com/YOUR-USERNAME/tutorial-express-api.git
 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;new branch] main -&amp;gt; main
branch &lt;span class="s1"&gt;'main'&lt;/span&gt; &lt;span class="nb"&gt;set &lt;/span&gt;up to track &lt;span class="s1"&gt;'origin/main'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;


PS&amp;gt; git init &lt;span class="nt"&gt;-b&lt;/span&gt; main
PS&amp;gt; git add &lt;span class="nt"&gt;-A&lt;/span&gt;
PS&amp;gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Express API with PORT, env vars and JSON logs"&lt;/span&gt;
&lt;span class="c"&gt;# Create an empty repository named tutorial-express-api on github.com/new first.&lt;/span&gt;
PS&amp;gt; git remote add origin https://github.com/YOUR-USERNAME/tutorial-express-api.git
PS&amp;gt; git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
To https://github.com/YOUR-USERNAME/tutorial-express-api.git
 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;new branch] main -&amp;gt; main
branch &lt;span class="s1"&gt;'main'&lt;/span&gt; &lt;span class="nb"&gt;set &lt;/span&gt;up to track &lt;span class="s1"&gt;'origin/main'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Deploy it with an environment variable
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;a href="https://console.light-cloud.com" rel="noopener noreferrer"&gt;Light Cloud console&lt;/a&gt;, click &lt;strong&gt;New...&lt;/strong&gt; , then &lt;strong&gt;Deploy from GitHub&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;tutorial-express-api&lt;/code&gt; and click it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Light Cloud shows &lt;strong&gt;Express / Backend&lt;/strong&gt; and &lt;strong&gt;Runs on a server&lt;/strong&gt; : unlike a static website, an API needs a running process, so it gets a container.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl1xn8kw6yt7iow1gqw2t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl1xn8kw6yt7iow1gqw2t.webp" alt="The detection banner Express Backend based on package.json and package-lock.json, with Runs on a server highlighted" width="799" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Advanced - build settings, environment variables, domain, scaling&lt;/strong&gt;. &lt;strong&gt;Port&lt;/strong&gt; is &lt;code&gt;8080&lt;/code&gt;. Light Cloud passes it to the app as &lt;code&gt;PORT&lt;/code&gt;, which is exactly what &lt;code&gt;server.js&lt;/code&gt; reads. Leave it as it is.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3d1bdv607ejysa4x95jk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3d1bdv607ejysa4x95jk.webp" alt="The Advanced section with Port 8080 highlighted" width="720" height="109"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Under &lt;strong&gt;Environment variables&lt;/strong&gt; , click &lt;strong&gt;Add variable&lt;/strong&gt;. Enter &lt;code&gt;GREETING&lt;/code&gt; as the name and &lt;code&gt;Hi&lt;/code&gt; as the value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs79sotgzwdgku8bhlpur.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs79sotgzwdgku8bhlpur.webp" alt="The Environment variables section with GREETING set to Hi" width="720" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy&lt;/strong&gt; and wait for &lt;strong&gt;Deployed&lt;/strong&gt; (72 seconds in my run; a container takes longer than a static site). The &lt;strong&gt;URL&lt;/strong&gt; card has the API's address:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1456md0g38h19vvd6sdf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1456md0g38h19vvd6sdf.webp" alt="The Production overview of tutorial-express-api with the Deployed badge and the URL card highlighted" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Call the live API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl https://main-tutorial-express-api-yourworkspace.light-cloud.io/
&lt;span class="go"&gt;{"message":"Hi from Express","node":"v22.23.3"}
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl https://main-tutorial-express-api-yourworkspace.light-cloud.io/hello/Ada
&lt;span class="go"&gt;{"message":"Hi, Ada!"}


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl.exe https://main-tutorial-express-api-yourworkspace.light-cloud.io/
&lt;span class="go"&gt;{"message":"Hi from Express","node":"v22.23.3"}
&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl.exe https://main-tutorial-express-api-yourworkspace.light-cloud.io/hello/Ada
&lt;span class="go"&gt;{"message":"Hi, Ada!"}

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The greeting is now &lt;code&gt;Hi&lt;/code&gt;: the value from the console, not the &lt;code&gt;Hello&lt;/code&gt; fallback in the code.&lt;/p&gt;

&lt;p&gt;The first request after a quiet period can take a few seconds (3.6 seconds in my run). With &lt;strong&gt;Min instances&lt;/strong&gt; at 0 the API sleeps when nobody uses it, which keeps it free, and wakes on the next request. &lt;a href="https://blog.light-cloud.com/tutorials/microservices-on-light-cloud-part-4-cold-starts" rel="noopener noreferrer"&gt;Cold starts, measured&lt;/a&gt; explains the trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Change a setting without touching the code
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;Settings&lt;/strong&gt; tab of &lt;strong&gt;Production&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Next to &lt;strong&gt;Environment Variables&lt;/strong&gt; , click &lt;strong&gt;Edit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Change the value of &lt;code&gt;GREETING&lt;/code&gt; to &lt;code&gt;Welcome&lt;/code&gt; and click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8h08uqfenkmrnfk22w6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8h08uqfenkmrnfk22w6.webp" alt="The Environment Variables editor with GREETING changed to Welcome and the Save button highlighted" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Saving redeploys the same commit with the new value. About a minute later (59 seconds in my run):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl https://main-tutorial-express-api-yourworkspace.light-cloud.io/
{"message":"Welcome from Express","node":"v22.23.3"}


PS&amp;gt; curl.exe https://main-tutorial-express-api-yourworkspace.light-cloud.io/
{"message":"Welcome from Express","node":"v22.23.3"}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Read the logs
&lt;/h2&gt;

&lt;p&gt;Open the &lt;strong&gt;Logs&lt;/strong&gt; tab. Every line your API printed is here, next to lines Light Cloud adds itself, such as &lt;code&gt;GET https://.../nope 404&lt;/code&gt; for each request:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6efee2hd1rkjew6qfddi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6efee2hd1rkjew6qfddi.webp" alt="The Logs tab with the request lines, a WARNING line for the 404 request and the listening on port 8080 line highlighted" width="799" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the lines are JSON with a &lt;code&gt;severity&lt;/code&gt;, each one has the right level: the request to &lt;code&gt;/nope&lt;/code&gt; is a &lt;strong&gt;WARNING&lt;/strong&gt; , the others are &lt;strong&gt;INFO&lt;/strong&gt;. Click a line to open it. Everything else the API logged is under &lt;strong&gt;JSON Payload&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fic2fy61k8koepnn0g1wr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fic2fy61k8koepnn0g1wr.webp" alt="An expanded log line with Severity WARNING and the JSON payload method GET, status 404, path /nope" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two more lines are worth finding after Step 7: &lt;code&gt;listening on port 8080&lt;/code&gt; from the new instance, and &lt;code&gt;SIGTERM received, shutting down&lt;/code&gt; from the old one. That is the graceful shutdown from Step 2 at work.&lt;/p&gt;

&lt;p&gt;To see only problems, set the &lt;strong&gt;level&lt;/strong&gt; filter to &lt;strong&gt;warnings and above&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The deploy fails: container failed to start and listen on the port
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable within the allocated timeout.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app listens on a fixed port instead of &lt;code&gt;PORT&lt;/code&gt;. I tried it with &lt;code&gt;app.listen(3000)&lt;/code&gt;: the deploy failed after about four minutes, and the previous version kept serving the whole time. Read the port with &lt;code&gt;Number(process.env.PORT) || 8080&lt;/code&gt;, commit and push. If your app must use another port, set &lt;strong&gt;Port&lt;/strong&gt; under &lt;strong&gt;Advanced&lt;/strong&gt; to match instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  The deploy fails in the build step with npm ci
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;npm ci&lt;/code&gt; needs a &lt;code&gt;package-lock.json&lt;/code&gt; that matches &lt;code&gt;package.json&lt;/code&gt;. Run &lt;code&gt;npm install&lt;/code&gt; on your machine, commit &lt;code&gt;package-lock.json&lt;/code&gt; and push again.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>tutorial</category>
      <category>node</category>
    </item>
  </channel>
</rss>
