Deploy a Dockerfile on Vercel
Yes, you heard it right, you can now run a Dockerfile on Vercel. Vercel was the go-to place where...
找到 2544 篇相关文章
Yes, you heard it right, you can now run a Dockerfile on Vercel. Vercel was the go-to place where...
Passei os últimos dias construindo o HookSafe, uma camada que fica entre a plataforma de pagamento e o servidor do cliente para garantir que nenhum webhook se perca. A promessa do produto é uma só: se o seu servidor cair, eu seguro o evento e insisto até entregar. Cometi três bugs no caminho. O que me fez escrever este texto não foi a burrice de cada um, foi perceber, depois, que os três tinham a mesma forma: todos faziam uma falha parecer um sucesso. Num sistema cujo produto é confiabilidade, é difícil imaginar categoria de bug mais cruel. Bug 1: engoli o erro, e o sistema jurou que tinha entregue A função que entrega o evento no servidor do cliente ficou assim: go resposta, err := clienteHTTP.Do(requisicao) if err != nil { return "", nil // <- olhe com carinho } Eu quis escrever return "", err . Escrevi nil . O efeito: apontei o destino para uma porta onde não havia nada escutando. O Do devolveu um belo connection refused . E a minha função respondeu ao worker: "sem erro, chefe". O worker, obediente, marcou o evento como entregue , com o status da resposta vazio, e seguiu a vida. No banco: id | pedido_id | status | tentativas | resposta ----+-----------+----------+------------+---------- 6 | 9002 | entregue | 0 | Um evento que nunca saiu do lugar, registrado como entregue. Se isso estivesse em produção, um cliente teria pagado, não receberia nada, e o meu painel mostraria, orgulhoso, que a entrega foi um sucesso. Aquele if err != nil { return err } que a gente reclama de repetir em Go existe exatamente por isso. A linguagem te obriga a decidir o que fazer com a falha, toda vez. O preço da verbosidade é que ninguém engole um erro sem querer... a menos que digite nil . Bug 2: o log mentiu Corrigi o primeiro bug, rodei de novo, e o worker começou a cuspir isto, a cada cinco segundos, para sempre: worker: erro ao marcar morto 7: ERROR: column "reposta" does not exist worker: evento 7 esgotou as tentativas, marcado como MORTO Leia as duas linhas de novo. A primeira diz
As developers , we often face challenges keeping our workspaces perfectly synchronized across devices and collaborators. Whether it’s dealing with slow cloud sync, merge conflicts, or latency issues, these problems can disrupt our workflow and productivity. That’s why I’m excited to introduce OrBit , a local-first workspace synchronization engine designed to keep your development environments in sync with sub-millisecond latency — all while supporting offline work and peer-to-peer collaboration. What is OrBit ? OrBit is built around a multi-layered architecture that combines the power of Rust, Tauri, and VS Code to deliver a seamless synchronization experience: Rust-based local watcher daemon: Monitors file system changes with kernel-level events for ultra-low latency. Tauri-based native desktop dashboard: Provides a lightweight, secure, and cross-platform interface to manage your sync settings. VS Code extension: Integrates directly with your editor for smooth, real-time syncing of your code workspace. Unlike traditional cloud-based sync solutions, OrBit uses peer-to-peer connections and Conflict-free Replicated Data Types (CRDTs) to ensure your workspaces stay consistent even during network partitions or offline periods. Key Features Real-time sync with sub-millisecond latency: Changes propagate instantly across your devices. Offline support: Work uninterrupted without internet, with automatic merging when reconnected. Conflict resolution: CRDTs handle concurrent edits gracefully, preventing data loss. Native desktop and editor integration: Manage sync easily via the desktop app and VS Code extension. Peer-to-peer architecture: No heavy cloud servers required, enhancing privacy and speed. Why OrBit ? OrBit is designed for developers who demand speed, reliability, and seamless collaboration. It eliminates the frustration of slow syncs and merge conflicts, letting you focus on coding. Whether you’re working solo across multiple devices or collaborating with a team,
I’ve been thinking about a small image-processing problem lately: how to reduce an image to a limited palette without making it look muddy. This comes up in a lot of places: pixel art tools printable pattern generators low-color previews LED matrix displays icons and small thumbnails craft or grid-based workflows The easy version is: pick the nearest color for every pixel. The hard version is: keep the important shapes readable after the palette gets much smaller. Nearest color is only the baseline A simple nearest-color pass usually works like this: Take each pixel. Compare it with every color in the target palette. Pick the closest one. Replace the pixel. That gives you a valid output, but not always a good one. The problem is that closest is local. It does not know whether the whole image still reads well. A face can lose warm midtones. A shadow can turn into a flat dark blob. A small highlight can disappear. Skin, fur, fabric, and background colors can collapse into the same bucket. So palette reduction is not just a color problem. It is also a structure problem. RGB distance can be misleading A common first attempt is Euclidean distance in RGB: function rgbDistance(a, b) { return Math.sqrt( (a.r - b.r) ** 2 + (a.g - b.g) ** 2 + (a.b - b.b) ** 2 ); } This is easy to implement, but it does not match human perception very well. Two colors can be numerically close in RGB and still feel different. Other colors can be farther apart numerically but visually acceptable. A better approach is to compare colors in a more perceptual color space, such as Lab or OKLab. You still have to be careful, but the distance metric starts closer to what the eye notices. Dithering helps, but it changes the style Error diffusion, like Floyd-Steinberg dithering, can preserve gradients and perceived detail with fewer colors. That is useful when the output is meant to look like a low-color image. But dithering is not always desirable. In grid-based outputs, it can create scattered single-p
Hello! We're kicking off another DEV Weekend Challenge, a short bite-sized challenge planned to fit...
How I moved from a traditional web application mindset to exploring local-first architecture I wanted to build a simple software tool for my personal use. Nothing complicated. Something in the category of tools people build for themselves: A personal expense tracker A budgeting application A private knowledge management tool A personal organization system The important characteristic was this: The data belonged to one person. It was not a social application. It was not a collaboration platform. It did not need users interacting with each other. There was no requirement for: Public profiles Sharing updates Real-time collaboration Social features It was simply a tool that helped one person manage their own information. When I started thinking about building it, my first instinct was the most natural one for me. I am a web application developer. My comfort zone is building web applications. So my first thought was: "Why not build a Ruby on Rails application?" Something like: User | Web Application | Ruby on Rails API | PostgreSQL Database This is an architecture I have worked with many times. The workflow is familiar: Create models Build controllers Add authentication Store data in a database Deploy the application Access it from anywhere This is a proven architecture. For many products, this is exactly the right approach. But while thinking about this project, I asked myself a different question: Am I choosing this architecture because the problem requires it, or because it is the architecture I already know? That question changed the direction completely. Understanding The Actual Problem Before choosing technology, I wanted to understand the nature of the problem. What kind of application was I actually building? There is a big difference between building: A social network A marketplace A collaboration platform A communication application versus building: A personal tool A private utility A single-user productivity application In the first category, the server is the
Last week I open sourced FableCut , a Premiere-style video editor that runs in the browser and that AI agents can operate. It hit the front page of Hacker News ( thread ), and the questions there made me realize the interesting part isn't the editor. It's one design decision: the project file is the interface. The usual way, and why I flipped it Most AI video tools hide the edit behind an API. You call addClip() , applyFilter() , and the tool owns the state. If you want a human to touch the result, you build a whole collaboration layer. FableCut does the opposite. The entire timeline lives in one JSON document, project.json : media, clips, tracks, keyframes, transitions, markers. The editor UI reads it. The export renders it. And anything that can write JSON can edit video: Claude Code through MCP, a Python script, jq , or you with a text editor. { "id" : "c_title" , "kind" : "text" , "track" : "V3" , "start" : 0 , "duration" : 2.2 , "props" : { "text" : "HANDMADE" , "font" : "Bebas Neue" , "glow" : 45 , "textAnim" : "letter-pop" } } That clip is a glowing kinetic caption. There is no API call that creates it. Writing it into the file IS creating it. SSE as a doorbell, not a data channel The first question on HN was "what's the benefit of SSE here?" Fair question, because the SSE channel does almost nothing, and that's the point. The server watches the project file with fs.watch , debounces 150ms, and pushes the literal string change to the browser. No payload. The browser re-fetches the project and re-renders. The whole mechanism is about 15 lines on a bare node:http server. Why not WebSockets? Because the data only flows one way. Everything that writes (the UI, an agent, a shell script) goes through REST or the filesystem. The browser only ever needs to hear "something changed, go look." An event with no payload can't arrive out of order, and a missed event costs nothing because the next fetch has the latest state anyway. The revision counter, or: how a human and
TLDR; I got tired of babysitting N terminal tabs of five different coding-agent CLIs. So I built agentproto — one daemon that drives Claude Code, Codex, Hermes, opencode, and Mastra through the same lifecycle, and actually supervises them. Why I built a daemon to drive every AI coding agent from one interface I have a confession: at any given moment I have Claude Code, Codex, and Hermes running in parallel terminal tabs, and I cannot remember which flag spawns which, which one eats --prompt , which one needs --cwd vs cd , and which one will hang forever if I close the laptop lid. simonw described the feeling on Hacker News recently — "Today I have Claude Code and Codex CLI and Codex Web running, often in parallel" — and called it a real jump in cognitive load compared to a year ago. aantix asked, also on HN: "how does everyone visually organize the multiple terminal tabs open for these numerous agents in various states?" I didn't have a good answer. So I built one. It's called agentproto . It is one daemon and one CLI that drives any coding-agent CLI — Claude Code, Codex, Hermes, opencode, Mastra, and a few more — through the same start / prompt / monitor / kill lifecycle, so you stop memorizing five different CLIs. On top of that lifecycle it adds the supervision layer people keep hand-rolling by hand: durable policy gates, nested orchestration, and multiplexed fan-in monitoring. MIT, no paid tier, the daemon itself is an MCP server. This is the story of why it exists. The hand-rolled watchdog The sharpest signal while I was building this came from other people independently re-inventing the same primitives in tmux scripts. On r/ClaudeAI, Confident_Chest5567 posted a writeup of orchestrating agents via tmux panes with a watchdog that resets dead sessions — "a swarm of agents that can keep themselves alive indefinitely." In the same thread, IssueConnect7471 (18 upvotes) described wiring a Redis pub/sub heartbeat plus dead-letter respawn between tmux panes, and arriv
The fourth call of the week Catherine calls from the Maisons-Laffitte site on a Tuesday afternoon in early May. "It's broken, but it's a quick fix." That's her line — I know it, and she's usually right. She describes it in three sentences: the newsletter export for the enrolled-students segment comes back with ninety-two names, the planning view shows ninety-two active courses, but the counter page shows eighty-nine. Three enrolled students missing. She'd checked the database directly — they're all there. "Why three steps for that?" She's not asking for my benefit. She's asking for herself. Except this time, hanging up, I realize it's the fourth time this week I've hung up thinking the same thing. Four Supabase incidents, four fixes, four closed tickets. And not a single exception raised by the database. I reopen the three previous ones and lay all four side by side on screen. This isn't four bugs. It's one failure mode, declinated four times. The first three Episode 1 was about the default GRANT s Supabase places on functions and policies. A SQL function created without an explicit REVOKE inherits anon access that nobody wrote in the migration, and that nobody caught in review because the diff doesn't show it. The function works. It's just callable from outside. [CANONICAL URL EPISODE 1: to fill in after publication of #48 — "3 Supabase security incidents, one shared root cause: SECURITY DEFINER inherits EXECUTE TO PUBLIC"] Episode 2, an ON DELETE SET NULL cascade coupled with a CHECK NOT NULL on the target column. The parent DELETE attempts the SET NULL , the CHECK rejects it, and the transaction surfaces an error we read as a deletion failure — while it actually masks a consistency assumption we'd held for three months. The query fails loudly, which is more charitable than the other three cases, but the diagnosis heads in the wrong direction because nobody had declared that the two constraints lived in tension. [CANONICAL URL EPISODE 2: to fill in after publicati
The request was eight words Monday morning. I open the outgoing email queue: six hundred and forty-seven drafts waiting, six hundred and seventy-two sent. Nobody clicks Send . First-contact emails are prepared by a pipeline and they sleep, because the last step assumes a human. That human, I had stopped believing she would have the time. I state the decision: automate sending . The response comes in seconds. Three levels of automation. Four channels. Three risk thresholds. All correct, all fit for a half-day architecture workshop. I had not asked for a workshop. Pauline walks behind me, glances at the screen, says nothing. Three timed reframes First reframe , brief: too strange, let's simplify . The agent drops two axes, keeps four residual layers, progressive warm-up over three weeks, deterministic anti-replay hash, configuration table in the database, manual Phase 1 followed by an automated Phase 2 to validate after two weeks of measurement. The target stays the same, that an email leaves without a human click. The path has grown accordingly. Second reframe , drier: simple, three safeguards, a kill-switch, we do this in one day . The agent re-architects, accepts the one-day target, keeps the three safeguards. But slips in three prostheses it calls industry standard : real-time dashboard, exponential retry, structured audit log in a new table. Each justifiable in isolation. None of them requested. Third reframe , shorter still: I don't understand why you're adding this . An opening line almost embarrassed, which I had never read from it before: "you're right, I'm over-engineering without necessity." And the version that should have arrived on the first round. A function that takes the draft record, checks three conditions, calls the send engine, returns. // lib/email-outbox.ts — generateFirstContactDraft (commit 3756e63) if ( ! EMAIL_REGEX . test ( input . email )) { return { success : false , error : ' email_invalide ' } } if ( BLACKLIST_EMAILS . has ( input . ema
In version 1.11, HashiCorp introduced Terraform Ephemeral resources and write-only attributes to allow for root configs that do not store secrets in the Terraform statefile. But many users ask about how they can adopt ephemerals. This blog attempts to lay out the ways secrets can be stored in state and how you should update your configurations to remove those secrets. Note: For a primer on ephemerals ( see this blog post ). Scenarios to consider: Data sources that fetch a static secret Resources that receive a secret Resources that generate a dynamic a secret Resources that fetch generated secrets to store in another 3rd party system Scenario 1: Data sources with static secrets Ephemeral resources can often be a drop-in replacement for data sources pulling static values: data "vault_kv_secret_v2" "static_kv" { mount = "kvv2" name = "my_password" } ephemeral "vault_kv_secret_v2" "static_kv" { mount = "kvv2" name = "my_password" } However, using these values has 1 specific difference. The attributes on a ephemeral resource are considered ephemeral and can only be used as ephemeral arguments. That means 2 places: Provider blocks Provider blocks are considered ephemeral, so ephemeral resources may populate arguments: provider "example" { password = tostring ( ephemeral . vault_kv_secret_v2 . static_kv . data . password ) } Write-only arguments Write-only arguments are special arguments that require the ephemeral taint for values: resource "aws_db_instance" "example" { ... password_wo = tostring ( ephemeral . vault_kv_secret_v2 . static_kv . data . password ) } If the resource you wish to pass a value to does not have an available ephemeral, open an issue with that provider. You can reference: this blog post this agent skill Scenario 2: Resources that receive a static secret Without duplicating to the section above, write-only arguments are a way to get secrets out of state. Above has guidance if the secret value comes from a data source, but what if its from a variable?
When people picture "coding," they picture fast typing and features coming to life. Nobody pictures the real majority of the job: staring at a stack trace or lets say a particular project trying to figure out why something that should work, isn't. Here's what nobody tells you starting out — getting good at debugging has almost nothing to do with how well you write code, and everything to do with how well you read. The real difference between beginners and experienced devs isn't complex knowledge — it's that experienced devs read carefully and form a hypothesis before touching anything. Beginners (me included) tend to skip straight to changing code and hoping. It feels faster. It rarely is. One thing i'd like to advise other fellow beginner devs is ....Slow down, read the error properly, and follow the stack trace to where it actually starts — not where it ends up. What's a bug that taught you this the hard way?
Showcasing Your GitHub Profile: A Guide to Effective Presentation In the world of software development, GitHub profiles serve as a modern-day portfolio, showcasing a developer's skills, projects, and contributions. Whether you are a seasoned developer or just starting out, presenting your GitHub profile effectively can make a significant difference in your professional journey. In this article, we will explore the essential elements of a compelling GitHub profile and provide tips to make your profile stand out in the crowded digital landscape. Understanding the Importance of Your GitHub Profile GitHub is more than just a repository hosting service; it is a platform where developers can collaborate, share their work, and build a professional network. Your GitHub profile is often the first impression a potential employer or collaborator will have of your technical capabilities. A well-crafted profile not only highlights your technical prowess but also your ability to communicate and work within a team. Key Elements of a Compelling GitHub Profile 1. Profile Picture and Bio First impressions matter, even in the digital world. Your profile picture should be professional and clear, giving a face to the name behind the code. Accompanying your picture should be a concise bio that succinctly describes who you are, your interests, and your areas of expertise. This personal touch can make your profile more relatable and memorable. 2. Featured Projects Highlighting a few key projects on your GitHub profile can effectively demonstrate your skills and interests. Choose projects that not only showcase your technical abilities but also reflect your passion and creativity. Provide a clear description of each project, the technologies used, and your specific contributions. This level of detail can help potential employers understand the depth of your knowledge and experience. 3. Consistent Activity An active GitHub profile signals to others that you are engaged in the development com
The issue was not the tools. It was opening five of them before deciding what the log file was for. The log file was already on the screen. A remote Windows workstation had failed a desktop build, and the relevant file was sitting in a local app directory, something like: C:\Users\<user>\AppData\Local\<app>\logs\build.log The remote session was working. The error was visible. The next step seemed small: get the log back to the local laptop, open it in a familiar editor, compare it with the issue notes, and pull out the part that mattered. That should have been a 30-second task. Instead, it turned into five context switches. The first context was the remote session The remote desktop session made sense. The build failed on that machine, the app was installed there, and the log path was easier to find visually than by guessing from memory. So far, nothing was wrong. The file was selected. The timestamp matched the failed run. The log looked useful. It probably had the stack trace, the missing dependency path, or the configuration mismatch that explained the build failure. Then came the small but surprisingly annoying question: How should this file leave the remote machine? That is where the workflow started to wobble. The second context was chat The first instinct in many teams is chat. Drop the file into a message to yourself, a teammate, or the debugging thread. It is fast, already open, and keeps the file near the conversation. For some files, that is the right move. A screenshot, a short error snippet, or a quick “does this look familiar?” artifact belongs naturally in the discussion. But a full log file is not always a chat artifact. If it goes into chat, will anyone know later whether it was the first failing run or the second? Will it be obvious which remote machine produced it? Will the file still be easy to find after the thread moves on? Chat was not wrong. It was just not clearly the right home for this specific file. So the workflow moved on. The third con
With increased adoption of AI, there is often an argument that code-reviews are now the new bottleneck. And I agree with this completely. Code-Reviews, especially the review you do yourself after AI has written your code, take time. But I would object to the notion that this is a bad thing. What is a bottleneck? A bottleneck is something that slows down the process. It becomes a point where work must get in a line, to pass through a narrow space. With the speed of AI producing code, code reviews become a bottleneck. But is having a bottleneck in the process always a bad thing? The value of slowing down I can only speak from my personal experience of developing software for roughly 7 years now. But in my experience, slowing down is not always bad. On the contrary, it can be very healthy. When you slow down, and take the time to really think about things, you often come up with insights that you would not have if you always rush through things. And these insights can be golden opportunities to change something for the better. Be that a subtle bug discovered, be that a design flaw addressed or something else - the list is long. But as British computer scientist Tony Hoare famously said: "There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies." But simplicity is hard "I would have written a shorter letter, but did not have the time." If it was Mark Twain or Blaise Pascal who said it is beside the point. The point is, there is a lot of truth in this quote. A writer of prose I know also confirmed what many senior software engineers know - to make something complex simple and easily comprehensible takes way more time and effort in the form of careful thought than it takes to leave it being complicated and hard to understand. AI is good at writing code quickly, yes. But is it also good at writing code which has high q
I built an LED Strip Tetris game — without writing a single line of code. No keyboard mashing. No debugging at 2 AM. No reading 500 pages of datasheets. Just natural language prompts, an AI agent, and a Tuya T5 AI Core board. Here's the full breakdown of how it works 👇 🧩 What Is LED Strip Tetris? LED Strip Tetris is a DIY hardware game built entirely through natural language prompts using TuyaOpen IDE and Claude Code. It runs on a Tuya T5 AI Core development board with a WS2812 LED strip (72 LEDs) and three color-matched buttons — red, green, and blue. Colored LEDs fall from the top of the strip; players press the matching button to shoot a colored LED upward and eliminate the falling one on contact. The entire game — firmware, game logic, hardware wiring, sound effects, compilation, and flashing — was generated by AI. Zero manual coding. 🔌 The Hardware (Ridiculously Simple) Component Role Tuya T5 AI Core Board Main MCU — runs game logic, drives LED strip and buttons WS2812 LED Strip (72 LEDs) Display — colored LEDs fall and get eliminated 3 Push Buttons (Red / Green / Blue) Input — shoot matching color upward to clear falling LEDs Speaker Sound effects on button press That's it. No custom PCB. No complex wiring harness. Just four components plugged into a dev board. 🤔 Why This Is a Big Deal Here's what building a hardware game normally looks like: Step Traditional Approach Vibe Coding with TuyaOpen IDE Dev environment setup Install toolchain, configure SDK, fight dependencies Copy a workflow link, paste into Claude Code, click confirm Game logic Write C code from scratch, design state machines Describe the game in one sentence, AI generates the code Hardware config Read datasheets, look up GPIO mappings, manually configure Tell AI which pins you're using, it handles the rest Sound effects Write audio decoding code, integrate codecs Give AI the file path, it decodes and compiles Debugging Serial logs, oscilloscope, hours of trial and error AI self-diagnoses compile
We are so excited to announce the winners of the June Solstice Game Jam, our celebration of the...
OpenAI found two unrelated bugs masquerading as one in ChatGPT's data infrastructure. Silent hardware corruption on one Azure host and an 18-year-old race condition in GNU libunwind's setcontext function with a one-instruction vulnerability window. The breakthrough came from switching to population-level crash analysis rather than examining individual core dumps. By Steef-Jan Wiggers
When an AI API call fails, the tempting reaction is to switch models or providers. That is often premature. A large share of 401, 429, model_not_found, timeout, and confusing billing issues are not model-quality problems. They are route-evidence problems. The request moved through a key, base URL, model ID, retry rule, fallback path, and billing record. If those pieces are not visible, changing the model can hide the real cause. Before you replace the model, debug the route. A practical route checklist Confirm the key scope. Is the API key attached to the right project, environment, and quota rule? A key that works in one workspace can fail in another because the limit, budget, or allowed model set is different. Confirm the base URL. Many OpenAI-compatible errors start with a request going to the wrong host, version path, or proxy. Check the exact Base URL used by the client, not the one written in a README from memory. Confirm the model ID. A model_not_found error is not always a provider outage. It can be a copied alias, a retired ID, a route that does not support that model, or a mismatch between public model names and API model IDs. Separate 401, 403, 404, and 429. These errors ask different questions: 401: is the key present and valid? 403: is the key allowed to use this route or model? 404/model_not_found: is the exact model ID available on this route? 429: is the limit coming from the user, key, project, provider, retry loop, or budget rule? Treating all of them as provider instability wastes time. Look for retry and fallback behavior. A single user action may trigger more than one model call. Agents, RAG pipelines, streaming clients, and SDK retries can quietly multiply traffic. If fallback is enabled, the served route may differ from the requested model. Check the usage and charge record. A successful response is not the end of the test. You should be able to explain which key made the call, which model was requested, which route served it, how many tokens
This post contains Railway referral links. If you sign up through one I get a bit of credit. I build Old Light , a real-time strategy game that runs in the browser. Claim stars, grow an economy, send fleets, all while other players and NPC empires do the same. The second a build finishes or a fleet lands, the server pushes it to every connected client over a WebSocket. That last part, a long-lived server holding an open socket, rules out most of the usual hosts. Here's what it ruled in. Why not Vercel or Netlify Serverless shines when your backend is stateless functions. It's the wrong shape the moment you need a socket that stays open: socket.io wants one process that lives for the whole session, and serverless boots per request and then freezes. You can bolt on a managed WebSocket service, but that's a second system to run and pay for. Railway runs your service as a normal long-lived process, so socket.io just connects. Fly.io does this too with more knobs to turn. I wanted to ship, so Railway won. Monorepo, two services Old Light is an npm workspaces monorepo: a shared types package, an Express plus TypeORM plus socket.io API, and a Vite web app served by a small Express server. On Railway that's two services on the same repo, each with its own root directory and build command, shared built first. They deploy as separate origins, so the web app reads the API's URL from VITE_API_URL . Vite bakes that in at build time, so it's a build variable, not a runtime one. Postgres is a plugin that injects DATABASE_URL , and production runs migrations rather than synchronize . WebSockets need nothing special until you run more than one instance, at which point you'd add a Redis socket.io adapter. I haven't left a single box yet. A healthcheck that stops version skew Two services don't go live at the same instant. Push a commit that touches both, the web finishes first, and for a minute your new frontend is calling API routes that don't exist yet. It 404s, then heals itself o