├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CHANGELOG.md ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── _config.yml ├── appveyor.yml ├── bin.js ├── index.js ├── lib ├── args.js ├── docker.js ├── file.js ├── logger.js ├── machine.js └── network.js ├── package.json ├── pm2.config.js ├── scripts └── dockerfile-version-verify.js └── test └── basic.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/* -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: freaker2k7 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: freaker2k7 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.9.0] - 01/08/2019 4 | Added optional **[Redis](https://redis.io) support** for big clusters (recommended for clusters with more than 50 workers/hosts). 5 | 6 | ## [1.8.9] - 01/08/2019 7 | Fixed CVE issue with docker-cli-js v2.5.2 ==> v2.5.3 & removed package-lock.json 8 | 9 | ## [1.8.8] - 30/07/2019 10 | Fixed volumes option (because of deprecated context var) 11 | 12 | ## [1.8.5] - 30/07/2019 13 | Added **pull** option as a HEAD request && **S3 support** for fully distributed clusters. 14 | 15 | ## [1.8.4] - 28/07/2019 16 | Edited some docs. 17 | 18 | ## [1.8.2] - 28/07/2019 19 | Added logger. 20 | 21 | ## [1.8.1] - 28/07/2019 22 | Cashed reading (in cluster mode) & fixed host issue in results (also in cluster mode). 23 | 24 | ## [1.8.0] - 28/07/2019 25 | **Stable Cluster mode!** 26 | 27 | ## [1.7.9] - 28/07/2019 28 | Added Liberapay badge. 29 | 30 | ## [1.7.8] - 27/07/2019 31 | Fixed test 32 | 33 | ## [1.7.7] - 27/07/2019 34 | Renamed npm-shrinkwrap.json to package-lock.json and added it to git. 35 | 36 | ## [1.7.5] - 27/07/2019 37 | Added npm-shrinkwrap.json to remove lodash's (4.17.11 => 4.17.15) vulnerability from docker-cli-js. 38 | 39 | ## [1.7.2] - 27/07/2019 40 | Updated docker-cli-js's lodash. 41 | 42 | ## [1.7.1] - 27/07/2019 43 | New bin file ./bin.js 44 | 45 | ## [1.5.9] - 27/07/2019 46 | Cluster mode using json files in a **shared folder** instead of Redis. 47 | 48 | ## [1.5.8] - 26/07/2019 49 | **Cluster mode** support (Only some issues with POST to /:id) with **[Redis]("https://redis.io" "Redis")**. 50 | 51 | ## [1.5.7] - 26/07/2019 52 | Badges :) 53 | 54 | ## [1.5.5] - 26/07/2019 55 | Back to express-rate-limit && set min version to NodeJS 6.0. 56 | 57 | ## [1.5.4] - 25/07/2019 58 | Returning to express-throttle for compatibitily (testing). 59 | 60 | ## [1.5.2] - 25/07/2019 61 | Fixed typo in filename. 62 | 63 | ## [1.5.1] - 25/07/2019 64 | Added test for [AppVeyor](https://appveyor.com/ "AppVeyor"). 65 | 66 | ## [1.5.0] - **Stable HTTPS support.** 67 | 68 | ## [1.4.4]-1.25/07/2019 69 | 4.9 - Fixed some typos. 70 | 71 | ## [1.4.2] - 24/07/2019 72 | Added HTTPS support & parametrized all env. variables. 73 | 74 | ## [1.4.1] - 24/07/2019 75 | Removed unused DS_CONTEXT env. variable & received JS code quality A+ score on [LGTM](https://lgtm.com "LGTM"). 76 | 77 | ## [1.2.9] - 23/07/2019 78 | Fixed some typos. 79 | 80 | ## [1.2.7] - 22/07/2019 81 | Replaced express-throttle with express-rate-limit. 82 | 83 | ## [1.2.5] - 22/07/2019 84 | Stable, simple express server with express-throttle. 85 | 86 | ## [1.0] - 20/07/2019 87 | **Release, woohoo!!** -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | dockerserver.io -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at thenetfreaker@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | Any pull-request will be honored and handled. 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | RUN curl -L https://get.docker.com | sh - 4 | RUN npm i -g docker-server@1.9.0 5 | 6 | CMD ["docker-server"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DockerServer 2 | Super lightweight & simple RESTFul stateless server for running [docker](https://docker.com/ "docker") containers on a remote machine(s) in a secure way. 3 | 4 | [![npm version](https://badge.fury.io/js/docker-server.svg)](https://badge.fury.io/js/docker-server) 5 | [![node version](https://img.shields.io/node/v/docker-server)](https://www.npmjs.com/package/docker-server) 6 | [![npm downloads](https://img.shields.io/npm/dw/docker-server.svg)](https://www.npmjs.com/package/docker-server) 7 | [![Gitter](https://badges.gitter.im/freaker2k7-dockerserver/community.svg)](https://gitter.im/freaker2k7-dockerserver/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 8 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/freaker2k7/dockerserver.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/freaker2k7/dockerserver/alerts/) 9 | [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/freaker2k7/dockerserver.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/freaker2k7/dockerserver/context:javascript) 10 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/freaker2k7/dockerserver/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/freaker2k7/dockerserver/?branch=master) 11 | [![License](https://img.shields.io/badge/license-Apache-brightgreen.svg)](https://opensource.org/licenses/Apache-2.0) 12 | [![Bungle size](https://img.shields.io/bundlephobia/minzip/docker-server)](https://bundlephobia.com/result?p=docker-server) 13 | [![Repo size](https://img.shields.io/github/repo-size/freaker2k7/dockerserver)](https://github.com/freaker2k7/dockerserver) 14 | [![Build status](https://ci.appveyor.com/api/projects/status/rwbo4jvqp4032boj/branch/master?svg=true)](https://ci.appveyor.com/project/freaker2k7/dockerserver/branch/master) 15 | [![Beerpay](https://beerpay.io/freaker2k7/dockerserver/badge.svg?style=flat)](https://beerpay.io/freaker2k7/dockerserver) 16 | [![Liberapay](http://img.shields.io/liberapay/receives/evgy.svg?logo=liberapay)](https://liberapay.com/evgy/) 17 | [![GitHub stars](https://img.shields.io/github/stars/freaker2k7/dockerserver.svg?style=social&label=Stars)](https://github.com/freaker2k7/dockerserver/stargazers/) 18 | [![Known Vulnerabilities](https://snyk.io//test/github/freaker2k7/dockerserver/badge.svg?targetFile=package.json)](https://snyk.io//test/github/freaker2k7/dockerserver) 19 | 20 | 21 |
22 | 23 | DockerServer Logo 24 | 25 |
26 | 27 | 28 | ## Install 29 | `npm i -g docker-server` 30 | 31 | Or 32 | 33 | `docker run -d -p 1717:1717 --restart=always --name=docker-server -v /var/run/docker.sock:/var/run/docker.sock -e "DS_TOKEN=my_secret_token" evgy/dockerserver` 34 | 35 | ## Background 36 | I needed to run a couple of containers on a remote machine and came to these conclusions: 37 | * Kubernetes is an overkill ! 38 | * docker-machine is also complicated ! 39 | * I just want to run a few containers on a remote machine. 40 | 41 | ## Approach 42 | Built a small REST server with NodeJS, using the `express` and `docker-cli-js` packages as a base. 43 | 44 | ### Design Principles 45 | * Keep the business logic **simple**! 46 | * It must be **stateless**! 47 | * **Docker** is (a) present. 48 | 49 | ### Current architecture 50 |
51 | How things work today
52 | The cluster diagram demonstrates a PUT request. 53 |
54 | 55 | 56 | #### Notes for the Cluster Mode: 57 | 58 | *\*0 - Connection between the load balancer and the docker-server.* 59 | 60 | *\*1 - Save the machine load to a [JSON](https://json.org "JSON") file in a shared folder (among all the machines).* 61 | 62 | 63 | **PUT method** 64 | 65 | *1 - Requests comes to any free (according to the load balancer) node to answer.* 66 | 67 | *2 - Get the most free (according to actual cpu-mem ratio) node (from redis or the shared storage, possibly on S3).* 68 | 69 | *3 - Resend the current request to that node (or process if it's the current node) and return the answer.* 70 | 71 | 72 | **For the rest of the methods** 73 | 74 | *Resent the current request to all the nodes and return the merged results.* 75 | 76 | ## Usage 77 | Install DockerServer on the machine that you want to run your containers. 78 | 79 | DockerServer can be run for a single session with: 80 | 81 | `$ docker-server` 82 | 83 | or as a service using [PM2](https://pm2.keymetrics.io/ "PM2"): 84 | 85 | `$ pm2 start /usr/lib/node_modules/docker-server/pm2.config.js` 86 | 87 | and if you want in addition to start it on startup just run: 88 | 89 | `$ pm2 startup` 90 | 91 | And of-course, as mentioned before, but using params, via docker itself: 92 | 93 | `$ docker run -d -p 1717:1717 --restart=always --name=docker-server -v /var/run/docker.sock:/var/run/docker.sock evgy/dockerserver docker-server --token my_secret_token` 94 | 95 | Or you can run in **HTTPS** mode: 96 | 97 | (Note that in this example I'm using [Let's Encrypt](https://letsencrypt.org/ "Let's Encrypt") and I'm using `readlink` because these files are symbolic links) 98 | 99 | `$ docker run -d -p 443:1717 --privileged --restart=always --name=docker-server -v /var/run/docker.sock:/var/run/docker.sock 100 | -v $(readlink -f /home/user/letsencrypt/config/live/your-domain.com/cert.pem):/certs/cert.pem:ro 101 | -v $(readlink -f /home/user/letsencrypt/config/live/your-domain.com/chain.pem):/certs/chain.pem:ro 102 | -v $(readlink -f /home/user/letsencrypt/config/live/your-domain.com/privkey.pem):/certs/privkey.pem:ro 103 | evgy/dockerserver docker-server --token my_secret_token --https` 104 | 105 | Note: The **--privileged** argument is only needed in order to use the 443 port, because all ports below 1024 are reserved by root. 106 | 107 | Moreover, you can run in a **Cluster mode** when you have a couple of machines to use: 108 | 109 | `$ docker run -d -p 1717:1717 --privileged --restart=always --name=docker-server -v /var/run/docker.sock:/var/run/docker.sock 110 | -v /some/shared/folder:/my/somewhy/custom/path evgy/dockerserver docker-server --token my_secret_token --cluster --folder /my/somewhy/custom/path` 111 | 112 | Or simply: 113 | 114 | `$ docker run -d -p 1717:1717 --privileged --restart=always --name=docker-server -v /var/run/docker.sock:/var/run/docker.sock 115 | -v /some/shared/folder:/tmp/docker-server evgy/dockerserver docker-server --token my_secret_token --cluster` 116 | 117 | Note: `/tmp/docker-server` is the default folder so you can easily and safely run it even without docker. 118 | 119 | Now, you can do "remote" docker operation using simple HTTP requests: 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 |
HTTP MethodEndpointDesc.Docker cmd
HEAD/:id*Pull an imagedocker pull :id
GET/List all the containersdocker ps -a
GET/:idShow the logs of a specific containerdocker logs :id
PUT/Run a containerdocker run...
POST/:idExecute a command in a containerdocker exec...
DELETE/:idDelete a container with such a name or an IDdocker rm -f :id
165 |
166 | 167 | ## Options 168 | ### Environment 169 | You can set the following environment variables to configure DockerServer: 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 |
Environment Var.Desc.Default
DS_PORTThe port on which the DockerServer is running1717
DS_TOKENThe secret token for the authorizationxxxxxx
188 | 189 | ### Parameters 190 | Also, you can start DockerServerwith these parameters: 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 260 | 261 | 262 | 263 | 264 | 267 | 268 | 269 | 270 | 271 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 |
ParamDesc.Default
--port [num]The port on which the DockerServer is running1717
--token [string]The secret token for the authorizationxxxxxx
--low_burst [num]Max number of requests per minute for Low burst.60
--mid_burst [num]Max number of requests per minute for Mid burst.180
--high_burst [num]Max number of requests per minute for High burst.300
--https 226 | Enable HTTPS mode.
227 | For this you must have the following files:
228 |     a. /certs/cert.pem
229 |     b. /certs/privkey.pem
230 |     c. /certs/chain.pem (optional, to support self-signed certs) 231 |
false
--clusterEnable Cluster mode.false
--refresh_rate [num]Milliseconds between writes to the shared memory30000
--cache_interval [num]Milliseconds to cache reads of all the machines3000
--folder [path]Shared folder between all docker-servers. (Used only in cluster mode)/tmp/docker-server
--redis [hostname] 257 | Shared redis server hostname.
258 | If set, the `--folder` param. becomes the key prefix (default: "DSC:") 259 |
null
--db_port [num] 265 | Shared server port number.
266 |
6379
--s3 [bucket-name] 272 | S3 bucket name, use with high refresh_rate.
273 | If set, the `--folder` param. becomes the Key. 274 |
null
--log_lovel [option]Log level [trace|debug|info|warn|error|fatal]info
--log_expiry [num]Time for a log to live in days.14
--log_max_size [num]Max log size in MB25
--helpShow he 
--versionShow current version  
303 | 304 | ### PUT Data 305 | When sending the PUT request, the following parameters are supported: 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 |
ParamDesc.DefaultDocker cmd
imageThe image for the run. (required)null 
nameThe name of the container.uuid4()--name
removeFlag to remove the container when it finishes --rmfalse--rm
detachFlag to detach the container -dfalse-d
portsMap of ports to publish.null-p
volumesMap of volumes to mount.null-v
dataCMD to run inside the container.null 
357 | 358 | ### POST Data 359 | When sending the POST request, the following parameters are supported: 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 |
ParamDesc.DefaultDocker cmd
ttyFlag to enable TTY modefalse-t
interactiveFlag to enable interactive modefalse-i
dataCMD to run inside the containernull 
387 | 388 | ## Examples 389 | NOTE: In the examples I assumed you're using the default port. 390 | 391 | 1. Get a list of all the containers: 392 | 393 | `$ curl -X GET http://1.2.3.4:1717/ -H 'Authorization: Basic base64EncodedToken'` 394 | 395 | 2. Run redis on the remote machine: 396 | 397 | `$ curl -X PUT http://1.2.3.4:1717/ -H 'Authorization: Basic base64EncodedToken' --data 'name=p-redis&image=redis&ports[1234]=6379'` 398 | 399 | And/or 400 | 401 | `$ curl -X PUT http://1.2.3.4:1717/ -H 'Authorization: Basic base64EncodedToken' --data 'name=v-redis&image=redis&volumes[/tmp/data]=/data'` 402 | 403 | 3. Remove our created container(s): 404 | 405 | `$ curl -X DELETE http://1.2.3.4:1717/p-redis -H 'Authorization: Basic base64EncodedToken'` 406 | 407 | And/or 408 | 409 | `$ curl -X DELETE http://1.2.3.4:1717/v-redis -H 'Authorization: Basic base64EncodedToken'` 410 | 411 | 412 | ## Changelog 413 | 414 | 1.9.0 - Added optional **[Redis](https://redis.io) support** for big clusters (recommended for clusters with more than 50 workers/hosts). 415 | 416 | 1.8.9 - Fixed CVE issue with docker-cli-js v2.5.2 ==> v2.5.3 & removed package-lock.json 417 | 418 | [See full changelog](https://github.com/freaker2k7/dockerserver/blob/master/CHANGELOG.md) 419 | 420 | ## Roadmap 421 | * Queue (for heavy loads) 422 | * Autoscaling 423 | 424 | ## License 425 | APACHE-2.0 (see the LICENSE files in the repository). 426 | 427 | ## Donate 428 | Running dockers is free, but **beer** is always welcome 429 | Beerpay 430 | 431 | or directly donate to our cause 432 | Donate using Liberapay 433 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | plugins: 3 | - jekyll-minifier 4 | - jekyll-feed 5 | - jekyll-seo-tag 6 | - jekyll-sitemap -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: 2 | - Visual Studio 2017 3 | - Ubuntu 4 | 5 | platform: 6 | - x86 7 | - x64 8 | 9 | environment: 10 | matrix: 11 | - nodejs_version: "6.0" 12 | - nodejs_version: "6.17" 13 | - nodejs_version: "7.10" 14 | - nodejs_version: "8.16" 15 | - nodejs_version: "9.11" 16 | - nodejs_version: "10.15" 17 | - nodejs_version: "11.15" 18 | - nodejs_version: "12.3" 19 | 20 | cache: 21 | - node_modules 22 | 23 | # Install scripts. (runs after repo cloning) 24 | install: 25 | # Get the latest stable version of Node.js or io.js 26 | - cmd: powershell Install-Product node $env:nodejs_version 27 | - sh: nvm install $nodejs_version 28 | # install modules 29 | - npm install 30 | 31 | # Post-install test scripts. 32 | test_script: 33 | # Output useful info for debugging. 34 | - node --version 35 | - npm --version 36 | # run tests 37 | - npm test 38 | 39 | # Don't actually build. 40 | build: off 41 | 42 | matrix: 43 | allow_failures: 44 | - nodejs_version: "12.3" 45 | image: Visual Studio 2017 -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./index.js'); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var throttle = require('express-rate-limit'); 3 | 4 | var args = require('./lib/args.js'); 5 | var log = require('./lib/logger.js').set(); 6 | var docker = require('./lib/docker.js'); 7 | var network = require('./lib/network.js'); 8 | 9 | var app = express(); 10 | 11 | var low_burst = throttle({ 'max': args.low_burst, 'windowMs': 60000 }); 12 | var mid_burst = throttle({ 'max': args.mid_burst, 'windowMs': 60000 }); 13 | var high_burst = throttle({ 'max': args.high_burst, 'windowMs': 60000 }); 14 | 15 | // Encode the token only once! 16 | var token = 'Basic ' + Buffer.from(args.token).toString('base64'); 17 | // Also, if you want to have a really long token, 18 | // see https://nodejs.org/api/cli.html#cli_max_http_header_size_size 19 | 20 | // The first middleware must be the token Auth. 21 | app.use(network.check(token)); 22 | 23 | // Body parsers 24 | app.use(express.json({limit: '10kb'})); 25 | app.use(express.urlencoded({extended: true, limit: '10kb'})); 26 | 27 | app.use(network.balance(args.port)); 28 | 29 | // Routes 30 | app.head('/:id*', low_burst, docker.pull); 31 | 32 | app.get('/', high_burst, docker.ps); 33 | 34 | app.get('/:id', high_burst, docker.logs); 35 | 36 | app.put('/', low_burst, docker.run); 37 | 38 | app.post('/:id', low_burst, docker.exec); 39 | 40 | app.delete('/:id', mid_burst, docker.rm); 41 | 42 | // Main listener 43 | var server = network.protocol(app, args.https); 44 | server.listen(args.port); 45 | log.info('Serving on ' + network.get_protocol() + '0.0.0.0:' + args.port); 46 | 47 | 48 | module.exports = Object.assign(docker, network, {'_app': app, '_server': server}); -------------------------------------------------------------------------------- /lib/args.js: -------------------------------------------------------------------------------- 1 | // This file is dedicated for all the cli arguments. 2 | 3 | var default_tmp_folder = '/tmp/docker-server'; 4 | 5 | var args = require('yargs') 6 | .option('port', {describe: 'DockerServer port.', type: 'number', default: parseInt(process.env.DS_PORT) || 1717}) 7 | .option('token', {describe: 'Secret tocket (recommended between 1024-4096 chars).', type: 'string', default: process.env.DS_TOKEN || 'xxxxxx'}) 8 | .option('low_burst', {describe: 'Max number of requests per minute for Low burst.', type: 'number', default: 60}) 9 | .option('mid_burst', {describe: 'Max number of requests per minute for Mid burst.', type: 'number', default: 180}) 10 | .option('high_burst', {describe: 'Max number of requests per minute for High burst.', type: 'number', default: 300}) 11 | .option('https', {describe: 'Flag to turn on the HTTPS mode.', type: 'boolean', default: false}) 12 | .option('cluster', {describe: 'Flag to turn on the Cluster mode.', type: 'boolean', default: false}) 13 | .option('refresh_rate', {describe: 'Milliseconds between writes to the shared memory.', type: 'number', default: 30000}) 14 | .option('cache_interval', {describe: 'Milliseconds to cache reads of all the machines.', type: 'number', default: 3000}) 15 | .option('folder', {describe: 'Shared folder between all docker-servers.', type: 'string', default: default_tmp_folder}) 16 | .option('redis', {describe: 'Shared redis server hostname.', type: 'string', default: null}) 17 | .option('db_port', {describe: 'Shared server port number.', type: 'number', default: 6379}) 18 | .option('s3', {describe: 'S3 bucket name, use with high refresh_rate.', type: 'string', default: null}) 19 | .option('log_level', {describe: 'Log level [trace|debug|info|warn|error|fatal]', type: 'string', default: 'info'}) 20 | .option('log_expiry', {describe: 'Time for a log to live in days.', type: 'number', default: 14}) 21 | .option('log_max_size', {describe: 'Max log size in MB.', type: 'number', default: 25}) 22 | .help('help', 'Show help.\nFor more documentation see https://github.com/freaker2k7/dockerserver') 23 | .argv; 24 | 25 | if (args.redis) { 26 | if (args.folder === default_tmp_folder) { 27 | // See https://github.com/freaker2k7/dockerserver#parameters 28 | // If --redis is set, the `--folder` parameter becomes the key prefix (default: "DSC:") 29 | args.folder = 'DSC:'; 30 | } 31 | args.expiry_seconds = parseInt(args.refresh_rate / 1000) * 2; 32 | } else { 33 | // Add a trailing slash to the folder path. 34 | args.folder = args.folder.replace(/\/+$/g, '') + '/'; 35 | } 36 | 37 | // Small fix for "winston" 38 | args.log_level = args.log_level.toLowerCase().replace(/trace/, 'silly'); 39 | 40 | 41 | module.exports = args; -------------------------------------------------------------------------------- /lib/docker.js: -------------------------------------------------------------------------------- 1 | var dockerCLI = require('docker-cli-js'); 2 | var uuid = require('uuid/v4'); 3 | 4 | var log = require('./logger.js').get(); 5 | 6 | var docker = new dockerCLI.Docker(); 7 | 8 | var handle_error = function(req, res) { 9 | return function(err) { 10 | res.status(500).json({ 11 | "code": 500, 12 | "error": err 13 | }); 14 | }; 15 | }; 16 | 17 | var check_id = function(req, res) { 18 | if (!req.params || !req.params.id) { 19 | res.status(400).json({ 20 | "code": 400, 21 | "error": 'No id or name supplied!' 22 | }); 23 | } 24 | }; 25 | 26 | 27 | module.exports = { 28 | 'pull': function(req, res) { 29 | log.debug('HEAD:' + req.params.id); 30 | check_id(req, res); 31 | 32 | return docker.command('pull ' + req.params.id).then(function(data) { 33 | log.debug('HEAD:' + req.params.id + '::' + JSON.stringify(data)); 34 | res.status(data.error && 400 || 200).end(); 35 | // HEAD requests don't have a response body: https://tools.ietf.org/html/rfc7231 36 | }).catch(handle_error(req, res)); 37 | }, 38 | 'ps': function(req, res) { 39 | return docker.command('ps -a').then(function(data) { 40 | log.debug('GET:' + JSON.stringify(data)); 41 | res.json(data.containerList || []); 42 | }).catch(handle_error(req, res)); 43 | }, 44 | 'logs': function(req, res) { 45 | check_id(req, res); 46 | 47 | return docker.command('logs ' + req.params.id).then(function(data) { 48 | log.debug('GET:' + req.params.id + '::' + JSON.stringify(data)); 49 | res.json(data); 50 | }).catch(handle_error(req, res)); 51 | }, 52 | 'run': function(req, res) { 53 | log.debug('PUT Request: ' + JSON.stringify(req.body)); 54 | if (!req.body || !req.body.image) { 55 | return res.status(400).json({ 56 | "code": 400, 57 | "error": 'No image supplied!' 58 | }); 59 | } 60 | 61 | var ports = ''; 62 | var volumes = ''; 63 | var data = ''; 64 | var detach = ''; 65 | var remove = ''; 66 | 67 | if (req.body.ports) { 68 | for (var k in req.body.ports) { 69 | ports += ' -p ' + k + ':' + req.body.ports[k]; 70 | } 71 | } 72 | if (req.body.volumes) { 73 | for (var k in req.body.volumes) { 74 | volumes += ' -v ' + k + ':' + req.body.volumes[k]; 75 | } 76 | } 77 | if (req.body.detach) { 78 | detach = ' --detach'; 79 | } 80 | if (req.body.remove) { 81 | remove = ' --rm'; 82 | } 83 | if (req.body.data) { 84 | data = ' ' + req.body.data; 85 | } 86 | 87 | return docker.command('run --name ' + (req.body.name || uuid()) + ports + volumes + remove + detach + ' ' + req.body.image + data).then(function(data) { 88 | log.debug('PUT:' + data); 89 | res.json(data); 90 | }).catch(handle_error(req, res)); 91 | }, 92 | 'exec': function(req, res) { 93 | log.debug('POST Request:' + JSON.stringify(req.body)); 94 | check_id(req, res); 95 | 96 | var data = ''; 97 | var interactive = ''; 98 | var tty = ''; 99 | 100 | if (req.body.interactive) { 101 | interactive = ' -i'; 102 | } 103 | if (req.body.tty) { 104 | tty = ' -t'; 105 | } 106 | if (req.body.data) { 107 | data = ' ' + req.body.data; 108 | } 109 | 110 | return docker.command('exec' + tty + interactive + ' ' + req.params.id + ' ' + data).then(function(data) { 111 | log.debug('POST:' + req.params.id + '::' + JSON.stringify(data)); 112 | res.json(data); 113 | }).catch(handle_error(req, res)); 114 | }, 115 | 'rm': function(req, res) { 116 | log.debug('DELETE Request: ' + JSON.stringify(req.body)); 117 | check_id(req, res); 118 | 119 | return docker.command('rm -f ' + req.params.id).then(function(data) { 120 | log.debug('DELETE (RM):' + req.params.id + '::' + JSON.stringify(data)); 121 | res.json(data); 122 | }).catch(handle_error(req, res)); 123 | } 124 | }; -------------------------------------------------------------------------------- /lib/file.js: -------------------------------------------------------------------------------- 1 | var args = require('./args.js'); 2 | var log = require('./logger.js').get(); 3 | 4 | var S3 = null; 5 | var fs = null; 6 | var redis = null; 7 | 8 | if (args.redis) { 9 | var REDIS = require('redis'); 10 | redis = REDIS.createClient(args.db_port, args.redis); 11 | redis.on('error', log.error); 12 | } else if (args.s3) { 13 | var AWS = require('aws-sdk'); 14 | S3 = new AWS.S3(); 15 | } else { 16 | fs = require('fs'); 17 | 18 | if (!fs.existsSync(args.folder)){ 19 | return fs.mkdirSync(args.folder, {recursive: true}); 20 | } 21 | } 22 | 23 | 24 | module.exports = { 25 | 'get': function(file, callback) { 26 | if (args.redis) { 27 | return redis.get(file, callback); 28 | } else if (args.s3) { 29 | return S3.getObject({Bucket: args.s3, Key: file}, function(err, data) { 30 | return callback(err, !err && Buffer.from(data.Body).toString('utf8')); 31 | }); 32 | } 33 | return fs.readFile(file, callback); 34 | }, 35 | 'set': function(file, data, callback) { 36 | if (args.redis) { 37 | return redis.set(file, data, function(err) { 38 | return !err && redis.expire(file, args.expiry_seconds); 39 | }); 40 | } else if (args.s3) { 41 | return S3.putObject({Bucket: args.s3, Key: file, Body: data}, function(err) { 42 | return callback(err); 43 | }); 44 | } 45 | return fs.writeFile(file, data, callback); 46 | }, 47 | 'del': function(file, callback) { 48 | if (args.redis) { 49 | return redis.expire(file, -1, callback); 50 | } else if (args.s3) { 51 | return S3.deleteObject({Bucket: args.s3, Key: file}, function(err) { 52 | return callback(err); 53 | }); 54 | } 55 | return fs.unlink(file, callback); 56 | }, 57 | 'list': function(dir, callback) { 58 | if (args.redis) { 59 | return redis.keys(dir + '*', function(err, data) { 60 | return callback(err, !err && data.map(function(v) { return v.replace(args.folder, ''); })); 61 | }); 62 | } else if (args.s3) { 63 | return S3.listObjectsV2({Bucket: args.s3, Prefix: dir}, function(err, data) { 64 | return callback(err, !err && data.Contents.map(function(v) { return v.Key; })); 65 | }); 66 | } 67 | return fs.readdir(dir, callback); 68 | } 69 | }; -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | require('winston-daily-rotate-file'); 3 | 4 | var args = require('./args.js'); 5 | 6 | module.exports = { 7 | 'set': function() { 8 | var transport; 9 | 10 | if (~['debug', 'silly'].indexOf(args.log_level)) { 11 | transport = new winston.transports.Console({ 12 | level: args.log_level 13 | }); 14 | } else { 15 | transport = new (winston.transports.DailyRotateFile)({ 16 | filename: '/tmp/docker-server-%DATE%.log', 17 | datePattern: 'YYYY-MM-DD-HH', 18 | zippedArchive: true, 19 | maxSize: args.log_max_size + 'm', 20 | maxFiles: args.log_expiry + 'd', 21 | level: args.log_level 22 | }); 23 | } 24 | 25 | return winston.createLogger({ 26 | name: 'ds-logger', 27 | transports: [transport] 28 | }); 29 | }, 30 | 'get': function() { 31 | return winston.loggers.get('ds-logger'); 32 | } 33 | }; -------------------------------------------------------------------------------- /lib/machine.js: -------------------------------------------------------------------------------- 1 | var ping = require('ping'); 2 | var si = require('systeminformation'); 3 | 4 | var args = require('./args.js'); 5 | var log = require('./logger.js').get(); 6 | var file = require('./file.js'); 7 | 8 | var last_read = null; 9 | var cached_machines = []; 10 | 11 | var localize = function(host) { 12 | return host.replace(/\d+\.\d+\.\d+\.\d+/, '0.0.0.0'); 13 | }; 14 | 15 | var readfile = function(filename, hostname) { 16 | return new Promise(function(resolve) { 17 | file.get(args.folder + filename, function(err, data) { 18 | log.debug('file:' + args.folder); 19 | log.debug('filename:' + filename); 20 | 21 | var obj = {'filename': filename}; 22 | data = JSON.parse(data); 23 | obj[data.load] = data; 24 | if (obj[data.load].host && obj[data.load].host.startsWith(hostname)) { 25 | obj[data.load].host = localize(obj[data.load].host); 26 | } 27 | resolve(obj); 28 | }); 29 | }); 30 | }; 31 | 32 | var get_all = function(IP) { 33 | return new Promise(function(resolve) { 34 | if (last_read && Date.now() < last_read + args.cache_interval) { 35 | return resolve(cached_machines); 36 | } 37 | 38 | return file.list(args.folder, function(err, files) { 39 | if (err) { 40 | log.error(err); 41 | return resolve({}); 42 | } 43 | 44 | var promises = []; 45 | var hostname = IP.split(':')[0]; 46 | for (var i = 0; i < files.length; ++i) { 47 | promises.push(readfile(files[i], hostname)); 48 | } 49 | 50 | return Promise.all(promises).then(function(objects) { 51 | var ret = {}; 52 | for (var i = 0; i < objects.length; ++i) { 53 | ret = Object.assign(ret, objects[i]); 54 | } 55 | 56 | last_read = Date.now(); 57 | cached_machines = ret; 58 | return resolve(ret); 59 | }); 60 | }); 61 | }); 62 | }; 63 | 64 | var get_valid_host = function(ips, IP) { 65 | return new Promise(function(resolve) { 66 | if (!ips || !ips.length) { 67 | return resolve(localize(IP)); 68 | } 69 | 70 | var ip = ips.pop(); 71 | 72 | return ping.sys.probe(ip.host.replace(/(^https?:\/\/)|(:\d+$)/g, ''), function(is_alive){ 73 | if (is_alive) { 74 | return resolve(ip.host); 75 | } 76 | file.del(args.folder + ip.filename, function(err) { 77 | if (err) { 78 | log.error('Probably someone else just removed it:' + err); 79 | } 80 | }); 81 | 82 | return get_valid_host(ips, IP).then(resolve); 83 | }); 84 | }); 85 | }; 86 | 87 | 88 | module.exports = { 89 | 'all': get_all, 90 | 'next': function (IP) { 91 | return new Promise(function(resolve) { 92 | return get_all(IP).then(function(ips) { 93 | return get_valid_host(Object.keys(ips).sort().reduce(function(m, k) { 94 | m[k] = ips[k]; 95 | return m; 96 | }, {}), IP).then(resolve); 97 | }); 98 | }); 99 | }, 100 | 'cpu': function(IP) { 101 | return si.mem(function(mem_data) { 102 | return si.currentLoad(function(cpu_data) { 103 | // (used_cpu / total_cpu) * (used_mem / total_mem) = load 104 | var load = cpu_data.currentload / 100 * mem_data.used / mem_data.total; 105 | return file.set(args.folder + 'ds-' + IP.replace(/[^0-9]/g, '') + '.json', JSON.stringify({ 106 | 'host': IP, 107 | 'load': load 108 | }), log.debug); 109 | }); 110 | }); 111 | } 112 | }; -------------------------------------------------------------------------------- /lib/network.js: -------------------------------------------------------------------------------- 1 | var args = require('./args.js'); 2 | var log = require('./logger.js').get(); 3 | 4 | var IP = null; 5 | var machine = null; 6 | var request = null; 7 | 8 | var get_protocol = function() { 9 | return 'http' + (args.https && 's' || '') + '://'; 10 | }; 11 | 12 | var get_req = function(req, host) { 13 | return new Promise(function(resolve) { 14 | return request({ 15 | method: req.method, 16 | url: host + req.originalUrl, 17 | headers: { 18 | 'docker-server': 'force', 19 | 'content-type': 'application/json', 20 | 'authorization': req.get('Authorization') 21 | }, 22 | body: req.method !== 'HEAD' && req.body && JSON.stringify(req.body) || undefined 23 | }, function (err, res, body) { 24 | log.debug('request:' + host + '::' + err + ' (' + (typeof body) + ') ' + JSON.stringify(body)); 25 | if (err) { 26 | log.error('err:' + host + '::' + err); 27 | return resolve({'body': [err], 'host': host}); 28 | } 29 | 30 | try { 31 | return resolve({'body': typeof body === 'string' && JSON.parse(body) || body, 'host': host}); 32 | } catch(e) { 33 | if (body.startsWith('error')) { 34 | body = {'error': body}; 35 | } else { 36 | log.error('request:' + host + '::' + err + ' (' + (typeof body) + ') ' + JSON.stringify(body)); 37 | } 38 | 39 | return resolve({'body': [body], 'host': host}); 40 | } 41 | }); 42 | }); 43 | }; 44 | 45 | var handle_cluster_request = function(req, res, next) { 46 | return function(ip) { 47 | if (ip === IP) { 48 | next(); 49 | } else { 50 | get_req(req, ip).then(function(ip_obj) { 51 | res.send(ip_obj.body || null); 52 | }); 53 | } 54 | }; 55 | }; 56 | 57 | var handle_cluster = function(req, res, next) { 58 | if (req.method === 'PUT') { 59 | machine.next(IP).then(handle_cluster_request(req, res, next)); 60 | } else if (~['GET', 'DELETE', 'HEAD', 'POST'].indexOf(req.method)) { 61 | machine.all(IP).then(function(ips) { 62 | var promises = []; 63 | for (var i in ips) { 64 | if (ips.hasOwnProperty(i)) { 65 | promises.push(get_req(req, ips[i].host)); 66 | } 67 | } 68 | 69 | return Promise.all(promises).then(function(responses) { 70 | var response = []; 71 | for (var i = 0; i < responses.length; ++i) { 72 | // Add the host to each result for more clearance for the client. 73 | if (responses[i].body.length) { 74 | for (var j in responses[i].body) { 75 | if (responses[i].body.hasOwnProperty(j)) { 76 | responses[i].body.host = responses[i].host; 77 | } 78 | } 79 | } else { 80 | responses[i].body.host = responses[i].host; 81 | } 82 | response = response.concat(responses[i].body); 83 | } 84 | 85 | res.send(req.method !== 'HEAD' && response || undefined); 86 | }); 87 | }); 88 | } else { 89 | res.sendStatus(400); 90 | } 91 | }; 92 | 93 | 94 | module.exports = { 95 | 'get_protocol': get_protocol, 96 | 'check': function(token) { 97 | return function (req, res, next) { 98 | if (req.get('Authorization') === token) { 99 | next(); 100 | } else { 101 | res.sendStatus(401); 102 | } 103 | }; 104 | }, 105 | 'protocol': function(app) { 106 | if (args.https) { 107 | const fs = require('fs'); 108 | 109 | if (fs.existsSync('/certs/privkey.pem') && fs.existsSync('/certs/cert.pem')) { 110 | try { 111 | return require('https').createServer({ 112 | key: fs.readFileSync('/certs/privkey.pem', 'utf8'), 113 | cert: fs.readFileSync('/certs/cert.pem', 'utf8'), 114 | ca: fs.existsSync('/certs/chain.pem') && fs.readFileSync('/certs/chain.pem', 'utf8') || undefined 115 | }, app); 116 | } catch (e) { 117 | log.error('Could not start secure server: ' + e); 118 | return require('http').createServer(app); 119 | } 120 | } 121 | } 122 | 123 | return require('http').createServer(app); 124 | }, 125 | 'balance': function(port) { 126 | if (args.cluster) { 127 | // This runs only once 128 | IP = get_protocol() + require('ip').address() + ':' + port; 129 | machine = require('./machine.js'); 130 | request = require('request'); 131 | 132 | machine.cpu(IP); 133 | setInterval(function() { machine.cpu(IP); }, args.refresh_rate); 134 | } 135 | 136 | return function (req, res, next) { 137 | // This runs every request 138 | if (args.cluster && req.get('docker-server') !== 'force') { 139 | handle_cluster(req, res, next); 140 | } else { 141 | next(); 142 | } 143 | }; 144 | } 145 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-server", 3 | "author": "Evgeny Kolyakov ", 4 | "description": "DockerServer - Super lightweight & simple RESTFul stateless server for running docker containers on a remote machine(s) in a secure way", 5 | "version": "1.9.0", 6 | "homepage": "https://dockerserver.io", 7 | "repository": "freaker2k7/dockerserver", 8 | "license": "Apache-2.0", 9 | "keywords": [ 10 | "docker", 11 | "server", 12 | "micro", 13 | "service", 14 | "rest", 15 | "restful", 16 | "http", 17 | "pm2", 18 | "api" 19 | ], 20 | "bin": { 21 | "docker-server": "./bin.js" 22 | }, 23 | "scripts": { 24 | "start": "node ./index.js", 25 | "test": "node ./test/basic.js", 26 | "prepublishOnly": "node ./scripts/dockerfile-version-verify.js" 27 | }, 28 | "dependencies": { 29 | "aws-sdk": "^2.502.0", 30 | "docker-cli-js": "2.5.3", 31 | "express": "^4.17.1", 32 | "express-rate-limit": "^5.0.0", 33 | "ip": "^1.1.5", 34 | "ping": "^0.2.2", 35 | "redis": "^2.8.0", 36 | "request": "^2.88.0", 37 | "systeminformation": "^5.6.4", 38 | "uuid": "^3.3.2", 39 | "winston": "^3.2.1", 40 | "winston-daily-rotate-file": "^3.10.0", 41 | "yargs": "^13.3.0" 42 | }, 43 | "engines": { 44 | "node": ">= 6.0" 45 | }, 46 | "files": [ 47 | "LICENSE", 48 | "README.md", 49 | "index.js", 50 | "bin.js", 51 | "pm2.config.js", 52 | "lib/" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /pm2.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "apps": [{ 3 | "id": "docker-server-" + ((Math.random() * 100000) % 1000), 4 | "name": "docker-server", 5 | "script": "index.js", 6 | "args": ["--trace-warnings"], 7 | "cwd": "/usr/lib/node_modules/docker-server", 8 | "watch": true, 9 | "error_file": "~/.pm2/logs/docker-server-error.log", 10 | "log_file": "~/.pm2/logs/docker-server.log", 11 | "instances": 1, 12 | "exec_mode": "fork", 13 | "env": { 14 | "NODE_ENV": "production" 15 | } 16 | }] 17 | }; -------------------------------------------------------------------------------- /scripts/dockerfile-version-verify.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | 4 | if (fs.existsSync('Dockerfile')) { 5 | var package = fs.readFileSync('package.json'); 6 | var data = fs.readFileSync('Dockerfile'); 7 | 8 | 9 | package = JSON.parse(package); 10 | data = data.toString().replace(/@\d+\.\d+\.\d+/, '@' + package.version); 11 | 12 | fs.writeFileSync('Dockerfile', data); 13 | } -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var dokcer_server = require('../index.js'); 2 | 3 | dokcer_server._server.close(); --------------------------------------------------------------------------------