├── .gitignore ├── Makefile ├── README.md ├── app_example.jpg ├── dash_app ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── app.py ├── config.py ├── layout.py ├── plots.py ├── requirements.txt └── style.py ├── docker-compose.yml ├── nginx ├── Dockerfile ├── nginx.conf ├── project.conf └── static │ ├── api_logo_pwrdBy_strava_horiz_light.png │ ├── api_logo_pwrdBy_strava_stack_light.png │ ├── btn_strava_connectwith_orange.png │ ├── btn_strava_connectwith_orange@2x.png │ └── stravaio_poster_2018.jpg └── run_docker.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | __pycache__ 3 | 4 | .vscode/ 5 | .idea/ 6 | .cache/ 7 | .DS_Store -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker-compose up --build -d 3 | 4 | clean: 5 | docker-compose down 6 | docker system prune -fa 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blueprint for docker-flask-gunicorn-nginx web application 2 | 3 | ![Example App](app_example.jpg?raw=true "Example App") 4 | 5 | Bootstrap example of a *Flask/Dash* app served via *Gunicorn* and *Nginx* using Docker containers 6 | 7 | Guildeline article can be found at https://sladkovm.github.io/webdev/2017/10/16/Deploying-Plotly-Dash-in-a-Docker-Container-on-Digitital-Ocean.html 8 | 9 | ## Run 10 | 11 | ```bash 12 | make build 13 | ``` 14 | 15 | In your browser (assuming the docker-machine runs on 192.168.99.100) go to: 16 | 17 | http://192.168.99.100 18 | 19 | To clean up the container mess, run 20 | ``` 21 | make clean 22 | ``` 23 | 24 | It will shut down all container and remove all images 25 | 26 | ## Prominent features: 27 | 28 | 1. Dockerized application orchestrated by docker-compose 29 | 2. Gunicorn as a WSGI and Nginx as a reverse proxy are included as services 30 | 3. Nginx is configured to serve static files, e.g. images, css etc. 31 | 4. Example of routing implementation in *Dash* app is shown 32 | 5. Build process uses *requirements.txt*, but *Pipenv* files are included to ease the development process 33 | 6. Bootstrap css is included 34 | 7. Standard Single Page App Layout with Header, Main and Footer is set up 35 | -------------------------------------------------------------------------------- /app_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sladkovm/docker-flask-gunicorn-nginx/d3a4c97ae2ca339d92ccd93e98ed1e35f6d59be4/app_example.jpg -------------------------------------------------------------------------------- /dash_app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.7 2 | 3 | RUN mkdir -p /home/project/dash_app 4 | WORKDIR /home/project/dash_app 5 | COPY requirements.txt /home/project/dash_app 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | 8 | COPY . /home/project/dash_app 9 | 10 | -------------------------------------------------------------------------------- /dash_app/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pylint = "*" 8 | 9 | [packages] 10 | dash = "*" 11 | dash-core-components = "*" 12 | dash-html-components = "*" 13 | dash-renderer = "*" 14 | gunicorn = ">=19.7.1" 15 | numpy = "*" 16 | 17 | [requires] 18 | python_version = "3.6" 19 | -------------------------------------------------------------------------------- /dash_app/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "86e8416e992cdb8b38ebc955772e8c61f517676b8ce4366d874e06f3c03aa2f0" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", 22 | "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" 23 | ], 24 | "version": "==2018.11.29" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "click": { 34 | "hashes": [ 35 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 36 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 37 | ], 38 | "version": "==7.0" 39 | }, 40 | "dash": { 41 | "hashes": [ 42 | "sha256:a312169d4d75290f40991f680377ee131c5b7c02c5755ebfbcd1753bc6999b2c" 43 | ], 44 | "index": "pypi", 45 | "version": "==0.35.1" 46 | }, 47 | "dash-core-components": { 48 | "hashes": [ 49 | "sha256:36c6fc2e4e452c37021ff067c6df033e749561fafd95af9500823d1f470b5995" 50 | ], 51 | "index": "pypi", 52 | "version": "==0.42.1" 53 | }, 54 | "dash-html-components": { 55 | "hashes": [ 56 | "sha256:e5d6247887741bf49038eae82f716be6a8b16b7c7b0b8e4a769bb4608feb0b8b" 57 | ], 58 | "index": "pypi", 59 | "version": "==0.13.4" 60 | }, 61 | "dash-renderer": { 62 | "hashes": [ 63 | "sha256:6a2e4d6410e1c4a9577a0aca27b95a1ac77024aae0c8b9c271a8f9518c697b89" 64 | ], 65 | "index": "pypi", 66 | "version": "==0.16.1" 67 | }, 68 | "decorator": { 69 | "hashes": [ 70 | "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", 71 | "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" 72 | ], 73 | "version": "==4.3.0" 74 | }, 75 | "flask": { 76 | "hashes": [ 77 | "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", 78 | "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" 79 | ], 80 | "version": "==1.0.2" 81 | }, 82 | "flask-compress": { 83 | "hashes": [ 84 | "sha256:468693f4ddd11ac6a41bca4eb5f94b071b763256d54136f77957cfee635badb3" 85 | ], 86 | "version": "==1.4.0" 87 | }, 88 | "gunicorn": { 89 | "hashes": [ 90 | "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", 91 | "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" 92 | ], 93 | "index": "pypi", 94 | "version": "==19.9.0" 95 | }, 96 | "idna": { 97 | "hashes": [ 98 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 99 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 100 | ], 101 | "version": "==2.8" 102 | }, 103 | "ipython-genutils": { 104 | "hashes": [ 105 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 106 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 107 | ], 108 | "version": "==0.2.0" 109 | }, 110 | "itsdangerous": { 111 | "hashes": [ 112 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 113 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 114 | ], 115 | "version": "==1.1.0" 116 | }, 117 | "jinja2": { 118 | "hashes": [ 119 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", 120 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" 121 | ], 122 | "version": "==2.10" 123 | }, 124 | "jsonschema": { 125 | "hashes": [ 126 | "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", 127 | "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02" 128 | ], 129 | "version": "==2.6.0" 130 | }, 131 | "jupyter-core": { 132 | "hashes": [ 133 | "sha256:927d713ffa616ea11972534411544589976b2493fc7e09ad946e010aa7eb9970", 134 | "sha256:ba70754aa680300306c699790128f6fbd8c306ee5927976cbe48adacf240c0b7" 135 | ], 136 | "version": "==4.4.0" 137 | }, 138 | "markupsafe": { 139 | "hashes": [ 140 | "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", 141 | "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", 142 | "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", 143 | "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", 144 | "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", 145 | "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", 146 | "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", 147 | "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", 148 | "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", 149 | "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", 150 | "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", 151 | "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", 152 | "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", 153 | "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", 154 | "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", 155 | "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", 156 | "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", 157 | "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", 158 | "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", 159 | "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", 160 | "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", 161 | "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", 162 | "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", 163 | "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", 164 | "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", 165 | "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", 166 | "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", 167 | "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" 168 | ], 169 | "version": "==1.1.0" 170 | }, 171 | "nbformat": { 172 | "hashes": [ 173 | "sha256:b9a0dbdbd45bb034f4f8893cafd6f652ea08c8c1674ba83f2dc55d3955743b0b", 174 | "sha256:f7494ef0df60766b7cabe0a3651556345a963b74dbc16bc7c18479041170d402" 175 | ], 176 | "version": "==4.4.0" 177 | }, 178 | "numpy": { 179 | "hashes": [ 180 | "sha256:0df89ca13c25eaa1621a3f09af4c8ba20da849692dcae184cb55e80952c453fb", 181 | "sha256:154c35f195fd3e1fad2569930ca51907057ae35e03938f89a8aedae91dd1b7c7", 182 | "sha256:18e84323cdb8de3325e741a7a8dd4a82db74fde363dce32b625324c7b32aa6d7", 183 | "sha256:1e8956c37fc138d65ded2d96ab3949bd49038cc6e8a4494b1515b0ba88c91565", 184 | "sha256:23557bdbca3ccbde3abaa12a6e82299bc92d2b9139011f8c16ca1bb8c75d1e95", 185 | "sha256:24fd645a5e5d224aa6e39d93e4a722fafa9160154f296fd5ef9580191c755053", 186 | "sha256:36e36b6868e4440760d4b9b44587ea1dc1f06532858d10abba98e851e154ca70", 187 | "sha256:3d734559db35aa3697dadcea492a423118c5c55d176da2f3be9c98d4803fc2a7", 188 | "sha256:416a2070acf3a2b5d586f9a6507bb97e33574df5bd7508ea970bbf4fc563fa52", 189 | "sha256:4a22dc3f5221a644dfe4a63bf990052cc674ef12a157b1056969079985c92816", 190 | "sha256:4d8d3e5aa6087490912c14a3c10fbdd380b40b421c13920ff468163bc50e016f", 191 | "sha256:4f41fd159fba1245e1958a99d349df49c616b133636e0cf668f169bce2aeac2d", 192 | "sha256:561ef098c50f91fbac2cc9305b68c915e9eb915a74d9038ecf8af274d748f76f", 193 | "sha256:56994e14b386b5c0a9b875a76d22d707b315fa037affc7819cda08b6d0489756", 194 | "sha256:73a1f2a529604c50c262179fcca59c87a05ff4614fe8a15c186934d84d09d9a5", 195 | "sha256:7da99445fd890206bfcc7419f79871ba8e73d9d9e6b82fe09980bc5bb4efc35f", 196 | "sha256:99d59e0bcadac4aa3280616591fb7bcd560e2218f5e31d5223a2e12a1425d495", 197 | "sha256:a4cc09489843c70b22e8373ca3dfa52b3fab778b57cf81462f1203b0852e95e3", 198 | "sha256:a61dc29cfca9831a03442a21d4b5fd77e3067beca4b5f81f1a89a04a71cf93fa", 199 | "sha256:b1853df739b32fa913cc59ad9137caa9cc3d97ff871e2bbd89c2a2a1d4a69451", 200 | "sha256:b1f44c335532c0581b77491b7715a871d0dd72e97487ac0f57337ccf3ab3469b", 201 | "sha256:b261e0cb0d6faa8fd6863af26d30351fd2ffdb15b82e51e81e96b9e9e2e7ba16", 202 | "sha256:c857ae5dba375ea26a6228f98c195fec0898a0fd91bcf0e8a0cae6d9faf3eca7", 203 | "sha256:cf5bb4a7d53a71bb6a0144d31df784a973b36d8687d615ef6a7e9b1809917a9b", 204 | "sha256:db9814ff0457b46f2e1d494c1efa4111ca089e08c8b983635ebffb9c1573361f", 205 | "sha256:df04f4bad8a359daa2ff74f8108ea051670cafbca533bb2636c58b16e962989e", 206 | "sha256:ecf81720934a0e18526177e645cbd6a8a21bb0ddc887ff9738de07a1df5c6b61", 207 | "sha256:edfa6fba9157e0e3be0f40168eb142511012683ac3dc82420bee4a3f3981b30e" 208 | ], 209 | "index": "pypi", 210 | "version": "==1.15.4" 211 | }, 212 | "plotly": { 213 | "hashes": [ 214 | "sha256:0877cafd49bae595615390437c20319f37c001cb9a17d3bc0c7741697952f731", 215 | "sha256:9489e8d772bdf700ef9dad55941c3e1b3430f71a08da4e8bfbd8f5838d274ff1" 216 | ], 217 | "version": "==3.5.0" 218 | }, 219 | "pytz": { 220 | "hashes": [ 221 | "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", 222 | "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" 223 | ], 224 | "version": "==2018.9" 225 | }, 226 | "requests": { 227 | "hashes": [ 228 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 229 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 230 | ], 231 | "version": "==2.21.0" 232 | }, 233 | "retrying": { 234 | "hashes": [ 235 | "sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b" 236 | ], 237 | "version": "==1.3.3" 238 | }, 239 | "six": { 240 | "hashes": [ 241 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 242 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 243 | ], 244 | "version": "==1.12.0" 245 | }, 246 | "traitlets": { 247 | "hashes": [ 248 | "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", 249 | "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" 250 | ], 251 | "version": "==4.3.2" 252 | }, 253 | "urllib3": { 254 | "hashes": [ 255 | "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", 256 | "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" 257 | ], 258 | "version": "==1.24.1" 259 | }, 260 | "werkzeug": { 261 | "hashes": [ 262 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", 263 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" 264 | ], 265 | "version": "==0.14.1" 266 | } 267 | }, 268 | "develop": { 269 | "astroid": { 270 | "hashes": [ 271 | "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", 272 | "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" 273 | ], 274 | "version": "==2.1.0" 275 | }, 276 | "isort": { 277 | "hashes": [ 278 | "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", 279 | "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", 280 | "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" 281 | ], 282 | "version": "==4.3.4" 283 | }, 284 | "lazy-object-proxy": { 285 | "hashes": [ 286 | "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", 287 | "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", 288 | "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", 289 | "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", 290 | "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", 291 | "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", 292 | "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", 293 | "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", 294 | "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", 295 | "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", 296 | "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", 297 | "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", 298 | "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", 299 | "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", 300 | "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", 301 | "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", 302 | "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", 303 | "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", 304 | "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", 305 | "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", 306 | "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", 307 | "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", 308 | "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", 309 | "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", 310 | "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", 311 | "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", 312 | "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", 313 | "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", 314 | "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" 315 | ], 316 | "version": "==1.3.1" 317 | }, 318 | "mccabe": { 319 | "hashes": [ 320 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 321 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 322 | ], 323 | "version": "==0.6.1" 324 | }, 325 | "pylint": { 326 | "hashes": [ 327 | "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", 328 | "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" 329 | ], 330 | "index": "pypi", 331 | "version": "==2.2.2" 332 | }, 333 | "six": { 334 | "hashes": [ 335 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 336 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 337 | ], 338 | "version": "==1.12.0" 339 | }, 340 | "typed-ast": { 341 | "hashes": [ 342 | "sha256:0555eca1671ebe09eb5f2176723826f6f44cca5060502fea259de9b0e893ab53", 343 | "sha256:0ca96128ea66163aea13911c9b4b661cb345eb729a20be15c034271360fc7474", 344 | "sha256:16ccd06d614cf81b96de42a37679af12526ea25a208bce3da2d9226f44563868", 345 | "sha256:1e21ae7b49a3f744958ffad1737dfbdb43e1137503ccc59f4e32c4ac33b0bd1c", 346 | "sha256:37670c6fd857b5eb68aa5d193e14098354783b5138de482afa401cc2644f5a7f", 347 | "sha256:46d84c8e3806619ece595aaf4f37743083f9454c9ea68a517f1daa05126daf1d", 348 | "sha256:5b972bbb3819ece283a67358103cc6671da3646397b06e7acea558444daf54b2", 349 | "sha256:6306ffa64922a7b58ee2e8d6f207813460ca5a90213b4a400c2e730375049246", 350 | "sha256:6cb25dc95078931ecbd6cbcc4178d1b8ae8f2b513ae9c3bd0b7f81c2191db4c6", 351 | "sha256:7e19d439fee23620dea6468d85bfe529b873dace39b7e5b0c82c7099681f8a22", 352 | "sha256:7f5cd83af6b3ca9757e1127d852f497d11c7b09b4716c355acfbebf783d028da", 353 | "sha256:81e885a713e06faeef37223a5b1167615db87f947ecc73f815b9d1bbd6b585be", 354 | "sha256:94af325c9fe354019a29f9016277c547ad5d8a2d98a02806f27a7436b2da6735", 355 | "sha256:b1e5445c6075f509d5764b84ce641a1535748801253b97f3b7ea9d948a22853a", 356 | "sha256:cb061a959fec9a514d243831c514b51ccb940b58a5ce572a4e209810f2507dcf", 357 | "sha256:cc8d0b703d573cbabe0d51c9d68ab68df42a81409e4ed6af45a04a95484b96a5", 358 | "sha256:da0afa955865920edb146926455ec49da20965389982f91e926389666f5cf86a", 359 | "sha256:dc76738331d61818ce0b90647aedde17bbba3d3f9e969d83c1d9087b4f978862", 360 | "sha256:e7ec9a1445d27dbd0446568035f7106fa899a36f55e52ade28020f7b3845180d", 361 | "sha256:f741ba03feb480061ab91a465d1a3ed2d40b52822ada5b4017770dfcb88f839f", 362 | "sha256:fe800a58547dd424cd286b7270b967b5b3316b993d86453ede184a17b5a6b17d" 363 | ], 364 | "markers": "python_version < '3.7' and implementation_name == 'cpython'", 365 | "version": "==1.1.1" 366 | }, 367 | "wrapt": { 368 | "hashes": [ 369 | "sha256:e03f19f64d81d0a3099518ca26b04550026f131eced2e76ced7b85c6b8d32128" 370 | ], 371 | "version": "==1.11.0" 372 | } 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /dash_app/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | import dash 3 | import dash_core_components as dcc 4 | import dash_html_components as html 5 | from dash.dependencies import Input, Output 6 | from config import config_app 7 | from layout import app_layout, make_header, make_main 8 | from plots import bar_plot, scatter_plot 9 | 10 | import sys 11 | # import logging 12 | # logger = logging.getLogger(__name__) 13 | # logger.addHandler(logging.StreamHandler(stream=sys.stderr)) 14 | # logger.setLevel(logging.DEBUG) 15 | 16 | 17 | server = Flask(__name__) 18 | 19 | 20 | app = dash.Dash(name='Bootstrap_docker_app', 21 | server=server, 22 | static_folder='static', 23 | csrf_protect=False) 24 | 25 | # Add css, js, container div with id='page-content' and location with id='url' 26 | app = config_app(app, debug=True) 27 | 28 | # Generate app layoute with 3 div elements: page-header, page-main, page-footer. 29 | # Content of each div is a function input 30 | app.layout = app_layout(header=make_header()) 31 | 32 | 33 | @app.callback(Output('page-main', 'children'), [Input('url', 'pathname')]) 34 | def routing(pathname): 35 | """Very basic router 36 | 37 | This callback function will read the current url 38 | and based on pathname value will populate the children of the page-main 39 | 40 | Returns: 41 | html.Div 42 | """ 43 | app.server.logger.info(pathname) 44 | 45 | if pathname == '/bar': 46 | rv = make_main(bar_plot) 47 | elif pathname == '/scatter': 48 | rv = make_main(scatter_plot) 49 | else: 50 | rv = make_main({'layout': {'title': 'empty plot: click on a Bar or Scatter link'}}) 51 | 52 | return rv 53 | 54 | 55 | if __name__ == '__main__': 56 | 57 | app.run_server(debug=True) 58 | -------------------------------------------------------------------------------- /dash_app/config.py: -------------------------------------------------------------------------------- 1 | import dash_html_components as html 2 | import dash_core_components as dcc 3 | 4 | 5 | def config_app(app, **kwargs): 6 | """Dash app configuration 7 | 8 | Parameters 9 | ---------- 10 | app: Dash app 11 | debug: optional, default=False 12 | 13 | Returns 14 | ------- 15 | app: Dash app 16 | With added css, ga and layout container with: 17 | app-layout: main div, should not be a target of an ouput callback 18 | page-content: container div, target for an ouput callback 19 | url: Location, target of an input callback 20 | """ 21 | 22 | if kwargs.get('debug', False): 23 | app.server.debug = True 24 | 25 | app.config.supress_callback_exceptions = True 26 | 27 | # Append CSS 28 | app.css.append_css({ 29 | 'external_url': 'https://maxcdn.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css' 30 | }) 31 | 32 | # Append JS 33 | app.scripts.append_script({ 34 | 'external_url': 'http://velometria.com/static/ga.js' 35 | }) 36 | app.scripts.append_script({ 37 | 'external_url': 'https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js' 38 | }) 39 | 40 | return app 41 | -------------------------------------------------------------------------------- /dash_app/layout.py: -------------------------------------------------------------------------------- 1 | import dash_core_components as dcc 2 | import dash_html_components as html 3 | from style import colors 4 | 5 | 6 | def app_layout(header=None, main=None, footer=None): 7 | """Returns app layout with the following elements: 8 | app-layout: main div, should not be a target of an ouput callback 9 | page-content: container div, target for an ouput callback 10 | url: Location, target of an input callback 11 | """ 12 | rv = html.Div( 13 | id='app-layout', 14 | children=[ 15 | dcc.Location(id='url', refresh=False), 16 | html.Div(id='page-content', 17 | className = 'container-fluid', 18 | children=[ 19 | html.Div(header, id='page-header'), 20 | html.Div(main, id='page-main'), 21 | html.Div(footer, id='page-footer') 22 | ] 23 | ) 24 | ] 25 | ) 26 | return rv 27 | 28 | 29 | def make_header(): 30 | """Returns a div with a header""" 31 | rv = html.Nav( 32 | className='navbar navbar-expand-md navbar-light bg-light', 33 | children = [ 34 | # Title on the left 35 | html.Span(html.A("Strava Poster", 36 | href='/', 37 | className='navbar-brand'), 38 | className='navbar-brand mr-auto w-50'), 39 | # Links on the right 40 | html.Ul( 41 | children = [ 42 | html.Li(html.A('Bar', 43 | href='/bar', 44 | className='nav-link lead' 45 | ), 46 | className = 'nav-item'), 47 | html.Li(html.A('Scatter', 48 | href='/scatter', 49 | className='nav-link lead' 50 | ), 51 | className = 'nav-item'), 52 | html.Li(html.A('Blog', 53 | href='https://sladkovm.github.io/', 54 | className='nav-link lead' 55 | ), 56 | className = 'nav-item'), 57 | html.Li(html.A( 58 | children='Velometria', 59 | href='http://velometria.com', 60 | className = 'nav-link lead' 61 | ), 62 | className = 'nav-pills'), 63 | ], 64 | className = 'nav navbar-nav ml-auto w-100 justify-content-end' 65 | ), 66 | ]) 67 | return rv 68 | 69 | 70 | def make_main(plot=html.Div()): 71 | """Returns a div with a plot""" 72 | rv = html.Div( 73 | style={'backgroundColor': colors['background']}, 74 | children=[ 75 | dcc.Graph( 76 | id='fig', 77 | figure=plot 78 | ) 79 | ] 80 | ) 81 | return rv 82 | 83 | -------------------------------------------------------------------------------- /dash_app/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.graph_objs as go 2 | import numpy as np 3 | from style import colors 4 | 5 | 6 | # define data declaratively as a dict 7 | bar_plot = { 8 | 'data': [ 9 | {'x': [1, 2, 3], 'y': [4, 1, 2], 'type': 'bar', 'name': 'SF'}, 10 | {'x': [1, 2, 3], 'y': [2, 4, 5], 'type': 'bar', 'name': u'Montréal'}, 11 | ], 12 | 'layout': { 13 | 'title': "Bar Plot", 14 | 'images': [ 15 | { 16 | 'source':'/static/api_logo_pwrdBy_strava_horiz_light.png', 17 | 'xref':"paper", 18 | 'yref':"paper", 19 | 'x':1, 20 | 'y':1.05, 21 | 'sizex':0.2, 22 | 'sizey':0.2, 23 | 'xanchor':"right", 24 | 'yanchor':"bottom" 25 | }], 26 | 'plot_bgcolor': colors['background'], 27 | 'paper_bgcolor': colors['background'], 28 | 'font': { 29 | 'color': colors['text'] 30 | } 31 | } 32 | } 33 | 34 | 35 | # use a go object 36 | scatter_plot = { 37 | 'data': [ 38 | go.Scatter({ 39 | 'name': 'Sin(x)', 40 | 'x': np.arange(10), 41 | 'y': np.sin(np.arange(10)) 42 | }) 43 | ], 44 | 'layout': { 45 | 'title': 'Scatter Plot', 46 | 'images': [ 47 | { 48 | 'source':'/static/api_logo_pwrdBy_strava_horiz_light.png', 49 | 'xref':"paper", 50 | 'yref':"paper", 51 | 'x':1, 52 | 'y':1.05, 53 | 'sizex':0.2, 54 | 'sizey':0.2, 55 | 'xanchor':"right", 56 | 'yanchor':"bottom" 57 | }], 58 | 'plot_bgcolor': colors['background'], 59 | 'paper_bgcolor': colors['background'], 60 | 'font': { 61 | 'color': colors['text'] 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /dash_app/requirements.txt: -------------------------------------------------------------------------------- 1 | dash 2 | dash-core-components 3 | dash-html-components 4 | dash-renderer 5 | numpy 6 | gunicorn>=19.7.1 7 | 8 | -------------------------------------------------------------------------------- /dash_app/style.py: -------------------------------------------------------------------------------- 1 | colors = { 2 | 'background': '#FFF', 3 | 'text': '#111' 4 | } 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | dash_app: 6 | container_name: dash_app 7 | restart: always 8 | build: ./dash_app 9 | ports: 10 | - "8000:8000" 11 | command: gunicorn -w 1 -b :8000 app:server 12 | 13 | 14 | nginx: 15 | container_name: nginx 16 | restart: always 17 | build: ./nginx 18 | ports: 19 | - "80:80" 20 | depends_on: 21 | - dash_app 22 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.15.8 2 | 3 | RUN rm /etc/nginx/nginx.conf 4 | COPY nginx.conf /etc/nginx/ 5 | 6 | RUN rm /etc/nginx/conf.d/default.conf 7 | COPY project.conf /etc/nginx/conf.d/ 8 | 9 | COPY ./static ./static -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # Define the user that will own and run the Nginx server 2 | user nginx; 3 | 4 | # Define the number of worker processes; recommended value is the number of 5 | # cores that are being used by your server 6 | worker_processes 1; 7 | 8 | # Define the location on the file system of the error log, plus the minimum 9 | # severity to log messages for 10 | error_log /var/log/nginx/error.log warn; 11 | 12 | # Define the file that will store the process ID of the main NGINX process 13 | pid /var/run/nginx.pid; 14 | 15 | 16 | # events block defines the parameters that affect connection processing. 17 | events { 18 | # Define the maximum number of simultaneous connections that can be opened by a worker process 19 | worker_connections 1024; 20 | } 21 | 22 | 23 | # http block defines the parameters for how NGINX should handle HTTP web traffic 24 | http { 25 | # Include the file defining the list of file types that are supported by NGINX 26 | include /etc/nginx/mime.types; 27 | 28 | # Define the default file type that is returned to the user 29 | default_type text/html; 30 | 31 | # Define the format of log messages. 32 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 33 | '$status $body_bytes_sent "$http_referer" ' 34 | '"$http_user_agent" "$http_x_forwarded_for"'; 35 | 36 | # Define the location of the log of access attempts to NGINX 37 | access_log /var/log/nginx/access.log main; 38 | 39 | # Define the parameters to optimize the delivery of static content 40 | sendfile on; 41 | tcp_nopush on; 42 | tcp_nodelay on; 43 | 44 | # Define the timeout value for keep-alive connections with the client 45 | keepalive_timeout 65; 46 | 47 | # Define the usage of the gzip compression algorithm to reduce the amount of data to transmit 48 | #gzip on; 49 | 50 | # Include additional parameters for virtual host(s)/server(s) 51 | include /etc/nginx/conf.d/*.conf; 52 | } 53 | -------------------------------------------------------------------------------- /nginx/project.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; 4 | server_name docker_flask_gunicorn_nginx; 5 | 6 | location / { 7 | proxy_pass http://dash_app:8000; 8 | 9 | # Do not change this 10 | proxy_set_header Host $host; 11 | proxy_set_header X-Real-IP $remote_addr; 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 13 | } 14 | 15 | location /static { 16 | rewrite ^/static(.*) /$1 break; 17 | root /static; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nginx/static/api_logo_pwrdBy_strava_horiz_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sladkovm/docker-flask-gunicorn-nginx/d3a4c97ae2ca339d92ccd93e98ed1e35f6d59be4/nginx/static/api_logo_pwrdBy_strava_horiz_light.png -------------------------------------------------------------------------------- /nginx/static/api_logo_pwrdBy_strava_stack_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sladkovm/docker-flask-gunicorn-nginx/d3a4c97ae2ca339d92ccd93e98ed1e35f6d59be4/nginx/static/api_logo_pwrdBy_strava_stack_light.png -------------------------------------------------------------------------------- /nginx/static/btn_strava_connectwith_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sladkovm/docker-flask-gunicorn-nginx/d3a4c97ae2ca339d92ccd93e98ed1e35f6d59be4/nginx/static/btn_strava_connectwith_orange.png -------------------------------------------------------------------------------- /nginx/static/btn_strava_connectwith_orange@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sladkovm/docker-flask-gunicorn-nginx/d3a4c97ae2ca339d92ccd93e98ed1e35f6d59be4/nginx/static/btn_strava_connectwith_orange@2x.png -------------------------------------------------------------------------------- /nginx/static/stravaio_poster_2018.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sladkovm/docker-flask-gunicorn-nginx/d3a4c97ae2ca339d92ccd93e98ed1e35f6d59be4/nginx/static/stravaio_poster_2018.jpg -------------------------------------------------------------------------------- /run_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo killing old docker processes 4 | docker-compose rm -fs 5 | 6 | echo building docker containers 7 | docker-compose up --build -d 8 | 9 | --------------------------------------------------------------------------------