diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js
index 2f10ea2fb8a..567ee0536ea 100644
--- a/packages/react-scripts/config/paths.js
+++ b/packages/react-scripts/config/paths.js
@@ -11,6 +11,7 @@
 
 var path = require('path');
 var fs = require('fs');
+var url = require('url');
 
 // Make sure any symlinks in the project folder are resolved:
 // https://github.com/facebookincubator/create-react-app/issues/637
@@ -40,6 +41,28 @@ var nodePaths = (process.env.NODE_PATH || '')
   .filter(folder => !path.isAbsolute(folder))
   .map(resolveApp);
 
+var envPublicUrl = process.env.PUBLIC_URL;
+
+function getPublicUrl(appPackageJson) {
+  return envPublicUrl || require(appPackageJson).homepage;
+}
+
+// We use `PUBLIC_URL` environment variable or "homepage" field to infer
+// "public path" at which the app is served.
+// Webpack needs to know it to put the right <script> hrefs into HTML even in
+// single-page apps that may serve index.html for nested URLs like /todos/42.
+// We can't use a relative path in HTML because we don't want to load something
+// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
+function getServedPath(appPackageJson) {
+  var publicUrl = getPublicUrl(appPackageJson)
+  if (!publicUrl) {
+    return '/'
+  } else if (envPublicUrl) {
+    return publicUrl;
+  }
+  return url.parse(publicUrl).pathname;
+}
+
 // config after eject: we're in ./config/
 module.exports = {
   appBuild: resolveApp('build'),
@@ -52,7 +75,9 @@ module.exports = {
   testsSetup: resolveApp('src/setupTests.js'),
   appNodeModules: resolveApp('node_modules'),
   ownNodeModules: resolveApp('node_modules'),
-  nodePaths: nodePaths
+  nodePaths: nodePaths,
+  publicUrl: getPublicUrl(resolveApp('package.json')),
+  servedPath: getServedPath(resolveApp('package.json'))
 };
 
 // @remove-on-eject-begin
@@ -73,7 +98,9 @@ module.exports = {
   appNodeModules: resolveApp('node_modules'),
   // this is empty with npm3 but node resolution searches higher anyway:
   ownNodeModules: resolveOwn('../node_modules'),
-  nodePaths: nodePaths
+  nodePaths: nodePaths,
+  publicUrl: getPublicUrl(resolveApp('package.json')),
+  servedPath: getServedPath(resolveApp('package.json'))
 };
 
 // config before publish: we're in ./packages/react-scripts/config/
@@ -89,7 +116,9 @@ if (__dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1)
     testsSetup: resolveOwn('../template/src/setupTests.js'),
     appNodeModules: resolveOwn('../node_modules'),
     ownNodeModules: resolveOwn('../node_modules'),
-    nodePaths: nodePaths
+    nodePaths: nodePaths,
+    publicUrl: getPublicUrl(resolveOwn('../package.json')),
+    servedPath: getServedPath(resolveOwn('../package.json'))
   };
 }
 // @remove-on-eject-end
diff --git a/packages/react-scripts/config/utils/ensureSlash.js b/packages/react-scripts/config/utils/ensureSlash.js
new file mode 100644
index 00000000000..f4382384f7e
--- /dev/null
+++ b/packages/react-scripts/config/utils/ensureSlash.js
@@ -0,0 +1,10 @@
+module.exports = function ensureSlash(path, needsSlash) {
+  var hasSlash = path.endsWith('/');
+  if (hasSlash && !needsSlash) {
+    return path.substr(path, path.length - 1);
+  } else if (!hasSlash && needsSlash) {
+    return path + '/';
+  } else {
+    return path;
+  }
+}
diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js
index 96fd632b795..47eea43fe84 100644
--- a/packages/react-scripts/config/webpack.config.dev.js
+++ b/packages/react-scripts/config/webpack.config.dev.js
@@ -15,6 +15,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
 var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
 var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
 var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
