├── tests ├── helloworld.bin ├── Dockerfile └── README.md ├── d-ealer.conf ├── Dockerfile ├── requirements.txt ├── LICENSE ├── .gitignore ├── README.md └── d-ealer.py /tests/helloworld.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eon01/d-ealer/HEAD/tests/helloworld.bin -------------------------------------------------------------------------------- /d-ealer.conf: -------------------------------------------------------------------------------- 1 | [logging] 2 | logger_level = logging.INFO 3 | handler_level = logging.INFO 4 | log_format = %(asctime)s - %(name)s - %(levelname)s - %(message)s 5 | log_file = d-ealer.log 6 | 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | ADD ./d-ealer.py / 3 | ADD ./requirements.txt / 4 | ADD ./d-ealer.conf / 5 | RUN pip install -r requirements.txt 6 | WORKDIR / 7 | ENTRYPOINT python d-ealer.py 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.0 2 | backports.ssl-match-hostname==3.5.0.1 3 | configparser==3.5.0 4 | docker==2.0.2 5 | docker-pycreds==0.2.1 6 | ipaddress==1.0.18 7 | logging==0.4.9.6 8 | packaging==16.8 9 | pyparsing==2.1.10 10 | requests==2.13.0 11 | six==1.10.0 12 | websocket-client==0.40.0 13 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.3 2 | 3 | MAINTAINER tomwillfixit 4 | 5 | RUN apk update && apk add curl && rm -rf /var/cache/apk/* 6 | 7 | COPY helloworld.bin / 8 | 9 | EXPOSE 80 10 | 11 | HEALTHCHECK --interval=5s --timeout=3s --retries=3 \ 12 | CMD curl -f http://localhost:80 || exit 1 13 | 14 | ENTRYPOINT ["/helloworld.bin"] 15 | 16 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing D-ealer With A Healthcheck 2 | 3 | This test uses this [repository](https://github.com/tomwillfixit/healthcheck) Dockerfile in order to create a container having a healthcheck. 4 | 5 | 6 | To test it, build it: 7 | 8 | ``` 9 | docker build -t helloworld:healthcheck . 10 | ``` 11 | 12 | Run it: 13 | 14 | ``` 15 | docker run -l com.dealer.activate=1 -d -P helloworld:healthcheck 16 | ``` 17 | 18 | Make sure the .bin file is executable ( ``` chmod +x ``` otherwise). 19 | 20 | ``` 21 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 22 | a79307e481b5 helloworld:healthcheck "/helloworld.bin" 47 seconds ago Up 46 seconds (healthy) 0.0.0.0:32769->80/tcp dazzling_brattain 23 | ``` 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Aymen EL Amri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d-ealer 2 | A Docker Healer - Auto Restarting Unhealthy Containers 3 | 4 | # How To 5 | 6 | d-ealer will check the health of all of your containers, if one of them is unhealty, it will restart it. 7 | 8 | d-ealer will only restart containers having the label ``` com.dealer.activate ``` set to ``` 1 ```. 9 | 10 | This is an example: 11 | 12 | ``` 13 | docker run -l com.dealer.activate=1 -d -P helloworld:healthcheck 14 | ``` 15 | 16 | 17 | # Configuration 18 | 19 | Please adapt the configuration to your need (**d-ealer.conf**): 20 | 21 | ``` 22 | [logging] 23 | logger_level = logging.INFO 24 | handler_level = logging.INFO 25 | log_format = %(asctime)s - %(name)s - %(levelname)s - %(message)s 26 | log_file = d-ealer.log 27 | ``` 28 | 29 | # Logging 30 | 31 | You can find logs in ``` d-ealer.log ``` file. 32 | You can mount your log file to your host, if you are using Docker. 33 | 34 | # Running d-ealer Using Docker 35 | 36 | ``` 37 | docker run -it --name d-ealer -v /var/run/docker.sock:/var/run/docker.sock -d eon01/d-ealer 38 | ``` 39 | 40 | # Common Problems: 41 | 42 | ## 'module' object has on attribute 'connection' 43 | 44 | This is an issue in docker-py, for me it was solved by: 45 | 46 | ``` 47 | pip install urllib3==1.14 48 | export PYTHONPATH=/usr/local/lib/python2.7/dist-packages:/usr/lib/python2.7/dist-packages 49 | ``` 50 | 51 | On docker-py github issues, other people solved this by: 52 | 53 | ``` 54 | apt-get install python-openssl 55 | ``` 56 | 57 | or 58 | 59 | ``` 60 | sudo pip install --upgrade pip 61 | ``` 62 | 63 | 64 | # ToDo 65 | - Allow users to choose the timeout and retries number ..etc 66 | 67 | # Links 68 | 69 | - Docker Hub: https://hub.docker.com/r/eon01/d-ealer/ 70 | - GitHub : https://github.com/eon01/d-ealer 71 | -------------------------------------------------------------------------------- /d-ealer.py: -------------------------------------------------------------------------------- 1 | import docker 2 | import logging 3 | import traceback 4 | import ConfigParser 5 | import time 6 | 7 | # start configuration parser 8 | parser = ConfigParser.ConfigParser() 9 | parser.read("d-ealer.conf") 10 | 11 | # reading variables 12 | logger_level = parser.get('logging', 'logger_level', raw = True) 13 | handler_level = parser.get('logging', 'handler_level', raw = True) 14 | log_format = parser.get('logging', 'log_format', raw = True) 15 | log_file = parser.get('logging', 'log_file') 16 | 17 | # set logger logging level 18 | logger = logging.getLogger(__name__) 19 | logger.setLevel(eval(logger_level)) 20 | 21 | # set handler logging level 22 | handler = logging.FileHandler(log_file) 23 | handler.setLevel(eval(handler_level)) 24 | 25 | # create a logging format 26 | formatter = logging.Formatter(log_format) 27 | handler.setFormatter(formatter) 28 | 29 | # add the handlers to the logger 30 | logger.addHandler(handler) 31 | 32 | def dealer(containers, s): 33 | label_check = 0 34 | for container in containers: 35 | try: 36 | label_check = container.attrs['Config']['Labels']['com.dealer.activate'] 37 | except KeyError: 38 | pass 39 | if label_check == 1: 40 | try: 41 | healthcheck = container.attrs['State']['Health']['Status'] 42 | 43 | if healthcheck == "unhealthy": 44 | container.restart() 45 | else: 46 | print healthcheck 47 | pass 48 | except KeyError: 49 | logger.error('healthcheck disabled on container %s. Passing' % container.name) 50 | pass 51 | except: 52 | logger.error('Unkown problem on container %s : %s. Please check Docker logs. Passing' % traceback.format_exc(),container.name) 53 | pass 54 | time.sleep(s) 55 | return 56 | 57 | if __name__ == "__main__": 58 | # start docker cli 59 | client = docker.from_env() 60 | containers = client.containers.list() 61 | 62 | s = 1 63 | 64 | # check healthchecks and restart 65 | while True: 66 | dealer(containers, s) 67 | 68 | 69 | --------------------------------------------------------------------------------