├── .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 |
5 |
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 |
--------------------------------------------------------------------------------