Adds support for Dockerfile customization (#732)

This commit is contained in:
Shayne Czyzewski 2022-10-13 18:46:07 +02:00 committed by GitHub
parent a84f3547fa
commit 8fc6fd1836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 121 additions and 10 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## v0.x.x.x (TBD)
### [NEW FEATURE] Dockerfile customization
You can now customize the default Wasp Dockerfile by either extending/replacing our build stages or using your own custom logic. To make use of this feature, simply add a Dockerfile to the root of your project and it will be appended to the bottom of the existing Wasp Dockerfile.
## v0.6.0.0 (2022/09/29)
### BREAKING CHANGES

View File

@ -19,6 +19,7 @@ import Wasp.Cli.Command.CreateNewProject (createNewProject)
import Wasp.Cli.Command.Db (runDbCommand, studio)
import qualified Wasp.Cli.Command.Db.Migrate as Command.Db.Migrate
import Wasp.Cli.Command.Deps (deps)
import Wasp.Cli.Command.Dockerfile (printDockerfile)
import Wasp.Cli.Command.Info (info)
import Wasp.Cli.Command.Start (start)
import qualified Wasp.Cli.Command.Telemetry as Telemetry
@ -40,6 +41,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
["build"] -> Command.Call.Build
["telemetry"] -> Command.Call.Telemetry
["deps"] -> Command.Call.Deps
["dockerfile"] -> Command.Call.Dockerfile
["info"] -> Command.Call.Info
["completion"] -> Command.Call.PrintBashCompletionInstruction
["completion:generate"] -> Command.Call.GenerateBashCompletionScript
@ -59,6 +61,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
Command.Call.Build -> runCommand build
Command.Call.Telemetry -> runCommand Telemetry.telemetry
Command.Call.Deps -> runCommand deps
Command.Call.Dockerfile -> runCommand printDockerfile
Command.Call.Info -> runCommand info
Command.Call.PrintBashCompletionInstruction -> runCommand printBashCompletionInstruction
Command.Call.GenerateBashCompletionScript -> runCommand generateBashCompletionScript
@ -96,6 +99,7 @@ printUsage =
cmd " build Generates full web app code, ready for deployment. Use when deploying or ejecting.",
cmd " telemetry Prints telemetry status.",
cmd " deps Prints the dependencies that Wasp uses in your project.",
cmd " dockerfile Prints the contents of the Wasp generated Dockerfile.",
cmd " info Prints basic information about current Wasp project.",
"",
title "EXAMPLES",

View File

@ -10,6 +10,7 @@ data Call
| Version
| Telemetry
| Deps
| Dockerfile
| Info
| PrintBashCompletionInstruction
| GenerateBashCompletionScript

View File

@ -0,0 +1,21 @@
module Wasp.Cli.Command.Dockerfile
( printDockerfile,
)
where
import Control.Monad.Except (throwError)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Text.IO as T.IO
import Wasp.Cli.Command (Command, CommandError (..))
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd)
import Wasp.Cli.Command.Compile (defaultCompileOptions)
import Wasp.Lib (compileAndRenderDockerfile)
printDockerfile :: Command ()
printDockerfile = do
waspProjectDir <- findWaspProjectRootDirFromCwd
dockerfileContentOrCompileErrors <- liftIO $ compileAndRenderDockerfile waspProjectDir (defaultCompileOptions waspProjectDir)
either
(throwError . CommandError "Displaying Dockerfile failed due to a compilation error in your Wasp project" . unwords)
(liftIO . T.IO.putStrLn)
dockerfileContentOrCompileErrors

View File

@ -33,3 +33,7 @@ COPY db/ ./db/
EXPOSE ${PORT}
WORKDIR /app/server
ENTRYPOINT ["npm", "run", "start-production"]
# Any user-defined Dockerfile contents will be appended below.
{=& userDockerfile =}

View File

