From ae756fd0d72c9eb59e111e25775b18fe7c2860dd Mon Sep 17 00:00:00 2001 From: defangdevs Date: Fri, 30 Oct 2026 13:11:58 -0700 Subject: [PATCH 1/9] Fix nodejs-react-postgres: managed Postgres, single-origin app Remove the API URL and the user-supplied DATABASE_URL. The server now serves the built client, and the database is a Defang managed Postgres service. Fixes #408 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RVYuG3wyXinFEZCmC9KuET --- samples/nodejs-react-postgres/.dockerignore | 3 ++ samples/nodejs-react-postgres/Dockerfile | 19 ++++++++ samples/nodejs-react-postgres/README.md | 11 ++--- samples/nodejs-react-postgres/compose.yaml | 48 +++++++++++-------- samples/nodejs-react-postgres/server/index.js | 1 + 5 files changed, 54 insertions(+), 28 deletions(-) create mode 100644 samples/nodejs-react-postgres/.dockerignore create mode 100644 samples/nodejs-react-postgres/Dockerfile diff --git a/samples/nodejs-react-postgres/.dockerignore b/samples/nodejs-react-postgres/.dockerignore new file mode 100644 index 000000000..ddeab3ef1 --- /dev/null +++ b/samples/nodejs-react-postgres/.dockerignore @@ -0,0 +1,3 @@ +**/node_modules +**/build +**/.env diff --git a/samples/nodejs-react-postgres/Dockerfile b/samples/nodejs-react-postgres/Dockerfile new file mode 100644 index 000000000..ba9bb7c3f --- /dev/null +++ b/samples/nodejs-react-postgres/Dockerfile @@ -0,0 +1,19 @@ +# Build the React client +FROM node:20-bookworm-slim AS client +WORKDIR /client +COPY client/package*.json ./ +RUN npm install +COPY client/ ./ +# Empty API URL: the client calls the server on its own origin +ENV REACT_APP_API_URL="" +RUN npm run build + +# Run the API server, which also serves the built client +FROM node:20-alpine +WORKDIR /app +COPY server/package*.json ./ +RUN npm install --omit=dev +COPY server/ ./ +COPY --from=client /client/build ./public +EXPOSE 3010 +CMD ["node", "index.js"] diff --git a/samples/nodejs-react-postgres/README.md b/samples/nodejs-react-postgres/README.md index 6059fbc57..6267fa98a 100644 --- a/samples/nodejs-react-postgres/README.md +++ b/samples/nodejs-react-postgres/README.md @@ -4,7 +4,7 @@ This sample project demonstrates how to deploy a full-stack application using React for the frontend, Node.js for the backend, and PostgreSQL for the database. -In this sample, we have set up the essential files you need to deploy in production using [Neon](https://neon.tech/) to host your database. We use a connection string to connect Neon to your code. By replacing the pre-configured connection string at .env and at the compose file to yours, you will be ready to deploy this sample with Neon. +In production, the Node.js server also serves the built React app, so one service handles both the UI and the API. The database is a Defang managed Postgres instance. ## Prerequisites @@ -30,15 +30,12 @@ For this sample, you will need to provide the following [configuration](https:// > Note that if you are using the 1-click deploy option, you can set these values as secrets in your GitHub repository and the action will automatically deploy them for you. -### `DATABASE_URL` -A connection to the database, which should be in the format of `postgres://username:password@host:port/dbname`. +### `POSTGRES_PASSWORD` +The password for the Postgres database. ```bash -defang config set DATABASE_URL +defang config set POSTGRES_PASSWORD ``` -### `REACT_APP_API_URL` -A URL for the React App. For this, you will need to update the `compose.yaml` file to replace `` with your username, which you can get by running `defang whoami`. - ## Deployment > [!NOTE] diff --git a/samples/nodejs-react-postgres/compose.yaml b/samples/nodejs-react-postgres/compose.yaml index f9d791fa5..4f85c3a9f 100644 --- a/samples/nodejs-react-postgres/compose.yaml +++ b/samples/nodejs-react-postgres/compose.yaml @@ -1,34 +1,40 @@ +name: nodejs-react-postgres services: - todo_list: + app: restart: unless-stopped build: - context: ./client + context: . dockerfile: Dockerfile - args: - - REACT_APP_API_URL=https://-server--3010.prod1.defang.dev ports: - mode: ingress - target: 3000 - published: 3000 - deploy: - resources: - reservations: - memory: 1g - depends_on: - - server - server: - restart: unless-stopped - build: - context: ./server - dockerfile: Dockerfile - ports: - - target: 3010 - mode: ingress + target: 3010 published: 3010 environment: - - DATABASE_URL + DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3010/todos"] + interval: 10s + timeout: 5s + retries: 5 deploy: resources: reservations: cpus: "0.5" memory: 512M + depends_on: + - db + db: + image: postgres:15 + x-defang-postgres: true + restart: unless-stopped + environment: + - POSTGRES_PASSWORD + ports: + - mode: host + target: 5432 + published: 5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 diff --git a/samples/nodejs-react-postgres/server/index.js b/samples/nodejs-react-postgres/server/index.js index 5b176633f..8ca113b15 100644 --- a/samples/nodejs-react-postgres/server/index.js +++ b/samples/nodejs-react-postgres/server/index.js @@ -5,6 +5,7 @@ const pool = require("./db") app.use(cors()); app.use(express.json()) +app.use(express.static(__dirname + "/public")) const createData = `CREATE TABLE IF NOT EXISTS todo( todo_id SERIAL PRIMARY KEY, From e42e88df23758f396b181dd32f99560d9db9ef25 Mon Sep 17 00:00:00 2001 From: defangdevs Date: Fri, 30 Oct 2026 13:18:20 -0700 Subject: [PATCH 2/9] nodejs-react-postgres: read POSTGRES_PASSWORD as config, not compose interpolation CI warned the variable was unset: ${VAR} interpolation reads the shell environment, not Defang config. Pass the bare secret and build the connection in db.js. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RVYuG3wyXinFEZCmC9KuET --- samples/nodejs-react-postgres/compose.yaml | 2 +- samples/nodejs-react-postgres/server/db.js | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/samples/nodejs-react-postgres/compose.yaml b/samples/nodejs-react-postgres/compose.yaml index 4f85c3a9f..ff64f7366 100644 --- a/samples/nodejs-react-postgres/compose.yaml +++ b/samples/nodejs-react-postgres/compose.yaml @@ -10,7 +10,7 @@ services: target: 3010 published: 3010 environment: - DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres + - POSTGRES_PASSWORD healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3010/todos"] interval: 10s diff --git a/samples/nodejs-react-postgres/server/db.js b/samples/nodejs-react-postgres/server/db.js index aaeff8a1e..53ce51f3e 100644 --- a/samples/nodejs-react-postgres/server/db.js +++ b/samples/nodejs-react-postgres/server/db.js @@ -2,10 +2,17 @@ const Pool = require("pg").Pool; var parse = require('pg-connection-string').parse; -var config = parse(process.env.DATABASE_URL) +// DATABASE_URL (local dev) or, when deployed, the managed Postgres password +var config = process.env.DATABASE_URL + ? parse(process.env.DATABASE_URL) + : { + host: "db", + port: 5432, + user: "postgres", + password: process.env.POSTGRES_PASSWORD, + database: "postgres", + } const pool = new Pool(config) - - -module.exports = pool; \ No newline at end of file +module.exports = pool; From 9a042c35818d9a365c1a2ed82373bebd16044229 Mon Sep 17 00:00:00 2001 From: defangdevs Date: Fri, 30 Oct 2026 13:24:53 -0700 Subject: [PATCH 3/9] nodejs-react-postgres: SSL for managed Postgres, db reservations, ignore .env.* Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RVYuG3wyXinFEZCmC9KuET --- samples/nodejs-react-postgres/.dockerignore | 1 + samples/nodejs-react-postgres/README.md | 2 ++ samples/nodejs-react-postgres/compose.yaml | 5 +++++ samples/nodejs-react-postgres/server/db.js | 2 ++ 4 files changed, 10 insertions(+) diff --git a/samples/nodejs-react-postgres/.dockerignore b/samples/nodejs-react-postgres/.dockerignore index ddeab3ef1..ce5a494ad 100644 --- a/samples/nodejs-react-postgres/.dockerignore +++ b/samples/nodejs-react-postgres/.dockerignore @@ -1,3 +1,4 @@ **/node_modules **/build **/.env +**/.env.* diff --git a/samples/nodejs-react-postgres/README.md b/samples/nodejs-react-postgres/README.md index 6267fa98a..bc4cf1c85 100644 --- a/samples/nodejs-react-postgres/README.md +++ b/samples/nodejs-react-postgres/README.md @@ -36,6 +36,8 @@ The password for the Postgres database. defang config set POSTGRES_PASSWORD ``` +The server connects to Postgres over SSL, as Defang managed Postgres requires. If you deploy to the Playground, where Postgres runs as a plain container, set `POSTGRES_SSL=false` on the `app` service. + ## Deployment > [!NOTE] diff --git a/samples/nodejs-react-postgres/compose.yaml b/samples/nodejs-react-postgres/compose.yaml index ff64f7366..fa374706e 100644 --- a/samples/nodejs-react-postgres/compose.yaml +++ b/samples/nodejs-react-postgres/compose.yaml @@ -29,6 +29,11 @@ services: restart: unless-stopped environment: - POSTGRES_PASSWORD + deploy: + resources: + reservations: + cpus: "0.5" + memory: 512M ports: - mode: host target: 5432 diff --git a/samples/nodejs-react-postgres/server/db.js b/samples/nodejs-react-postgres/server/db.js index 53ce51f3e..6ecd98edb 100644 --- a/samples/nodejs-react-postgres/server/db.js +++ b/samples/nodejs-react-postgres/server/db.js @@ -11,6 +11,8 @@ var config = process.env.DATABASE_URL user: "postgres", password: process.env.POSTGRES_PASSWORD, database: "postgres", + // Defang managed Postgres requires SSL; set POSTGRES_SSL=false for a plain Postgres + ssl: process.env.POSTGRES_SSL === "false" ? false : { rejectUnauthorized: false }, } const pool = new Pool(config) From 12e34b65421dcc76e9d8ae315fd1df8352720ac4 Mon Sep 17 00:00:00 2001 From: defangdevs Date: Fri, 30 Oct 2026 13:30:02 -0700 Subject: [PATCH 4/9] nodejs-react-postgres: retry DB init until the database is reachable Staging deploy crashed with ENOTFOUND db: the app started before the db service's DNS name resolved. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RVYuG3wyXinFEZCmC9KuET --- samples/nodejs-react-postgres/server/index.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/samples/nodejs-react-postgres/server/index.js b/samples/nodejs-react-postgres/server/index.js index 8ca113b15..1b3adbd04 100644 --- a/samples/nodejs-react-postgres/server/index.js +++ b/samples/nodejs-react-postgres/server/index.js @@ -12,7 +12,24 @@ const createData = `CREATE TABLE IF NOT EXISTS todo( description VARCHAR(255) )` -pool.query(createData) +// The database may not be reachable yet when the app starts, so retry +async function initDb(retries = 30) { + for (let i = 0; ; i++) { + try { + await pool.query(createData) + return + } catch (error) { + if (i >= retries) throw error + console.error(`database not ready (${error.message}), retrying...`) + await new Promise((resolve) => setTimeout(resolve, 2000)) + } + } +} + +initDb().catch((error) => { + console.error(error) + process.exit(1) +}) //Routes From f76bab8ecea329e4d4918b33f5117d9690abf440 Mon Sep 17 00:00:00 2001 From: defangdevs Date: Fri, 30 Oct 2026 13:41:23 -0700 Subject: [PATCH 5/9] nodejs-react-postgres: drop published port on db, as in the rails sample Staging could not resolve "db" and the harness tested the db port as a URL. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RVYuG3wyXinFEZCmC9KuET --- samples/nodejs-react-postgres/compose.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/nodejs-react-postgres/compose.yaml b/samples/nodejs-react-postgres/compose.yaml index fa374706e..c3066f073 100644 --- a/samples/nodejs-react-postgres/compose.yaml +++ b/samples/nodejs-react-postgres/compose.yaml @@ -37,7 +37,6 @@ services: ports: - mode: host target: 5432 - published: 5432 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s From 7beb07d8441972ace2c418d2e348b484184d8b92 Mon Sep 17 00:00:00 2001 From: defangdevs Date: Fri, 30 Oct 2026 14:32:31 -0700 Subject: [PATCH 6/9] nodejs-react-postgres: use Debian base so the short service name "db" resolves Staging logged getaddrinfo ENOTFOUND db from the Alpine image; the rails sample, which resolves db fine, runs on glibc. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RVYuG3wyXinFEZCmC9KuET --- samples/nodejs-react-postgres/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/nodejs-react-postgres/Dockerfile b/samples/nodejs-react-postgres/Dockerfile index ba9bb7c3f..5cd5cef6b 100644 --- a/samples/nodejs-react-postgres/Dockerfile +++ b/samples/nodejs-react-postgres/Dockerfile @@ -9,7 +9,7 @@ ENV REACT_APP_API_URL="" RUN npm run build # Run the API server, which also serves the built client -FROM node:20-alpine +FROM node:20-bookworm-slim WORKDIR /app COPY server/package*.json ./ RUN npm install --omit=dev From c6fe27e84787c501d2aec731c9d16958626fba5b Mon Sep 17 00:00:00 2001 From: defangdevs Date: Fri, 30 Oct 2026 14:38:55 -0700 Subject: [PATCH 7/9] nodejs-react-postgres: back to the Alpine runtime image The Debian image did not fix name resolution and has no wget, which the app healthcheck uses. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RVYuG3wyXinFEZCmC9KuET --- samples/nodejs-react-postgres/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/nodejs-react-postgres/Dockerfile b/samples/nodejs-react-postgres/Dockerfile index 5cd5cef6b..ba9bb7c3f 100644 --- a/samples/nodejs-react-postgres/Dockerfile +++ b/samples/nodejs-react-postgres/Dockerfile @@ -9,7 +9,7 @@ ENV REACT_APP_API_URL="" RUN npm run build # Run the API server, which also serves the built client -FROM node:20-bookworm-slim +FROM node:20-alpine WORKDIR /app COPY server/package*.json ./ RUN npm install --omit=dev From 1837b59f25d0f05442266bcbe30e0697900e185b Mon Sep 17 00:00:00 2001 From: defangdevs Date: Fri, 30 Oct 2026 14:46:40 -0700 Subject: [PATCH 8/9] loadtest: skip non-HTTP service endpoints A database's internal host:port endpoint was tested with an HTTP GET and failed with 'unsupported protocol scheme', so any sample with a host-mode database service could not pass. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RVYuG3wyXinFEZCmC9KuET --- tools/testing/deployer/deployer.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/testing/deployer/deployer.go b/tools/testing/deployer/deployer.go index 9463beefc..cd793d57f 100644 --- a/tools/testing/deployer/deployer.go +++ b/tools/testing/deployer/deployer.go @@ -3,6 +3,7 @@ package deployer import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -14,7 +15,6 @@ import ( "regexp" "strings" "time" - "encoding/json" "defang.io/tools/testing/detector" "defang.io/tools/testing/logger" @@ -223,8 +223,13 @@ func (d *CliDeployer) RunDeployTest(ctx context.Context, t test.TestInfo) (*test return result, fmt.Errorf(result.Message) } - result.TotalServices = len(services) for _, svc := range services { + // Non-HTTP endpoints (e.g. a database's internal host:port) cannot be tested with a GET + if svc.Endpoint != "" && !strings.HasPrefix(svc.Endpoint, "http://") && !strings.HasPrefix(svc.Endpoint, "https://") { + log.Printf(" * Skipping non-HTTP endpoint %v of service %v", svc.Endpoint, svc.Service) + continue + } + result.TotalServices++ if svc.Endpoint == "" { continue } From e70588356c7a586959c9e86024dbdc75557217f6 Mon Sep 17 00:00:00 2001 From: defangdevs Date: Fri, 30 Oct 2026 14:49:02 -0700 Subject: [PATCH 9/9] nodejs-react-postgres: pass the db host as a compose env var The Defang CLI rewrites service names in compose env values (db becomes db-), but not a hostname hard-coded in the app. That caused ENOTFOUND db on staging. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RVYuG3wyXinFEZCmC9KuET --- samples/nodejs-react-postgres/compose.yaml | 3 ++- samples/nodejs-react-postgres/server/db.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/nodejs-react-postgres/compose.yaml b/samples/nodejs-react-postgres/compose.yaml index c3066f073..b5d065f47 100644 --- a/samples/nodejs-react-postgres/compose.yaml +++ b/samples/nodejs-react-postgres/compose.yaml @@ -10,7 +10,8 @@ services: target: 3010 published: 3010 environment: - - POSTGRES_PASSWORD + POSTGRES_HOST: db + POSTGRES_PASSWORD: healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3010/todos"] interval: 10s diff --git a/samples/nodejs-react-postgres/server/db.js b/samples/nodejs-react-postgres/server/db.js index 6ecd98edb..691fe8285 100644 --- a/samples/nodejs-react-postgres/server/db.js +++ b/samples/nodejs-react-postgres/server/db.js @@ -2,11 +2,11 @@ const Pool = require("pg").Pool; var parse = require('pg-connection-string').parse; -// DATABASE_URL (local dev) or, when deployed, the managed Postgres password +// DATABASE_URL (local dev) or, when deployed, POSTGRES_HOST and POSTGRES_PASSWORD var config = process.env.DATABASE_URL ? parse(process.env.DATABASE_URL) : { - host: "db", + host: process.env.POSTGRES_HOST || "localhost", port: 5432, user: "postgres", password: process.env.POSTGRES_PASSWORD,