Skip to content

Commit

Permalink
[New Example] with docker - multiple deployment environments (vercel#…
Browse files Browse the repository at this point in the history
…34015)

## Documentation / Examples

- [x] Make sure the linting passes by running `yarn lint`

---
## Context

Having 3 environments:
- Development: for doing testing
- Staging: for doing UAT testing
- Production: for users

In each environment, the Next.js application makes API calls to the corresponding API gateway:
- Development: https://api-development.com
- Staging: https://api-staging.com
- Production: https://api-production.com

Using `NEXT_PUBLIC_API_URL` for the `baseUrl` of [axios](https://axios-http.com/docs/intro).

Since the `NEXT_PUBLIC_API_URL` is replaced during _build time_, we have to manage to provide the corresponding `.env.production` files for Docker at _build time_ for each environment. 

## Solution

Since we are using CI services for dockerization, we could setup the CI to inject the correct `.env.production` file into the cloned source code, (this is actually what we did). Doing that would require us to touch the CI settings.

Another way is using multiple Dockerfile (the former only need to use one Dockerfile), and the trick is copying the corresponding `env*.sample` and rename it to `.env.production` then putting it into the Docker context. Doing this way, everything is managed in the source code.

```
> Dockerfile

# Development environment
COPY .env.development.sample .env.production

# Staging environment
COPY .env.staging.sample .env.production

# Production environment
COPY .env.production.sample .env.production
```

Testing these images locally is also simple, by issuing the corresponding Makefile commands we can simulate exactly how the image will be built in the CI environment.

## How to use
For development environment:

```
make build-development
make start-development
```

For staging environment:

```
make build-staging
make start-staging
```

For production environment:

```
make build-production
make start-production
```

## Conclusion

This example shows one way to solve the three-environment model in software development when building a Next.js application. There might be another better way and I would love to know about them as well. 

I'm making this example because I can't find any example about this kind of problem.



Co-authored-by: Tú Nguyễn <93700515+tunguyen-ct@users.noreply.github.com>
  • Loading branch information
2 people authored and natew committed Feb 16, 2022
1 parent b279c41 commit 25fc80c
Show file tree
Hide file tree
Showing 23 changed files with 549 additions and 0 deletions.
7 changes: 7 additions & 0 deletions examples/with-docker-multi-env/.dockerignore
@@ -0,0 +1,7 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
docker
1 change: 1 addition & 0 deletions examples/with-docker-multi-env/.env
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=http://localhost
1 change: 1 addition & 0 deletions examples/with-docker-multi-env/.env.development.sample
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=https://api-development.com
1 change: 1 addition & 0 deletions examples/with-docker-multi-env/.env.production.sample
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=https://api-production.com
1 change: 1 addition & 0 deletions examples/with-docker-multi-env/.env.staging.sample
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=https://api-staging.com
38 changes: 38 additions & 0 deletions examples/with-docker-multi-env/.gitignore
@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel

# lock files
yarn.lock
package-lock.json
35 changes: 35 additions & 0 deletions examples/with-docker-multi-env/Makefile
@@ -0,0 +1,35 @@
.PHONY: build-development
build-development: ## Build the development docker image.
docker compose -f docker/development/docker-compose.yml build

.PHONY: start-development
start-development: ## Start the development docker container.
docker compose -f docker/development/docker-compose.yml up -d

.PHONY: stop-development
stop-development: ## Stop the development docker container.
docker compose -f docker/development/docker-compose.yml down

.PHONY: build-staging
build-staging: ## Build the staging docker image.
docker compose -f docker/staging/docker-compose.yml build

.PHONY: start-staging
start-staging: ## Start the staging docker container.
docker compose -f docker/staging/docker-compose.yml up -d

.PHONY: stop-staging
stop-staging: ## Stop the staging docker container.
docker compose -f docker/staging/docker-compose.yml down

.PHONY: build-production
build-production: ## Build the production docker image.
docker compose -f docker/production/docker-compose.yml build

.PHONY: start-production
start-production: ## Start the production docker container.
docker compose -f docker/production/docker-compose.yml up -d

.PHONY: stop-production
stop-production: ## Stop the production docker container.
docker compose -f docker/production/docker-compose.yml down
62 changes: 62 additions & 0 deletions examples/with-docker-multi-env/README.md
@@ -0,0 +1,62 @@
# With Docker - Multiple Deployment Environments

This examples shows how to use Docker with Next.js and deploy to multiple environment with different env values. Based on the [deployment documentation](https://nextjs.org/docs/deployment#docker-image).

## How to use

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
npx create-next-app --example with-docker-multi-env nextjs-docker-multi-env
# or
yarn create next-app --example with-docker-multi-env nextjs-docker-multi-env
```

Enter the values in the `.env.development.sample`, `.env.staging.sample`, `.env.production.sample` files to be used for each environments.

## Using Docker and Makefile

### Development environment - for doing testing

```
make build-development
make start-development
```

Open http://localhost:3001

### Staging environment - for doing UAT testing

```
make build-staging
make start-staging
```

Open http://localhost:3002

### Production environment - for users

```
make build-production
make start-production
```

Open http://localhost:3003

## Running Locally

First, run the development server:

```bash
npm run dev
# or
yarn dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.

[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.

The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
45 changes: 45 additions & 0 deletions examples/with-docker-multi-env/docker/development/Dockerfile
@@ -0,0 +1,45 @@
# 1. Install dependencies only when needed
FROM node:16-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat

WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# 2. Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# This will do the trick, use the corresponding env file for each environment.
COPY .env.development.sample .env.production
RUN yarn build

# 3. Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static


USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]
@@ -0,0 +1,10 @@
version: '3'

services:
with-docker-multi-env-development:
build:
context: ../../
dockerfile: docker/development/Dockerfile
image: with-docker-multi-env-development
ports:
- '3001:3000'
45 changes: 45 additions & 0 deletions examples/with-docker-multi-env/docker/production/Dockerfile
@@ -0,0 +1,45 @@
# 1. Install dependencies only when needed
FROM node:16-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat

WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# 2. Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# This will do the trick, use the corresponding env file for each environment.
COPY .env.production.sample .env.production
RUN yarn build

# 3. Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static


USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]
@@ -0,0 +1,10 @@
version: '3'

services:
with-docker-multi-env-production:
build:
context: ../../
dockerfile: docker/production/Dockerfile
image: with-docker-multi-env-production
ports:
- '3003:3000'
45 changes: 45 additions & 0 deletions examples/with-docker-multi-env/docker/staging/Dockerfile
@@ -0,0 +1,45 @@
# 1. Install dependencies only when needed
FROM node:16-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat

WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# 2. Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# This will do the trick, use the corresponding env file for each environment.
COPY .env.staging.sample .env.production
RUN yarn build

# 3. Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static


USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]
10 changes: 10 additions & 0 deletions examples/with-docker-multi-env/docker/staging/docker-compose.yml
@@ -0,0 +1,10 @@
version: '3'

services:
with-docker-multi-env-staging:
build:
context: ../../
dockerfile: docker/staging/Dockerfile
image: with-docker-multi-env-staging
ports:
- '3002:3000'
5 changes: 5 additions & 0 deletions examples/with-docker-multi-env/next.config.js
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
outputStandalone: true,
},
}
13 changes: 13 additions & 0 deletions examples/with-docker-multi-env/package.json
@@ -0,0 +1,13 @@
{
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
7 changes: 7 additions & 0 deletions examples/with-docker-multi-env/pages/_app.js
@@ -0,0 +1,7 @@
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}

export default MyApp
5 changes: 5 additions & 0 deletions examples/with-docker-multi-env/pages/api/hello.js
@@ -0,0 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default function hello(req, res) {
res.status(200).json({ name: 'John Doe' })
}

0 comments on commit 25fc80c

Please sign in to comment.