@ -11,7 +11,7 @@
"file",
"Dockerfile"
],
"faae4a6f87557e624d9c7631ec6f3ed20e31115f2c177bfe19b3f52d163d86e9"
"102dded28312bbe81553d71eb878ed4f7766fda51503e08e75ffd9931ff1a8bb"
],
[
[

View File

@ -25,3 +25,7 @@ COPY db/ ./db/
EXPOSE ${PORT}
WORKDIR /app/server
ENTRYPOINT ["npm", "run", "start-production"]
# Any user-defined Dockerfile contents will be appended below.

View File

@ -11,7 +11,7 @@
"file",
"Dockerfile"
],
"faae4a6f87557e624d9c7631ec6f3ed20e31115f2c177bfe19b3f52d163d86e9"
"102dded28312bbe81553d71eb878ed4f7766fda51503e08e75ffd9931ff1a8bb"
],
[
[

View File

@ -25,3 +25,7 @@ COPY db/ ./db/
EXPOSE ${PORT}
WORKDIR /app/server
ENTRYPOINT ["npm", "run", "start-production"]
# Any user-defined Dockerfile contents will be appended below.

View File

@ -11,7 +11,7 @@
"file",
"Dockerfile"
],
"faae4a6f87557e624d9c7631ec6f3ed20e31115f2c177bfe19b3f52d163d86e9"
"102dded28312bbe81553d71eb878ed4f7766fda51503e08e75ffd9931ff1a8bb"
],
[
[

View File

@ -25,3 +25,7 @@ COPY db/ ./db/
EXPOSE ${PORT}
WORKDIR /app/server
ENTRYPOINT ["npm", "run", "start-production"]
# Any user-defined Dockerfile contents will be appended below.

View File

@ -11,7 +11,7 @@
"file",
"Dockerfile"
],
"f94ec7a7e7db084cdda6a0860d68693252da43d7ddd28ba990222ea0831c5467"
"0134f44513be86f913897e47699114aaa1f1b497152cabe93755b992957b95e6"
],
[
[

View File

@ -27,3 +27,7 @@ COPY db/ ./db/
EXPOSE ${PORT}
WORKDIR /app/server
ENTRYPOINT ["npm", "run", "start-production"]
# Any user-defined Dockerfile contents will be appended below.

View File

@ -0,0 +1 @@
## HELLO!

View File

@ -17,6 +17,7 @@ where
import Data.List (find)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import StrongPath (Abs, Dir, File', Path')
import Wasp.AppSpec.Action (Action)
import Wasp.AppSpec.Core.Decl (Decl, IsDecl, takeDecls)
@ -50,7 +51,9 @@ data AppSpec = AppSpec
dotEnvClientFile :: Maybe (Path' Abs File'),
-- | If true, it means project is being compiled for production/deployment -> it is being "built".
-- If false, it means project is being compiled for development purposes (e.g. "wasp start").
isBuild :: Bool
isBuild :: Bool,
-- | The contents of the optional user Dockerfile found in the root of the wasp project source.
userDockerfileContents :: Maybe Text
}
-- TODO: Make this return "Named" declarations?

View File

@ -2,19 +2,25 @@
module Wasp.Generator.DockerGenerator
( genDockerFiles,
genDockerfile,
compileAndRenderDockerfile,
)
where
import Data.Aeson (object, (.=))
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import StrongPath (File', Path', Rel, relfile)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.Generator.Common (ProjectRootDir, latestMajorNodeVersion)
import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft)
import Wasp.Generator.Monad (Generator)
import Wasp.Generator.FileDraft (FileDraft (..), createTemplateFileDraft)
import qualified Wasp.Generator.FileDraft.TemplateFileDraft as TmplFD
import Wasp.Generator.Monad (Generator, GeneratorError, runGenerator)
import Wasp.Generator.ServerGenerator (areServerPatchesUsed)
import Wasp.Generator.Templates (TemplatesDir)
import Wasp.Generator.Templates (TemplatesDir, compileAndRenderTemplate)
import qualified Wasp.SemanticVersion as SV
genDockerFiles :: AppSpec -> Generator [FileDraft]
@ -32,7 +38,8 @@ genDockerfile spec = do
object
[ "usingPrisma" .= not (null $ AS.getDecls @AS.Entity.Entity spec),
"nodeMajorVersion" .= show (SV.major latestMajorNodeVersion),
"usingServerPatches" .= usingServerPatches
"usingServerPatches" .= usingServerPatches,
"userDockerfile" .= fromMaybe "" (AS.userDockerfileContents spec)
]
)
@ -43,3 +50,14 @@ genDockerignore _ =
([relfile|.dockerignore|] :: Path' (Rel ProjectRootDir) File')
([relfile|dockerignore|] :: Path' (Rel TemplatesDir) File')
Nothing
-- | Helper to return what the Dockerfile content will be based on the AppSpec.
compileAndRenderDockerfile :: AppSpec -> IO (Either (NonEmpty GeneratorError) Text)
compileAndRenderDockerfile spec = do
let (_, generatorResult) = runGenerator $ genDockerfile spec
case generatorResult of
Left generatorErrors -> return $ Left generatorErrors
Right (FileDraftTemplateFd draft) -> do
content <- compileAndRenderTemplate (TmplFD._srcPathInTmplDir draft) (fromMaybe (object []) (TmplFD._tmplData draft))
return $ Right content
Right _ -> error "Attempted to display Dockerfile, but it was not a Template FileDraft!"

View File

