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
6 changes: 5 additions & 1 deletion .github/workflows/build_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
# Changing the checked out ref has wide implications:
# Be aware that all git-diff operations in the repo rely on the
# default behavior of GitHub actions (ref on push, merge-commit on PR).
# 2 last commits so that we can create a diff with HEAD~1
fetch-depth: 2
persist-credentials: false
- run: echo "${{ github.event.after }}"
- name: Setup node
Expand Down
10 changes: 0 additions & 10 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -747,9 +747,7 @@ jobs:
test-new-tests-dev:
name: Test new and changed tests for flakes (dev)
needs: ['optimize-ci', 'changes', 'build-native', 'build-next']
# test-new-tests-if
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
# test-new-tests-end-if

strategy:
fail-fast: false
Expand All @@ -776,9 +774,7 @@ jobs:
test-new-tests-start:
name: Test new and changed tests for flakes (prod)
needs: ['optimize-ci', 'changes', 'build-native', 'build-next']
# test-new-tests-if
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
# test-new-tests-end-if

strategy:
fail-fast: false
Expand Down Expand Up @@ -814,9 +810,7 @@ jobs:
needs: ['optimize-ci', 'changes']
# `docs-only` and `is-release` mirror the cases where `build-and-deploy`
# resolves its deploy target to `skipped` and never publishes a tarball.
# test-new-tests-if
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' && needs.changes.outputs.is-release == 'false' }}
# test-new-tests-end-if
runs-on: ubuntu-latest
# Outer bound only. The script gives up first, so a tarball that never
# arrives is reported by it rather than by the runner killing the job.
Expand Down Expand Up @@ -850,9 +844,7 @@ jobs:
test-new-tests-deploy:
name: Test new and changed tests when deployed
needs: ['optimize-ci', 'changes', 'wait-for-preview-tarball']
# test-new-tests-if
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
# test-new-tests-end-if

strategy:
fail-fast: false
Expand Down Expand Up @@ -883,9 +875,7 @@ jobs:
test-new-tests-deploy-cache-components:
name: Test new and changed tests when deployed (cache components)
needs: ['optimize-ci', 'changes', 'wait-for-preview-tarball']
# test-new-tests-if
if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }}
# test-new-tests-end-if

strategy:
fail-fast: false
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build_reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ jobs:

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Changing the checked out ref has wide implications:
# Be aware that all git-diff operations in the repo rely on the
# default behavior of GitHub actions (ref on push, merge-commit on PR).
fetch-depth: 25
persist-credentials: false

Expand Down
6 changes: 5 additions & 1 deletion examples/with-docker-export-output/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ test-results/
*~
*.log

# Environment variables (only commit template files)
# Environment variables - keep credentials out of the build context.
# A static export has no run-time server, so anything left in the context is
# baked into the files served to every visitor. `.env.production` is
# deliberately not listed: use it for non-secret build-time config only. See
# the "Environment Variables" section of README.md.
.env
.env*.local
.env.development
Expand Down
34 changes: 34 additions & 0 deletions examples/with-docker-export-output/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,40 @@ Both Dockerfiles support multiple package managers:

The Dockerfiles automatically detect which lockfile is present and use the appropriate package manager.

## Environment Variables

The [`.dockerignore`](./.dockerignore) in this example **excludes `.env`**, so a local development file — which usually holds real credentials — is never copied into the build context or the final image.

With `output: "export"` there is **no server at run time**: Nginx (or `serve`) only hands out the prebuilt files in `out/`. `docker run -e …` therefore has no effect on the application. Values used by the exported application must be present while `next build` runs. Any value inlined or rendered into `out/` is served to every visitor.

### Non-secret configuration: use `.env.production`

`.env.production` is intentionally **not** ignored, so `next build` picks it up:

```bash
# .env.production
NEXT_PUBLIC_SITE_URL=https://example.com
```

### Per-environment values: use a build argument

To build the same source for several environments, pass the value into the build:

```dockerfile
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
# ... before the build step
```

```bash
docker build \
--build-arg NEXT_PUBLIC_SITE_URL=https://example.com \
-t nextjs-export-nginx .
```