+var ensureSlash = require('./utils/ensureSlash');
 var getClientEnvironment = require('./env');
 var paths = require('./paths');
 
@@ -29,7 +30,7 @@ var publicPath = '/';
 // `publicUrl` is just like `publicPath`, but we will provide it to our app
 // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
 // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
-var publicUrl = '';
+var publicUrl = ensureSlash(paths.servedPath, false);
 // Get environment variables to inject into our app.
 var env = getClientEnvironment(publicUrl);
 
diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js
index 058db0d7921..f005c11f8ad 100644
--- a/packages/react-scripts/config/webpack.config.prod.js
+++ b/packages/react-scripts/config/webpack.config.prod.js
@@ -16,6 +16,7 @@ var ExtractTextPlugin = require('extract-text-webpack-plugin');
 var ManifestPlugin = require('webpack-manifest-plugin');
 var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
 var url = require('url');
+var ensureSlash = require('./utils/ensureSlash');
 var paths = require('./paths');
 var getClientEnvironment = require('./env');
 
@@ -24,31 +25,13 @@ var getClientEnvironment = require('./env');
 var path = require('path');
 // @remove-on-eject-end
 
-function ensureSlash(path, needsSlash) {
-  var hasSlash = path.endsWith('/');
-  if (hasSlash && !needsSlash) {
-    return path.substr(path, path.length - 1);
-  } else if (!hasSlash && needsSlash) {
-    return path + '/';
-  } else {
-    return path;
-  }
-}
-
-// We use "homepage" field to infer "public path" at which the app is served.
-// Webpack needs to know it to put the right <script> hrefs into HTML even in
-// single-page apps that may serve index.html for nested URLs like /todos/42.
-// We can't use a relative path in HTML because we don't want to load something
-// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
-var homepagePath = require(paths.appPackageJson).homepage;
-var homepagePathname = homepagePath ? url.parse(homepagePath).pathname : '/';
 // Webpack uses `publicPath` to determine where the app is being served from.
 // It requires a trailing slash, or the file assets will get an incorrect path.
-var publicPath = ensureSlash(homepagePathname, true);
+var publicPath = ensureSlash(paths.servedPath, true);
 // `publicUrl` is just like `publicPath`, but we will provide it to our app
 // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
-// Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
-var publicUrl = ensureSlash(homepagePathname, false);
+// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
+var publicUrl = ensureSlash(paths.servedPath, false);
 // Get environment variables to inject into our app.
 var env = getClientEnvironment(publicUrl);
 
