├── .dockerignore ├── .github ├── DISCUSSION_TEMPLATE │ └── Help.yml └── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── .gitignore ├── Dockerfile ├── Dockerfile.afp ├── Dockerfile.smb ├── LICENSE ├── README.md ├── entrypoint.sh ├── healthcheck.sh ├── password.txt ├── s6 ├── .s6-svscan │ ├── crash │ └── finish ├── avahi │ └── run ├── dbus │ └── run ├── nmbd │ └── run └── smbd │ └── run ├── supervisord.afp.conf ├── timemachine-compose-afp.yml ├── timemachine-compose-smb-arm7l.yml ├── timemachine-compose-smb.yml ├── timemachine-compose.yml ├── timemachine-k3s.yaml └── util └── prune_hub_tags.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/Help.yml: -------------------------------------------------------------------------------- 1 | title: "[Help]: " 2 | labels: ["Help"] 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | ## Read Me First! 8 | 9 | Thanks for using my image! Hopefully it's helpful to you. If you're looking for help with this container image, related to the packaging or function of this Docker image, you're in the right place. If you are certain you have found a bug in the packaging, configuration, or general function of this image, go ahead and create a [Bug Report Issue](https://github.com/mbentley/docker-timemachine/issues/new/choose). In order to better help you, it would be great if you can provide as much information as you can below. 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Describe Your Issue or Question 14 | description: | 15 | Please provide a clear and concise description of what you're trying to achieve and what the problem is that you're facing. 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: expected 20 | attributes: 21 | label: Expected Behavior 22 | description: | 23 | A clear and concise description of what you expect to happen. 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: reproduce 28 | attributes: 29 | label: Steps to Reproduce 30 | description: | 31 | Steps to reproduce the unexpected behavior with as much detail as possible. 32 | placeholder: | 33 | 1. 34 | 2. 35 | 3. 36 | 4. 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: docker-run 41 | attributes: 42 | label: How You're Launching the Container 43 | description: | 44 | Include your complete `docker run` or compose file to make analysis easier. 45 | render: plain 46 | validations: 47 | required: true 48 | - type: textarea 49 | id: logs 50 | attributes: 51 | label: Container Logs 52 | description: | 53 | Collect logs by using something similar to `docker logs timemachine >& output.log` if needed and attach them or copy out the relevant portions of the error. When in doubt, do both. I can't help without logs! 54 | placeholder: | 55 | logs 56 | go 57 | here 58 | render: plain 59 | validations: 60 | required: true 61 | - type: textarea 62 | id: maclogs 63 | attributes: 64 | label: Time Machine client logs 65 | description: | 66 | If you can run the container but backups fail, collect logs from the **client** by using something similar to `printf '\e[3J' && log show --predicate 'subsystem == "com.apple.TimeMachine"' --info --last 6h | grep -F 'eMac' | grep -Fv 'etat' | awk -F']' '{print substr($0,1,19), $NF}'`, adjusting the time from `6h` to something resonable to catch the error at least once and attach them. I can't help without logs! 67 | placeholder: | 68 | logs 69 | go 70 | here 71 | render: plain 72 | validations: 73 | required: false 74 | - type: textarea 75 | id: t2m2logs 76 | attributes: 77 | label: Time Machine client diagnostics logs using The Time Machine Mechanic 78 | description: | 79 | If you can run the container but backups fail, another tool that can help with diagnosing the issue is [The Time Machine Mechanic](https://eclecticlight.co/consolation-t2m2-and-log-utilities/) which can collect logs from your Mac (the **client**) and attempt to pinpoint the root cause of the backups failing. 80 | placeholder: | 81 | logs 82 | go 83 | here 84 | render: plain 85 | validations: 86 | required: false 87 | - type: textarea 88 | id: host-info 89 | attributes: 90 | label: Additional host information 91 | description: | 92 | Provide additional information about your host (docker engine info, host OS). This is optional but can be helpful if your backups are failing. 93 | placeholder: | 94 | # docker info 95 | 96 | 97 | # cat /etc/os-release 98 | 99 | render: plain 100 | validations: 101 | required: false 102 | - type: textarea 103 | id: additional 104 | attributes: 105 | label: Additional Context 106 | description: | 107 | Add any other context about the issue here. 108 | validations: 109 | required: false 110 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug with this Docker image. 3 | title: '[Bug]: ' 4 | assignees: 5 | - mbentley 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## Read Me First! 11 | 12 | Thanks for using my image! Hopefully it's helpful to you. You are about to report a bug you found related to the packaging or function of this Docker image, not the software itself. If you have a problem with the software that is not related to this Docker image, check out the [Samba mailing lists](https://www.samba.org/samba/archives.html). If you're not sure if it is a problem with the Docker image or the software, file an issue here and I can assist in determining where the responsibility lies. Apple has changed how Time Machine functions behind the scenes a number of times and it requires samba configuration changes to resolve but a lack of documentation makes it difficult to track down. 13 | - type: textarea 14 | id: description 15 | attributes: 16 | label: Describe the Bug 17 | description: | 18 | Please provide a clear and concise description of what the bug is. 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: expected 23 | attributes: 24 | label: Expected Behavior 25 | description: | 26 | A clear and concise description of what you expected to happen. 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: reproduce 31 | attributes: 32 | label: Steps to Reproduce 33 | description: | 34 | Steps to reproduce the behavior with as much detail as possile. 35 | placeholder: | 36 | 1. 37 | 2. 38 | 3. 39 | 4. 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: docker-run 44 | attributes: 45 | label: How You're Launching the Container 46 | description: | 47 | Include your complete `docker run` or compose file to make analysis easier. 48 | render: plain 49 | validations: 50 | required: true 51 | - type: textarea 52 | id: logs 53 | attributes: 54 | label: Container Logs 55 | description: | 56 | Collect logs by using something similar to `docker logs timemachine >& output.log` if needed and attach them or copy out the relevant portions of the error. When in doubt, do both. I can't help without logs! 57 | placeholder: | 58 | logs 59 | go 60 | here 61 | render: plain 62 | validations: 63 | required: true 64 | - type: textarea 65 | id: maclogs 66 | attributes: 67 | label: Time Machine client logs 68 | description: | 69 | If you can run the container but backups fail, collect logs from the **client** by using something similar to `printf '\e[3J' && log show --predicate 'subsystem == "com.apple.TimeMachine"' --info --last 6h | grep -F 'eMac' | grep -Fv 'etat' | awk -F']' '{print substr($0,1,19), $NF}'`, adjusting the time from `6h` to something resonable to catch the error at least once and attach them. I can't help without logs! 70 | placeholder: | 71 | logs 72 | go 73 | here 74 | render: plain 75 | validations: 76 | required: false 77 | - type: textarea 78 | id: t2m2logs 79 | attributes: 80 | label: Time Machine client diagnostics logs using The Time Machine Mechanic 81 | description: | 82 | If you can run the container but backups fail, another tool that can help with diagnosing the issue is [The Time Machine Mechanic](https://eclecticlight.co/consolation-t2m2-and-log-utilities/) which can collect logs from your Mac (the **client**) and attempt to pinpoint the root cause of the backups failing. 83 | placeholder: | 84 | logs 85 | go 86 | here 87 | render: plain 88 | validations: 89 | required: false 90 | - type: textarea 91 | id: host-info 92 | attributes: 93 | label: Additional host information 94 | description: | 95 | Provide additional information about your host (docker engine info, host OS). This is optional but can be helpful if your backups are failing. 96 | placeholder: | 97 | # docker info 98 | 99 | 100 | # cat /etc/os-release 101 | 102 | render: plain 103 | validations: 104 | required: false 105 | - type: textarea 106 | id: additional 107 | attributes: 108 | label: Additional Context 109 | description: | 110 | Add any other context about the problem here. 111 | validations: 112 | required: false 113 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: mbentley/timemachine Discussions 4 | url: https://github.com/mbentley/docker-timemachine/discussions/categories/help 5 | about: For help using this container image, start a discussion. Keep issues related to bugs & features, please. 6 | - name: Samba mailing lists 7 | url: https://www.samba.org/samba/archives.html 8 | about: For questions or issues related to Samba, use the 'samba' mailing list. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: For feature requests related to this Docker image only. 3 | title: '[Feature]: ' 4 | assignees: 5 | - mbentley 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## Read Me First! 11 | 12 | Thanks for using my image! Hopefully it's helpful to you. You are about to report a bug you found related to the packaging or function of this Docker image, not the software itself. If you have a problem with the software that is not related to this Docker image, check out the [Samba mailing lists](https://www.samba.org/samba/archives.html). If you're not sure if it is a problem with the Docker image or the software, file an issue here and I can assist in determining where the responsibility lies. Apple has changed how Time Machine functions behind the scenes a number of times and it requires samba configuration changes to resolve but a lack of documentation makes it difficult to track down. 13 | - type: textarea 14 | id: feature-problem 15 | attributes: 16 | label: What problem are you looking to solve? 17 | description: | 18 | A clear and concise description of what the problem or challenge is that you'd like to be addressed. 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: solution 23 | attributes: 24 | label: Describe the solution that you have in mind 25 | description: | 26 | A clear and concise description of what you want to happen or a proposed method to solving the problem. If you have expectations around how the problem would be solved from a user experience perspective, add that information here. 27 | validations: 28 | required: false 29 | - type: textarea 30 | id: additional 31 | attributes: 32 | label: Additional Context 33 | description: | 34 | Add any other context about the feature here. 35 | validations: 36 | required: false 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | multi-user/ 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | Dockerfile.smb -------------------------------------------------------------------------------- /Dockerfile.afp: -------------------------------------------------------------------------------- 1 | # rebased/repackaged base image that only updates existing packages 2 | FROM mbentley/debian:jessie 3 | LABEL maintainer="Matt Bentley " 4 | 5 | ARG DEBIAN_FRONTEND=noninteractive 6 | ENV NETATALK_VERSION="3.1.19" 7 | 8 | RUN apt-get update &&\ 9 | apt-get install -y avahi-daemon supervisor &&\ 10 | apt-get install -y --no-install-recommends build-essential bzip2 ca-certificates curl libavahi-common-dev libavahi-client-dev libcrack2-dev libevent-2.0-5 libevent-dev libssl-dev libgcrypt11-dev libkrb5-dev libpam0g-dev libwrap0-dev libdb-dev libmysqlclient-dev libacl1-dev libldap2-dev tracker &&\ 11 | mkdir -p /tmp/netatalk-${NETATALK_VERSION} &&\ 12 | cd /tmp/netatalk-${NETATALK_VERSION} &&\ 13 | curl -L "https://github.com/Netatalk/netatalk/releases/download/netatalk-$(echo "${NETATALK_VERSION}" | sed 's/\./-/g')/netatalk-${NETATALK_VERSION}.tar.bz2" -o "netatalk-${NETATALK_VERSION}.tar.bz2" &&\ 14 | tar xf "netatalk-${NETATALK_VERSION}.tar.bz2" --directory "/tmp/netatalk-${NETATALK_VERSION}" --strip-components=1 &&\ 15 | ./configure \ 16 | --with-init-style=debian-sysv \ 17 | --with-cracklib \ 18 | --with-acls \ 19 | --enable-fhs \ 20 | --enable-krbV-uam \ 21 | --with-pam-confdir=/etc/pam.d \ 22 | --with-dbus-sysconf-dir=/etc/dbus-1/system.d \ 23 | --with-tracker-pkgconfig-version=0.16 &&\ 24 | make &&\ 25 | make install &&\ 26 | apt-get purge -y build-essential curl libavahi-common-dev libavahi-client-dev libcrack2-dev libevent-dev libssl-dev libgcrypt11-dev libkrb5-dev libpam0g-dev libwrap0-dev libdb-dev libmysqlclient-dev libacl1-dev libldap2-dev tracker &&\ 27 | apt-get install -y libavahi-client3 libcrack2 libldap-2.4-2 libmysqlclient18 libwrap0 &&\ 28 | apt-get -y autoremove &&\ 29 | rm -rf /var/lib/apt/lists/* &&\ 30 | cd &&\ 31 | mkdir /var/run/dbus &&\ 32 | rm -rf /tmp/* 33 | 34 | COPY supervisord.afp.conf /etc/supervisord.conf 35 | COPY entrypoint.sh healthcheck.sh / 36 | 37 | EXPOSE 548 38 | VOLUME ["/opt/timemachine","/var/netatalk","/var/log/supervisor"] 39 | HEALTHCHECK --retries=3 --interval=15s --timeout=5s CMD /healthcheck.sh 40 | ENTRYPOINT ["/entrypoint.sh"] 41 | CMD ["/usr/bin/supervisord","-c","/etc/supervisord.conf"] 42 | -------------------------------------------------------------------------------- /Dockerfile.smb: -------------------------------------------------------------------------------- 1 | # rebased/repackaged base image that only updates existing packages 2 | FROM mbentley/alpine:latest 3 | LABEL maintainer="Matt Bentley " 4 | 5 | # install samba and s6; create supporting directories 6 | RUN apk add --no-cache attr avahi avahi-compat-libdns_sd avahi-tools dbus samba-common-tools s6 samba-server &&\ 7 | touch /etc/samba/lmhosts &&\ 8 | rm /etc/samba/smb.conf &&\ 9 | rm /etc/avahi/services/*.service &&\ 10 | mkdir /var/run/dbus 11 | 12 | # copy in necessary supporting config files 13 | COPY s6 /etc/s6 14 | COPY entrypoint.sh / 15 | 16 | #VOLUME ["/var/lib/samba","/var/cache/samba","/run/samba"] 17 | 18 | ENTRYPOINT ["/entrypoint.sh"] 19 | CMD ["s6-svscan","/etc/s6"] 20 | -------------------------------------------------------------------------------- /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 | Copyright 2016 Matthew Bentley 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mbentley/timemachine 2 | 3 | docker image to run Samba or AFP (netatalk) to provide a compatible Time Machine for MacOS 4 | 5 | ## Image Tags 6 | 7 | ### Multi-arch Tags 8 | 9 | The following tags have multi-arch support for `amd64`, `armv7l`, and `arm64` and will automatically pull the correct tag based on your system's architecture: 10 | 11 | `latest`, `smb` 12 | 13 | __Note__: The `afp` tag has been deprecated in terms of new feature updates and is only available for `amd64`. 14 | 15 | ### Date Specific Tags 16 | 17 | The `smb` tags also have unique manifests that are generated daily. These are in the format `smb-YYYYMMDD` (e.g. - `smb-20210730`) and can be viewed on [Docker Hub](https://hub.docker.com/repository/docker/mbentley/timemachine/tags?page=1&ordering=last_updated&name=smb-20). Each one of these tags will be generated daily and is essentially a point in time snapshot of the `smb` tag's manifest that you can pin to if you wish. Please note that these tags will remain available on Docker Hub for __6 months__ and will not receive security fixes. You will need to update to newer tags as they are published in order to get updated images. If you do not care about specific image digests to pin to, I would suggest just using the `smb` tag. 18 | 19 | ### Explicit Architecture Tags 20 | 21 | These tags will explicitly pull the image for the listed architecture and are bit for bit identical to the multi-arch tags images. 22 | 23 | #### [`amd64`](https://hub.docker.com/repository/docker/mbentley/timemachine/tags?page=1&ordering=last_updated&name=amd64) 24 | 25 | * `latest-smb-amd64`, `smb-amd64` - SMB image based off of alpine:latest 26 | * `afp`, `afp-amd64` - AFP image based off of debian:jessie 27 | * Deprecated but still available; not being regularly built - **This image may have unpatched security vulnerabilities** 28 | 29 | #### [`armv7l`](https://hub.docker.com/repository/docker/mbentley/timemachine/tags?page=1&ordering=last_updated&name=armv7l) 30 | 31 | * `latest-smb-armv7l`, `smb-armv7l` - SMB image based off of alpine:latest for the `armv7l` architecture 32 | 33 | #### [`arm64`](https://hub.docker.com/repository/docker/mbentley/timemachine/tags?page=1&ordering=last_updated&name=arm64) 34 | 35 | * `latest-smb-arm64`, `smb-arm64` - SMB image based off of alpine:latest for the `arm64` architecture 36 | 37 | __Warning__: I would strongly suggest migrating to the SMB image as AFP is being deprecated by Apple and I've found it to be much more stable. I do not plan on adding any new features to the AFP based config and I [switched the default image in the `latest` tag to the SMB variant on October 15, 2020](https://github.com/mbentley/docker-timemachine/issues/38). 38 | 39 | To pull this image: 40 | `docker pull mbentley/timemachine:smb` 41 | 42 | ## Example usage for SMB 43 | 44 | __Note__: If you update the `TM_USERNAME` value, that will change the path for the persistent volume. See [persistent data path](#persistent-data-path) for more details. 45 | 46 | Example usage with `--net=host` to allow Avahi discovery; with commonly used environment variables set to their default values: 47 | 48 | ``` 49 | docker run -d --restart=always \ 50 | --name timemachine \ 51 | --net=host \ 52 | -e TM_USERNAME="timemachine" \ 53 | -e TM_GROUPNAME="timemachine" \ 54 | -e PASSWORD="timemachine" \ 55 | -e TM_UID="1000" \ 56 | -e TM_GID="1000" \ 57 | -e SET_PERMISSIONS="false" \ 58 | -e VOLUME_SIZE_LIMIT="0" \ 59 | -v /path/on/host/to/backup/to/for/timemachine:/opt/timemachine \ 60 | --tmpfs /run/samba \ 61 | mbentley/timemachine:smb 62 | ``` 63 | 64 | Example usage with exposing ports _without_ Avahi discovery; with commonly used environment variables set to their default values: 65 | 66 | ``` 67 | docker run -d --restart=always \ 68 | --name timemachine \ 69 | --hostname timemachine \ 70 | -p 137:137/udp \ 71 | -p 138:138/udp \ 72 | -p 139:139 \ 73 | -p 445:445 \ 74 | -e TM_USERNAME="timemachine" \ 75 | -e TM_GROUPNAME="timemachine" \ 76 | -e PASSWORD="timemachine" \ 77 | -e TM_UID="1000" \ 78 | -e TM_GID="1000" \ 79 | -e SET_PERMISSIONS="false" \ 80 | -e VOLUME_SIZE_LIMIT="0" \ 81 | -v /path/on/host/to/backup/to/for/timemachine:/opt/timemachine \ 82 | --tmpfs /run/samba \ 83 | mbentley/timemachine:smb 84 | ``` 85 | 86 | ### Kubernetes support 87 | 88 | The images are also compatible with Kubernetes. 89 | Checkout [timemachine-k3s.yaml](https://github.com/mbentley/docker-timemachine/blob/master/timemachine-k3s.yaml) as an example for running a TimeMachine backup server on a single-node [k3s](https://k3s.io) cluster running (on a Raspberry Pi 4). 90 | 91 | ### Tips for Automatic Discovery w/Avahi 92 | 93 | This works best with `--net=host` so that discovery can be broadcast. Otherwise, you will need to expose the above ports and then you must manually map the share in Finder for it to show up (open `Finder`, click `Shared`, and connect as `smb://hostname-or-ip/TimeMachine` with your TimeMachine credentials). Using `--net=host` only works if you do not already run Samba or Avahi on the host! Alternatively, you can use the `SMB_PORT` option to change the port that Samba uses. See below for another workaround if you do not wish to change the Samba port. 94 | 95 | ### Known Issues 96 | 97 | #### Processes fail to start; container has high CPU usage 98 | 99 | If the container isn't starting and you're seeing logs like `Failed to start message bus: Failed to bind socket`, and possibly have other symptoms like seeing high CPU usage from the container, it could be that your are hitting the `nofile` ulimit. Make sure your compose file or `docker run` command have the `nofile` ulimits adjusted to increase the defaults. Check the examples in the README or the example compose files in this repository. 100 | 101 | #### Unable to start the `armv7l` image 102 | 103 | If you are running the `armv7l` image, you may see and error when trying to start the container: 104 | 105 | ``` 106 | s6-svscan: warning: unable to iopause: Operation not permitted 107 | ``` 108 | 109 | This is due to an issue with the `libseccomp2` package. You have two options: 110 | 111 | 1. Disable seccomp for the container by adding the `--security-opt seccomp=unconfined` argument (this has security implications) 112 | 1. Install a backported version of `libseccomp2`: 113 | 114 | ``` 115 | wget http://ftp.us.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.1-1~bpo10+1_armhf.deb 116 | sudo dpkg -i libseccomp2_2.5.1-1~bpo10+1_armhf.deb 117 | ``` 118 | 119 | This issue has been observed on Raspberry Pi OS (formerly known as Raspbian) based on Debian 10 (Buster) but may also be found on other distros as they may commonly use the `libseccomp2` package version `2.3.3-4`. 120 | 121 | #### Conflicts with Samba and/or Avahi on the Host 122 | 123 | __Note__: If you are already running Samba/Avahi on your Docker host (or you're wanting to run this on your NAS), you should be aware that using `--net=host` will cause a conflict with the Samba/Avahi install. Raspberry Pi users: be aware that there is already an mDNS responder running on the stock Raspberry Pi OS image that will conflict with the mDNS responder in the container. 124 | 125 | If your host is running Avahi, you can configure it to act as a reflector, and the container advertisements will be broadcast to your host network without using `--net=host`. To do this, edit the avahi config (`/etc/avahi/avahi-daemon.conf`) on the host: 126 | 127 | * set `enable-reflector=yes` 128 | * set `cache-entries-max=0` - this prevents issues with Apple devices reporting duplicate names and adding/incrementing numbers in their name (references: and ) 129 | 130 | Then set the `ADVERTISED_HOSTNAME` environment variable in your container config to the mDNS hostname of your host, *without* the `.local` suffix. 131 | 132 | As an alternative, you can use the [`macvlan` driver in Docker](https://docs.docker.com/network/macvlan/) which will allow you to map a static IP address to your container. If you have issues setting up Time Machine with the configuration, feel free to open an issue and I can assist - this is how I persoanlly run time machine. 133 | 134 | 1. Create a `macvlan` Docker network (assuming your local subnet is `192.168.1.0/24`, the default gateway is `192.168.1.1`, and `eth0` for the host's network interface): 135 | 136 | ```bash 137 | docker network create -d macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=eth0 macvlan1 138 | ``` 139 | 140 | On devices such as Synology DSM, the primary network interface may be `ovs_eth0` due to the usage of Open vSwitch. If you are unsure of your primary network interface, this command may help: 141 | 142 | ```bash 143 | $ route | grep ^default | awk '{print $NF}' 144 | eth0 145 | ``` 146 | 147 | The `macvlan` driver can use another network interface as the documentation states above but in cases where multiple network interfaces may exist and they might not all be connected, choosing the primary network interface is generally safe. 148 | 149 | 1. Add `--network macvlan1` and `--ip 192.168.1.x` to your `docker run` command where `192.168.1.x` is a static IP to assign to Time Machine 150 | 151 | ##### Example macvlan setup using docker-compose 152 | 153 | ``` 154 | services: 155 | timemachine: 156 | hostname: timemachine 157 | mac_address: "AA:BB:CC:DD:EE:FF" 158 | networks: 159 | timemachine: 160 | ipv4_address: 192.168.1.x 161 | 162 | networks: 163 | timemachine: 164 | driver: macvlan 165 | driver_opts: 166 | parent: eth0 167 | ipam: 168 | config: 169 | - subnet: 192.168.1.0/24 170 | ip_range: 192.168.1.0/24 171 | gateway: 192.168.1.1 172 | ``` 173 | 174 | 1. `hostname`, `mac_address`, and `ipv4_address` are optional, but can be used to control how it is configured on the network. If not defined, random values will be used. 175 | 1. This config requires [docker-compose version](https://docs.docker.com/compose/compose-file/) `1.27.0+` which implements the [compose specification](https://github.com/compose-spec/compose-spec/blob/master/spec.md). 176 | 177 | #### Volume & File system Permissions 178 | 179 | If you're using an external volume like in the example above, you will need to set the filesystem permissions on disk. By default, the `timemachine` user is `1000:1000`. 180 | 181 | The backing data store for your persistent time machine data _must_ support extended file attributes (`xattr`). Remote file systems, such as NFS, will very likely not support `xattr`s. See [#61](https://github.com/mbentley/docker-timemachine/issues/61) for more details. This image will check and try to set `xattr`s to a test file in `/opt/${TM_USERNAME}` to warn the user if they are not supported but this will not prevent the image from running. 182 | 183 | #### Persistent Data Path 184 | 185 | If you change the `TM_USERNAME` value, it will change the persistent data path from `/opt/timemachine` to `/opt/`. Failure to map this appropriately will lead to data being stored inside the container and not in the volume you have specified! 186 | 187 | #### Default credentials 188 | 189 | * Username: `timemachine` 190 | * Password: `timemachine` 191 | 192 | ### Optional variables for SMB 193 | 194 | | Variable | Default | Description | 195 | | :------- | :------ | :---------- | 196 | | `ADVERTISED_HOSTNAME` | _not set_ | Avahi will advertise the smb services at this hostname instead of the local hostname (useful in Docker without `--net=host`). **Do not set this if you don't know what you're doing!** | 197 | | `CUSTOM_SMB_AUTH` | `no` | set to yes, indicates that you want Samba to attempt to authenticate users using the [NTLM Encrypted Password Response](https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html#idm7319) | 198 | | `CUSTOM_SMB_CONF` | `false` | indicates that you are going to bind mount a custom config to `/etc/samba/smb.conf` if set to `true` | 199 | | `CUSTOM_SMB_PROTO` | `SMB2` | indicates that you want to allow another value from [Samba Protocol List](https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html#CLIENTMAXPROTOCOL) | 200 | | `CUSTOM_USER` | `false` | indicates that you are going to bind mount `/etc/password`, `/etc/group`, and `/etc/shadow`; and create data directories if set to `true` | 201 | | `DEBUG_LEVEL` | `1` | sets the debug level for `nmbd` and `smbd` | 202 | | `EXTERNAL_CONF` | _not set_ | specifies a directory in which individual variable files, ending in `.conf`, for multiple users; see [Adding Multiple Users & Shares](#adding-multiple-users--shares) for more info | 203 | | `HIDE_SHARES` | `no` | set to `yes` if you would like only the share(s) a user can access to appear | 204 | | `MIMIC_MODEL` | `TimeCapsule8,119` | sets the value of time machine to mimic | 205 | | `TM_USERNAME` | `timemachine` | sets the username time machine runs as | 206 | | `TM_GROUPNAME` | `timemachine` | sets the group name time machine runs as | 207 | | `TM_UID` | `1000` | sets the UID of the `TM_USERNAME` user | 208 | | `TM_GID` | `1000` | sets the GID of the `TM_GROUPNAME` group | 209 | | `PASSWORD` | `timemachine` | sets the password for the `timemachine` user | 210 | | `SET_PERMISSIONS` | `false` | set to `true` to have the entrypoint set ownership and permission on the `/opt/` in the container | 211 | | `SHARE_NAME` | `TimeMachine` | sets the name of the timemachine share to TimeMachine by default | 212 | | `SMB_INHERIT_PERMISSIONS` | `no` | if yes, permissions for new files will be forced to match the parent folder | 213 | | `SMB_NFS_ACES` | `no` | value of `fruit:nfs_aces`; support for querying and modifying the UNIX mode of directory entries via NFS ACEs | 214 | | `SMB_METADATA` | `stream` | value of `fruit:metadata`; controls where the OS X metadata stream is stored | 215 | | `SMB_PORT` | `445` | sets the port that Samba will be available on | 216 | | `SMB_VFS_OBJECTS` | `fruit streams_xattr` | value of `vfs objects` | 217 | | `VOLUME_SIZE_LIMIT` | `0` | sets the maximum size of the time machine backup; a unit can also be passed (e.g. - `1 T`). See the [Samba docs](https://www.samba.org/samba/docs/current/man-html/vfs_fruit.8.html) under the `fruit:time machine max size` section for more details | 218 | | `WORKGROUP` | `WORKGROUP` | set the Samba workgroup name | 219 | | `IGNORE_DOS_ATTRIBUTES` | `false` | If set to `true` Samba will ignore DOS attributes. This is accomplished by setting [store dos attributes](https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html#STOREDOSATTRIBUTES), [map hidden](https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html#MAPHIDDEN), [map system](https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html#MAPSYSTEM), [map archive](https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html#MAPARCHIVE) and [map readonly](https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html#MAPREADONLY) to `no` in the `[global]` section. | 220 | 221 | ### Adding Multiple Users & Shares 222 | 223 | In order to add multiple users who have their own shares, you will need to create a file for each user and put them in a directory. The file name __must__ end in `.conf` or it will not be parsed and the contents must be environment variable formatted proper and include all of the values below in the example. Only `VOLUME_SIZE_LIMIT` can be empty if you do not want to set a quota. 224 | 225 | #### Example `EXTERNAL_CONF` File 226 | 227 | This is an example to create a user named `foo`. The `EXTERNAL_CONF` variable should point to the _directory_ that contains the user definition files. Create multiple files with different attributes to create multiple users and shares. 228 | 229 | `foo.conf` 230 | 231 | ``` 232 | TM_USERNAME=foo 233 | TM_GROUPNAME=foogroup 234 | PASSWORD=foopass 235 | SHARE_NAME=foo 236 | VOLUME_SIZE_LIMIT="1 T" 237 | TM_UID=1000 238 | TM_GID=1000 239 | ``` 240 | 241 | #### Example run command for `EXTERNAL_CONF` 242 | 243 | This run command has the necessary path to where the external user files will be mounted (set in `EXTERNAL_CONF`) and the volume mount that matches the path specified in `EXTERNAL_CONF`. 244 | 245 | __Note__: You will need to either bind mount `/opt` or each `SHARE_NAME` directory under `/opt` for each user. 246 | 247 | ``` 248 | docker run -d --restart=always \ 249 | --name timemachine \ 250 | --net=host \ 251 | --ulimit nofile=65536:65536 \ 252 | -e ADVERTISED_HOSTNAME="" \ 253 | -e CUSTOM_SMB_CONF="false" \ 254 | -e CUSTOM_USER="false" \ 255 | -e DEBUG_LEVEL="1" \ 256 | -e MIMIC_MODEL="TimeCapsule8,119" \ 257 | -e EXTERNAL_CONF="/users" \ 258 | -e HIDE_SHARES="no" \ 259 | -e TM_USERNAME="timemachine" \ 260 | -e TM_GROUPNAME="timemachine" \ 261 | -e TM_UID="1000" \ 262 | -e TM_GID="1000" \ 263 | -e PASSWORD="timemachine" \ 264 | -e SET_PERMISSIONS="false" \ 265 | -e SHARE_NAME="TimeMachine" \ 266 | -e SMB_INHERIT_PERMISSIONS="no" \ 267 | -e SMB_NFS_ACES="no" \ 268 | -e SMB_METADATA="stream" \ 269 | -e SMB_PORT="445" \ 270 | -e SMB_VFS_OBJECTS="fruit streams_xattr" \ 271 | -e VOLUME_SIZE_LIMIT="0" \ 272 | -e WORKGROUP="WORKGROUP" \ 273 | -v /path/on/host/to/backup/to/for/timemachine:/opt \ 274 | -v /path/on/host/to/user/file/directory:/users \ 275 | --tmpfs /run/samba \ 276 | mbentley/timemachine:smb 277 | ``` 278 | 279 | ### Using a password file 280 | 281 | This is an example to using Docker secrets to pass the password via a file 282 | 283 | `password.txt` 284 | 285 | ``` 286 | my_secret_password 287 | ``` 288 | 289 | ### Example docker-compose file 290 | 291 | The follow example shows the key values required for in your compose file. 292 | 293 | ``` 294 | version: "3.3" # or greater 295 | services: 296 | timemachine: 297 | # ... 298 | environment: 299 | - PASSWORD_FILE=/run/secrets/password 300 | # ... 301 | secrets: 302 | - password 303 | 304 | secrets: 305 | password: 306 | file: ./password.txt 307 | ``` 308 | 309 | ## AFP Examples and Variables 310 | 311 |
Click to expand 312 | 313 | ## Example docker-compose usage for AFP 314 | 315 | ``` 316 | docker compose -f timemachine-compose.yml up -d 317 | ``` 318 | 319 | ## Example `docker run` usage for AFP 320 | 321 | Example usage with `--net=host` to allow Avahi discovery to function: 322 | 323 | ``` 324 | docker run -d --restart=always \ 325 | --net=host \ 326 | --name timemachine \ 327 | -e CUSTOM_AFP_CONF="false" \ 328 | -e CUSTOM_USER="false" \ 329 | -e LOG_LEVEL="info" \ 330 | -e MIMIC_MODEL="TimeCapsule6,106" \ 331 | -e TM_USERNAME="timemachine" \ 332 | -e TM_GROUPNAME="timemachine" \ 333 | -e TM_UID="1000" \ 334 | -e TM_GID="1000" \ 335 | -e PASSWORD="timemachine" \ 336 | -e SET_PERMISSIONS="false" \ 337 | -e SHARE_NAME="TimeMachine" \ 338 | -e VOLUME_SIZE_LIMIT="0" \ 339 | -v /path/on/host/to/backup/to/for/timemachine:/opt/timemachine \ 340 | -v timemachine-netatalk:/var/netatalk \ 341 | -v timemachine-logs:/var/log/supervisor \ 342 | mbentley/timemachine:afp 343 | ``` 344 | 345 | Example usage with exposing ports _without_ Avahi discovery: 346 | 347 | ``` 348 | docker run -d --restart=always \ 349 | --name timemachine \ 350 | --hostname timemachine \ 351 | -p 548:548 \ 352 | -p 636:636 \ 353 | -e CUSTOM_AFP_CONF="false" \ 354 | -e CUSTOM_USER="false" \ 355 | -e LOG_LEVEL="info" \ 356 | -e MIMIC_MODEL="TimeCapsule6,106" \ 357 | -e TM_USERNAME="timemachine" \ 358 | -e TM_GROUPNAME="timemachine" \ 359 | -e TM_UID="1000" \ 360 | -e TM_GID="1000" \ 361 | -e PASSWORD="timemachine" \ 362 | -e SET_PERMISSIONS="false" \ 363 | -e SHARE_NAME="TimeMachine" \ 364 | -e VOLUME_SIZE_LIMIT="0" \ 365 | -v /path/on/host/to/backup/to/for/timemachine:/opt/timemachine \ 366 | -v timemachine-netatalk:/var/netatalk \ 367 | -v timemachine-logs:/var/log/supervisor \ 368 | mbentley/timemachine:afp 369 | ``` 370 | 371 | This works best with `--net=host` so that discovery can be broadcast. Otherwise, you will need to expose the above ports and then you must manually map the share in Finder for it to show up (open `Finder`, click `Shared`, and connect as `afp://hostname-or-ip/TimeMachine` with your TimeMachine credentials). 372 | 373 | Optional variables for AFP: 374 | 375 | | Variable | Default | Description | 376 | | :------- | :------ | :---------- | 377 | | `CUSTOM_AFP_CONF` | `false` | indicates that you are going to bind mount a custom config to `/etc/netatalk/afp.conf` if set to `true` | 378 | | `CUSTOM_USER` | `false` | indicates that you are going to bind mount `/etc/password`, `/etc/group`, and `/etc/shadow`; and create data directories if set to `true` | 379 | | `LOG_LEVEL` | `info` | sets the netatalk log level | 380 | | `MIMIC_MODEL` | `TimeCapsule6,106` | sets the value of time machine to mimic | 381 | | `TM_USERNAME` | `timemachine` | sets the username time machine runs as | 382 | | `TM_GROUPNAME` | `timemachine` | sets the group name time machine runs as | 383 | | `TM_UID` | `1000` | sets the UID of the `TM_USERNAME` user | 384 | | `TM_GID` | `1000` | sets the GID of the `TM_GROUPNAME` group | 385 | | `PASSWORD` | `timemachine` | sets the password for the `timemachine` user | 386 | | `SET_PERMISSIONS` | `false` | set to `true` to have the entrypoint set ownership and permission on `/opt/timemachine` | 387 | | `SHARE_NAME` | `TimeMachine` | sets the name of the timemachine share to TimeMachine by default | 388 | | `VOLUME_SIZE_LIMIT` | `0` | sets the maximum size of the time machine backup in MiB ([mebibyte](https://en.wikipedia.org/wiki/Mebibyte)) | 389 | 390 |
391 | 392 | Thanks for [odarriba](https://github.com/odarriba) and [arve0](https://github.com/arve0) for their examples to start from. 393 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # set default values 4 | LOG_LEVEL="${LOG_LEVEL:-info}" 5 | SET_PERMISSIONS="${SET_PERMISSIONS:-false}" 6 | SHARE_NAME="${SHARE_NAME:-TimeMachine}" 7 | CUSTOM_AFP_CONF="${CUSTOM_AFP_CONF:-false}" 8 | CUSTOM_SMB_AUTH="${CUSTOM_SMB_AUTH:-no}" 9 | CUSTOM_SMB_CONF="${CUSTOM_SMB_CONF:-false}" 10 | CUSTOM_SMB_PROTO="${CUSTOM_SMB_PROTO:-SMB2}" 11 | SMB_PORT="${SMB_PORT:-445}" 12 | CUSTOM_USER="${CUSTOM_USER:-false}" 13 | TM_USERNAME="${TM_USERNAME:-timemachine}" 14 | TM_GROUPNAME="${TM_GROUPNAME:-timemachine}" 15 | VOLUME_SIZE_LIMIT="${VOLUME_SIZE_LIMIT:-0}" 16 | WORKGROUP="${WORKGROUP:-WORKGROUP}" 17 | EXTERNAL_CONF="${EXTERNAL_CONF:-}" 18 | HIDE_SHARES="${HIDE_SHARES:-no}" 19 | SMB_VFS_OBJECTS="${SMB_VFS_OBJECTS:-fruit streams_xattr}" 20 | SMB_INHERIT_PERMISSIONS="${SMB_INHERIT_PERMISSIONS:-no}" 21 | SMB_NFS_ACES="${SMB_NFS_ACES:-no}" 22 | SMB_METADATA="${SMB_METADATA:-stream}" 23 | IGNORE_DOS_ATTRIBUTES="${IGNORE_DOS_ATTRIBUTES:-false}" 24 | 25 | # support both PUID/TM_UID and PGID/TM_GID 26 | PUID="${PUID:-1000}" 27 | PGID="${PGID:-${PUID}}" 28 | TM_UID="${TM_UID:-${PUID}}" 29 | TM_GID="${TM_GID:-${PGID:-${TM_UID}}}" 30 | 31 | 32 | # common functions 33 | password_var_or_file() { 34 | # check PASSWORD and PASSWORD_FILE are both not set 35 | if [ -n "${PASSWORD}" ] && [ -n "${PASSWORD_FILE}" ] 36 | then 37 | echo "ERROR: PASSWORD and PASSWORD_FILE can not both be set. Please choose 1" 38 | exit 1 39 | fi 40 | 41 | # check to see if if PASSWORD_FILE is set 42 | if [ -n "${PASSWORD_FILE}" ] 43 | then 44 | # cat the password file to save the contents to the env var 45 | PASSWORD=$(cat "${PASSWORD_FILE}") 46 | else 47 | # if no password file passed; set the password from the env var 48 | PASSWORD="${PASSWORD:-timemachine}" 49 | fi 50 | } 51 | 52 | set_password() { 53 | # check to see what the password should be set to 54 | if [ "${PASSWORD}" = "timemachine" ] 55 | then 56 | echo "INFO: Using default password: timemachine" 57 | else 58 | echo "INFO: Setting password from environment variable" 59 | fi 60 | 61 | # set the password 62 | printf "INFO: " 63 | echo "${TM_USERNAME}":"${PASSWORD}" | chpasswd 64 | } 65 | 66 | samba_user_setup() { 67 | # set up user in Samba 68 | printf "INFO: Samba - Created " 69 | smbpasswd -L -a -n "${TM_USERNAME}" 70 | printf "INFO: Samba - " 71 | smbpasswd -L -e -n "${TM_USERNAME}" 72 | printf "INFO: Samba - setting password\n" 73 | printf "%s\n%s\n" "${PASSWORD}" "${PASSWORD}" | smbpasswd -L -s "${TM_USERNAME}" 74 | } 75 | 76 | create_user_directory() { 77 | # create user directory if needed 78 | if [ ! -d "/opt/${TM_USERNAME}" ] 79 | then 80 | mkdir "/opt/${TM_USERNAME}" 81 | fi 82 | } 83 | 84 | createdir() { 85 | # create directory, if needed 86 | if [ ! -d "${1}" ] 87 | then 88 | echo "INFO: Creating ${1}" 89 | mkdir -p "${1}" 90 | fi 91 | 92 | # set permissions, if needed 93 | if [ -n "${2}" ] 94 | then 95 | chmod "${2}" "${1}" 96 | fi 97 | } 98 | 99 | create_smb_user() { 100 | # validate that none of the required environment variables are empty 101 | if [ -z "${TM_USERNAME}" ] || [ -z "${TM_GROUPNAME}" ] || [ -z "${PASSWORD}" ] || [ -z "${SHARE_NAME}" ] || [ -z "${TM_UID}" ] || [ -z "${TM_GID}" ] 102 | then 103 | echo "ERROR: Missing one or more of the following variables; unable to create user" 104 | echo " Hint: Is the variable missing or not set in ${USER_FILE}?" 105 | echo " TM_USERNAME=${TM_USERNAME}" 106 | echo " TM_GROUPNAME=${TM_GROUPNAME}" 107 | echo " PASSWORD=$(if [ -n "${PASSWORD}" ]; then printf "";fi)" 108 | echo " SHARE_NAME=${SHARE_NAME}" 109 | echo " TM_UID=${TM_UID}" 110 | echo " TM_GID=${TM_GID}" 111 | exit 1 112 | fi 113 | 114 | # create custom user, group, and directories if CUSTOM_USER is not true 115 | if [ "${CUSTOM_USER}" != "true" ] 116 | then 117 | # check to see if group exists; if not, create it 118 | if grep -q -E "^${TM_GROUPNAME}:" /etc/group > /dev/null 2>&1 119 | then 120 | echo "INFO: Group ${TM_GROUPNAME} exists; skipping creation" 121 | else 122 | # make sure the group doesn't already exist with a different name 123 | if awk -F ':' '{print $3}' /etc/group | grep -q "^${TM_GID}$" 124 | then 125 | EXISTING_GROUP="$(grep ":${TM_GID}:" /etc/group | awk -F ':' '{print $1}')" 126 | echo "INFO: Group already exists with a different name; renaming '${EXISTING_GROUP}' to '${TM_GROUPNAME}'..." 127 | sed -i "s/^${EXISTING_GROUP}:/${TM_GROUPNAME}:/g" /etc/group 128 | else 129 | echo "INFO: Group ${TM_GROUPNAME} doesn't exist; creating..." 130 | # create the group 131 | addgroup -g "${TM_GID}" "${TM_GROUPNAME}" 132 | fi 133 | fi 134 | # check to see if user exists; if not, create it 135 | if id -u "${TM_USERNAME}" > /dev/null 2>&1 136 | then 137 | echo "INFO: User ${TM_USERNAME} exists; skipping creation" 138 | else 139 | echo "INFO: User ${TM_USERNAME} doesn't exist; creating..." 140 | # create the user 141 | adduser -u "${TM_UID}" -G "${TM_GROUPNAME}" -h "/opt/${TM_USERNAME}" -s /bin/false -D "${TM_USERNAME}" 142 | 143 | # set the user's password if necessary 144 | set_password 145 | fi 146 | 147 | # create user directory if necessary 148 | create_user_directory 149 | else 150 | echo "INFO: CUSTOM_USER=true; skipping user, group, and data directory creation; using pre-existing values in /etc/passwd, /etc/group, and /etc/shadow" 151 | fi 152 | 153 | # write smb.conf if CUSTOM_SMB_CONF is not true 154 | if [ "${CUSTOM_SMB_CONF}" != "true" ] 155 | then 156 | echo "INFO: CUSTOM_SMB_CONF=false; generating [${SHARE_NAME}] section of /etc/samba/smb.conf..." 157 | echo " 158 | [${SHARE_NAME}] 159 | path = /opt/${TM_USERNAME} 160 | inherit permissions = ${SMB_INHERIT_PERMISSIONS} 161 | read only = no 162 | valid users = ${TM_USERNAME} 163 | vfs objects = ${SMB_VFS_OBJECTS} 164 | fruit:time machine = yes 165 | fruit:time machine max size = ${VOLUME_SIZE_LIMIT}" >> /etc/samba/smb.conf 166 | else 167 | # CUSTOM_SMB_CONF was specified; make sure the file exists 168 | if [ -f "/etc/samba/smb.conf" ] 169 | then 170 | echo "INFO: CUSTOM_SMB_CONF=true; skipping generating smb.conf and using provided /etc/samba/smb.conf" 171 | else 172 | # there is no /etc/samba/smbp.conf; exit 173 | echo "ERROR: CUSTOM_SMB_CONF=true but you did not bind mount a config to /etc/samba/smb.conf; exiting." 174 | exit 1 175 | fi 176 | fi 177 | 178 | # set up user in Samba 179 | samba_user_setup 180 | 181 | # set user permissions 182 | set_permissions 183 | } 184 | 185 | set_permissions() { 186 | # set ownership and permissions, if requested 187 | if [ "${SET_PERMISSIONS}" = "true" ] 188 | then 189 | # set the ownership of the directory time machine will use 190 | printf "INFO: " 191 | chown -v "${TM_USERNAME}":"${TM_GROUPNAME}" "/opt/${TM_USERNAME}" 192 | 193 | # change the permissions of the directory time machine will use 194 | printf "INFO: " 195 | chmod -v 770 "/opt/${TM_USERNAME}" 196 | else 197 | echo "INFO: SET_PERMISSIONS=false; not setting ownership and permissions for /opt/${TM_USERNAME}" 198 | fi 199 | } 200 | 201 | write_avahi_adisk_service() { 202 | # $1 = DK_NUMBER, $2 = SHARE_NAME 203 | echo "INFO: Avahi - adding the 'dk${1}', '${2}' share txt-record to /etc/avahi/services/smbd.service..." 204 | # write the '_adisk._tcp' service definition 205 | echo " dk${1}=adVN=${2},adVF=0x82" >> /etc/avahi/services/smbd.service 206 | } 207 | 208 | 209 | # check to see if the password should be set from a file (secret) or env var 210 | password_var_or_file 211 | 212 | # check to see if if we are using the alpine or debian base images (debian version uses AFP; alpine uses SMB) 213 | # this is needed because of differences in syntax for adding groups and users 214 | if [ -z "${NETATALK_VERSION}" ] 215 | then 216 | # this is the SMB version running alpine 217 | 218 | # set default version for timecapsule w/SMB 219 | MIMIC_MODEL="${MIMIC_MODEL:-TimeCapsule8,119}" 220 | 221 | # write global smb.conf if CUSTOM_SMB_CONF is not true 222 | if [ "${CUSTOM_SMB_CONF}" != "true" ] 223 | then 224 | echo "INFO: CUSTOM_SMB_CONF=false; generating [global] section of /etc/samba/smb.conf..." 225 | echo "[global] 226 | access based share enum = ${HIDE_SHARES} 227 | hide unreadable = ${HIDE_SHARES} 228 | inherit permissions = ${SMB_INHERIT_PERMISSIONS} 229 | load printers = no 230 | log file = /var/log/samba/log.%m 231 | logging = file 232 | max log size = 1000 233 | security = user 234 | server min protocol = ${CUSTOM_SMB_PROTO} 235 | ntlm auth = ${CUSTOM_SMB_AUTH} 236 | server role = standalone server 237 | smb ports = ${SMB_PORT} 238 | workgroup = ${WORKGROUP} 239 | vfs objects = ${SMB_VFS_OBJECTS} 240 | fruit:aapl = yes 241 | fruit:nfs_aces = ${SMB_NFS_ACES} 242 | fruit:model = ${MIMIC_MODEL} 243 | fruit:metadata = ${SMB_METADATA} 244 | fruit:veto_appledouble = no 245 | fruit:posix_rename = yes 246 | fruit:zero_file_id = yes 247 | fruit:wipe_intentionally_left_blank_rfork = yes 248 | fruit:delete_empty_adfiles = yes" > /etc/samba/smb.conf 249 | # add options for ignoreing dos attributes, if desired 250 | if [ "${IGNORE_DOS_ATTRIBUTES}" = "true" ] 251 | then 252 | echo " store dos attributes = no 253 | map hidden = no 254 | map system = no 255 | map archive = no 256 | map readonly = no" >> /etc/samba/smb.conf 257 | fi 258 | fi 259 | 260 | # mkdir if needed 261 | createdir /var/lib/samba/private 700 262 | createdir /var/log/samba/cores 700 263 | 264 | # write avahi config file (smbd.service) to customize services advertised 265 | echo "INFO: Avahi - generating base configuration in /etc/avahi/services/smbd.service..." 266 | SERVICE_NAME="%h" 267 | HOSTNAME_XML="" 268 | if [ -n "${ADVERTISED_HOSTNAME}" ] 269 | then 270 | echo "INFO: Avahi - using ${ADVERTISED_HOSTNAME} as hostname." 271 | SERVICE_NAME="${ADVERTISED_HOSTNAME}" 272 | HOSTNAME_XML="${ADVERTISED_HOSTNAME}.local" 273 | fi 274 | echo " 275 | 276 | 277 | 278 | ${SERVICE_NAME} 279 | 280 | _smb._tcp 281 | ${SMB_PORT} 282 | ${HOSTNAME_XML} 283 | 284 | 285 | _device-info._tcp 286 | 9 287 | ${HOSTNAME_XML} 288 | model=${MIMIC_MODEL} 289 | 290 | 291 | _adisk._tcp 292 | 9 293 | ${HOSTNAME_XML} 294 | sys=adVF=0x100" > /etc/avahi/services/smbd.service 295 | 296 | # check to see if we should create one or many users 297 | if [ -z "${EXTERNAL_CONF}" ] 298 | then 299 | # write the individual share info for avahi discovery 300 | write_avahi_adisk_service 0 "${SHARE_NAME}" 301 | 302 | # EXTERNAL_CONF not set; assume we are creating one user; create user 303 | create_smb_user 304 | else 305 | # EXTERNAL_CONF is set; assume we are creating multiple users 306 | if [ ! -d "${EXTERNAL_CONF}" ] 307 | then 308 | echo "ERROR: The value of EXTERNAL_CONF should be a directory mounted inside the container; ${EXTERNAL_CONF} was not found" 309 | exit 1 310 | fi 311 | 312 | # initialize the DK_NUMBER variable at 0 313 | DK_NUMBER=0 314 | 315 | # loop through each user file in the EXTERNAL_CONF directory to load the variables 316 | for USER_FILE in "${EXTERNAL_CONF}"/*.conf 317 | do 318 | echo "INFO: Loading values from ${USER_FILE}" 319 | # source the variable file 320 | # shellcheck disable=SC1090 321 | . "${USER_FILE}" 322 | 323 | # write the individual share info for avahi discovery 324 | write_avahi_adisk_service "${DK_NUMBER}" "${SHARE_NAME}" 325 | 326 | # check to see if we are using a password file 327 | if [ -n "${PASSWORD_FILE}" ] 328 | then 329 | # cat the password file to save the contents to the env var 330 | PASSWORD=$(cat "${PASSWORD_FILE}") 331 | fi 332 | 333 | # create the user with the specified values 334 | create_smb_user 335 | 336 | # make sure we clear any previously set variables after a loop 337 | unset TM_USERNAME TM_GROUPNAME PASSWORD SHARE_NAME VOLUME_SIZE_LIMIT TM_UID TM_GID 338 | 339 | # increment DK_NUMBER 340 | DK_NUMBER=$((DK_NUMBER+1)) 341 | done 342 | fi 343 | 344 | # finish writing the avahi discovery file 345 | echo "INFO: Avahi - completing the configuration in /etc/avahi/services/smbd.service..." 346 | echo " 347 | " >> /etc/avahi/services/smbd.service 348 | 349 | # cleanup PID files 350 | for PIDFILE in nmbd samba-bgqd smbd 351 | do 352 | if [ -f /run/samba/${PIDFILE}.pid ] 353 | then 354 | echo "INFO: ${PIDFILE} PID exists; removing..." 355 | rm -v /run/samba/${PIDFILE}.pid 356 | fi 357 | done 358 | 359 | # cleanup dbus PID file 360 | if [ -f /run/dbus/dbus.pid ] 361 | then 362 | echo "INFO: dbus PID exists; removing..." 363 | rm -v /run/dbus/dbus.pid 364 | fi 365 | 366 | # cleanup avahi PID file 367 | if [ -f /run/avahi-daemon/pid ] 368 | then 369 | echo "INFO: avahi PID exists; removing..." 370 | rm -v /run/avahi-daemon/pid 371 | fi 372 | else 373 | # this is the AFP version running debian 374 | 375 | # set default version for timecapsule w/AFP 376 | MIMIC_MODEL="${MIMIC_MODEL:-TimeCapsule6,106}" 377 | 378 | # create custom user, group, and directories if CUSTOM_USER is not true 379 | if [ "${CUSTOM_USER}" != "true" ] 380 | then 381 | # check to see if group exists; if not, create it 382 | if grep -q -E "^${TM_GROUPNAME}:" /etc/group > /dev/null 2>&1 383 | then 384 | echo "INFO: Group exists; skipping creation" 385 | else 386 | # make sure the group doesn't already exist with a different name 387 | if awk -F ':' '{print $3}' /etc/group | grep -q "^${TM_GID}$" 388 | then 389 | EXISTING_GROUP="$(grep ":${TM_GID}:" /etc/group | awk -F ':' '{print $1}')" 390 | echo "INFO: Group already exists with a different name; renaming '${EXISTING_GROUP}' to '${TM_GROUPNAME}'..." 391 | groupmod -n "${TM_GROUPNAME}" "${EXISTING_GROUP}" 392 | else 393 | echo "INFO: Group doesn't exist; creating..." 394 | # create the group 395 | groupadd -g "${TM_GID}" "${TM_GROUPNAME}" 396 | fi 397 | fi 398 | 399 | # check to see if user exists; if not, create it 400 | if id -u "${TM_USERNAME}" > /dev/null 2>&1 401 | then 402 | echo "INFO: User exists; skipping creation" 403 | else 404 | echo "INFO: User doesn't exist; creating..." 405 | # create the user 406 | useradd -u "${TM_UID}" -g "${TM_GROUPNAME}" -d "/opt/${TM_USERNAME}" -s /bin/false "${TM_USERNAME}" 407 | 408 | # set the user's password if necessary 409 | set_password 410 | fi 411 | 412 | # create user directory if necessary 413 | create_user_directory 414 | else 415 | echo "INFO: CUSTOM_USER=true; skipping user, group, and data directory creation; using pre-existing values in /etc/passwd, /etc/group, and /etc/shadow" 416 | fi 417 | 418 | # mkdir if needed 419 | createdir /etc/netatalk 420 | createdir /var/netatalk/CNID 421 | 422 | # write afp.conf if CUSTOM_AFP_CONF is not true 423 | if [ "${CUSTOM_AFP_CONF}" != "true" ] 424 | then 425 | echo "INFO: CUSTOM_AFP_CONF=false; generating /etc/netatalk/afp.conf..." 426 | echo "[Global] 427 | mimic model = ${MIMIC_MODEL} 428 | log level = default:${LOG_LEVEL} 429 | log file = /dev/stdout 430 | zeroconf = yes 431 | 432 | [${SHARE_NAME}] 433 | path = /opt/${TM_USERNAME} 434 | valid users = ${TM_USERNAME} 435 | time machine = yes 436 | # the max size of the data folder (in MiB) 437 | vol size limit = ${VOLUME_SIZE_LIMIT}" > /etc/netatalk/afp.conf 438 | else 439 | # CUSTOM_AFP_CONF was specified; make sure the file exists 440 | if [ -f "/etc/netatalk/afp.conf" ] 441 | then 442 | echo "INFO: CUSTOM_AFP_CONF=true; skipping generating afp.conf and using provided /etc/netatalk/afp.conf" 443 | else 444 | # there is no /etc/netatalk/afp.conf; exit 445 | echo "ERROR: CUSTOM_AFP_CONF=true but you did not bind mount a config to /etc/netatalk/afp.conf; exiting." 446 | exit 1 447 | fi 448 | fi 449 | 450 | # set user permissions 451 | set_permissions 452 | 453 | # cleanup dbus PID file 454 | if [ -f /var/run/dbus/pid ] 455 | then 456 | echo "INFO: dbus PID exists; removing..." 457 | rm -v /var/run/dbus/pid 458 | fi 459 | 460 | # cleanup netatalk PID file 461 | if [ -f /var/run/netatalk.pid ] 462 | then 463 | echo "INFO: netatalk PID exists; removing..." 464 | rm -v /var/run/netatalk.pid 465 | fi 466 | 467 | # cleanup avahi-daemon PID file 468 | if [ -f /var/run/avahi-daemon/pid ] 469 | then 470 | echo "INFO: avahi-daemon PID exists; removing..." 471 | rm -v /var/run/avahi-daemon/pid 472 | fi 473 | fi 474 | 475 | # only run test if running samba version 476 | if [ -z "${NETATALK_VERSION}" ] 477 | then 478 | # perform quick test to see if xattrs are supported on a path found in smb.conf 479 | echo "INFO: running test for xattr support on your time machine persistent storage location..." 480 | 481 | # get the first path found in smb.conf 482 | TEST_PATH="$({ grep -m 1 "path =" < /etc/samba/smb.conf | awk '{print $3}' ; } 2>/dev/null)" 483 | if [ -z "${TEST_PATH}" ] 484 | then 485 | echo "WARN: unable to get test path from smb.conf; unable to test for xattr support" 486 | else 487 | # execute test by touching a file and trying to set xattrs to the file, capture the result, and remove the test file 488 | touch "${TEST_PATH}/xattr-test" || echo "WARN: unable to write test file (is your persistent storage location read only or an invalid path?)" 489 | TEST_RESULT="$(setfattr -n user.test -v "hello" "${TEST_PATH}/xattr-test" >/dev/null 2>&1; echo $?)" 490 | rm -f "${TEST_PATH}/xattr-test" 491 | 492 | # check to see what the results of the set xattrs test was 493 | if [ "${TEST_RESULT}" = "0" ] 494 | then 495 | echo "INFO: xattr test successful - your persistent data store supports xattrs" 496 | else 497 | echo "WARN: xattr test failure - unable to set xattrs on your persistent data store. Time machine backups may fail!" 498 | fi 499 | fi 500 | fi 501 | 502 | # output filesystem types detected 503 | for DIR in /opt/* 504 | do 505 | DETECTED_FS="$(df -TP "${DIR}" | grep -v ^Filesystem | awk '{print $2}')" 506 | 507 | # output based on detected fs 508 | case ${DETECTED_FS} in 509 | overlay) 510 | echo "WARN: Detected filesystem for ${DIR} is ${DETECTED_FS}! This likely means that your data is being stored inside the container, not in a volume! See https://github.com/mbentley/docker-timemachine#persistent-data-path" 511 | ;; 512 | *) 513 | echo "INFO: Detected filesystem for ${DIR} is ${DETECTED_FS}" 514 | ;; 515 | esac 516 | done 517 | 518 | # run CMD 519 | echo "INFO: entrypoint complete; executing '${*}'" 520 | exec "${@}" 521 | -------------------------------------------------------------------------------- /healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # initialize variables 6 | EXITCODE="0" 7 | PROCESSES="dbus-daemon afpd avahi-daemon" 8 | 9 | # check to see if processes are running 10 | for i in ${PROCESSES} 11 | do 12 | if pgrep "${i}" >/dev/null 2>&1 13 | then 14 | # process is running 15 | echo "${i} is running" 16 | else 17 | # process is not running 18 | echo "${i} is NOT running" 19 | EXITCODE="1" 20 | fi 21 | done 22 | 23 | # exit returning proper exit code 24 | exit ${EXITCODE} 25 | -------------------------------------------------------------------------------- /password.txt: -------------------------------------------------------------------------------- 1 | secret_password -------------------------------------------------------------------------------- /s6/.s6-svscan/crash: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Executing .s6-svscan/crash with arguments ${*}" 4 | -------------------------------------------------------------------------------- /s6/.s6-svscan/finish: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # do nothing 4 | echo "Executing .s6-svscan/finish with arguments ${*}" 5 | -------------------------------------------------------------------------------- /s6/avahi/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | while [ ! -S "/var/run/dbus/system_bus_socket" ] 4 | do 5 | echo "dbus socket not yet available; sleeping..." 6 | sleep .5 7 | done 8 | 9 | exec avahi-daemon --no-chroot 10 | -------------------------------------------------------------------------------- /s6/dbus/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec dbus-daemon --system --nofork --nosyslog 4 | -------------------------------------------------------------------------------- /s6/nmbd/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DEBUG_LEVEL="${DEBUG_LEVEL:-1}" 4 | 5 | exec /usr/sbin/nmbd --foreground --no-process-group --debug-stdout --debuglevel="${DEBUG_LEVEL}" < /dev/null 6 | -------------------------------------------------------------------------------- /s6/smbd/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DEBUG_LEVEL="${DEBUG_LEVEL:-1}" 4 | 5 | exec /usr/sbin/smbd --foreground --no-process-group --debug-stdout --debuglevel="${DEBUG_LEVEL}" < /dev/null 6 | -------------------------------------------------------------------------------- /supervisord.afp.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon = true 3 | user = root 4 | logfile = /var/log/supervisor/supervisord.log 5 | pidfile = /run/supervisord.pid 6 | stdout_logfile_maxbytes = 50MB 7 | stdout_logfile_backups = 5 8 | 9 | [program:dbus] 10 | command = dbus-daemon --system --nofork 11 | priority = 1 12 | stdout_logfile = /var/log/supervisor/dbus.log 13 | stdout_logfile_maxbytes = 50MB 14 | stdout_logfile_backups = 5 15 | redirect_stderr = true 16 | 17 | [program:avahi-daemon] 18 | command = avahi-daemon --no-chroot 19 | priority = 2 20 | stdout_logfile = /var/log/supervisor/avahi-daemon.log 21 | stdout_logfile_maxbytes = 50MB 22 | stdout_logfile_backups = 5 23 | redirect_stderr = true 24 | 25 | [program:netatalk] 26 | command = netatalk -F /etc/netatalk/afp.conf -d 27 | priority = 3 28 | stdout_logfile = /var/log/supervisor/netatalk.log 29 | stdout_logfile_maxbytes = 50MB 30 | stdout_logfile_backups = 5 31 | redirect_stderr = true 32 | -------------------------------------------------------------------------------- /timemachine-compose-afp.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | timemachine: 4 | network_mode: "host" 5 | environment: 6 | - CUSTOM_AFP_CONF=false 7 | - LOG_LEVEL=info 8 | - MIMIC_MODEL=TimeCapsule6,106 9 | - TM_USERNAME=timemachine 10 | - TM_GROUPNAME=timemachine 11 | - TM_UID=1000 12 | - TM_GID=1000 13 | - PASSWORD=timemachine 14 | - SET_PERMISSIONS=false 15 | - SHARE_NAME=TimeMachine 16 | - VOLUME_SIZE_LIMIT=0 17 | restart: unless-stopped 18 | ports: 19 | - "548:548" 20 | - "636:636" 21 | volumes: 22 | - /path/to/your/timemachine/volume:/opt/timemachine 23 | - timemachine-netatalk:/var/netatalk 24 | - timemachine-logs:/var/log/supervisor 25 | ulimits: 26 | nofile: 27 | soft: 65536 28 | hard: 65536 29 | container_name: timemachine 30 | image: mbentley/timemachine:latest 31 | 32 | volumes: 33 | timemachine-netatalk: 34 | timemachine-logs: 35 | -------------------------------------------------------------------------------- /timemachine-compose-smb-arm7l.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | timemachine: 4 | hostname: timemachine 5 | mac_address: "AA:BB:CC:DD:EE:FF" 6 | networks: 7 | timemachine: 8 | ipv4_address: 192.168.1.100 9 | environment: 10 | - TM_USERNAME=timemachine 11 | - TM_GROUPNAME=timemachine 12 | - PASSWORD=timemachine 13 | - TM_UID=1000 14 | - TM_GID=1000 15 | - SET_PERMISSIONS=false 16 | - VOLUME_SIZE_LIMIT=0 17 | restart: unless-stopped 18 | volumes: 19 | - /path/to/your/timemachine/volume:/opt/timemachine 20 | tmpfs: 21 | - /run/samba 22 | ulimits: 23 | nofile: 24 | soft: 65536 25 | hard: 65536 26 | container_name: timemachine 27 | image: mbentley/timemachine:smb-armv7l 28 | 29 | networks: 30 | timemachine: 31 | driver: macvlan 32 | driver_opts: 33 | parent: eth0 34 | ipam: 35 | config: 36 | - subnet: 192.168.1.0/24 37 | ip_range: 192.168.1.0/24 38 | gateway: 192.168.1.1 39 | -------------------------------------------------------------------------------- /timemachine-compose-smb.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | timemachine: 4 | network_mode: "host" 5 | environment: 6 | - TM_USERNAME=timemachine 7 | - TM_GROUPNAME=timemachine 8 | - PASSWORD=timemachine 9 | - TM_UID=1000 10 | - TM_GID=1000 11 | - SET_PERMISSIONS=false 12 | - VOLUME_SIZE_LIMIT=0 13 | restart: unless-stopped 14 | ports: 15 | - "137:137/udp" 16 | - "138:138/udp" 17 | - "139:139" 18 | - "445:445" 19 | volumes: 20 | - /path/to/your/timemachine/volume:/opt/timemachine 21 | tmpfs: 22 | - /run/samba 23 | ulimits: 24 | nofile: 25 | soft: 65536 26 | hard: 65536 27 | container_name: timemachine 28 | image: mbentley/timemachine:smb 29 | -------------------------------------------------------------------------------- /timemachine-compose.yml: -------------------------------------------------------------------------------- 1 | timemachine-compose-smb.yml -------------------------------------------------------------------------------- /timemachine-k3s.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: timemachine 5 | --- 6 | apiVersion: storage.k8s.io/v1 7 | kind: StorageClass 8 | metadata: 9 | name: local-path-retain 10 | provisioner: rancher.io/local-path 11 | reclaimPolicy: Retain 12 | volumeBindingMode: WaitForFirstConsumer 13 | --- 14 | apiVersion: v1 15 | kind: Service 16 | metadata: 17 | name: timemachine-udp 18 | namespace: timemachine 19 | labels: 20 | app: timemachine 21 | spec: 22 | ports: 23 | - port: 137 24 | name: udp137 25 | protocol: UDP 26 | - port: 138 27 | name: udp138 28 | protocol: UDP 29 | type: ClusterIP 30 | selector: 31 | app: timemachine 32 | --- 33 | apiVersion: v1 34 | kind: Service 35 | metadata: 36 | name: timemachine-tcp 37 | namespace: timemachine 38 | labels: 39 | app: timemachine 40 | spec: 41 | ports: 42 | - port: 139 43 | name: tcp139 44 | - port: 445 45 | name: tcp445 46 | type: ClusterIP 47 | selector: 48 | app: timemachine 49 | --- 50 | apiVersion: apps/v1 51 | kind: StatefulSet 52 | metadata: 53 | name: timemachine 54 | namespace: timemachine 55 | spec: 56 | serviceName: timemachine-udp 57 | replicas: 1 # Never go > 1! 58 | selector: 59 | matchLabels: 60 | app: timemachine 61 | template: 62 | metadata: 63 | labels: 64 | app: timemachine 65 | spec: 66 | hostNetwork: true # otherwise the auto-discovery does not work 67 | containers: 68 | - name: timemachine 69 | image: mbentley/timemachine:smb 70 | ports: 71 | - containerPort: 137 72 | name: udp137 73 | protocol: UDP 74 | - containerPort: 138 75 | name: udp138 76 | protocol: UDP 77 | - containerPort: 139 78 | name: tcp139 79 | - containerPort: 445 80 | name: tcp445 81 | volumeMounts: 82 | - name: tm 83 | mountPath: /opt/timemachine 84 | - name: run-samba 85 | mountPath: /run/samba 86 | env: 87 | - name: ADVERTISED_HOSTNAME 88 | value: "" 89 | - name: CUSTOM_SMB_CONF 90 | value: "false" 91 | - name: CUSTOM_USER 92 | value: "false" 93 | - name: DEBUG_LEVEL 94 | value: "1" 95 | - name: MIMIC_MODEL 96 | value: "TimeCapsule8,119" 97 | - name: EXTERNAL_CONF 98 | value: "" 99 | - name: HIDE_SHARES 100 | value: "no" 101 | - name: TM_USERNAME 102 | value: "timemachine" 103 | - name: TM_GROUPNAME 104 | value: "timemachine" 105 | - name: TM_UID 106 | value: "1000" 107 | - name: TM_GID 108 | value: "1000" 109 | - name: PASSWORD 110 | value: "timemachine" 111 | - name: SET_PERMISSIONS 112 | value: "false" 113 | - name: SHARE_NAME 114 | value: "TimeMachine" 115 | - name: SMB_INHERIT_PERMISSIONS 116 | value: "no" 117 | - name: SMB_NFS_ACES 118 | value: "no" 119 | - name: SMB_METADATA 120 | value: "stream" 121 | - name: SMB_PORT 122 | value: "445" 123 | - name: SMB_VFS_OBJECTS 124 | value: "fruit streams_xattr" 125 | - name: VOLUME_SIZE_LIMIT 126 | value: "0" 127 | - name: WORKGROUP 128 | value: "WORKGROUP" 129 | volumes: 130 | - name: var-lib-samba 131 | emptyDir: {} 132 | - name: var-cache-samba 133 | emptyDir: {} 134 | - name: run-samba 135 | emptyDir: {} 136 | volumeClaimTemplates: 137 | - metadata: 138 | name: tm 139 | spec: 140 | storageClassName: local-path-retain 141 | accessModes: [ "ReadWriteOnce" ] 142 | resources: 143 | requests: 144 | storage: 500Gi 145 | -------------------------------------------------------------------------------- /util/prune_hub_tags.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # set namespace & repository name 4 | IMAGE_REPO="mbentley/timemachine" 5 | 6 | # get the date, in seconds, for when we should purge tags before 7 | PURGE_DATE="$(date --date='-6 months' +%s)" 8 | 9 | # get Docker Hub credentials 10 | HUB_AUTH="$(jq -r '.auths."https://index.docker.io/v1/".auth' "${HOME}/.docker/config.json" | base64 -d)" 11 | 12 | # make sure we received credentials 13 | if [ -z "${HUB_AUTH}" ] 14 | then 15 | echo "ERROR: authorization data not found (have you performed a \"docker login\")?" 16 | exit 1 17 | else 18 | # get a token 19 | TOKEN=$(curl -s -H "Content-Type: application/json" -X POST -d '{"username": "'"$(echo "${HUB_AUTH}" | awk -F ':' '{print $1}')"'", "password": "'"$(echo "${HUB_AUTH}" | awk -F ':' '{print $2}')"'"}' https://hub.docker.com/v2/users/login/ | jq -r .token) 20 | fi 21 | 22 | # make sure we received a docker hub token 23 | if [ -z "${TOKEN}" ] 24 | then 25 | echo "ERROR: failed to get a Docker Hub auth token" 26 | exit 1 27 | fi 28 | 29 | # initialize default loop variables 30 | TAG_PAGE="1" 31 | NEXT_PAGE="" 32 | 33 | while [ "${NEXT_PAGE}" != "null" ] 34 | do 35 | # get a page of tags 36 | PAGE_OF_TAGS="$(curl -s -H "Authorization: JWT ${TOKEN}" "https://hub.docker.com/v2/repositories/${IMAGE_REPO}/tags?page=${TAG_PAGE}&page_size=50")" 37 | 38 | # set the next page variable 39 | NEXT_PAGE="$(echo "${PAGE_OF_TAGS}" | jq -r .next)" 40 | 41 | # add the tags from the page to the list of tags 42 | HUB_TAGS="${HUB_TAGS} 43 | $(echo "${PAGE_OF_TAGS}" | jq -r '.results[] | .name + " " + .tag_last_pushed')" 44 | 45 | # increment the tag page 46 | TAG_PAGE=$((TAG_PAGE+1)) 47 | done 48 | 49 | # trim off any blank lines from the list of tags 50 | HUB_TAGS="$(echo "${HUB_TAGS}" | sed '/^[[:space:]]*$/d')" 51 | 52 | while read -r TAG_NAME TAG_LAST_PUSHED 53 | do 54 | # check to see if we should skip the tag 55 | if echo "${TAG_NAME}" | grep -qE '(^latest$)|(^afp$)|(^smb$)|(-arm64$)|(-amd64$)|(-armv7l$)' 56 | then 57 | echo "skip tag ${TAG_NAME}" 58 | else 59 | # convert the date to seconds 60 | TAG_LAST_PUSHED=$(date -d "${TAG_LAST_PUSHED}" +%s) 61 | 62 | # compare the tag last pushed date to the purge date cutoff 63 | if [ "${TAG_LAST_PUSHED}" -lt "${PURGE_DATE}" ] 64 | then 65 | # purge tag 66 | echo "tag age $(date -d "@${TAG_LAST_PUSHED}" +%Y-%m-%d), threshold $(date -d "@${PURGE_DATE}" +%Y-%m-%d), ${TAG_NAME}, removing" 67 | 68 | # delete the tag 69 | curl -s -H "Authorization: JWT ${TOKEN}" -X DELETE "https://hub.docker.com/v2/repositories/${IMAGE_REPO}/tags/${TAG_NAME}/" 70 | else 71 | # do not purge tag 72 | echo "tag age $(date -d "@${TAG_LAST_PUSHED}" +%Y-%m-%d), threshold $(date -d "@${PURGE_DATE}" +%Y-%m-%d), ${TAG_NAME}, NOT removing" 73 | fi 74 | fi 75 | done < <(echo "${HUB_TAGS}") 76 | --------------------------------------------------------------------------------