> [!IMPORTANT]
> A static export cannot keep a secret. `NEXT_PUBLIC_*` values are inlined verbatim into the exported JavaScript, and values without the prefix are read while pages are prerendered, so anything derived from them can end up in the exported HTML. Keep API keys and other credentials out of the build entirely and call them from a separate backend.

## Deployment

This example can be deployed to any container-based platform:
Expand Down
7 changes: 6 additions & 1 deletion examples/with-docker/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ playwright.config.*
*~
*.log

# Environment variables (only commit template files)
# Environment variables - keep credentials out of the build context.
# Anything left in the context is loaded by `next build` and copied into
# `.next/standalone`, so it ships inside the runner image. `.env.production` is
# deliberately not listed: use it for non-secret build-time config, and pass
# real secrets at run time with `docker run -e`. See the "Environment Variables"
# section of README.md.
.env
.env*.local
.env.development
Expand Down
61 changes: 61 additions & 0 deletions examples/with-docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,67 @@ To switch to Alpine, simply change the `NODE_VERSION` ARG in the Dockerfile to `
> [!IMPORTANT]
> **Node.js Version Maintenance**: This Dockerfile uses Node.js 24.13.0-slim, which was the latest LTS version at the time of writing. To ensure security and stay up-to-date, regularly check and update the `NODE_VERSION` ARG in the Dockerfile to the latest Node.js LTS version. Check the latest version at [Nodejs official website](https://nodejs.org/) and browse available Node.js images on [Docker Hub](https://hub.docker.com/_/node).

## Environment Variables

The [`.dockerignore`](./.dockerignore) in this example **excludes `.env`**, so a local development file — which usually holds real credentials — is never copied into the build context or the final image.

The consequence is worth knowing up front: a value you rely on from `.env` is `undefined` inside the container, even though the same code works with `next build && next start` locally. Use one of the following instead.

### Secrets and server-only values: pass them at run time

Values read on the server at request time (Route Handlers, dynamically rendered Server Components, Server Actions) are read from the environment when the request happens, so they need nothing at build time:

```bash
docker run -p 3000:3000 -e MY_SECRET=value nextjs-standalone-image
```

or in [`compose.yml`](./compose.yml):

```yaml
services:
nextjs-standalone:
environment:
MY_SECRET: value
# or, to read a file that is not committed:
# env_file:
# - .env.production.local
```

Prefer this wherever it works: it keeps one image promotable across environments instead of baking values into a per-environment build.

### Non-secret build-time configuration: use `.env.production`

`.env.production` is intentionally **not** ignored, so it is available to `next build` and loaded by the server at run time. Use it only for values that are safe to publish:

```bash
# .env.production
NEXT_PUBLIC_SITE_URL=https://example.com
```

### Public values needed in the client bundle: use a build argument

`NEXT_PUBLIC_*` values referenced from Client Components are inlined into the JavaScript sent to the browser, so they have to be present while `next build` runs. Add them to the builder stage:

```dockerfile
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
# ... before the build step
```

```bash
docker build \
--build-arg NEXT_PUBLIC_SITE_URL=https://example.com \
-t nextjs-standalone-image .
```

> [!IMPORTANT]
> Never pass secrets as build arguments. They are recoverable from the image history, and `NEXT_PUBLIC_*` values are sent to the browser by definition.

> [!IMPORTANT]
> Any env file that **is** present in the build context is also copied into `.next/standalone` by `output: "standalone"`, and this Dockerfile copies that directory wholesale into the runner stage. The file therefore ships inside the image and is readable by anyone who can pull it. Keep credentials out of committed env files and pass them at run time.

To build a separate image per environment instead, see [`with-docker-multi-env`](../with-docker-multi-env).

## Deployment

This example can be deployed to any container-based platform:
Expand Down
6 changes: 0 additions & 6 deletions scripts/create-release-branch.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,6 @@ async function main() {
.replace(`['canary']`, `['${branchName}']`)
.replace(/[\s]{1,}('test-new-tests-.+',)/g, '')

buildAndTest = buildAndTest.replace(
/(^[ \t]*)# test-new-tests-if\n(^[ \t]*)if:.*\n(^[ \t]*)# test-new-tests-end-if/gm,
(_, indent1, indent2, indent3) =>
`${indent1}# test-new-tests-if\n${indent2}if: false\n${indent3}# test-new-tests-end-if`
)