diff --git a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js
index a179aa7cbb2..414229b22ac 100644
--- a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js
+++ b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js
@@ -3,22 +3,30 @@ import initDOM from './initDOM'
 
 describe('Integration', () => {
   describe('Environment variables', () => {
+    it('file env variables', async () => {
+      const doc = await initDOM('file-env-variables')
+
+      expect(doc.getElementById('feature-file-env-variables').textContent).to.equal('fromtheenvfile.')
+    })
+
     it('NODE_PATH', async () => {
       const doc = await initDOM('node-path')
 
       expect(doc.getElementById('feature-node-path').childElementCount).to.equal(4)
     })
 
-    it('shell env variables', async () => {
-      const doc = await initDOM('shell-env-variables')
+    it('PUBLIC_URL', async () => {
+      const doc = await initDOM('public-url')
 
-      expect(doc.getElementById('feature-shell-env-variables').textContent).to.equal('fromtheshell.')
+      expect(doc.getElementById('feature-public-url').textContent).to.equal('http://www.example.org/spa.')
+      expect(doc.querySelector('head link[rel="shortcut icon"]').getAttribute('href'))
+        .to.equal('http://www.example.org/spa/favicon.ico')
     })
 
-    it('file env variables', async () => {
-      const doc = await initDOM('file-env-variables')
+    it('shell env variables', async () => {
+      const doc = await initDOM('shell-env-variables')
 
-      expect(doc.getElementById('feature-file-env-variables').textContent).to.equal('fromtheenvfile.')
+      expect(doc.getElementById('feature-shell-env-variables').textContent).to.equal('fromtheshell.')
     })
   })
 })
diff --git a/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js b/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js
index cec02227493..7cf8134b79f 100644
--- a/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js
+++ b/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js
@@ -15,9 +15,11 @@ if (process.env.E2E_FILE) {
   const markup = fs.readFileSync(file, 'utf8')
   getMarkup = () => markup
 
+  const pathPrefix = process.env.PUBLIC_URL.replace(/^https?:\/\/[^\/]+\/?/, '')
+
   resourceLoader = (resource, callback) => callback(
     null,
-    fs.readFileSync(path.join(path.dirname(file), resource.url.pathname), 'utf8')
+    fs.readFileSync(path.join(path.dirname(file), resource.url.pathname.replace(pathPrefix, '')), 'utf8')
   )
 } else if (process.env.E2E_URL) {
   getMarkup = () => new Promise(resolve => {
@@ -37,7 +39,7 @@ if (process.env.E2E_FILE) {
 
 export default feature => new Promise(async resolve => {
   const markup = await getMarkup()
-  const host = process.env.E2E_URL || 'http://localhost:3000'
+  const host = process.env.E2E_URL || 'http://www.example.org/spa:3000'
   const doc = jsdom.jsdom(markup, {
     features: {
       FetchExternalResources: ['script', 'css'],
diff --git a/packages/react-scripts/fixtures/kitchensink/src/App.js b/packages/react-scripts/fixtures/kitchensink/src/App.js
index cf93b4c132b..be9465ece36 100644
--- a/packages/react-scripts/fixtures/kitchensink/src/App.js
+++ b/packages/react-scripts/fixtures/kitchensink/src/App.js
@@ -32,7 +32,8 @@ class App extends React.Component {
   }
 
   componentDidMount() {
-    switch (location.hash.slice(1)) {
+    const feature = location.hash.slice(1)
+    switch (feature) {
       case 'array-destructuring':
         require.ensure([], () => this.setFeature(require('./features/syntax/ArrayDestructuring').default));
         break;
@@ -87,6 +88,9 @@ class App extends React.Component {
       case 'promises':
         require.ensure([], () => this.setFeature(require('./features/syntax/Promises').default));
         break;
+      case 'public-url':
+        require.ensure([], () => this.setFeature(require('./features/env/PublicUrl').default));
+        break;
       case 'rest-and-default':
         require.ensure([], () => this.setFeature(require('./features/syntax/RestAndDefault').default));
         break;
@@ -106,6 +110,9 @@ class App extends React.Component {
         require.ensure([], () => this.setFeature(require('./features/webpack/UnknownExtInclusion').default));
         break;
       default:
+        if (feature) {
+          throw new Error(`Missing feature "${feature}"`);
+        }
         this.setFeature(null);
         break;
     }
diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.js b/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.js
new file mode 100644
index 00000000000..bbb7b958b61
--- /dev/null
+++ b/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.js
@@ -0,0 +1,5 @@
+import React from 'react'
+
+export default () => (
+  <span id="feature-public-url">{process.env.PUBLIC_URL}.</span>
+)
diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.test.js
new file mode 100644
index 00000000000..31e699ece0b
--- /dev/null
+++ b/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.test.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import PublicUrl from './PublicUrl';
+
+describe('PUBLIC_URL', () => {
+  it('renders without crashing', () => {
+    const div = document.createElement('div');
+    ReactDOM.render(<PublicUrl />, div);
+  });
+});
diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js
index 42be50d43a8..873069c0dc6 100644
--- a/packages/react-scripts/scripts/build.js
+++ b/packages/react-scripts/scripts/build.js
@@ -21,6 +21,7 @@ require('dotenv').config({silent: true});
 var chalk = require('chalk');
 var fs = require('fs-extra');
 var path = require('path');
+var url = require('url');
 var filesize = require('filesize');
 var gzipSize = require('gzip-size').sync;
 var webpack = require('webpack');
@@ -158,15 +159,16 @@ function build(previousSizeMap) {
 
     var openCommand = process.platform === 'win32' ? 'start' : 'open';
     var appPackage  = require(paths.appPackageJson);
-    var homepagePath = appPackage.homepage;
+    var publicUrl = paths.publicUrl;
     var publicPath = config.output.publicPath;
-    if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) {
+    var publicPathname = url.parse(publicPath).pathname;
+    if (publicUrl && publicUrl.indexOf('.github.io/') !== -1) {
       // "homepage": "http://user.github.io/project"
-      console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.');
-      console.log('You can control this with the ' + chalk.green('homepage') + ' field in your '  + chalk.cyan('package.json') + '.');
+      console.log('The project was built assuming it is hosted at ' + chalk.green(publicPathname) + '.');
+      console.log('You can control this with the ' + chalk.green('homepage') + ' field in your '  + chalk.cyan('package.json') + ', or with the ' + chalk.green('PUBLIC_URL') + ' environment variable.');
       console.log();
       console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
-      console.log('To publish it at ' + chalk.green(homepagePath) + ', run:');
+      console.log('To publish it at ' + chalk.green(publicUrl) + ', run:');
       // If script deploy has been added to package.json, skip the instructions
       if (typeof appPackage.scripts.deploy === 'undefined') {
         console.log();
@@ -193,20 +195,20 @@ function build(previousSizeMap) {
     } else if (publicPath !== '/') {
       // "homepage": "http://mywebsite.com/project"
       console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.');
-      console.log('You can control this with the ' + chalk.green('homepage') + ' field in your '  + chalk.cyan('package.json') + '.');
+      console.log('You can control this with the ' + chalk.green('homepage') + ' field in your '  + chalk.cyan('package.json') + ', or with the ' + chalk.green('PUBLIC_URL') + ' environment variable.');
       console.log();
       console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
       console.log();
     } else {
-      // no homepage or "homepage": "http://mywebsite.com"
-      console.log('The project was built assuming it is hosted at the server root.');
-      if (homepagePath) {
+      if (publicUrl) {
         // "homepage": "http://mywebsite.com"
-        console.log('You can control this with the ' + chalk.green('homepage') + ' field in your '  + chalk.cyan('package.json') + '.');
+        console.log('The project was built assuming it is hosted at ' + chalk.green(publicUrl) +  '.');
+        console.log('You can control this with the ' + chalk.green('homepage') + ' field in your '  + chalk.cyan('package.json') + ', or with the ' + chalk.green('PUBLIC_URL') + ' environment variable.');
         console.log();
       } else {
         // no homepage
-        console.log('To override this, specify the ' + chalk.green('homepage') + ' in your '  + chalk.cyan('package.json') + '.');
+        console.log('The project was built assuming it is hosted at the server root.');
+        console.log('To override this, specify the ' + chalk.green('homepage') + ' in your '  + chalk.cyan('package.json') + ', or with the ' + chalk.green('PUBLIC_URL') + ' environment variable.');
         console.log('For example, add this to build it for GitHub Pages:')
         console.log();
         console.log('  ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(','));
diff --git a/packages/react-scripts/scripts/eject.js b/packages/react-scripts/scripts/eject.js
index b8f9d313143..daa5958e9f9 100644
--- a/packages/react-scripts/scripts/eject.js
+++ b/packages/react-scripts/scripts/eject.js
@@ -43,12 +43,6 @@ prompt(
     }
   }
 
-  var folders = [
-    'config',
-    path.join('config', 'jest'),
-    'scripts'
-  ];
-
   var files = [
     path.join('config', 'env.js'),
     path.join('config', 'paths.js'),
@@ -57,11 +51,20 @@ prompt(
     path.join('config', 'webpack.config.prod.js'),
     path.join('config', 'jest', 'cssTransform.js'),
     path.join('config', 'jest', 'fileTransform.js'),
+    path.join('config', 'utils', 'ensureSlash.js'),
     path.join('scripts', 'build.js'),
     path.join('scripts', 'start.js'),
-    path.join('scripts', 'test.js')
+    path.join('scripts', 'test.js'),
   ];
 
+  var folders = files.reduce(function(prevFolders, file) {
+    var dirname = path.dirname(file);
+    if (prevFolders.indexOf(dirname) === -1) {
+      return prevFolders.concat(dirname);
+    }
+    return prevFolders;
+  }, []);
+
   // Ensure that the app folder is clean and we won't override any files
   folders.forEach(verifyAbsent);
   files.forEach(verifyAbsent);
diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js
index 8615fb074a9..c326b9b94d9 100644
--- a/packages/react-scripts/scripts/start.js
+++ b/packages/react-scripts/scripts/start.js
@@ -241,7 +241,7 @@ function runDevServer(host, port, protocol) {
     // project directory is dangerous because we may expose sensitive files.
     // Instead, we establish a convention that only files in `public` directory
     // get served. Our build script will copy `public` into the `build` folder.
-    // In `index.html`, you can get URL of `public` folder with %PUBLIC_PATH%:
+    // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
     // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
     // In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
     // Note that we only recommend to use `public` folder as an escape hatch
diff --git a/tasks/e2e-kitchensink.sh b/tasks/e2e-kitchensink.sh
index 892230ab747..3db6ef8d1d0 100755
--- a/tasks/e2e-kitchensink.sh
+++ b/tasks/e2e-kitchensink.sh
@@ -111,7 +111,11 @@ create_react_app --scripts-version=$scripts_path --internal-testing-template=$ro
 cd test-kitchensink
 
 # Test the build
-NODE_PATH=src REACT_APP_SHELL_ENV_MESSAGE=fromtheshell npm run build
+REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \
+  NODE_PATH=src \
+  PUBLIC_URL=http://www.example.org/spa/ \
+  npm run build
+
 # Check for expected output
 test -e build/*.html
 test -e build/static/js/main.*.js
@@ -127,6 +131,7 @@ tmp_server_log=`mktemp`
 PORT=3001 \
   REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \
   NODE_PATH=src \
+  PUBLIC_URL=http://www.example.org/spa/ \
   nohup npm start &>$tmp_server_log &
 grep -q 'The app is running at:' <(tail -f $tmp_server_log)
 E2E_URL="http://localhost:3001" \
@@ -138,6 +143,7 @@ E2E_URL="http://localhost:3001" \
 E2E_FILE=./build/index.html \
   CI=true \
   NODE_PATH=src \
+  PUBLIC_URL=http://www.example.org/spa/ \
   node_modules/.bin/mocha --require babel-register --require babel-polyfill integration/*.test.js
 
 # ******************************************************************************
@@ -157,7 +163,11 @@ npm link $root_path/packages/react-scripts
 rm .babelrc
 
 # Test the build
-NODE_PATH=src REACT_APP_SHELL_ENV_MESSAGE=fromtheshell npm run build
+REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \
+  NODE_PATH=src \
+  PUBLIC_URL=http://www.example.org/spa/ \
+  npm run build
+
 # Check for expected output
 test -e build/*.html
 test -e build/static/js/main.*.js
@@ -173,6 +183,7 @@ tmp_server_log=`mktemp`
 PORT=3002 \
   REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \
   NODE_PATH=src \
+  PUBLIC_URL=http://www.example.org/spa/ \
   nohup npm start &>$tmp_server_log &
 grep -q 'The app is running at:' <(tail -f $tmp_server_log)
 E2E_URL="http://localhost:3002" \
@@ -186,6 +197,7 @@ E2E_FILE=./build/index.html \
   CI=true \
   NODE_ENV=production \
   NODE_PATH=src \
+  PUBLIC_URL=http://www.example.org/spa/ \
   node_modules/.bin/mocha --require babel-register --require babel-polyfill integration/*.test.js
 
 # Cleanup