├── .gitignore ├── .idea ├── misc.xml ├── vcs.xml ├── modules.xml └── docker-image-unison.iml ├── docker-compose.yml ├── supervisord.conf ├── monitrc ├── supervisor.daemon.conf ├── precopy_appsync.sh ├── LICENSE ├── .github └── workflows │ └── build.yml ├── Dockerfile ├── README.md └── entrypoint.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/workspace.xml 2 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | unison: 4 | build: 5 | context: . 6 | container_name: 'unison' 7 | image: 'ghcr.io/eugenmayer/unison:2.52.1-4.12.0' 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/docker-image-unison.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | logfile=/tmp/supervisord.log 3 | loglevel=info 4 | pidfile=/tmp/supervisord.pid 5 | nodaemon=true 6 | 7 | [unix_http_server] 8 | file=/tmp/supervisor.sock 9 | 10 | [rpcinterface:supervisor] 11 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 12 | 13 | [supervisorctl] 14 | serverurl=unix:///tmp/supervisor.sock 15 | 16 | [include] 17 | files=/etc/supervisor.conf.d/*.conf -------------------------------------------------------------------------------- /monitrc: -------------------------------------------------------------------------------- 1 | check process unison with pidfile /var/run/unison.pid 2 | # Wait for supervisor to be ready, then start unison and store the PID 3 | start program = "/bin/bash -c 'while [ ! -S /tmp/supervisor.sock ]; do sleep 1; done && supervisorctl start unison && supervisorctl pid unison > /var/run/unison.pid'" 4 | stop program = "/bin/bash -c 'supervisorctl stop unison && rm -rf /var/run/unison.pid'" 5 | # Restart if CPU usage is high based on cycles configuration. Value is replaced inline during startup 6 | if cpu usage > 50% for {{MONIT_HIGH_CPU_CYCLES}} cycles then restart 7 | -------------------------------------------------------------------------------- /supervisor.daemon.conf: -------------------------------------------------------------------------------- 1 | [program:unison] 2 | command = unison %(ENV_UNISON_ARGS)s %(ENV_UNISON_WATCH_ARGS)s %(ENV_UNISON_SRC)s %(ENV_UNISON_DEST)s 3 | user = %(ENV_OWNER)s 4 | directory = %(ENV_APP_VOLUME)s 5 | environment=HOME="%(ENV_OWNER_HOMEDIR)s",USER="%(ENV_OWNER)s" 6 | stdout_logfile=/dev/stdout 7 | stdout_logfile_maxbytes=0 8 | redirect_stderr = true 9 | autorestart=true 10 | 11 | [program:monit] 12 | command = monit -c /etc/monitrc -d %(ENV_MONIT_INTERVAL)s -I 13 | stdout_logfile=/dev/stdout 14 | stdout_logfile_maxbytes=0 15 | redirect_stderr = true 16 | autorestart = true 17 | autostart = %(ENV_MONIT_ENABLE)s 18 | -------------------------------------------------------------------------------- /precopy_appsync.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | APP_VOLUME=${APP_VOLUME:-/app_sync} 4 | HOST_VOLUME=${HOST_VOLUME:-/host_sync} 5 | OWNER_UID=${OWNER_UID:-0} 6 | 7 | if [ ! -f /unison/initial_sync_finished ]; then 8 | echo "doing initial sync with unison" 9 | # we use ruby due to http://mywiki.wooledge.org/BashFAQ/050 10 | time ruby -e '`unison #{ENV["UNISON_ARGS"]} #{ENV["UNISON_SYNC_PREFER"]} #{ENV["UNISON_EXCLUDES"]} -numericids -auto -batch /host_sync /app_sync`' 11 | #time cp -au $HOST_VOLUME/. $APP_VOLUME 12 | echo "chown ing file to uid $OWNER_UID" 13 | chown -R ${OWNER_UID} ${APP_VOLUME} 14 | touch /unison/initial_sync_finished 15 | echo "initial sync done using unison" 16 | else 17 | echo "skipping initial copy with unison" 18 | fi 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eugen Mayer 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 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build-and-push 2 | 3 | on: push 4 | 5 | env: 6 | IMAGE_FQDN: ghcr.io/eugenmayer/unison 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | versions: 14 | - { ocaml: "4.12.0", unison: "2.52.1" } 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v2 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v2 22 | - name: Login to GHCR 23 | uses: docker/login-action@v2 24 | with: 25 | registry: ghcr.io 26 | username: ${{ github.repository_owner }} 27 | password: ${{ github.token }} 28 | - name: Build 29 | uses: docker/build-push-action@v3 30 | with: 31 | context: . 32 | platforms: linux/amd64,linux/arm64 33 | push: false 34 | tags: ${{ env.IMAGE_FQDN }}:${{ matrix.versions.unison }}-${{ matrix.versions.ocaml }} 35 | build-args: | 36 | OCAML_VERSION=${{ matrix.versions.ocaml }} 37 | UNISON_VERSION=${{ matrix.versions.unison }} 38 | # push only on main 39 | - name: Build and push 40 | if: github.ref == 'refs/heads/main' 41 | uses: docker/build-push-action@v3 42 | with: 43 | context: . 44 | platforms: linux/amd64,linux/arm64 45 | push: true 46 | tags: ${{ env.IMAGE_FQDN }}:${{ matrix.versions.unison }}-${{ matrix.versions.ocaml }} 47 | build-args: | 48 | OCAML_VERSION=${{ matrix.versions.ocaml }} 49 | UNISON_VERSION=${{ matrix.versions.unison }} 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=alpine:3.12 2 | FROM $BASE_IMAGE 3 | 4 | ARG OCAML_VERSION=4.12.0 5 | ARG UNISON_VERSION=2.52.1 6 | 7 | ENV UNISON_ARGS '' 8 | ENV UNISON_WATCH_ARGS '' 9 | ENV APP_VOLUME '/app_sync' 10 | ENV UNISON_SRC '/app_sync' 11 | ENV UNISON_DEST '/host_sync' 12 | 13 | RUN apk update \ 14 | && apk add --no-cache --virtual .build-deps build-base curl git build-base coreutils \ 15 | && curl -L http://caml.inria.fr/pub/distrib/ocaml-${OCAML_VERSION:0:4}/ocaml-${OCAML_VERSION}.tar.gz --output - | tar zxv -C /tmp \ 16 | && cd /tmp/ocaml-${OCAML_VERSION} \ 17 | && ./configure \ 18 | && make world \ 19 | && make opt \ 20 | && umask 022 \ 21 | && make install \ 22 | && make clean \ 23 | && apk del .build-deps \ 24 | && rm -rf /tmp/ocaml-${OCAML_VERSION} 25 | 26 | RUN apk update \ 27 | && apk add --no-cache --virtual .build-deps build-base curl git build-base coreutils \ 28 | && apk add --no-cache bash inotify-tools monit supervisor rsync ruby \ 29 | && curl -L https://github.com/bcpierce00/unison/archive/v$UNISON_VERSION.tar.gz --output - | tar zxv -C /tmp \ 30 | && cd /tmp/unison-${UNISON_VERSION} \ 31 | && sed -i -e 's/GLIBC_SUPPORT_INOTIFY 0/GLIBC_SUPPORT_INOTIFY 1/' src/fsmonitor/linux/inotify_stubs.c \ 32 | && make UISTYLE=text NATIVE=true STATIC=true \ 33 | && cp src/unison src/unison-fsmonitor /usr/local/bin \ 34 | && apk del binutils .build-deps \ 35 | && apk add --no-cache libgcc libstdc++ \ 36 | && rm -rf /tmp/unison-${UNISON_VERSION} \ 37 | && apk add --no-cache --repository http://dl-4.alpinelinux.org/alpine/v3.12/testing/ shadow \ 38 | && apk add --no-cache tzdata 39 | 40 | # These can be overridden later 41 | ENV TZ="Europe/Berlin" \ 42 | LANG="C.UTF-8" \ 43 | UNISON_DIR="/data" \ 44 | HOME="/root" 45 | 46 | COPY entrypoint.sh /entrypoint.sh 47 | COPY precopy_appsync.sh /usr/local/bin/precopy_appsync 48 | COPY monitrc /etc/monitrc 49 | 50 | RUN mkdir -p /docker-entrypoint.d \ 51 | && chmod +x /entrypoint.sh \ 52 | && mkdir -p /etc/supervisor.conf.d \ 53 | && mkdir /unison \ 54 | && chmod +x /usr/local/bin/precopy_appsync \ 55 | && chmod u=rw,g=,o= /etc/monitrc 56 | 57 | COPY supervisord.conf /etc/supervisord.conf 58 | COPY supervisor.daemon.conf /etc/supervisor.conf.d/supervisor.daemon.conf 59 | 60 | ENTRYPOINT ["/entrypoint.sh"] 61 | CMD ["supervisord"] 62 | ############# ############# ############# 63 | ############# /SHARED / ############# 64 | ############# ############# ############# 65 | 66 | VOLUME /unison 67 | EXPOSE 5000 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | **HINT**: The docker images are no longer published on docker hub - only to ghcr.io! 4 | 5 | This image is the unison-image for [docker-sync](https://github.com/EugenMayer/docker-sync) and published ghcr.io [eugenmayer/unison](https://hub.docker.com/r/eugenmayer/unison/). 6 | 7 | The tags are structured as `ghcr.io/eugenmayer/unison:$UNISON_VERSION-$OCAML_VERSION-$ARCH` so for example 8 | 9 | ``` 10 | # this will pull the AMD or ARM version, depending on your current arch 11 | docker pull ghcr.io/eugenmayer/unison:2.52.1-4.12.0 12 | ``` 13 | 14 | ### What does it do ? 15 | 16 | This image simply runs an unison server on the internal port `5000` with the specified user/uid. If the user/uid doesn't 17 | exist, it is created/modified on startup. 18 | 19 | You can also combine it with OSXFS as it's done in docker-sync native_osx. 20 | 21 | ### Docker Sync related 22 | 23 | The image is used by docker-sync by default, unless it is overridden using the configuration option _\_image_ in [docker-sync.yml](https://docker-sync.readthedocs.io/en/latest/getting-started/configuration.html#references). The image uses the latest OCaml and Unison versions available at the time of release. Incase other versions needs to be used (which matches the versions used with docker-sync on the host), build a new docker-image-unison image as follows: 24 | 25 | ## Building 26 | 27 | You can build your own image using 28 | 29 | `docker build --build-arg "OCAML_VERSION=" --build-arg "UNISON_VERSION=" -t custom-docker-image-unison .` 30 | 31 | where `ocaml-version` is any OCaml version available as source-code [here](http://caml.inria.fr/pub/distrib/) and `unison-version` is any Unison version available as source code [here](https://github.com/bcpierce00/unison/releases/). 32 | 33 | Or for arm base builds change the image using BASE_IMAGE 34 | 35 | `docker build --build-arg "BASE_IMAGE=arm64v8/alpine:3.12" --build-arg "OCAML_VERSION=" --build-arg "UNISON_VERSION=" -t custom-docker-image-unison .` 36 | 37 | ### Build Examples 38 | 39 | For example, 40 | 41 | `docker build --build-arg "OCAML_VERSION=4.12.0" --build-arg "UNISON_VERSION=2.52.1" -t custom-docker-image-unison .` 42 | 43 | The configuration in the docker-sync.yml would then be: 44 | 45 | _unison_image_: 'custom-docker-image-unison' 46 | 47 | A lot of credits go to [mickaelperrin](https://github.com/mickaelperrin) - most of the work has been done by him initially. 48 | 49 | ## Documentation 50 | 51 | You can configure how unison runs by using the following ENV variables: 52 | - `UNISON_SRC` th unison src - default is `/app_sync` 53 | - `UNISON_DEST` th unison dest - default is `/host_sync` 54 | - `APP_VOLUME` specifies the directory created in the container to store the synced files, `/app_sync` by default 55 | - `OWNER_UID` specifies **the ID of the user** on which the unison process run and the owner of the synced files. 56 | - `MAX_INOTIFY_WATCHES` increases the limit of inotify watches if you need to sync folders with lots of files. 57 | - `UNISON_ARGS` Pass individual args to unison. 58 | - `UNISON_WATCH_ARGS` Pass individual watch args for unison 59 | 60 | ## Credits 61 | 62 | - Big thanks at [mickaelperrin](https://github.com/mickaelperrin) for putting hard work into getting this production ready. 63 | 64 | ## License 65 | 66 | What the others did, so: 67 | This docker image is licensed under GPLv3 because Unison is licensed under GPLv3 and is included in the image. See LICENSE. 68 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if [ "$1" == 'supervisord' ]; then 5 | ################### ################### ################### 6 | ################### general core shared ################### 7 | ################### ################### ################### 8 | APP_VOLUME=${APP_VOLUME:-/app_sync} 9 | HOST_VOLUME=${HOST_VOLUME:-/host_sync} 10 | OWNER_UID=${OWNER_UID:-0} 11 | #GROUP_ID=${GROUP_ID:-1000} 12 | 13 | [ ! -d ${APP_VOLUME} ] && mkdir -p ${APP_VOLUME} 14 | 15 | # if the user did not set anything particular to use, we use root 16 | # since this means, no special user has been created on the target container 17 | # thus it is most probably root to run the daemon and thats a good default then 18 | if [ -z ${OWNER_UID} ];then 19 | OWNER_UID=0 20 | fi 21 | 22 | # if the user with the uid does not exist, create him, otherwise reuse him 23 | if ! cut -d: -f3 /etc/passwd | grep -q ${OWNER_UID}; then 24 | echo "no user has uid $OWNER_UID - creating user" 25 | 26 | # If user doesn't exist on the system 27 | useradd -u ${OWNER_UID} dockersync -m 28 | else 29 | if [ ${OWNER_UID} == 0 ]; then 30 | # in case it is root, we need a special treatment 31 | echo "user with uid $OWNER_UID already exist and its root" 32 | else 33 | # we actually rename the user to unison, since we do not care about 34 | # the username on the sync container, it will be matched to whatever the target container uses for this uid 35 | # on the target container anyway, no matter how our user is name here 36 | echo "user with uid $OWNER_UID already exist" 37 | existing_user_with_uid=$(awk -F: "/:$OWNER_UID:/{print \$1}" /etc/passwd) 38 | OWNER=`getent passwd "$OWNER_UID" | cut -d: -f1` 39 | mkdir -p /home/$OWNER 40 | usermod --home /home/${OWNER} $OWNER 41 | chown -R $OWNER /home/${OWNER} 42 | fi 43 | fi 44 | export OWNER_HOMEDIR=`getent passwd ${OWNER_UID} | cut -f6 -d:` 45 | # OWNER should actually be dockersync in all cases the user did not match a system user 46 | export OWNER=`getent passwd "$OWNER_UID" | cut -d: -f1` 47 | 48 | # see https://wiki.alpinelinux.org/wiki/Setting_the_timezone 49 | if [ -n ${TZ} ] && [ -f /usr/share/zoneinfo/${TZ} ]; then 50 | ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime 51 | echo ${TZ} > /etc/timezone 52 | fi 53 | 54 | # Check if a script is available in /docker-entrypoint.d and source it 55 | for f in /docker-entrypoint.d/*; do 56 | case "$f" in 57 | *.sh) echo "$0: running $f"; . "$f" ;; 58 | *) echo "$0: ignoring $f" ;; 59 | esac 60 | done 61 | ################### ################### ################### 62 | ################### / general core shared/ ################ 63 | ################### ################### ################### 64 | 65 | ################### ################### ################### 66 | ################### now unison specific ################### 67 | ################### ################### ################### 68 | # Increase the maximum watches for inotify for very large repositories to be watched 69 | # Needs the privilegied docker option 70 | [ ! -z ${MAX_INOTIFY_WATCHES} ] && echo fs.inotify.max_user_watches=${MAX_INOTIFY_WATCHES} | tee -a /etc/sysctl.conf && sysctl -p || true 71 | 72 | MONIT_ENABLE=${MONIT_ENABLE:-false} 73 | MONIT_INTERVAL=${MONIT_INTERVAL:-5} 74 | MONIT_HIGH_CPU_CYCLES=${MONIT_HIGH_CPU_CYCLES:-2} 75 | 76 | sed -i -e "s/{{MONIT_HIGH_CPU_CYCLES}}/$MONIT_HIGH_CPU_CYCLES/g" /etc/monitrc 77 | 78 | export MONIT_ENABLE 79 | export MONIT_INTERVAL 80 | fi 81 | 82 | exec "$@" 83 | --------------------------------------------------------------------------------