diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright José Lorenzo Rodríguez (c) 2020
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Author name here nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,101 @@
+# dockerfile-composer
+
+Build Dockerfiles using Haskell with an embedded DSL.
+
+- [Writing Dockerfiles in Haskell](#writing-dockerfiles-in-haskell)
+- [Using the QuasiQuoter](#using-the-quasiquoter)
+- [Templating Dockerfiles in Haskell](#templating-dockerfiles-in-haskell)
+- [Using IO in the DSL](#using-io-in-the-dsl)
+
+## Pretty-printing files
+
+```haskell
+import Language.Docker
+main = do
+    Right d <- parseFile "./Dockerfile"
+    putStr (prettyPrint d)
+```
+
+## Writing Dockerfiles in Haskell
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+import Language.Docker
+
+main = putDockerfileStr $ do
+    from "node"
+    run "apt-get update"
+    run ["apt-get", "install", "something"]
+    -- ...
+```
+
+## Using the QuasiQuoter
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import Language.Docker
+main = putDockerfileStr $ do
+    from "node"
+    run "apt-get update"
+    [edockerfile|
+    RUN apt-get update
+    CMD node something.js
+    |]
+    -- ...
+```
+
+## Templating Dockerfiles in Haskell
+
+```haskell
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+import Control.Monad
+import Language.Docker
+import Data.String (fromString)
+import qualified Data.Text.Lazy.IO as L
+
+tags = ["7.8", "7.10", "8"]
+cabalSandboxBuild packageName = do
+    let cabalFile = packageName ++ ".cabal"
+    run "cabal sandbox init"
+    run "cabal update"
+    add [fromString cabalFile] (fromString $ "/app/" ++ cabalFile)
+    run "cabal install --only-dep -j"
+    add "." "/app/"
+    run "cabal build"
+main =
+    forM_ tags $ \tag -> do
+        let df = toDockerfileText $ do
+            from ("haskell" `tagged` tag)
+            cabalSandboxBuild "mypackage"
+        L.writeFile ("./examples/templating-" ++ tag ++ ".dockerfile") df
+```
+
+## Using IO in the DSL
+By default the DSL runs in the `Identity` monad. By running in IO we can
+support more features like file globbing:
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+import           Language.Docker
+import qualified System.Directory     as Directory
+import qualified System.FilePath      as FilePath
+import qualified System.FilePath.Glob as Glob
+import Data.List.NonEmpty (fromList)
+import qualified Data.Text.Lazy.IO    as L
+
+main = do
+    str <- toDockerfileTextIO $ do
+        fs <- liftIO $ do
+            cwd <- Directory.getCurrentDirectory
+            fs <- Glob.glob "./test/*.hs"
+	    let relativeFiles = map (FilePath.makeRelative cwd) fs
+            return (fromList relativeFiles)
+        from "ubuntu"
+	copy $ (toSources fs) `to` "/app/"
+    L.putStr str
+```
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/dockerfile-creator.cabal b/dockerfile-creator.cabal
new file mode 100644
--- /dev/null
+++ b/dockerfile-creator.cabal
@@ -0,0 +1,83 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 301ffca16b19023e556f9590e046688e1b2da2f00506cf1f5f353972eb71ff45
+
+name:           dockerfile-creator
+version:        0.1.0.0
+description:    Embedded DSL to create Dockerfiles using Haskell. Please see the README on GitHub at <https://github.com/hadolint/dockerfile-creator#readme>
+homepage:       https://github.com/hadolint/dockerfile-creator#readme
+bug-reports:    https://github.com/hadolint/dockerfile-creator/issues
+author:         José Loreno Rodríguez
+maintainer:     jose.zap@gmail.com
+copyright:      2020 José Lorenzo Rodríguez
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/hadolint/dockerfile-creator
+
+library
+  exposed-modules:
+      Language.Docker.Creator
+      Language.Docker.EDSL
+      Language.Docker.EDSL.Quasi
+  other-modules:
+      Language.Docker.EDSL.Types
+      Language.Docker.Syntax.Lift
+      Paths_dockerfile_creator
+  hs-source-dirs:
+      src
+  build-depends:
+      base >=4.7 && <5
+    , bytestring >=0.10
+    , data-default-class
+    , free
+    , language-docker >=8.1
+    , megaparsec >=8.0
+    , mtl
+    , template-haskell
+    , text
+    , th-lift
+    , th-lift-instances
+    , time
+  default-language: Haskell2010
+
+test-suite dockerfile-composer-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Language.Docker.EDSL.QuasiSpec
+      Language.Docker.EDSLSpec
+      Language.Docker.ExamplesSpec
+      Paths_dockerfile_creator
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      Glob
+    , base >=4.7 && <5
+    , bytestring >=0.10
+    , data-default-class
+    , directory
+    , dockerfile-creator
+    , filepath
+    , free
+    , hspec
+    , language-docker >=8.1
+    , megaparsec >=8.0
+    , mtl
+    , process
+    , template-haskell
+    , text
+    , th-lift
+    , th-lift-instances
+    , time
+  default-language: Haskell2010
diff --git a/src/Language/Docker/Creator.hs b/src/Language/Docker/Creator.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Docker/Creator.hs
@@ -0,0 +1,78 @@
+module Language.Docker.Creator
+  (
+    -- * Writting Dockerfiles (@Language.Docker.EDSL@)
+    Language.Docker.EDSL.toDockerfileText,
+    Language.Docker.EDSL.toDockerfile,
+    Language.Docker.EDSL.putDockerfileStr,
+    Language.Docker.EDSL.writeDockerFile,
+    Language.Docker.EDSL.toDockerfileTextIO,
+    Language.Docker.EDSL.toDockerfileIO,
+    Language.Docker.EDSL.runDockerfileIO,
+    Language.Docker.EDSL.runDockerfileTextIO,
+    Control.Monad.IO.Class.liftIO,
+    Language.Docker.EDSL.from,
+
+    -- ** Constructing base images
+    Language.Docker.EDSL.tagged,
+    Language.Docker.EDSL.untagged,
+    Language.Docker.EDSL.digested,
+    Language.Docker.EDSL.aliased,
+
+    -- ** Syntax
+    Language.Docker.EDSL.add,
+    Language.Docker.EDSL.user,
+    Language.Docker.EDSL.label,
+    Language.Docker.EDSL.stopSignal,
+    Language.Docker.EDSL.copy,
+    Language.Docker.EDSL.copyFromStage,
+    Language.Docker.EDSL.to,
+    Language.Docker.EDSL.fromStage,
+    Language.Docker.EDSL.ownedBy,
+    Language.Docker.EDSL.toSources,
+    Language.Docker.EDSL.toTarget,
+    Language.Docker.EDSL.run,
+    Language.Docker.EDSL.runArgs,
+    Language.Docker.EDSL.cmd,
+    Language.Docker.EDSL.cmdArgs,
+    Language.Docker.EDSL.healthcheck,
+    Language.Docker.EDSL.check,
+    Language.Docker.EDSL.interval,
+    Language.Docker.EDSL.timeout,
+    Language.Docker.EDSL.startPeriod,
+    Language.Docker.EDSL.retries,
+    Language.Docker.EDSL.workdir,
+    Language.Docker.EDSL.expose,
+    Language.Docker.EDSL.ports,
+    Language.Docker.EDSL.tcpPort,
+    Language.Docker.EDSL.udpPort,
+    Language.Docker.EDSL.variablePort,
+    Language.Docker.EDSL.portRange,
+    Language.Docker.EDSL.udpPortRange,
+    Language.Docker.EDSL.volume,
+    Language.Docker.EDSL.entrypoint,
+    Language.Docker.EDSL.entrypointArgs,
+    Language.Docker.EDSL.maintainer,
+    Language.Docker.EDSL.env,
+    Language.Docker.EDSL.arg,
+    Language.Docker.EDSL.comment,
+    Language.Docker.EDSL.onBuild,
+    Language.Docker.EDSL.onBuildRaw,
+    Language.Docker.EDSL.embed,
+    Language.Docker.EDSL.Quasi.edockerfile,
+
+    -- ** Support types for the EDSL
+    Language.Docker.EDSL.EDockerfileM,
+    Language.Docker.EDSL.EDockerfileTM,
+    Language.Docker.EDSL.Types.EBaseImage (..),
+
+    -- * QuasiQuoter (@Language.Docker.EDSL.Quasi@)
+    Language.Docker.EDSL.Quasi.dockerfile,
+
+    -- * Instruction and InstructionPos helpers
+    Language.Docker.EDSL.instructionPos,
+) where
+
+import qualified Control.Monad.IO.Class
+import qualified Language.Docker.EDSL
+import qualified Language.Docker.EDSL.Quasi
+import qualified Language.Docker.EDSL.Types
diff --git a/src/Language/Docker/EDSL.hs b/src/Language/Docker/EDSL.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Docker/EDSL.hs
@@ -0,0 +1,382 @@
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Language.Docker.EDSL where
+
+import Control.Monad.Free
+import Control.Monad.Free.TH
+import Control.Monad.Trans.Free (FreeT, iterTM)
+import Control.Monad.Writer
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.ByteString.Lazy.Char8 as B8
+import Data.Default.Class (def)
+import Data.List.NonEmpty (NonEmpty)
+import Data.String (fromString)
+import Data.Text (Text)
+import qualified Data.Text as Text
+import qualified Data.Text.Lazy as L
+import qualified Data.Text.Lazy.Encoding as E
+import Language.Docker.EDSL.Types
+import qualified Language.Docker.PrettyPrint as PrettyPrint
+import qualified Language.Docker.Syntax as Syntax
+
+-- | The type of 'Identity' based EDSL blocks
+type EDockerfileM = Free EInstruction
+
+-- | The type of free monad EDSL blocks
+type EDockerfileTM = FreeT EInstruction
+
+type EInstructionM = Free EInstruction
+
+type EInstructionTM = FreeT EInstruction
+
+makeFree ''EInstruction
+
+runDockerWriter :: (MonadWriter [Syntax.Instruction Text] m) => EDockerfileM a -> m a
+runDockerWriter = iterM runD
+
+runDockerWriterIO ::
+  (Monad m, MonadTrans t, MonadWriter [Syntax.Instruction Text] (t m)) =>
+  EDockerfileTM m a ->
+  t m a
+runDockerWriterIO = iterTM runD
+
+runDef :: MonadWriter [t] m => (t1 -> t) -> t1 -> m b -> m b
+runDef f a n = tell [f a] >> n
+
+runDef2 :: MonadWriter [t] m => (t1 -> t2 -> t) -> t1 -> t2 -> m b -> m b
+runDef2 f a b n = tell [f a b] >> n
+
+runD :: MonadWriter [Syntax.Instruction Text] m => EInstruction (m b) -> m b
+runD (From (EBaseImage name t d a p) n) = runDef Syntax.From (Syntax.BaseImage name t d a p) n
+runD (CmdArgs as n) = runDef Syntax.Cmd as n
+runD (Shell as n) = runDef Syntax.Shell as n
+runD (AddArgs s d c n) = runDef Syntax.Add (Syntax.AddArgs s d c) n
+runD (User u n) = runDef Syntax.User u n
+runD (Label ps n) = runDef Syntax.Label ps n
+runD (StopSignal s n) = runDef Syntax.Stopsignal s n
+runD (CopyArgs s d c f n) = runDef Syntax.Copy (Syntax.CopyArgs s d c f) n
+runD (RunArgs as fs n) = runDef Syntax.Run (Syntax.RunArgs as fs) n
+runD (Workdir d n) = runDef Syntax.Workdir d n
+runD (Expose ps n) = runDef Syntax.Expose ps n
+runD (Volume v n) = runDef Syntax.Volume v n
+runD (EntrypointArgs e n) = runDef Syntax.Entrypoint e n
+runD (Maintainer m n) = runDef Syntax.Maintainer m n
+runD (Env ps n) = runDef Syntax.Env ps n
+runD (Arg k v n) = runDef2 Syntax.Arg k v n
+runD (Comment c n) = runDef Syntax.Comment c n
+runD (Healthcheck c n) = runDef Syntax.Healthcheck c n
+runD (OnBuildRaw i n) = runDef Syntax.OnBuild i n
+runD (Embed is n) = do
+  tell (map Syntax.instruction is)
+  n
+
+instructionPos :: Syntax.Instruction args -> Syntax.InstructionPos args
+instructionPos i = Syntax.InstructionPos i "" 0
+
+-- | Runs the Dockerfile EDSL and returns a 'Dockerfile' you can pretty print
+-- or manipulate
+toDockerfile :: EDockerfileM a -> Syntax.Dockerfile
+toDockerfile e =
+  let (_, w) = runWriter (runDockerWriter e)
+   in map instructionPos w
+
+-- | runs the Dockerfile EDSL and returns a 'Data.Text.Lazy' using
+-- 'Language.Docker.PrettyPrint'
+--
+-- @
+-- import Language.Docker
+--
+-- main :: IO ()
+-- main = print $ toDockerfileText $ do
+--     from (tagged "fpco/stack-build" "lts-6.9")
+--     add ["."] "/app/language-docker"
+--     workdir "/app/language-docker"
+--     run "stack build --test --only-dependencies"
+--     cmd "stack test"
+-- @
+toDockerfileText :: EDockerfileM a -> L.Text
+toDockerfileText = PrettyPrint.prettyPrint . toDockerfile
+
+-- | Writes the dockerfile to the given file path after pretty-printing it
+--
+-- @
+-- import Language.Docker
+--
+-- main :: IO ()
+-- main = writeDockerFile "build.Dockerfile" $ toDockerfile $ do
+--     from (tagged "fpco/stack-build" "lts-6.9")
+--     add ["."] "/app/language-docker"
+--     workdir "/app/language-docker"
+--     run "stack build --test --only-dependencies"
+--     cmd "stack test"
+-- @
+writeDockerFile :: Text -> Syntax.Dockerfile -> IO ()
+writeDockerFile filename =
+  BL.writeFile (Text.unpack filename) . E.encodeUtf8 . PrettyPrint.prettyPrint
+
+-- | Prints the dockerfile to stdout. Mainly used for debugging purposes
+--
+-- @
+-- import Language.Docker
+--
+-- main :: IO ()
+-- main = putDockerfileStr $ do
+--     from (tagged "fpco/stack-build" "lts-6.9")
+--     add ["."] "/app/language-docker"
+--     workdir "/app/language-docker"
+--     run "stack build --test --only-dependencies"
+--     cmd "stack test"
+-- @
+putDockerfileStr :: EDockerfileM a -> IO ()
+putDockerfileStr = B8.putStrLn . E.encodeUtf8 . PrettyPrint.prettyPrint . toDockerfile
+
+-- | Use a docker image in a FROM instruction without a tag
+--
+-- The following two examples are equivalent
+--
+-- @
+-- from $ untagged "fpco/stack-build"
+-- @
+--
+-- Is equivalent to, when having OverloadedStrings:
+--
+-- @
+-- from "fpco/stack-build"
+-- @
+untagged :: Text -> EBaseImage
+untagged s = EBaseImage (fromString . Text.unpack $ s) Nothing Nothing Nothing Nothing
+
+-- | Use a specific tag for a docker image. This function is meant
+-- to be used as an infix operator.
+--
+-- @
+-- from $ "fpco/stack-build" `tagged` "lts-10.3"
+-- @
+tagged :: Syntax.Image -> Syntax.Tag -> EBaseImage
+tagged imageName tag = EBaseImage imageName (Just tag) Nothing Nothing Nothing
+
+-- | Adds a digest checksum so a FROM instruction
+-- This function is meant to be used as an infix operator.
+--
+-- @
+-- from $ "fpco/stack-build" `digested` "sha256:abcdef123"
+-- @
+digested :: EBaseImage -> Syntax.Digest -> EBaseImage
+digested (EBaseImage n t _ a p) d = EBaseImage n t (Just d) a p
+
+-- | Alias a FROM instruction to be used as a build stage.
+-- This function is meant to be used as an infix operator.
+--
+-- @
+-- from $ "fpco/stack-build" `aliased` "builder"
+-- @
+aliased :: EBaseImage -> Syntax.ImageAlias -> EBaseImage
+aliased (EBaseImage n t d _ p) a = EBaseImage n t d (Just a) p
+
+-- | Create a RUN instruction with the given arguments.
+--
+-- @
+-- run "apt-get install wget"
+-- @
+run :: MonadFree EInstruction m => Syntax.Arguments Text -> m ()
+run as = runArgs as def
+
+-- | Create an ENTRYPOINT instruction with the given arguments.
+--
+-- @
+-- entrypoint "/usr/local/bin/program --some-flag"
+-- @
+entrypoint :: MonadFree EInstruction m => Syntax.Arguments Text -> m ()
+entrypoint = entrypointArgs
+
+-- | Create a CMD instruction with the given arguments.
+--
+-- @
+-- cmd "my-program --some-flag"
+-- @
+cmd :: MonadFree EInstruction m => Syntax.Arguments Text -> m ()
+cmd = cmdArgs
+
+-- | Create a COPY instruction. This function is meant to be
+-- used with the compinators 'to', 'fromStage' and 'ownedBy'
+--
+-- @
+-- copy $ ["foo.js", "bar.js"] `to` "."
+-- copy $ ["some_file"] `to` "/some/path" `fromStage` "builder"
+-- @
+copy :: MonadFree EInstruction m => Syntax.CopyArgs -> m ()
+copy (Syntax.CopyArgs sources dest ch src) = copyArgs sources dest ch src
+
+-- | Create a COPY instruction from a given build stage.
+-- This is a shorthand version of using 'copy' with combinators.
+--
+-- @
+-- copyFromStage "builder" ["foo.js", "bar.js"] "."
+-- @
+copyFromStage ::
+  MonadFree EInstruction m =>
+  Syntax.CopySource ->
+  NonEmpty Syntax.SourcePath ->
+  Syntax.TargetPath ->
+  m ()
+copyFromStage stage source dest = copy $ Syntax.CopyArgs source dest Syntax.NoChown stage
+
+-- | Create an ADD instruction. This is often used as a shorthand version
+-- of copy when no extra options are needed. Currently there is no way to
+-- pass extra options to ADD, so you are encouraged to use 'copy' instead.
+--
+-- @
+-- add ["foo.js", "bar.js"] "."
+-- @
+add :: MonadFree EInstruction m => NonEmpty Syntax.SourcePath -> Syntax.TargetPath -> m ()
+add sources dest = addArgs sources dest Syntax.NoChown
+
+-- | Converts a NonEmpty list of strings to a NonEmpty list of 'Syntax.SourcePath'
+--
+-- This is a convenience function when you need to pass a non-static list of
+-- strings that you build somewhere as an argument for 'copy' or 'add'
+--
+-- @
+-- someFiles <- glob "*.js"
+-- copy $ (toSources someFiles) `to` "."
+-- @
+toSources :: NonEmpty Text -> NonEmpty Syntax.SourcePath
+toSources = fmap Syntax.SourcePath
+
+-- | Converts a Text into a 'Syntax.TargetPath'
+--
+-- This is a convenience function when you need to pass a string variable
+-- as an argument for 'copy' or 'add'
+--
+-- @
+-- let destination = buildSomePath pwd
+-- add ["foo.js"] (toTarget destination)
+-- @
+toTarget :: Text -> Syntax.TargetPath
+toTarget = Syntax.TargetPath
+
+-- | Adds the --from= option to a COPY instruction.
+--
+-- This function is meant to be used as an infix operator:
+--
+-- @
+-- copy $ ["foo.js"] `to` "." `fromStage` "builder"
+-- @
+fromStage :: Syntax.CopyArgs -> Syntax.CopySource -> Syntax.CopyArgs
+fromStage args src = args {Syntax.sourceFlag = src}
+
+-- | Adds the --chown= option to a COPY instruction.
+--
+-- This function is meant to be used as an infix operator:
+--
+-- @
+-- copy $ ["foo.js"] `to` "." `ownedBy` "www-data:www-data"
+-- @
+ownedBy :: Syntax.CopyArgs -> Syntax.Chown -> Syntax.CopyArgs
+ownedBy args owner = args {Syntax.chownFlag = owner}
+
+-- | Usedto join source paths with atarget path as an arguments for 'copy'
+--
+-- This function is meant to be used as an infix operator:
+--
+-- @
+-- copy $ ["foo.js"] `to` "." `ownedBy`
+-- @
+to :: NonEmpty Syntax.SourcePath -> Syntax.TargetPath -> Syntax.CopyArgs
+to sources dest = Syntax.CopyArgs sources dest Syntax.NoChown Syntax.NoSource
+
+ports :: [Syntax.Port] -> Syntax.Ports
+ports = Syntax.Ports
+
+tcpPort :: Int -> Syntax.Port
+tcpPort = flip Syntax.Port Syntax.TCP
+
+udpPort :: Int -> Syntax.Port
+udpPort = flip Syntax.Port Syntax.UDP
+
+variablePort :: Text -> Syntax.Port
+variablePort varName = Syntax.PortStr ("$" <> varName)
+
+portRange :: Int -> Int -> Syntax.Port
+portRange a b = Syntax.PortRange a b Syntax.TCP
+
+udpPortRange :: Int -> Int -> Syntax.Port
+udpPortRange a b = Syntax.PortRange a b Syntax.UDP
+
+check :: Syntax.Arguments args -> Syntax.Check args
+check command =
+  Syntax.Check
+    Syntax.CheckArgs
+      { Syntax.checkCommand = command,
+        Syntax.interval = Nothing,
+        Syntax.timeout = Nothing,
+        Syntax.startPeriod = Nothing,
+        Syntax.retries = Nothing
+      }
+
+interval :: Syntax.Check args -> Integer -> Syntax.Check args
+interval ch secs =
+  case ch of
+    Syntax.NoCheck -> Syntax.NoCheck
+    Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.interval = Just $ fromInteger secs}
+
+timeout :: Syntax.Check args -> Integer -> Syntax.Check args
+timeout ch secs =
+  case ch of
+    Syntax.NoCheck -> Syntax.NoCheck
+    Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.timeout = Just $ fromInteger secs}
+
+startPeriod :: Syntax.Check args -> Integer -> Syntax.Check args
+startPeriod ch secs =
+  case ch of
+    Syntax.NoCheck -> Syntax.NoCheck
+    Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.startPeriod = Just $ fromInteger secs}
+
+retries :: Syntax.Check args -> Integer -> Syntax.Check args
+retries ch tries =
+  case ch of
+    Syntax.NoCheck -> Syntax.NoCheck
+    Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.retries = Just $ fromInteger tries}
+
+noCheck :: Syntax.Check args
+noCheck = Syntax.NoCheck
+
+-- | ONBUILD Dockerfile instruction
+--
+-- Each nested instruction gets emitted as a separate @ONBUILD@ block
+--
+-- @
+-- 'toDockerfile' $ do
+--     from "node"
+--     run "apt-get update"
+--     onBuild $ do
+--         run "echo more-stuff"
+--         run "echo here"
+-- @
+onBuild :: MonadFree EInstruction m => EDockerfileM a -> m ()
+onBuild b = mapM_ (onBuildRaw . Syntax.instruction) (toDockerfile b)
+
+-- | A version of 'toDockerfile' which allows IO actions
+toDockerfileIO :: MonadIO m => EDockerfileTM m t -> m Syntax.Dockerfile
+toDockerfileIO e = fmap snd (runDockerfileIO e)
+
+-- | A version of 'toDockerfileText' which allows IO actions
+toDockerfileTextIO :: MonadIO m => EDockerfileTM m t -> m L.Text
+toDockerfileTextIO e = fmap snd (runDockerfileTextIO e)
+
+-- | Just runs the EDSL's writer monad
+runDockerfileIO :: MonadIO m => EDockerfileTM m t -> m (t, Syntax.Dockerfile)
+runDockerfileIO e = do
+  (r, w) <- runWriterT (runDockerWriterIO e)
+  return (r, map instructionPos w)
+
+-- | Runs the EDSL's writer monad and pretty-prints the result
+runDockerfileTextIO :: MonadIO m => EDockerfileTM m t -> m (t, L.Text)
+runDockerfileTextIO e = do
+  (r, w) <- runDockerfileIO e
+  return (r, PrettyPrint.prettyPrint w)
diff --git a/src/Language/Docker/EDSL/Quasi.hs b/src/Language/Docker/EDSL/Quasi.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Docker/EDSL/Quasi.hs
@@ -0,0 +1,48 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Language.Docker.EDSL.Quasi where
+
+import qualified Data.Text as Text
+import Language.Docker.EDSL
+import qualified Language.Docker.Parser as Parser
+import Language.Docker.Syntax.Lift ()
+import Language.Haskell.TH
+import Language.Haskell.TH.Quote
+import Language.Haskell.TH.Syntax
+import Text.Megaparsec (errorBundlePretty)
+
+-- | Quasiquoter for embedding dockerfiles on the EDSL
+--
+-- @
+-- putStr $ 'toDockerfile' $ do
+--     from "node"
+--     run "apt-get update"
+--     [edockerfile|
+--     RUN apt-get update
+--     CMD node something.js
+--     |]
+-- @
+edockerfile :: QuasiQuoter
+edockerfile = dockerfile {quoteExp = edockerfileE}
+
+edockerfileE :: String -> ExpQ
+edockerfileE e =
+  case Parser.parseText (Text.pack e) of
+    Left err -> fail (errorBundlePretty err)
+    Right d -> [|embed d|]
+
+dockerfile :: QuasiQuoter
+dockerfile =
+  QuasiQuoter
+    { quoteExp = dockerfileE,
+      quoteDec = error "Can't use Dockerfile as a declaration",
+      quotePat = error "Can't use Dockerfile as a pattern",
+      quoteType = error "Can't use Dockerfile as a type"
+    }
+
+dockerfileE :: String -> ExpQ
+dockerfileE e =
+  case Parser.parseText (Text.pack e) of
+    Left err -> fail (errorBundlePretty err)
+    Right d -> lift d
diff --git a/src/Language/Docker/EDSL/Types.hs b/src/Language/Docker/EDSL/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Docker/EDSL/Types.hs
@@ -0,0 +1,90 @@
+{-# LANGUAGE DeriveFunctor #-}
+
+module Language.Docker.EDSL.Types where
+
+import Data.List.NonEmpty (NonEmpty)
+import Data.String
+import Data.Text (Text)
+import qualified Language.Docker.Syntax as Syntax
+
+data EBaseImage
+  = EBaseImage
+      Syntax.Image
+      (Maybe Syntax.Tag)
+      (Maybe Syntax.Digest)
+      (Maybe Syntax.ImageAlias)
+      (Maybe Syntax.Platform)
+  deriving (Show, Eq, Ord)
+
+instance IsString EBaseImage where
+  fromString s = EBaseImage (fromString s) Nothing Nothing Nothing Nothing
+
+data EInstruction next
+  = From
+      EBaseImage
+      next
+  | AddArgs
+      (NonEmpty Syntax.SourcePath)
+      Syntax.TargetPath
+      Syntax.Chown
+      next
+  | User
+      Text
+      next
+  | Label
+      Syntax.Pairs
+      next
+  | StopSignal
+      Text
+      next
+  | CopyArgs
+      (NonEmpty Syntax.SourcePath)
+      Syntax.TargetPath
+      Syntax.Chown
+      Syntax.CopySource
+      next
+  | RunArgs
+      (Syntax.Arguments Text)
+      Syntax.RunFlags
+      next
+  | CmdArgs
+      (Syntax.Arguments Text)
+      next
+  | Shell
+      (Syntax.Arguments Text)
+      next
+  | Workdir
+      Syntax.Directory
+      next
+  | Expose
+      Syntax.Ports
+      next
+  | Volume
+      Text
+      next
+  | EntrypointArgs
+      (Syntax.Arguments Text)
+      next
+  | Maintainer
+      Text
+      next
+  | Env
+      Syntax.Pairs
+      next
+  | Arg
+      Text
+      (Maybe Text)
+      next
+  | Comment
+      Text
+      next
+  | Healthcheck
+      (Syntax.Check Text)
+      next
+  | OnBuildRaw
+      (Syntax.Instruction Text)
+      next
+  | Embed
+      [Syntax.InstructionPos Text]
+      next
+  deriving (Functor)
diff --git a/src/Language/Docker/Syntax/Lift.hs b/src/Language/Docker/Syntax/Lift.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Docker/Syntax/Lift.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Language.Docker.Syntax.Lift where
+
+import Data.Fixed (Fixed)
+import Data.Time.Clock (DiffTime)
+import Instances.TH.Lift () -- Defines Lift instances for ByteString and Text
+
+import Language.Docker.Syntax
+import Language.Haskell.TH.Lift
+import Language.Haskell.TH.Syntax ()
+
+deriveLift ''Fixed
+
+deriveLift ''DiffTime
+
+deriveLift ''Protocol
+
+deriveLift ''Port
+
+deriveLift ''Ports
+
+deriveLift ''Registry
+
+deriveLift ''Image
+
+deriveLift ''ImageAlias
+
+deriveLift ''Tag
+
+deriveLift ''Digest
+
+deriveLift ''BaseImage
+
+deriveLift ''Arguments
+
+deriveLift ''Instruction
+
+deriveLift ''InstructionPos
+
+deriveLift ''SourcePath
+
+deriveLift ''TargetPath
+
+deriveLift ''Chown
+
+deriveLift ''CopySource
+
+deriveLift ''CopyArgs
+
+deriveLift ''AddArgs
+
+deriveLift ''Duration
+
+deriveLift ''Retries
+
+deriveLift ''CheckArgs
+
+deriveLift ''Check
+
+deriveLift ''BindOpts
+
+deriveLift ''CacheSharing
+
+deriveLift ''CacheOpts
+
+deriveLift ''TmpOpts
+
+deriveLift ''SecretOpts
+
+deriveLift ''RunMount
+
+deriveLift ''RunSecurity
+
+deriveLift ''RunNetwork
+
+deriveLift ''RunFlags
+
+deriveLift ''RunArgs
diff --git a/test/Language/Docker/EDSL/QuasiSpec.hs b/test/Language/Docker/EDSL/QuasiSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Language/Docker/EDSL/QuasiSpec.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE QuasiQuotes #-}
+module Language.Docker.EDSL.QuasiSpec
+  where
+
+import           Language.Docker.EDSL
+import           Language.Docker.EDSL.Quasi
+import           Language.Docker.Syntax
+import           Test.Hspec
+
+spec :: Spec
+spec = do
+    describe "dockerfile" $
+        it "parses a dockerfile and returns its ast" $ do
+            let df = map instruction [dockerfile|
+                                                FROM node
+                                                RUN apt-get update
+                                                CMD ["node", "something.js"]
+                                                |]
+            df `shouldBe` [ From (BaseImage "node" Nothing Nothing Nothing Nothing)
+                          , Run "apt-get update"
+                          , Cmd ["node", "something.js"]
+                          ]
+
+    describe "edockerfile" $
+        it "lets us use parsed dockerfiles seamlessly in our DSL" $ do
+            let d = do
+                    from ("node" `aliased` "node-build")
+                    expose (ports [tcpPort 8080, variablePort "PORT"])
+                    [edockerfile|
+                                RUN apt-get update
+                                CMD node something.js
+                                |]
+                df = map instruction (toDockerfile d)
+            df `shouldBe` [ From (BaseImage
+                                            { image = "node"
+                                            , alias = Just "node-build"
+                                            , tag = Nothing
+                                            , digest = Nothing
+                                            , platform = Nothing}
+                                 )
+                          , Expose (Ports [Port 8080 TCP, PortStr "$PORT"])
+                          , Run "apt-get update"
+                          , Cmd "node something.js"
+                          ]
diff --git a/test/Language/Docker/EDSLSpec.hs b/test/Language/Docker/EDSLSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Language/Docker/EDSLSpec.hs
@@ -0,0 +1,135 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+module Language.Docker.EDSLSpec where
+
+import           Control.Monad.IO.Class
+import           Data.List                       (sort)
+import           Language.Docker.EDSL
+import           Language.Docker.PrettyPrint
+import qualified Language.Docker.Syntax      as Syntax
+import           System.Directory
+import           System.FilePath
+import           System.FilePath.Glob
+import           Test.Hspec
+import qualified Data.Text.Lazy as L
+import qualified Data.Text as Text
+import           Data.Semigroup ((<>))
+
+printed :: [L.Text] -> L.Text
+printed = L.unlines
+
+spec :: Spec
+spec = do
+    describe "toDockerfile s" $
+        it "allows us to write haskell code that represents Dockerfiles" $ do
+            let r = map Syntax.instruction $ toDockerfile (do
+                        from "node"
+                        cmdArgs ["node", "-e", "'console.log(\'hey\')'"])
+            r `shouldBe` [ Syntax.From $
+                             Syntax.BaseImage "node"
+                             Nothing
+                             Nothing
+                             Nothing
+                             Nothing
+                         , Syntax.Cmd ["node", "-e", "'console.log(\'hey\')'"]
+                         ]
+
+    describe "prettyPrint $ toDockerfile s" $ do
+        it "allows us to write haskell code that represents Dockerfiles" $ do
+            let r = prettyPrint $ toDockerfile (do
+                        from "node"
+                        shell ["cmd", "/S"]
+                        entrypoint ["/tini", "--"]
+                        cmdArgs ["node", "-e", "'console.log(\'hey\')'"]
+                        healthcheck $ check "curl -f http://localhost/ || exit 1" `interval` 300)
+            r `shouldBe` printed [ "FROM node"
+                                 , "SHELL [\"cmd\", \"/S\"]"
+                                 , "ENTRYPOINT [\"/tini\", \"--\"]"
+                                 , "CMD [\"node\", \"-e\", \"'console.log(\'hey\')'\"]"
+                                 , "HEALTHCHECK --interval=300s CMD curl -f http://localhost/ || exit 1"
+                                 ]
+        it "print expose instructions correctly" $ do
+            let r = prettyPrint $ toDockerfile (do
+                        from "scratch"
+                        expose $ ports [variablePort "PORT", tcpPort 80, udpPort 51]
+                        expose $ ports [portRange 90 100]
+                        expose $ ports [udpPortRange 190 200])
+            r `shouldBe` printed [ "FROM scratch"
+                                 , "EXPOSE $PORT 80/tcp 51/udp"
+                                 , "EXPOSE 90-100"
+                                 , "EXPOSE 190-200/udp"
+                                 ]
+
+        it "onBuild let's us nest statements" $ do
+            let r = prettyPrint $ toDockerfile $ do
+                        from "node"
+                        cmdArgs ["node", "-e", "'console.log(\'hey\')'"]
+                        onBuild $ do
+                            run "echo \"hello world\""
+                            run "echo \"hello world2\""
+            r `shouldBe` printed [ "FROM node"
+                                 , "CMD [\"node\", \"-e\", \"'console.log(\'hey\')'\"]"
+                                 , "ONBUILD RUN echo \"hello world\""
+                                 , "ONBUILD RUN echo \"hello world2\""
+                                 ]
+
+        it "parses and prints from aliases correctly" $ do
+            let r = prettyPrint $ toDockerfile $ do
+                        from $ "node" `tagged` "10.1" `aliased` "node-build"
+                        run "echo foo"
+            r `shouldBe` printed [ "FROM node:10.1 AS node-build"
+                                 , "RUN echo foo"
+                                 ]
+
+        it "parses and prints from with a registry" $ do
+            let r = prettyPrint $ toDockerfile $ do
+                        from "opensuse/tumbleweed"
+                        run "echo foo"
+            r `shouldBe` printed [ "FROM opensuse/tumbleweed"
+                                 , "RUN echo foo"
+                                 ]
+
+        it "parses and prints copy instructions" $ do
+            let r = prettyPrint $ toDockerfile $ do
+                        from "scratch"
+                        copy $ ["foo.js"] `to` "bar.js"
+                        copy $ ["foo.js", "bar.js"] `to` "."
+                        copy $ ["foo.js", "bar.js"] `to` "baz/"
+                        copy $ ["something"] `to` "crazy" `fromStage` "builder"
+                        copy $ ["this"] `to` "that" `fromStage` "builder" `ownedBy` "www-data"
+            r `shouldBe` printed [ "FROM scratch"
+                                 , "COPY foo.js bar.js"
+                                 , "COPY foo.js bar.js ./"
+                                 , "COPY foo.js bar.js baz/"
+                                 , "COPY --from=builder something crazy"
+                                 , "COPY --chown=www-data --from=builder this that"
+                                 ]
+        it "quotes label and env correctly" $ do
+            let r = prettyPrint $ toDockerfile $ do
+                        from "scratch"
+                        label [("email", "Example <example@example.com>")]
+                        label [("escape", "Escape this\" thing")]
+                        env [("foo", "bar baz")]
+                        env [("double_escape", "escape this \\\"")]
+            r `shouldBe` printed [ "FROM scratch"
+                                 , "LABEL email=\"Example <example@example.com>\""
+                                 , "LABEL escape=\"Escape this\\\" thing\""
+                                 , "ENV foo=\"bar baz\""
+                                 , "ENV double_escape=\"escape this \\\"\""
+                                 ]
+
+    describe "toDockerfileTextIO" $
+        it "let's us run in the IO monad" $ do
+            -- TODO - "glob" is a really useful combinator
+            str <- toDockerfileTextIO $ do
+                fs <- liftIO $ do
+                    cwd <- getCurrentDirectory
+                    fs <- glob "./test/Language/Docker/*.hs"
+                    return (map (makeRelative cwd) (sort fs))
+                from "ubuntu"
+                let file = Text.pack . takeFileName
+                mapM_ (\f -> add [Syntax.SourcePath (Text.pack f)] (Syntax.TargetPath $ "/app/" <> file f)) fs
+            str `shouldBe` printed [ "FROM ubuntu"
+                                   , "ADD ./test/Language/Docker/EDSLSpec.hs /app/EDSLSpec.hs"
+                                   , "ADD ./test/Language/Docker/ExamplesSpec.hs /app/ExamplesSpec.hs"
+                                   ]
diff --git a/test/Language/Docker/ExamplesSpec.hs b/test/Language/Docker/ExamplesSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Language/Docker/ExamplesSpec.hs
@@ -0,0 +1,20 @@
+module Language.Docker.ExamplesSpec where
+
+import           Control.Monad
+import           Data.Monoid
+import           System.Directory
+import           System.FilePath
+import           System.FilePath.Glob
+import           System.Process
+import           Test.Hspec
+
+stackRunGhc e = callProcess "stack" ["runghc", "--package", "language-docker", e]
+
+spec :: Spec
+spec = do
+    cwd <- runIO getCurrentDirectory
+    exampleSources <- runIO $ glob "./examples/*.hs"
+    forM_ exampleSources $ \exampleSource -> do
+        let exampleSource' = makeRelative cwd exampleSource
+        describe exampleSource $ it ("stack runghc " <> exampleSource') $
+            stackRunGhc exampleSource
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
