├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitlint ├── .release-please-manifest.json ├── .yamllint.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── defaults └── main.yml ├── handlers └── main.yml ├── meta └── main.yml ├── release-please-config.json ├── renovate.json ├── requirements.yml ├── tasks ├── install.yml ├── main.yml └── uninstall.yml ├── templates ├── env.j2 └── unit.j2 ├── tests ├── ci.yml └── inventory └── vars ├── Debian.yml ├── RedHat.yml └── Rocky.yml /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Checklist 9 | 12 | 13 | - [ ] Keep pull requests small so they can be easily reviewed. 14 | - [ ] Update the documentation. 15 | - [ ] Link this PR to related issues. 16 | 17 | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | pull_request: {} 7 | push: 8 | branches: 9 | - main 10 | - master 11 | 12 | jobs: 13 | yamllint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: yamllint . 18 | 19 | ansible-syntax: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Prepare Ansible config for tests 24 | run: printf '[defaults]\nroles_path=../' > ansible.cfg 25 | - name: Ansible syntax check 26 | run: ansible-playbook tests/ci.yml -i tests/inventory --syntax-check 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | branches: 8 | - main 9 | - master 10 | 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | 15 | jobs: 16 | release: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: googleapis/release-please-action@v4 20 | with: 21 | token: ${{ secrets.RELEASE_PLEASE_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitlint: -------------------------------------------------------------------------------- 1 | [general] 2 | ignore=body-is-missing 3 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "2.10.1" 3 | } 4 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | rules: 4 | line-length: disable 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.10.1](https://github.com/mhutter/ansible-docker-systemd-service/compare/v2.10.0...v2.10.1) (2024-08-13) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * Fixed error when using external vars for defining container_name. ([#61](https://github.com/mhutter/ansible-docker-systemd-service/issues/61)) ([1a25a62](https://github.com/mhutter/ansible-docker-systemd-service/commit/1a25a6219e1671252dfd2e526e94183f63f17d76)) 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Hi and thanks for your interest in this project! Below are some guidelines on how to contribute to it. 4 | 5 | **Table of contents** 6 | 7 | * [Submitting Issues](#submitting-issues) 8 | * [Contributing A Patch](#contributing-a-patch) 9 | * [Running the tests](#running-the-tests) 10 | 11 | 12 | ## Submitting Issues 13 | 14 | When reporting an issue, make sure to include enough information so that others can reproduce the bug. This usually includes describing the steps you did to reproduce the issue, the expected outcome, and the actual outcome. 15 | 16 | 17 | ## Contributing A Patch 18 | 19 | Before you start working on a feature or fix, please submit an issue describing your proposed changes. 20 | 21 | Once your proposal is accepted, fork the repo and start developing and testing (see below) your changes. 22 | 23 | Ensure that your changes remain backwards compatible if possible. 24 | 25 | When opening the pull request, title it following [Conventional Commits] styling. 26 | 27 | 28 | 29 | ## Running the tests 30 | 31 | 1. Ensure you have [yamllint] and Ansible installed. 32 | 1. Run yamllint: 33 | 34 | yamllint . 35 | 36 | 1. Run an Ansible syntax check: 37 | 38 | ansible-playbook tests/ci.yml -i tests/inventory --syntax-check 39 | 40 | 41 | [Conventional commits]: https://www.conventionalcommits.org/ 42 | [yamllint]: https://yamllint.readthedocs.io/ 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Manuel Hutter 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker role `mhutter.docker-systemd-service` 2 | 3 | Generic role for creating systemd services to manage docker containers. 4 | 5 | ## Example 6 | 7 | Example of a Systemd unit for your app "myapp" that links to an already existing container "mysql": 8 | 9 | ```yaml 10 | - name: Start WebApp 11 | include_role: 12 | name: mhutter.docker-systemd-service 13 | vars: 14 | container_name: myapp 15 | container_image: myapp:latest 16 | container_links: ["mysql"] 17 | container_volumes: 18 | - "/data/uploads:/data/uploads" 19 | container_ports: 20 | - "3000:3000" 21 | container_hosts: 22 | - "host.docker.internal:host-gateway" 23 | container_env: 24 | MYSQL_ROOT_PASSWORD: "{{ mysql_root_pw }}" 25 | container_labels: 26 | - "traefik.enable=true" 27 | ``` 28 | 29 | This will create: 30 | 31 | - A file containing the env vars (either `/etc/sysconfig/myapp` or `/etc/default/myapp`). 32 | - A systemd unit which starts and stops the container. The unit will be called `_container.service` to avoid name clashes. 33 | 34 | ### Role variables 35 | 36 | - `container_name` (**required**) - name of the container 37 | 38 | #### Docker container specifics 39 | 40 | - `container_image` (**required**) - Docker image the service uses 41 | - `container_args` - arbitrary list of arguments to the `docker run` command as a string 42 | - `container_cmd` (default: _[]_) - optional list of commands to the container run command (the part after the image name) 43 | - `container_env` - key/value pairs of ENV vars that need to be present 44 | - `container_volumes` (default: _[]_) - List of `-v` arguments 45 | - `container_host_network` (default: _false_) - Whether the host network should be used 46 | - `container_ports` (default: _[]_) - List of `-p` arguments 47 | - `container_hosts` (default: _[]_) - List of `--add-host` arguments 48 | - `container_links` (default: _[]_) - List of `--link` arguments 49 | - `container_labels` (default: _[]_) - List of `-l` arguments 50 | - `container_docker_pull` (default: _yes_) - whether the docker image should be pulled 51 | - `container_docker_pull_force_source` (default: _yes_) - whether the docker image pull should be executed at every time (see [`docker_image.force_source`](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_image_module.html#parameter-force_source)) 52 | - `container_cap_add` (default _[]_) - List of capabilities to add 53 | - `container_cap_drop` (default _{}_) - List of capabilities to drop 54 | - `container_network` (default _""_) - [Network settings](https://docs.docker.com/engine/reference/run/#network-settings) 55 | - `container_user` (default _""_) - [User settings](https://docs.docker.com/engine/reference/run/#user) 56 | - `container_hostname` (default _""_) - Container host name: `--hostname` flag 57 | - `container_devices` (default _[]_) - List of devices to add 58 | - `container_privileged` (default _false_) - Whether the container should be privileged 59 | - `container_start_post` - Optional command to be run by systemd after the container has started 60 | 61 | #### Systemd service specifics 62 | 63 | - `service_enabled` (default: _yes_) - whether the service should be enabled 64 | - `service_masked` (default: _no_) - whether the service should be masked 65 | - `service_state` (default: _started_) - state the service should be in - set to 66 | `absent` to remove the service. 67 | - `service_restart` (default: _yes_) - whether the service should be restarted on changes 68 | - `service_name` (default: `_container`) - name of the systemd service 69 | - `service_systemd_options` (default: _[]_) - Extra options to include in systemd service file 70 | - `service_systemd_unit_options`: (default `{"After": "docker.service", "PartOf": "docker.service", "Requires": "docker.service"}`), key/value defining the content of the `[Unit]` service section. 71 | 72 | ## Installation 73 | 74 | This role requires the [docker python module](https://pypi.org/project/docker/). Install it with `pip3 install docker` or `apt install python3-docker` (or drop the `3` for python 2.x). 75 | 76 | Put this in your `requirements.yml`: 77 | 78 | ```yml 79 | - role: mhutter.docker-systemd-service 80 | ``` 81 | 82 | and run `ansible-galaxy install -r requirements.yml`. 83 | 84 | ## Gotchas 85 | 86 | - When the unit or env file is changed, systemd gets reloaded but existing containers are NOT restarted. 87 | - Make sure to quote values for `container_ports`, `container_hosts`, `container_volumes` and so on, especially if they contain colons (`:`). Otherwise YAML will interpret them as hashes/maps and ansible will throw up. 88 | 89 | ## About orchestrating Docker containers using systemd. 90 | 91 | The concept behind this is to define `systemd` units for every docker container. This has some benefits: 92 | 93 | - `systemd` is a well-known interface 94 | - all services are controllable via the same tool (`systemctl`) 95 | - all logs are accessible via the same tool (`journalctl`) 96 | - dependencies can be defined 97 | - startup behaviour can be defined 98 | - by correctly defining the unit (see below), we can ensure we always have a clean container. 99 | 100 | Here is an example `myapp_container.service` unit file (about what's produced by above code): 101 | 102 | [Unit] 103 | # define dependencies 104 | After=docker.service 105 | PartOf=docker.service 106 | Requires=docker.service 107 | 108 | [Service] 109 | # Load ENV vars from a file. Note that this env vars will only be 110 | # accessible in the context of the Exec* commands, and not within the 111 | # container itself. To make env-vars accessible within the Container, we use 112 | # the `--env-file` flag for the `docker run` command. 113 | EnvironmentFile=/etc/sysconfig/myapp 114 | 115 | # Even though we explicitly run the container using the `--rm` flag, there 116 | # may be leftover containers (eg. after a system-, docker- or app-crash). 117 | # Starting a container with an existing name will always fail. 118 | 119 | ExecStartPre=-/usr/bin/docker rm -f myapp 120 | 121 | # actually run the container. 122 | # `--name` to identify the container 123 | # `--rm` ensure the container is removed after stopping 124 | # `--env-file` make ENV vars accessible to app 125 | # `--link mysql` link to a container named `mysql`. The DB will then be 126 | # accesible at `mysql:3306` 127 | # `-v` mount `/data/uploads` into the container 128 | # `-p 3000:3000` expose port 3000 on the network 129 | ExecStart=/usr/bin/docker run --name myapp --rm --env-file /etc/sysconfig/myapp --link mysql -v /data/uploads:/data/uploads -p 3000:3000 registry.cust.net/myapp/myapp:latest 130 | # note that there is no `--restart` parameter. This is because restarting 131 | # is taken care of by `systemd`. 132 | 133 | # Stop command. 134 | ExecStop=/usr/bin/docker stop myapp 135 | 136 | # Ensure log messages are correctly tagged in the system log. 137 | SyslogIdentifier=myapp 138 | 139 | # Auto-Restart the container after a crash. 140 | Restart=always 141 | 142 | 143 | [Install] 144 | # make sure service is started after docker is up 145 | WantedBy=docker.service 146 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | container_name: "{{ name }}" 3 | container_docker_pull: true 4 | container_docker_pull_force_source: true 5 | container_labels: [] 6 | container_cmd: [] 7 | container_host_network: false 8 | container_network: "" 9 | container_user: "" 10 | container_hostname: "" 11 | container_links: [] 12 | container_ports: [] 13 | container_hosts: [] 14 | container_volumes: [] 15 | container_cap_add: [] 16 | container_cap_drop: [] 17 | container_devices: [] 18 | container_privileged: false 19 | container_args: "" 20 | docker_path: "/usr/bin/docker" 21 | service_name: "{{ container_name }}_container" 22 | service_systemd_options: [] 23 | service_systemd_unit_options: 24 | After: docker.service 25 | PartOf: docker.service 26 | Requires: docker.service 27 | service_enabled: true 28 | service_masked: false 29 | service_state: started 30 | service_restart: true 31 | template_env_path: "env.j2" 32 | template_unit_path: "unit.j2" 33 | -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "restart container" 3 | service: 4 | name: '{{ service_name }}.service' 5 | state: restarted 6 | when: service_restart and service_state != "stopped" and not enable_and_start.changed 7 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | role_name: docker-systemd-service 4 | description: Create Systemd services for docker containers. 5 | author: Manuel Hutter 6 | license: MIT 7 | 8 | min_ansible_version: 2.8 9 | 10 | platforms: 11 | - name: EL 12 | versions: 13 | - 7 14 | - name: Ubuntu 15 | versions: 16 | - xenial 17 | - yakkety 18 | - zesty 19 | - bionic 20 | - focal 21 | 22 | galaxy_tags: 23 | - system 24 | - systemd 25 | - server 26 | - docker 27 | 28 | dependencies: [] 29 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "changelog-path": "CHANGELOG.md", 5 | "release-type": "simple", 6 | "bump-minor-pre-major": false, 7 | "bump-patch-for-minor-pre-major": false, 8 | "draft": false, 9 | "prerelease": false 10 | } 11 | }, 12 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 13 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>mhutter/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | - name: community.docker 3 | version: 4.0.0 4 | -------------------------------------------------------------------------------- /tasks/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create ENV file for {{ service_name }}.service 3 | template: 4 | src: "{{ template_env_path }}" 5 | dest: "{{ sysconf_dir }}/{{ container_name }}" 6 | owner: root 7 | group: root 8 | mode: '0600' 9 | when: container_env is defined 10 | notify: restart container 11 | 12 | - name: Pull image {{ container_image }} 13 | community.docker.docker_image: 14 | name: '{{ container_image }}' 15 | force_source: '{{ container_docker_pull_force_source | bool }}' 16 | source: pull 17 | when: container_docker_pull 18 | notify: restart container 19 | 20 | - name: Create unit {{ service_name }}.service 21 | template: 22 | src: "{{ template_unit_path }}" 23 | dest: /etc/systemd/system/{{ service_name }}.service 24 | owner: root 25 | group: root 26 | mode: '0644' 27 | notify: restart container 28 | 29 | - name: Enable and start {{ container_name }} 30 | systemd: 31 | name: '{{ service_name }}.service' 32 | daemon_reload: true 33 | enabled: "{{ service_enabled }}" 34 | masked: "{{ service_masked }}" 35 | state: "{{ service_state }}" 36 | register: enable_and_start 37 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Load distro-specific vars 3 | include_vars: "{{ ansible_os_family }}.yml" 4 | tags: always 5 | 6 | - include_tasks: install.yml 7 | when: service_state != "absent" 8 | - include_tasks: uninstall.yml 9 | when: service_state == "absent" 10 | -------------------------------------------------------------------------------- /tasks/uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Disable and stop {{ container_name }} 3 | systemd: 4 | name: '{{ service_name }}.service' 5 | enabled: false 6 | state: stopped 7 | 8 | - name: Remove ENV file for {{ service_name }}.service 9 | file: 10 | path: "{{ sysconf_dir }}/{{ container_name }}" 11 | state: absent 12 | 13 | - name: Remove unit {{ service_name }}.service 14 | file: 15 | path: /etc/systemd/system/{{ service_name }}.service 16 | state: absent 17 | 18 | - name: Reload systemd units 19 | systemd: 20 | daemon_reload: true 21 | changed_when: false 22 | -------------------------------------------------------------------------------- /templates/env.j2: -------------------------------------------------------------------------------- 1 | {% for k,v in container_env|dictsort %} 2 | {{ k }}={{ v }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /templates/unit.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | {% macro params(name, vals) %} 3 | {% for v in vals %}{{ name }} {{ v }} {% endfor %} 4 | {% endmacro %} 5 | {% set service_systemd_options_keys = service_systemd_options | selectattr("key") | map(attribute="key") | list %} 6 | [Unit] 7 | {% for key, value in service_systemd_unit_options | dictsort %} 8 | {{ key }}={{ value }} 9 | {% endfor %} 10 | 11 | [Service] 12 | {% for item in service_systemd_options %} 13 | {{ item['key'] }}={{ item['value'] }} 14 | {% endfor %} 15 | {% if container_env is defined %} 16 | {% if not 'EnvironmentFile' in service_systemd_options_keys %} 17 | EnvironmentFile={{ sysconf_dir }}/{{ container_name }} 18 | {% endif %} 19 | {% endif %} 20 | {% if not 'ExecStartPre' in service_systemd_options_keys %} 21 | ExecStartPre=-{{ docker_path }} rm -f {{ container_name }} 22 | {% endif %} 23 | {% if not 'ExecStart' in service_systemd_options_keys %} 24 | ExecStart={{ docker_path }} run \ 25 | --name {{ container_name }} \ 26 | --rm \ 27 | {% if container_env is defined %}--env-file {{ sysconf_dir }}/{{ container_name }} {% endif %}\ 28 | {{ params('--volume', container_volumes) }}\ 29 | {% if container_host_network == true %}--network host {% else %}{{ params('--publish', container_ports) }}{% endif %}\ 30 | {% if container_network %}--network {{ container_network }}{% endif %} \ 31 | {% if container_user %}--user {{ container_user }}{% endif %} \ 32 | {% if container_hostname %}--hostname {{ container_hostname }}{% endif %} \ 33 | {{ params('--link', container_links) }}\ 34 | {{ params('--add-host', container_hosts) }}\ 35 | {{ params('--label', container_labels) }}\ 36 | {{ params('--cap-add', container_cap_add) }}\ 37 | {{ params('--cap-drop', container_cap_drop) }}\ 38 | {{ params('--device', container_devices) }}\ 39 | {% if container_privileged == true %}--privileged{% endif %}\ 40 | {{ container_args | trim }} \ 41 | {{ container_image }} {% if container_cmd is string %}{{ container_cmd | trim }}{% else %}{{ container_cmd | join(' ') | trim }}{% endif %} 42 | {% endif %} 43 | 44 | {% if not 'ExecStop' in service_systemd_options_keys %} 45 | ExecStop={{ docker_path }} stop {{ container_name }} 46 | {% endif %} 47 | {% if container_start_post is defined %} 48 | ExecStartPost=-{{ container_start_post }} 49 | {% endif %} 50 | 51 | {% if not 'SyslogIdentifier' in service_systemd_options_keys %} 52 | SyslogIdentifier={{ container_name }} 53 | {% endif %} 54 | {% if not 'Restart' in service_systemd_options_keys %} 55 | Restart=always 56 | {% endif %} 57 | {% if not 'RestartSec' in service_systemd_options_keys %} 58 | RestartSec=10s 59 | {% endif %} 60 | 61 | [Install] 62 | WantedBy=docker.service 63 | -------------------------------------------------------------------------------- /tests/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | remote_user: root 4 | tasks: 5 | - include_role: 6 | name: ansible-docker-systemd-service 7 | vars: 8 | name: myapp 9 | container_image: myapp:latest 10 | container_links: 11 | - 'mysql' 12 | container_volumes: 13 | - '/data/uploads:/data/uploads' 14 | container_ports: 15 | - '3000:3000' 16 | container_hosts: 17 | - 'host.docker.internal:host-gateway' 18 | container_env: 19 | MYSQL_ROOT_PASSWORD: "very sekr1t" 20 | -------------------------------------------------------------------------------- /tests/inventory: -------------------------------------------------------------------------------- 1 | localhost -------------------------------------------------------------------------------- /vars/Debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sysconf_dir: /etc/default 3 | -------------------------------------------------------------------------------- /vars/RedHat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sysconf_dir: /etc/sysconfig 3 | -------------------------------------------------------------------------------- /vars/Rocky.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sysconf_dir: /etc/sysconfig 3 | --------------------------------------------------------------------------------