Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions samples/nodejs-react-postgres/.dockerignore
Comment thread
defangdevs marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/node_modules
**/build
**/.env
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
**/.env.*
19 changes: 19 additions & 0 deletions samples/nodejs-react-postgres/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
11 changes: 5 additions & 6 deletions samples/nodejs-react-postgres/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -30,14 +30,13 @@ 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 `<YOUR_USERNAME>` with your username, which you can get by running `defang whoami`.
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

Expand Down
45 changes: 28 additions & 17 deletions samples/nodejs-react-postgres/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
name: nodejs-react-postgres
services:
todo_list:
app:
restart: unless-stopped
build:
context: ./client
context: .
dockerfile: Dockerfile
args:
- REACT_APP_API_URL=https://<YOUR_USERNAME>-server--3010.prod1.defang.dev
ports:
- mode: ingress
target: 3000
published: 3000
target: 3010
published: 3010
environment:
POSTGRES_HOST: db
POSTGRES_PASSWORD:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3010/todos"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
reservations:
memory: 1g
cpus: "0.5"
memory: 512M
depends_on:
- server
server:
- db
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
db:
image: postgres:15
Comment thread
defangdevs marked this conversation as resolved.
Resolved
x-defang-postgres: true
restart: unless-stopped
build:
context: ./server
dockerfile: Dockerfile
ports:
- target: 3010
mode: ingress
published: 3010
environment:
- DATABASE_URL
- POSTGRES_PASSWORD
deploy:
resources:
reservations:
cpus: "0.5"
memory: 512M
ports:
- mode: host
target: 5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
17 changes: 13 additions & 4 deletions samples/nodejs-react-postgres/server/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ 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, POSTGRES_HOST and POSTGRES_PASSWORD
var config = process.env.DATABASE_URL
? parse(process.env.DATABASE_URL)
: {
host: process.env.POSTGRES_HOST || "localhost",
port: 5432,
user: "postgres",
password: process.env.POSTGRES_PASSWORD,
database: "postgres",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// Defang managed Postgres requires SSL; set POSTGRES_SSL=false for a plain Postgres
ssl: process.env.POSTGRES_SSL === "false" ? false : { rejectUnauthorized: false },
Comment thread
defangdevs marked this conversation as resolved.
Resolved
}

const pool = new Pool(config)



module.exports = pool;
module.exports = pool;
20 changes: 19 additions & 1 deletion samples/nodejs-react-postgres/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,31 @@ 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,
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
Expand Down
9 changes: 7 additions & 2 deletions tools/testing/deployer/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package deployer
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -14,7 +15,6 @@ import (
"regexp"
"strings"
"time"
"encoding/json"

"defang.io/tools/testing/detector"
"defang.io/tools/testing/logger"
Expand Down Expand Up @@ -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
}
Expand Down
Loading