├── .circleci └── config.yml ├── .kodiak.toml ├── README.md ├── compose └── v3 │ ├── defaults.dhall │ ├── package.dhall │ └── types.dhall ├── example ├── docker-compose-deploy.dhall └── docker-compose-deploy.yml └── s ├── fmt ├── install ├── lint └── test /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | lint: 5 | docker: 6 | - image: circleci/python:3.7 7 | steps: 8 | - checkout 9 | - run: 10 | name: install deps 11 | command: ./s/install 12 | - run: 13 | name: lint 14 | command: ./s/lint 15 | 16 | test: 17 | docker: 18 | - image: circleci/python:3.7 19 | steps: 20 | - checkout 21 | - run: 22 | name: install deps 23 | command: ./s/install 24 | - run: 25 | name: test 26 | command: ./s/test 27 | 28 | workflows: 29 | version: 2 30 | test: 31 | jobs: 32 | - lint 33 | - test 34 | -------------------------------------------------------------------------------- /.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [merge.message] 4 | title = "pull_request_title" 5 | body = "pull_request_body" 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dhall-docker-compose 2 | 3 | > A library for writing [Docker Compose](https://docs.docker.com/compose/) 4 | > files in [Dhall](https://dhall-lang.org). 5 | 6 | **Note:** Only version 3.0 of the Docker Compose config is supported currently. 7 | 8 | If you wish to add another version it is probably worth spending the time to 9 | write a json-spec to dhall converter. See [dhall-kubernetes' 10 | generator](https://github.com/dhall-lang/dhall-kubernetes). 11 | 12 | ## Why? 13 | 14 | To experiment with Dhall for config. 15 | 16 | ## Usage 17 | 18 | ```dhall 19 | -- for the imports in your docker-compose.dhall file you should either download 20 | -- the dhall files or use the URL imports. 21 | let Compose = ./compose/v3/package.dhall 22 | 23 | in Compose.Config::{ 24 | -- your config here 25 | } 26 | ``` 27 | 28 | ## Dev 29 | 30 | 1. [install dhall-to-yaml](https://github.com/dhall-lang/dhall-lang/wiki/Getting-started%3A-Generate-JSON-or-YAML#os-x---install-using-brew) 31 | 32 | > `brew install dhall-json` 33 | 34 | 2. generate yaml 35 | 36 | ```sh 37 | dhall-to-yaml --file example/docker-compose-deploy.dhall --output ./example/docker-compose-deploy.yml --explain 38 | ``` 39 | 40 | Note that the yaml keys are alphabetized in the generated yaml. 41 | -------------------------------------------------------------------------------- /compose/v3/defaults.dhall: -------------------------------------------------------------------------------- 1 | let Map = 2 | https://raw.githubusercontent.com/dhall-lang/dhall-lang/master/Prelude/Map/Type 3 | 4 | let types = ./types.dhall 5 | 6 | let ServiceVolumeLong = 7 | { type = None Text 8 | , source = None Text 9 | , target = None Text 10 | , read_only = None Bool 11 | , bind = None { propagation : Optional Text } 12 | , volume = None { nocopy : Optional Bool } 13 | , tmpfs = None { size : Optional Text } 14 | } 15 | 16 | let ServiceSecretLong = 17 | { source = None Text 18 | , target = None Text 19 | , uid = None Natural 20 | , gid = None Natural 21 | , mode = None Text 22 | } 23 | 24 | let ServiceNetwork = 25 | { aliases = None (List Text) 26 | , ipv4_address = None Text 27 | , ipv6_address = None Text 28 | } 29 | 30 | let Service = 31 | { deploy = None types.Deploy 32 | , build = None types.Build 33 | , cap_add = None (List Text) 34 | , cap_drop = None (List Text) 35 | , cgroup_parent = None Text 36 | , command = None types.StringOrList 37 | , container_name = None Text 38 | , depends_on = None (List Text) 39 | , devices = None (List Text) 40 | , dns = None types.StringOrList 41 | , dns_search = None (List Text) 42 | , domainname = None Text 43 | , entrypoint = None types.StringOrList 44 | , env_file = None types.StringOrList 45 | , environment = None types.ListOrDict 46 | , expose = None (List types.StringOrNumber) 47 | , external_links = None (List Text) 48 | , extra_hosts = None types.ListOrDict 49 | , healthcheck = None types.Healthcheck 50 | , hostname = None Text 51 | , image = None Text 52 | , ipc = None Text 53 | , runtime = None Text 54 | , labels = None types.Labels 55 | , links = None (List Text) 56 | , logging = None types.Logging 57 | , mac_address = None Text 58 | , network_mode = None Text 59 | , networks = None types.ServiceNetworks 60 | , pid = None Text 61 | , ports = None (List types.StringOrNumber) 62 | , privileged = None Bool 63 | , read_only = None Bool 64 | , restart = None Text 65 | , secrets = None (List types.ServiceSecret) 66 | , security_opt = None (List Text) 67 | , shm_size = None types.StringOrNumber 68 | , sysctls = None types.ListOrDict 69 | , stdin_open = None Bool 70 | , stop_grace_period = None Text 71 | , stop_signal = None Text 72 | , tmpfs = None types.StringOrList 73 | , tty = None Bool 74 | , ulimits = None (Map Text types.Ulimits) 75 | , user = None Text 76 | , userns_mode = None Text 77 | , volumes = None (List types.ServiceVolume) 78 | , working_dir = None Text 79 | } 80 | : types.Service 81 | 82 | let Network = { external = None Bool, name = None Text } 83 | 84 | let Secret = 85 | { file = None Text 86 | , environment = None Text 87 | , external = None Bool 88 | , name = None Text 89 | } 90 | : types.Secret 91 | 92 | let Volume = 93 | { driver = None Text 94 | , driver_opts = None types.DriverOpts 95 | , ipam = None types.Ipam 96 | , external = None types.External 97 | } 98 | : types.Volume 99 | 100 | let Healthcheck = 101 | { disable = None Bool 102 | , interval = None Text 103 | , retries = None Natural 104 | , test = None types.StringOrList 105 | , timeout = None Text 106 | } 107 | : types.Healthcheck 108 | 109 | let ComposeConfig = 110 | { services = None types.Services 111 | , networks = None types.Networks 112 | , volumes = None types.Volumes 113 | , secrets = None types.Secrets 114 | } 115 | : types.ComposeConfig 116 | 117 | in { Secret 118 | , ServiceSecretLong 119 | , ServiceVolumeLong 120 | , ServiceNetwork 121 | , Service 122 | , Network 123 | , Volume 124 | , ComposeConfig 125 | , Healthcheck 126 | } 127 | -------------------------------------------------------------------------------- /compose/v3/package.dhall: -------------------------------------------------------------------------------- 1 | let defaults = ./defaults.dhall 2 | 3 | let types = ./types.dhall 4 | 5 | in types 6 | // { ServiceVolumeLong = 7 | { Type = types.ServiceVolumeLong 8 | , default = defaults.ServiceVolumeLong 9 | } 10 | , ServiceSecretLong = 11 | { Type = types.ServiceSecretLong 12 | , default = defaults.ServiceSecretLong 13 | } 14 | , ServiceNetwork = 15 | { Type = types.ServiceNetwork, default = defaults.ServiceNetwork } 16 | , Service = { Type = types.Service, default = defaults.Service } 17 | , Network = { Type = types.Network, default = defaults.Network } 18 | , Volume = { Type = types.Volume, default = defaults.Volume } 19 | , Secret = { Type = types.Secret, default = defaults.Secret } 20 | , Config = 21 | { Type = types.ComposeConfig, default = defaults.ComposeConfig } 22 | , Healthcheck = 23 | { Type = types.Healthcheck, default = defaults.Healthcheck } 24 | } 25 | -------------------------------------------------------------------------------- /compose/v3/types.dhall: -------------------------------------------------------------------------------- 1 | let Map = 2 | https://raw.githubusercontent.com/dhall-lang/dhall-lang/master/Prelude/Map/Type 3 | 4 | let StringOrNumber 5 | : Type 6 | = < String : Text | Number : Natural > 7 | 8 | let ListOrDict 9 | : Type 10 | = < Dict : Map Text Text | List : List (Optional StringOrNumber) > 11 | 12 | let Build 13 | : Type 14 | = < String : Text 15 | | Object : 16 | { context : Text 17 | , dockerfile : Text 18 | , args : ListOrDict 19 | , ssh : ListOrDict 20 | } 21 | > 22 | 23 | let StringOrList 24 | : Type 25 | = < String : Text | List : List Text > 26 | 27 | let Healthcheck 28 | : Type 29 | = { disable : Optional Bool 30 | , interval : Optional Text 31 | , retries : Optional Natural 32 | , test : Optional StringOrList 33 | , timeout : Optional Text 34 | } 35 | 36 | let Labels 37 | : Type 38 | = < Object : Map Text Text | List : List Text > 39 | 40 | let Options 41 | : Type 42 | = Map Text (Optional StringOrNumber) 43 | 44 | let Logging 45 | : Type 46 | = { driver : Text, options : Optional Options } 47 | 48 | let Network 49 | : Type 50 | = { external : Optional Bool, name : Optional Text } 51 | 52 | let Networks 53 | : Type 54 | = < List : List Text | Map : Map Text Network > 55 | 56 | let Ulimits 57 | : Type 58 | = < Int : Natural | Object : { hard : Natural, soft : Natural } > 59 | 60 | let Resource 61 | : Type 62 | = { cpus : Text, memory : Text } 63 | 64 | let Deploy 65 | : Type 66 | = { mode : Text 67 | , replicas : Natural 68 | , labels : Labels 69 | , update_config : 70 | { parallelism : Natural 71 | , delay : Text 72 | , failure_action : Text 73 | , monitor : Text 74 | , max_failure_ratio : Natural 75 | } 76 | , resources : { limits : Resource, reservations : Resource } 77 | , restartPolicy : 78 | { condition : Text 79 | , delay : Text 80 | , maxAttempts : Natural 81 | , window : Text 82 | } 83 | , placement : { constraints : List Text } 84 | } 85 | 86 | let ServiceSecretLong 87 | : Type 88 | = { source : Optional Text 89 | , target : Optional Text 90 | , uid : Optional Natural 91 | , gid : Optional Natural 92 | , mode : Optional Text 93 | } 94 | 95 | let ServiceSecret 96 | : Type 97 | = < Short : Text | Long : ServiceSecretLong > 98 | 99 | let ServiceVolumeLong 100 | : Type 101 | = { type : Optional Text 102 | , source : Optional Text 103 | , target : Optional Text 104 | , read_only : Optional Bool 105 | , bind : Optional { propagation : Optional Text } 106 | , volume : Optional { nocopy : Optional Bool } 107 | , tmpfs : Optional { size : Optional Text } 108 | } 109 | 110 | let ServiceVolume 111 | : Type 112 | = < Short : Text | Long : ServiceVolumeLong > 113 | 114 | let ServiceNetwork 115 | : Type 116 | = { aliases : Optional (List Text) 117 | , ipv4_address : Optional Text 118 | , ipv6_address : Optional Text 119 | } 120 | 121 | let ServiceNetworks 122 | : Type 123 | = < List : List Text | Map : Map Text ServiceNetwork > 124 | 125 | let Service 126 | : Type 127 | = { deploy : Optional Deploy 128 | , build : Optional Build 129 | , cap_add : Optional (List Text) 130 | , cap_drop : Optional (List Text) 131 | , cgroup_parent : Optional Text 132 | , command : Optional StringOrList 133 | , container_name : Optional Text 134 | , depends_on : Optional (List Text) 135 | , devices : Optional (List Text) 136 | , dns : Optional StringOrList 137 | , dns_search : Optional (List Text) 138 | , domainname : Optional Text 139 | , entrypoint : Optional StringOrList 140 | , env_file : Optional StringOrList 141 | , environment : Optional ListOrDict 142 | , expose : Optional (List StringOrNumber) 143 | , external_links : Optional (List Text) 144 | , extra_hosts : Optional ListOrDict 145 | , healthcheck : Optional Healthcheck 146 | , hostname : Optional Text 147 | , image : Optional Text 148 | , ipc : Optional Text 149 | , runtime : Optional Text 150 | , labels : Optional Labels 151 | , links : Optional (List Text) 152 | , logging : Optional Logging 153 | , mac_address : Optional Text 154 | , network_mode : Optional Text 155 | , networks : Optional ServiceNetworks 156 | , pid : Optional Text 157 | , ports : Optional (List StringOrNumber) 158 | , privileged : Optional Bool 159 | , read_only : Optional Bool 160 | , restart : Optional Text 161 | , secrets : Optional (List ServiceSecret) 162 | , security_opt : Optional (List Text) 163 | , shm_size : Optional StringOrNumber 164 | , sysctls : Optional ListOrDict 165 | , stdin_open : Optional Bool 166 | , stop_grace_period : Optional Text 167 | , stop_signal : Optional Text 168 | , tmpfs : Optional StringOrList 169 | , tty : Optional Bool 170 | , ulimits : Optional (Map Text Ulimits) 171 | , user : Optional Text 172 | , userns_mode : Optional Text 173 | , volumes : Optional (List ServiceVolume) 174 | , working_dir : Optional Text 175 | } 176 | 177 | let DriverOpts 178 | : Type 179 | = Map Text StringOrNumber 180 | 181 | let Ipam 182 | : Type 183 | = { driver : Text, config : List { subnet : Text } } 184 | 185 | let External 186 | : Type 187 | = < Bool : Bool | Object : { name : Text } > 188 | 189 | let Volume 190 | : Type 191 | = { driver : Optional Text 192 | , driver_opts : Optional DriverOpts 193 | , ipam : Optional Ipam 194 | , external : Optional External 195 | } 196 | 197 | let Volumes 198 | : Type 199 | = Map Text (Optional Volume) 200 | 201 | let Secret 202 | : Type 203 | = { file : Optional Text 204 | , environment : Optional Text 205 | , external : Optional Bool 206 | , name : Optional Text 207 | } 208 | 209 | let Secrets 210 | : Type 211 | = Map Text (Optional Secret) 212 | 213 | let Services 214 | : Type 215 | = Map Text Service 216 | 217 | let ComposeConfig 218 | : Type 219 | = { secrets : Optional Secrets 220 | , services : Optional Services 221 | , networks : Optional Networks 222 | , volumes : Optional Volumes 223 | } 224 | 225 | in { ComposeConfig 226 | , Secret 227 | , Secrets 228 | , Services 229 | , Service 230 | , ServiceSecret 231 | , ServiceSecretLong 232 | , ServiceVolume 233 | , ServiceVolumeLong 234 | , ServiceNetwork 235 | , ServiceNetworks 236 | , StringOrNumber 237 | , Deploy 238 | , Build 239 | , StringOrList 240 | , ListOrDict 241 | , Healthcheck 242 | , Labels 243 | , Logging 244 | , Network 245 | , Networks 246 | , Ulimits 247 | , Volumes 248 | , Volume 249 | , Options 250 | , DriverOpts 251 | , Ipam 252 | , External 253 | } 254 | -------------------------------------------------------------------------------- /example/docker-compose-deploy.dhall: -------------------------------------------------------------------------------- 1 | let map = 2 | https://raw.githubusercontent.com/dhall-lang/dhall-lang/master/Prelude/List/map 3 | 4 | let Entry = 5 | https://raw.githubusercontent.com/dhall-lang/dhall-lang/master/Prelude/Map/Entry 6 | 7 | let Compose = ../compose/v3/package.dhall 8 | 9 | let logging = 10 | Some 11 | { driver = "syslog" 12 | , options = Some 13 | [ { mapKey = "syslog-address" 14 | , mapValue = Some 15 | ( Compose.StringOrNumber.String 16 | "udp://logs.papertrailapp.com:50183" 17 | ) 18 | } 19 | , { mapKey = "tag" 20 | , mapValue = Some (Compose.StringOrNumber.String "{{.Name}}") 21 | } 22 | ] 23 | } 24 | 25 | let nginxService = 26 | Compose.Service::{ 27 | , image = Some "recipeyak/nginx:latest" 28 | , ports = Some [ Compose.StringOrNumber.String "80:80" ] 29 | , volumes = Some 30 | [ Compose.ServiceVolume.Short "react-static-files:/var/app/dist" 31 | , Compose.ServiceVolume.Short "django-static-files:/var/app/django/static" 32 | ] 33 | , logging 34 | , depends_on = Some [ "django", "react" ] 35 | } 36 | 37 | let djangoService = 38 | Compose.Service::{ 39 | , restart = Some "always" 40 | , image = Some "recipeyak/django:latest" 41 | , env_file = Some (Compose.StringOrList.List [ ".env-production" ]) 42 | , command = Some (Compose.StringOrList.String "sh bootstrap-prod.sh") 43 | , volumes = Some [ Compose.ServiceVolume.Short "django-static-files:/var/app/static-files" ] 44 | , logging 45 | , depends_on = Some [ "db" ] 46 | } 47 | 48 | let dbService = 49 | Compose.Service::{ 50 | , image = Some "postgres:10.1" 51 | , command = Some 52 | ( Compose.StringOrList.List 53 | [ "-c" 54 | , "shared_preload_libraries=\"pg_stat_statements\"" 55 | , "-c" 56 | , "pg_stat_statements.max=10000" 57 | , "-c" 58 | , "pg_stat_statements.track=all" 59 | ] 60 | ) 61 | , ports = Some [ Compose.StringOrNumber.String "5432:5432" ] 62 | , volumes = Some [ Compose.ServiceVolume.Short "pgdata:/var/lib/postgresql/data/" ] 63 | , logging 64 | , healthcheck = Some Compose.Healthcheck::{ 65 | , test = Some (Compose.StringOrList.String "checkpg.sh") 66 | , timeout = Some "10s" 67 | } 68 | } 69 | 70 | let reactService = 71 | Compose.Service::{ 72 | , image = Some "recipeyak/react:latest" 73 | , command = Some (Compose.StringOrList.String "sh bootstrap.sh") 74 | , env_file = Some (Compose.StringOrList.List [ ".env-production" ]) 75 | , volumes = Some [ Compose.ServiceVolume.Short "react-static-files:/var/app/dist" ] 76 | , logging 77 | } 78 | 79 | let buildStubService = 80 | Compose.Service::{ 81 | , build = Some 82 | ( Compose.Build.Object 83 | { dockerfile = "DockerfileStub" 84 | , context = "." 85 | , args = 86 | Compose.ListOrDict.List 87 | ([] : List (Optional Compose.StringOrNumber)) 88 | , ssh = 89 | Compose.ListOrDict.List 90 | [ Some (Compose.StringOrNumber.String "default") ] 91 | } 92 | ) 93 | } 94 | 95 | let toEntry = 96 | \(name : Text) -> 97 | { mapKey = name 98 | , mapValue = Some Compose.Volume::{ driver = Some "local" } 99 | } 100 | 101 | let Output 102 | : Type 103 | = Entry Text (Optional Compose.Volume.Type) 104 | 105 | let volumes 106 | : Compose.Volumes 107 | = map 108 | Text 109 | Output 110 | toEntry 111 | [ "pgdata", "django-static-files", "react-static-files" ] 112 | 113 | let services 114 | : Compose.Services 115 | = toMap 116 | { nginx = nginxService 117 | , db = dbService 118 | , react = reactService 119 | , django = djangoService 120 | , buildStub = buildStubService 121 | } 122 | 123 | in Compose.Config::{ services = Some services, volumes = Some volumes } 124 | -------------------------------------------------------------------------------- /example/docker-compose-deploy.yml: -------------------------------------------------------------------------------- 1 | services: 2 | buildStub: 3 | build: 4 | args: [] 5 | context: '.' 6 | dockerfile: DockerfileStub 7 | ssh: 8 | - default 9 | db: 10 | command: 11 | - "-c" 12 | - "shared_preload_libraries=\"pg_stat_statements\"" 13 | - "-c" 14 | - pg_stat_statements.max=10000 15 | - "-c" 16 | - pg_stat_statements.track=all 17 | healthcheck: 18 | test: checkpg.sh 19 | timeout: "10s" 20 | image: postgres:10.1 21 | logging: 22 | driver: syslog 23 | options: 24 | syslog-address: udp://logs.papertrailapp.com:50183 25 | tag: "{{.Name}}" 26 | ports: 27 | - "5432:5432" 28 | volumes: 29 | - pgdata:/var/lib/postgresql/data/ 30 | django: 31 | command: sh bootstrap-prod.sh 32 | depends_on: 33 | - db 34 | env_file: 35 | - ".env-production" 36 | image: recipeyak/django:latest 37 | logging: 38 | driver: syslog 39 | options: 40 | syslog-address: udp://logs.papertrailapp.com:50183 41 | tag: "{{.Name}}" 42 | restart: always 43 | volumes: 44 | - django-static-files:/var/app/static-files 45 | nginx: 46 | depends_on: 47 | - django 48 | - react 49 | image: recipeyak/nginx:latest 50 | logging: 51 | driver: syslog 52 | options: 53 | syslog-address: udp://logs.papertrailapp.com:50183 54 | tag: "{{.Name}}" 55 | ports: 56 | - "80:80" 57 | volumes: 58 | - react-static-files:/var/app/dist 59 | - django-static-files:/var/app/django/static 60 | react: 61 | command: sh bootstrap.sh 62 | env_file: 63 | - ".env-production" 64 | image: recipeyak/react:latest 65 | logging: 66 | driver: syslog 67 | options: 68 | syslog-address: udp://logs.papertrailapp.com:50183 69 | tag: "{{.Name}}" 70 | volumes: 71 | - react-static-files:/var/app/dist 72 | volumes: 73 | django-static-files: 74 | driver: local 75 | pgdata: 76 | driver: local 77 | react-static-files: 78 | driver: local 79 | -------------------------------------------------------------------------------- /s/fmt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | dhall format --transitive compose/v3/package.dhall 6 | -------------------------------------------------------------------------------- /s/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | readonly URLS=( 6 | "https://github.com/dhall-lang/dhall-haskell/releases/download/1.38.0/dhall-1.38.0-x86_64-linux.tar.bz2" 7 | "https://github.com/dhall-lang/dhall-haskell/releases/download/1.38.0/dhall-json-1.7.5-x86_64-linux.tar.bz2" 8 | ) 9 | 10 | # CI install 11 | 12 | main() { 13 | mkdir -p ~/.local/bin 14 | 15 | for url in "${URLS[@]}"; do 16 | download_name="download.tar.bz2" 17 | curl -L "${url}" -o "${download_name}" 18 | tar xf "${download_name}" ./bin 19 | cp ./bin/* ~/.local/bin 20 | rm -rf ./bin "${download_name}" 21 | done 22 | } 23 | 24 | main "${@}" 25 | -------------------------------------------------------------------------------- /s/lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | dhall format --check --transitive compose/v3/package.dhall 6 | -------------------------------------------------------------------------------- /s/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | readonly TEMP_DIR=$(mktemp -d) 6 | 7 | readonly OUT="${TEMP_DIR}/docker-compose-deploy.yml" 8 | 9 | dhall-to-yaml --file "./example/docker-compose-deploy.dhall" --output "${OUT}" --explain 10 | 11 | diff "${OUT}" "./example/docker-compose-deploy.yml" 12 | --------------------------------------------------------------------------------