await fs.promises.writeFile(buildAndTestPath, buildAndTest)

const commitMessage = 'setup release branch'
Expand Down
10 changes: 2 additions & 8 deletions scripts/get-changed-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,15 @@ export function getDeployManifestChangedTests(
}

/**
* Detects changed tests files by comparing the current branch with `origin/canary`
* Detects changed (see {@link getDiffRevision}) tests files.
* Returns tests separated by test mode (dev/prod), as well as the corresponding commit hash
* that the current branch is pointing to
*/
export default async function getChangedTests() {
/** @type import('execa').Options */
const EXECA_OPTS = { shell: true }

const { branchName, remoteUrl, commitSha, isCanary } = await getGitInfo()

if (isCanary) {
console.log(`Skipping flake detection for canary`)
return { devTests: [], prodTests: [], deployTests: [], commitSha }
}
const { branchName, remoteUrl, commitSha } = await getGitInfo()

const diffRevision = await getDiffRevision()

Expand All @@ -138,7 +133,6 @@ export default async function getChangedTests() {
{
branchName,
remoteUrl,
isCanary,
commitSha,
},
`\ngit diff:\n${changesResult.stderr}\n${changesResult.stdout}`
Expand Down
37 changes: 19 additions & 18 deletions scripts/git-info.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const exec = promisify(execOrig)

/**
* Gets git repository information from the environment
* @returns {Promise<{branchName: string, remoteUrl: string, commitSha: string, isCanary: boolean}>}
* @returns {Promise<{branchName: string, remoteUrl: string, commitSha: string}>}
*/
export async function getGitInfo() {
let eventData = {}
Expand All @@ -34,30 +34,31 @@ export async function getGitInfo() {
process.env.GITHUB_SHA ||
(await exec('git rev-parse HEAD')).stdout.trim()

const isCanary =
branchName === 'canary' && remoteUrl.includes('vercel/next.js')

return { branchName, remoteUrl, commitSha, isCanary }
return { branchName, remoteUrl, commitSha }
}

/**
* Determines the appropriate git diff revision based on the environment
* @returns {Promise<string>} The git revision to diff against
*/
export async function getDiffRevision() {
if (
process.env.GITHUB_ACTIONS === 'true' &&
process.env.GITHUB_EVENT_NAME === 'pull_request'
) {
// GH Actions for `pull_request` run on the merge commit so HEAD~1:
// 1. includes all changes in the PR
// e.g. in
// A-B-C-main - F
// \ /
// D-E-branch
// GH actions for `branch` runs on F, so a diff for HEAD~1 includes the diff of D and E combined
// 2. Includes all changes of the commit for pushes
return 'HEAD~1'
if (process.env.GITHUB_ACTIONS === 'true') {
const eventName = process.env.GITHUB_EVENT_NAME
switch (eventName) {
// GH Actions for `pull_request` run on the merge commit by default so HEAD~1:
// 1. includes all changes in the PR
// e.g. in
// A-B-C-main - F
// \ /
// D-E-branch
// GH actions for `branch` runs on F, so a diff for HEAD~1 includes the diff of D and E combined
// 2. Includes all changes of the commit for pushes (assuming the push event is from a squash merge)
case 'pull_request':
case 'push':
return 'HEAD~1'
default:
throw new Error(`Unsupported GITHUB_EVENT_NAME: ${eventName}`)
}
} else {
try {
await exec('git remote set-branches --add origin canary')
Expand Down
4 changes: 2 additions & 2 deletions scripts/run-for-change.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const CHANGE_ITEM_GROUPS = {
}

async function main() {
const { branchName, remoteUrl, isCanary } = await getGitInfo()
const { branchName, remoteUrl } = await getGitInfo()
const diffRevision = await getDiffRevision()

const changesResult = await exec(
Expand All @@ -82,7 +82,7 @@ async function main() {
return { stdout: '' }
})

console.error({ branchName, remoteUrl, isCanary, changesResult })
console.error({ branchName, remoteUrl, changesResult })
const changedFilesOutput = changesResult.stdout

const typeIndex = process.argv.indexOf('--type')
Expand Down
Loading
Loading