diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..e3916e43e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# excluded because we'll likely need to rebuild this. +node_modules + +# static assets are rebuild in the docker container. +dist + +# tests are not run in the docker container. +__tests__ + +# we won't use the .git folder in production. +.git + +# hide the environment config. +.env + +# don't include logs. +npm-debug.log* +yarn-error.log + +# hide OS specific files. +.idea/ +.vs +.docz +*.swp +*.DS_STORE + +# hide generated files. +*.css.d.ts +__generated__ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..8bae06482 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM node:8-alpine + +# Install installation dependancies. +RUN apk --no-cache add git + +# Create app directory. +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +# Setup the environment for production. +ENV NODE_ENV production + +# Bundle application source. +COPY . /usr/src/app + +# Install build static assets and clear caches. +RUN NODE_ENV=development npm install && \ + npm run compile && \ + npm run build && \ + npm prune --production + +FROM node:8-alpine + +# Create app directory +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +# Copy the compiled source into the new stage. +COPY --from=0 /usr/src/app . + +# Setup the environment +ENV PATH /usr/src/app/bin:$PATH +ENV PORT 5000 +EXPOSE 5000 +ENV NODE_ENV production + +# Store the current git revision. +ARG REVISION_HASH +ENV REVISION_HASH=${REVISION_HASH} + +CMD ["npm", "run", "start"] diff --git a/package-lock.json b/package-lock.json index cca10df6f..bb67ffe78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1690,6 +1690,15 @@ "commander": "*" } }, + "@types/compression-webpack-plugin": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@types/compression-webpack-plugin/-/compression-webpack-plugin-0.4.2.tgz", + "integrity": "sha512-kAjvB1XBtGx7xBKiDTqQDE/zldh0LTroHeyK0p7Jogc/ggRYdPTDoyKhcAzK8RLCsGcVHAlOXLhN8gbfL93JhA==", + "dev": true, + "requires": { + "@types/webpack": "*" + } + }, "@types/connect": { "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", @@ -6482,6 +6491,19 @@ "vary": "~1.1.2" } }, + "compression-webpack-plugin": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-1.1.11.tgz", + "integrity": "sha512-ZVWKrTQhtOP7rDx3M/koXTnRm/iwcYbuCdV+i4lZfAIe32Mov7vUVM0+8Vpz4q0xH+TBUZxq+rM8nhtkDH50YQ==", + "dev": true, + "requires": { + "cacache": "^10.0.1", + "find-cache-dir": "^1.0.0", + "neo-async": "^2.5.0", + "serialize-javascript": "^1.4.0", + "webpack-sources": "^1.0.1" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -10129,8 +10151,11 @@ "resolved": "https://registry.npmjs.org/fluent-intl-polyfill/-/fluent-intl-polyfill-0.1.0.tgz", "integrity": "sha1-ETOUSrJHeINHOZVZaIPg05z4hc8=", "dev": true, - "requires": { - "intl-pluralrules": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b" + "dependencies": { + "intl-pluralrules": { + "version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b", + "from": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b" + } } }, "fluent-langneg": { @@ -11295,7 +11320,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/graphql-playground-html/-/graphql-playground-html-1.6.0.tgz", "integrity": "sha512-et3huQFEuAZgAiUfs9a+1Wo/JDX94k7XqNRc8LhpGT8k2NwIhMAbZKqudVF/Ww4+XDoEB4LUTSFGRPBYvKrcKQ==", - "dev": true, "requires": { "graphql-config": "2.0.0" }, @@ -11304,7 +11328,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.0.0.tgz", "integrity": "sha512-//hZmROEk79zzPlH6SVTQeXd8NVV65rquz1zxZeO6oEuX5KNnii8+oznLu7d897EfJ+NShTZtsY9FMmxxkWmJw==", - "dev": true, "requires": { "graphql-import": "^0.4.0", "graphql-request": "^1.4.0", @@ -11319,7 +11342,6 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.2.tgz", "integrity": "sha512-JvKsVOR/U5QguBtEvTt0ozQ49uh1C6cW8O1xR6krQpJZIxjLYqpgusLUddTiVkka6Q/A4/AXBohY85jPudxYDg==", - "dev": true, "requires": { "graphql-playground-html": "1.6.0" } @@ -12545,11 +12567,6 @@ "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", "dev": true }, - "intl-pluralrules": { - "version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b", - "from": "github:projectfluent/IntlPluralRules#module", - "dev": true - }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", diff --git a/package.json b/package.json index 13bc586c8..b66f7ebbe 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "fs-extra": "^6.0.1", "graphql": "^0.13.2", "graphql-config": "^2.0.1", + "graphql-playground-middleware-express": "^1.7.2", "graphql-redis-subscriptions": "^1.5.0", "graphql-tools": "^3.0.5", "ioredis": "^3.2.2", @@ -83,6 +84,7 @@ "@types/chokidar": "^1.7.5", "@types/classnames": "^2.2.4", "@types/commander": "^2.12.2", + "@types/compression-webpack-plugin": "^0.4.2", "@types/consolidate": "0.0.34", "@types/convict": "^4.2.0", "@types/cross-spawn": "^6.0.0", @@ -143,6 +145,7 @@ "classnames": "^2.2.5", "commander": "^2.16.0", "comment-json": "^1.1.3", + "compression-webpack-plugin": "^1.1.11", "copy-webpack-plugin": "^4.5.1", "cross-spawn": "^6.0.5", "css-loader": "^0.28.11", @@ -159,7 +162,6 @@ "fluent-intl-polyfill": "^0.1.0", "fluent-langneg": "^0.1.0", "fluent-react": "^0.8.0", - "graphql-playground-middleware-express": "^1.7.2", "graphql-schema-typescript": "^1.2.1", "gulp": "^4.0.0", "gulp-babel": "^8.0.0-beta.2", diff --git a/src/core/build/createWebpackConfig.ts b/src/core/build/createWebpackConfig.ts index cd2924338..6085d8899 100644 --- a/src/core/build/createWebpackConfig.ts +++ b/src/core/build/createWebpackConfig.ts @@ -1,4 +1,5 @@ import CaseSensitivePathsPlugin from "case-sensitive-paths-webpack-plugin"; +import CompressionPlugin from "compression-webpack-plugin"; import HtmlWebpackPlugin, { Options } from "html-webpack-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; import path from "path"; @@ -111,6 +112,8 @@ export default function createWebpackConfig({ filename: "assets/css/[name].[hash].css", chunkFilename: "assets/css/[id].[hash].css", }), + // Pre-compress all the assets as they will be served as is. + new CompressionPlugin({}), ] : [ // Add module names to factory functions so they appear in browser profiler. diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts index 1559a37e5..863af1b2d 100644 --- a/src/core/server/app/index.ts +++ b/src/core/server/app/index.ts @@ -14,6 +14,7 @@ import { handleSubscriptions } from "talk-server/graph/common/subscriptions/midd import { Schemas } from "talk-server/graph/schemas"; import TenantCache from "talk-server/services/tenant/cache"; +import { cacheHeadersMiddleware } from "talk-server/app/middleware/cacheHeaders"; import { errorHandler } from "talk-server/app/middleware/error"; import { accessLogger, errorLogger } from "./middleware/logging"; import serveStatic from "./middleware/serveStatic"; @@ -47,13 +48,14 @@ export async function createApp(options: AppOptions): Promise { // Mount the router. parent.use( + "/", await createRouter(options, { passport, }) ); // Static Files - parent.use("/assets", serveStatic); + parent.use("/assets", cacheHeadersMiddleware("1w"), serveStatic); // Error Handling parent.use(notFoundMiddleware); diff --git a/src/core/server/app/middleware/cacheHeaders.ts b/src/core/server/app/middleware/cacheHeaders.ts new file mode 100644 index 000000000..dc43b9708 --- /dev/null +++ b/src/core/server/app/middleware/cacheHeaders.ts @@ -0,0 +1,27 @@ +import { RequestHandler } from "express"; +import ms from "ms"; + +export const nocacheMiddleware: RequestHandler = (req, res, next) => { + // Set cache control headers to prevent browsers/cdn's from caching these + // requests. + res.set({ "Cache-Control": "no-cache, no-store, must-revalidate" }); + + next(); +}; + +export const cacheHeadersMiddleware = (duration: string): RequestHandler => { + const maxAge = duration ? Math.floor(ms(duration) / 1000) : false; + if (!maxAge) { + return nocacheMiddleware; + } + + return (req, res, next) => { + // Set cache control headers to encourage browsers/cdn's to cache these + // requests if we aren't in private mode. + res.set({ + "Cache-Control": `public, max-age=${maxAge}`, + }); + + next(); + }; +}; diff --git a/src/core/server/app/router.ts b/src/core/server/app/router.ts index c298a40aa..6b43549c0 100644 --- a/src/core/server/app/router.ts +++ b/src/core/server/app/router.ts @@ -13,6 +13,10 @@ import tenantMiddleware from "talk-server/app/middleware/tenant"; import managementGraphMiddleware from "talk-server/graph/management/middleware"; import tenantGraphMiddleware from "talk-server/graph/tenant/middleware"; +import { + cacheHeadersMiddleware, + nocacheMiddleware, +} from "talk-server/app/middleware/cacheHeaders"; import { AppOptions } from "./index"; import playground from "./middleware/playground"; @@ -123,7 +127,7 @@ export async function createRouter(app: AppOptions, options: RouterOptions) { // Create a router. const router = express.Router(); - router.use("/api", await createAPIRouter(app, options)); + router.use("/api", nocacheMiddleware, await createAPIRouter(app, options)); if (app.config.get("env") === "development") { // Tenant GraphiQL @@ -146,7 +150,7 @@ export async function createRouter(app: AppOptions, options: RouterOptions) { } // Handle the stream handler. - router.get("/embed/stream", streamHandler); + router.get("/embed/stream", cacheHeadersMiddleware("1h"), streamHandler); return router; }