@ -4,11 +4,16 @@ module Wasp.Lib
ProjectRootDir,
findWaspFile,
analyzeWaspProject,
compileAndRenderDockerfile,
)
where
import Control.Arrow (left)
import Control.Monad.Extra (whenMaybeM)
import Data.List (find, isSuffixOf)
import Data.List.NonEmpty (NonEmpty, fromList, toList)
import Data.Text (Text)
import qualified Data.Text.IO as T.IO
import StrongPath (Abs, Dir, File', Path', relfile)
import qualified StrongPath as SP
import System.Directory (doesDirectoryExist, doesFileExist)
@ -23,6 +28,7 @@ import Wasp.Error (showCompilerErrorForTerminal)
import qualified Wasp.ExternalCode as ExternalCode
import qualified Wasp.Generator as Generator
import Wasp.Generator.Common (ProjectRootDir)
import qualified Wasp.Generator.DockerGenerator as DockerGenerator
import Wasp.Generator.ServerGenerator.Common (dotEnvServer)
import Wasp.Generator.WebAppGenerator.Common (dotEnvClient)
import qualified Wasp.Util.IO as Util.IO
@ -74,6 +80,7 @@ analyzeWaspProject waspDir options = do
maybeDotEnvServerFile <- findDotEnvServer waspDir
maybeDotEnvClientFile <- findDotEnvClient waspDir
maybeMigrationsDir <- findMigrationsDir waspDir
maybeUserDockerfileContents <- loadUserDockerfileContents waspDir
return $
Right
AS.AppSpec
@ -83,7 +90,8 @@ analyzeWaspProject waspDir options = do
AS.migrationsDir = maybeMigrationsDir,
AS.dotEnvServerFile = maybeDotEnvServerFile,
AS.dotEnvClientFile = maybeDotEnvClientFile,
AS.isBuild = CompileOptions.isBuild options
AS.isBuild = CompileOptions.isBuild options,
AS.userDockerfileContents = maybeUserDockerfileContents
}
analyzerWarnings <- warnIfDotEnvPresent waspDir
return (analyzerWarnings, appSpecOrAnalyzerErrors)
@ -130,3 +138,17 @@ findMigrationsDir waspDir = do
let migrationsAbsPath = waspDir SP.</> dbMigrationsDirInWaspProjectDir
migrationsExists <- doesDirectoryExist $ SP.fromAbsDir migrationsAbsPath
return $ if migrationsExists then Just migrationsAbsPath else Nothing
loadUserDockerfileContents :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe Text)
loadUserDockerfileContents waspDir = do
let dockerfileAbsPath = SP.toFilePath $ waspDir SP.</> [relfile|Dockerfile|]
whenMaybeM (doesFileExist dockerfileAbsPath) $ T.IO.readFile dockerfileAbsPath
compileAndRenderDockerfile :: Path' Abs (Dir WaspProjectDir) -> CompileOptions -> IO (Either [CompileError] Text)
compileAndRenderDockerfile waspDir compileOptions = do
(_, appSpecOrAnalyzerErrors) <- analyzeWaspProject waspDir compileOptions
case appSpecOrAnalyzerErrors of
Left errors -> return . Left . toList $ errors
Right appSpec -> do
dockerfileOrGeneratorErrors <- DockerGenerator.compileAndRenderDockerfile appSpec
return $ left (map show . toList) dockerfileOrGeneratorErrors

View File

@ -115,6 +115,7 @@ library
-- 'array' is used by code generated by Alex for src/Analyzer/Parser/Lexer.x
, array ^>= 0.5.4
, deepseq ^>= 1.4.4
, extra ^>= 1.7.10
other-modules: Paths_waspc
exposed-modules:
FilePath.Extra
@ -308,6 +309,7 @@ library cli-lib
, path
, path-io
, strong-path
, text
, utf8-string
, uuid
, waspc
@ -325,6 +327,7 @@ library cli-lib
Wasp.Cli.Command.Db
Wasp.Cli.Command.Db.Migrate
Wasp.Cli.Command.Deps
Wasp.Cli.Command.Dockerfile
Wasp.Cli.Command.Info
Wasp.Cli.Command.Start
Wasp.Cli.Command.Telemetry

View File

@ -143,3 +143,15 @@ and carefully follow their instructions (i.e. do you want to create a new app or
That is it!
NOTE: Make sure you set this URL as the `WASP_WEB_CLIENT_URL` environment variable in Heroku.
## Customizing the Dockerfile
By default, Wasp will generate a multi-stage Dockerfile that is capable of building an image with your Wasp-generated server code and running it, along with any pending migrations, as in the deployment scenario above. If you need to customize this Dockerfile, you may do so by adding a Dockerfile to your project root directory. If present, Wasp will append the contents of this file to the _bottom_ of our default Dockerfile.
Since the last definition in a Dockerfile wins, you can override or continue from any existing build stages. You could also choose not to use any of our build stages and have your own custom Dockerfile used as-is. A few notes are in order:
- if you override an intermediate build stage, no later build stages will be used unless you reproduce them below
- the contents of the Dockerfile are dynamic, based on the features you use, and may change in future releases as well, so please verify the contents have not changed from time to time
- be sure to supply an `ENTRYPOINT` in your final build stage or it will not have any effect
To see what your project's (potentially combined) Dockerfile will look like, run: `wasp dockerfile`
Here are the official docker docs on [multi-stage builds](https://docs.docker.com/build/building/multi-stage/). Please join our Discord if you have any questions, or if the customization hook provided here is not sufficient for your needs!