Skip to content

Commit 663c23c

Browse files
DiluDevXAman Varshneyclaude
authored
feat: add rest-express-docker-aws-ec2 deployment example (#8473)
* feat: add rest-express-docker-aws-ec2 deployment example * fix: address Copilot review feedback * address PR review: input validation, error handling, env guard, dynamic port, generate scripts * address PR review: author lookup, 201 on create, authorId index, typecheck script, README improvements * address PR review: try/catch on all handlers, 201 for POST /user * fix: resolve runtime module resolution and build configuration issues - Update Dockerfile CMD to use 'dist/src/index.js' to match TypeScript output structure without rootDir constraint - Update docker-compose.yml command to use correct compiled path - Fix prisma.config.ts to use process.env.DATABASE_URL ?? '' instead of env() to avoid build-time failures when DATABASE_URL is not set - Add 'declaration: true' to tsconfig.json for proper type definitions - All API endpoints now pass e2e tests: POST /user, POST /post, GET /feed, PUT /publish/:id, GET /post/:id, DELETE /post/:id - Validation and error handling confirmed working (400, 404, 500 responses) * fix(rest-express-docker-aws-ec2): fix Dockerfile CMD path, bump Prisma, add test entry - Fix CMD path: dist/src/index.js -> dist/index.js (tsconfig rootDir is src/) - Bump @prisma/client, @prisma/adapter-pg, prisma to 7.5.0 - Add skip entry in tests/deployment-platforms.test.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * revert: restore correct Dockerfile CMD path dist/src/index.js is the correct path — TypeScript infers rootDir as project root (not src/) because prisma/generated files are transitively included, making dist/src/index.js the actual output location. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Aman Varshney <aman@Amans-MacBook-Pro.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5f80ff8 commit 663c23c

18 files changed

Lines changed: 634 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
.env
4+
prisma/generated
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/DATABASE"
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Deploy to AWS EC2
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- latest
8+
9+
jobs:
10+
deploy:
11+
runs-on: ubuntu-latest
12+
13+
env:
14+
AWS_REGION: ${{ vars.AWS_REGION }}
15+
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
16+
CONTAINER_NAME: ${{ vars.CONTAINER_NAME }}
17+
CONTAINER_PORT: ${{ vars.CONTAINER_PORT }}
18+
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Configure AWS credentials
24+
uses: aws-actions/configure-aws-credentials@v4
25+
with:
26+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
27+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
28+
aws-region: ${{ env.AWS_REGION }}
29+
30+
- name: Set up Docker Buildx
31+
uses: docker/setup-buildx-action@v3
32+
33+
- name: Login to Amazon ECR
34+
id: login-ecr
35+
uses: aws-actions/amazon-ecr-login@v2
36+
37+
- name: Build and push Docker image
38+
uses: docker/build-push-action@v6
39+
with:
40+
context: .
41+
push: true
42+
tags: |
43+
${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
44+
${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
45+
cache-from: type=gha
46+
cache-to: type=gha,mode=max
47+
48+
- name: Deploy to EC2
49+
uses: appleboy/ssh-action@8743aa11bfbda97acb45c151ae7a2e0b203f1914
50+
env:
51+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
52+
with:
53+
host: ${{ secrets.EC2_HOST }}
54+
username: ${{ secrets.EC2_USER }}
55+
key: ${{ secrets.EC2_SSH_KEY }}
56+
envs: ECR_REGISTRY,ECR_REPOSITORY,AWS_REGION,CONTAINER_NAME,CONTAINER_PORT
57+
script: |
58+
set -eu
59+
60+
# Auth
61+
aws ecr get-login-password --region "$AWS_REGION" | \
62+
docker login --username AWS --password-stdin "$ECR_REGISTRY"
63+
64+
# Pull latest image
65+
docker pull "$ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}"
66+
67+
# Stop and remove existing container if running
68+
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
69+
docker stop "$CONTAINER_NAME"
70+
docker rm "$CONTAINER_NAME"
71+
fi
72+
73+
# Run database migrations
74+
docker run --rm \
75+
-e DATABASE_URL="${{ secrets.DATABASE_URL }}" \
76+
"$ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}" \
77+
npx prisma migrate deploy
78+
79+
# Run new container
80+
docker run -d \
81+
--name "$CONTAINER_NAME" \
82+
--restart unless-stopped \
83+
-p "$CONTAINER_PORT:3000" \
84+
-e DATABASE_URL="${{ secrets.DATABASE_URL }}" \
85+
"$ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}"
86+
87+
# Verify container is running
88+
sleep 5
89+
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
90+
echo "Container $CONTAINER_NAME is running"
91+
docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
92+
else
93+
echo "Container $CONTAINER_NAME failed to start"
94+
docker logs "$CONTAINER_NAME" --tail 50
95+
exit 1
96+
fi
97+
98+
# Prune old images
99+
docker image prune -f
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
.env
4+
prisma/generated
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Stage 1: builder
2+
FROM node:20-alpine AS builder
3+
4+
WORKDIR /app
5+
6+
COPY package*.json ./
7+
COPY prisma ./prisma
8+
COPY prisma.config.ts ./
9+
RUN npm install
10+
RUN npx prisma generate
11+
12+
COPY tsconfig.json ./
13+
COPY src ./src
14+
RUN npm run build
15+
16+
# Stage 2: runner
17+
FROM node:20-alpine AS runner
18+
19+
WORKDIR /app
20+
21+
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
22+
23+
COPY package*.json ./
24+
COPY prisma ./prisma
25+
COPY prisma.config.ts ./
26+
RUN npm install --omit=dev
27+
28+
COPY --from=builder /app/dist ./dist
29+
COPY --from=builder /app/prisma/generated ./prisma/generated
30+
31+
USER appuser
32+
33+
EXPOSE 3000
34+
35+
CMD ["node", "dist/src/index.js"]
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# REST API with Express, Docker & AWS EC2
2+
3+
This example shows how to deploy a **Prisma REST API** (Express + TypeScript) to **AWS EC2** using **Docker** and **GitHub Actions**.
4+
5+
## Prerequisites
6+
7+
- [Docker](https://www.docker.com/) and Docker Compose (for the Docker path)
8+
- [Node.js](https://nodejs.org/) 20+ and a local PostgreSQL instance (for the non-Docker path)
9+
- An AWS account (for deployment only)
10+
11+
## Getting started
12+
13+
### 1. Clone the repository
14+
15+
```sh
16+
git clone https://github.com/prisma/prisma-examples.git --depth=1
17+
cd prisma-examples/deployment-platforms/rest-express-docker-aws-ec2
18+
```
19+
20+
### 2. Run the app
21+
22+
Choose one of the two local development paths below.
23+
24+
---
25+
26+
#### Option A — Docker Compose (recommended)
27+
28+
Copy the example env file (Docker Compose sets `DATABASE_URL` automatically, so no edits needed):
29+
30+
```sh
31+
cp .env.example .env
32+
```
33+
34+
Start the app and a local Postgres database:
35+
36+
```sh
37+
docker compose up --build
38+
```
39+
40+
The server is now running at `http://localhost:3000`. Migrations are applied automatically on startup.
41+
42+
---
43+
44+
#### Option B — Local Node.js + PostgreSQL
45+
46+
Create a `.env` file and set `DATABASE_URL` to your local database:
47+
48+
```sh
49+
cp .env.example .env
50+
# edit .env and set DATABASE_URL, e.g.:
51+
# DATABASE_URL="postgresql://prisma:prisma@localhost:5432/prisma"
52+
```
53+
54+
Install dependencies and generate the Prisma Client:
55+
56+
```sh
57+
npm install
58+
npx prisma migrate dev
59+
```
60+
61+
Start the development server:
62+
63+
```sh
64+
npm run dev
65+
```
66+
67+
The server is now running at `http://localhost:3000`.
68+
69+
---
70+
71+
### 3. Use the REST API
72+
73+
Create a user:
74+
75+
```sh
76+
curl -X POST http://localhost:3000/user \
77+
-H "Content-Type: application/json" \
78+
-d '{"email": "alice@prisma.io", "name": "Alice"}'
79+
```
80+
81+
Create a post:
82+
83+
```sh
84+
curl -X POST http://localhost:3000/post \
85+
-H "Content-Type: application/json" \
86+
-d '{"title": "Hello Prisma", "content": "My first post", "authorEmail": "alice@prisma.io"}'
87+
```
88+
89+
Publish a post:
90+
91+
```sh
92+
curl -X PUT http://localhost:3000/publish/1
93+
```
94+
95+
Fetch all published posts:
96+
97+
```sh
98+
curl http://localhost:3000/feed
99+
```
100+
101+
Fetch a single post:
102+
103+
```sh
104+
curl http://localhost:3000/post/1
105+
```
106+
107+
Delete a post:
108+
109+
```sh
110+
curl -X DELETE http://localhost:3000/post/1
111+
```
112+
113+
---
114+
115+
## Deploying to AWS EC2
116+
117+
### 1. Set up AWS prerequisites
118+
119+
**ECR repository** — create one if you haven't already:
120+
121+
```sh
122+
aws ecr create-repository --repository-name my-prisma-app --region us-east-1
123+
```
124+
125+
**EC2 instance** — launch an instance (Amazon Linux 2 or Ubuntu) and install Docker:
126+
127+
```sh
128+
# Amazon Linux 2
129+
sudo yum update -y
130+
sudo amazon-linux-extras install docker -y
131+
sudo service docker start
132+
sudo usermod -aG docker ec2-user
133+
```
134+
135+
The EC2 instance needs permission to pull images from ECR. Choose one option:
136+
137+
- **Option 1 (recommended):** Attach an IAM instance role with the `AmazonEC2ContainerRegistryReadOnly` policy. No credentials are stored on the instance.
138+
- **Option 2:** Run `aws configure` on the instance and enter an IAM access key that has ECR read permissions.
139+
140+
This is required by the `deploy.yml` step that runs `aws ecr get-login-password` on the instance before pulling the Docker image.
141+
142+
Make sure port `3000` (or your chosen `CONTAINER_PORT`) is open in the instance's security group.
143+
144+
### 2. Configure GitHub secrets and variables
145+
146+
In your repository, go to **Settings → Secrets and variables → Actions** and add:
147+
148+
**Secrets** (sensitive values):
149+
150+
| Name | Description |
151+
|---|---|
152+
| `AWS_ACCESS_KEY_ID` | AWS IAM access key with ECR and EC2 permissions |
153+
| `AWS_SECRET_ACCESS_KEY` | Corresponding secret key |
154+
| `EC2_HOST` | Public IP or DNS of your EC2 instance |
155+
| `EC2_USER` | SSH username (e.g. `ec2-user` or `ubuntu`) |
156+
| `EC2_SSH_KEY` | Private SSH key used to connect to EC2 |
157+
| `DATABASE_URL` | PostgreSQL connection string for your production database |
158+
159+
**Variables** (non-sensitive values):
160+
161+
| Name | Example value |
162+
|---|---|
163+
| `AWS_REGION` | `us-east-1` |
164+
| `ECR_REPOSITORY` | `my-prisma-app` |
165+
| `CONTAINER_NAME` | `prisma-app` |
166+
| `CONTAINER_PORT` | `3000` |
167+
168+
### 3. How deployment works
169+
170+
Copy [`.github/workflows/deploy.yml`](./.github/workflows/deploy.yml) to `.github/workflows/` at the root of **your own repository**. Pushing to `main` or `latest` triggers the workflow, which performs the following steps:
171+
172+
1. Authenticates with AWS using the configured IAM credentials.
173+
2. Builds a Docker image using Buildx with GitHub Actions layer caching for faster rebuilds.
174+
3. Pushes the image to ECR tagged with both the commit SHA and `latest`.
175+
4. SSHs into your EC2 instance.
176+
5. Runs `prisma migrate deploy` against your production database in a one-off container.
177+
6. Pulls the new image.
178+
7. Stops and removes the old container if one is running.
179+
8. Starts the new container with `DATABASE_URL` injected at runtime.
180+
9. Waits 5 seconds and verifies the container is running — prints logs and exits non-zero on failure.
181+
10. Prunes old images to keep the EC2 disk clean.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
services:
2+
app:
3+
build: .
4+
ports:
5+
- "3000:3000"
6+
environment:
7+
DATABASE_URL: postgresql://prisma:prisma@postgres:5432/prisma
8+
depends_on:
9+
postgres:
10+
condition: service_healthy
11+
command: >
12+
sh -c "npx prisma migrate deploy && node dist/src/index.js"
13+
14+
postgres:
15+
image: postgres:16-alpine
16+
environment:
17+
POSTGRES_USER: prisma
18+
POSTGRES_PASSWORD: prisma
19+
POSTGRES_DB: prisma
20+
ports:
21+
- "5432:5432"
22+
healthcheck:
23+
test: ["CMD-SHELL", "pg_isready -U prisma"]
24+
interval: 5s
25+
timeout: 5s
26+
retries: 5
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "rest-express-docker-aws-ec2",
3+
"version": "1.0.0",
4+
"license": "MIT",
5+
"scripts": {
6+
"build": "prisma generate && tsc",
7+
"typecheck": "tsc --noEmit",
8+
"dev": "prisma generate && npm run typecheck && tsx src/index.ts",
9+
"start": "node dist/index.js"
10+
},
11+
"dependencies": {
12+
"@prisma/adapter-pg": "7.5.0",
13+
"@prisma/client": "7.5.0",
14+
"dotenv": "^17.2.1",
15+
"express": "5.1.0",
16+
"pg": "^8.16.3",
17+
"prisma": "7.5.0"
18+
},
19+
"devDependencies": {
20+
"@types/express": "5.0.5",
21+
"@types/node": "22.18.12",
22+
"@types/pg": "^8.15.6",
23+
"tsx": "^4.20.6",
24+
"typescript": "5.8.2"
25+
}
26+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from 'prisma/config'
2+
import 'dotenv/config'
3+
4+
export default defineConfig({
5+
schema: 'prisma/schema.prisma',
6+
migrations: {
7+
path: 'prisma/migrations',
8+
},
9+
datasource: {
10+
url: process.env.DATABASE_URL ?? '',
11+
},
12+
})

0 commit comments

Comments
 (0)