diff --git a/samples/nodejs-react-postgres/.dockerignore b/samples/nodejs-react-postgres/.dockerignore new file mode 100644 index 000000000..ce5a494ad --- /dev/null +++ b/samples/nodejs-react-postgres/.dockerignore @@ -0,0 +1,4 @@ +**/node_modules +**/build +**/.env +**/.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..bc4cf1c85 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,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 `` 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 diff --git a/samples/nodejs-react-postgres/compose.yaml b/samples/nodejs-react-postgres/compose.yaml index f9d791fa5..b5d065f47 100644 --- a/samples/nodejs-react-postgres/compose.yaml +++ b/samples/nodejs-react-postgres/compose.yaml @@ -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://-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 + db: + image: postgres:15 + 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 diff --git a/samples/nodejs-react-postgres/server/db.js b/samples/nodejs-react-postgres/server/db.js index aaeff8a1e..691fe8285 100644 --- a/samples/nodejs-react-postgres/server/db.js +++ b/samples/nodejs-react-postgres/server/db.js @@ -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", + // 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) - - -module.exports = pool; \ No newline at end of file +module.exports = pool; diff --git a/samples/nodejs-react-postgres/server/index.js b/samples/nodejs-react-postgres/server/index.js index 5b176633f..1b3adbd04 100644 --- a/samples/nodejs-react-postgres/server/index.js +++ b/samples/nodejs-react-postgres/server/index.js @@ -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 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 }