diff --git a/Dockerfile b/Dockerfile index 3cbd281..301b85a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,33 @@ FROM golang:1-alpine AS build WORKDIR /build -RUN apk add make COPY . . -env GOPROXY=off -RUN make build +RUN GOPROXY=off CGO_ENABLED=0 go build -trimpath -FROM alpine:3.17 +# Using latest may break at some point, but will hopefully be convenient most of the time. +FROM alpine:latest WORKDIR /mox -COPY --from=build /build/mox /mox/mox -CMD ["/mox/mox", "serve"] +COPY --from=build /build/mox /bin/mox + +RUN apk add --no-cache libcap-utils + +# Allow binding to privileged ports, <1024. +RUN setcap 'cap_net_bind_service=+ep' /bin/mox + +# SMTP for incoming message delivery. +EXPOSE 25/tcp +# SMTP/submission with TLS. +EXPOSE 465/tcp +# SMTP/submission without initial TLS. +EXPOSE 587/tcp +# HTTP for internal account and admin pages. +EXPOSE 80/tcp +# HTTPS for ACME (Let's Encrypt), MTA-STS and autoconfig. +EXPOSE 443/tcp +# IMAP with TLS. +EXPOSE 993/tcp +# IMAP without initial TLS. +EXPOSE 143/tcp +# Prometheus metrics. +EXPOSE 8010/tcp + +CMD ["/bin/mox", "serve"] diff --git a/Dockerfile.imaptest b/Dockerfile.imaptest index e76ac98..8f8b26c 100644 --- a/Dockerfile.imaptest +++ b/Dockerfile.imaptest @@ -1,6 +1,6 @@ -FROM alpine:3.17 +FROM alpine:latest -RUN apk update && apk add wget build-base +RUN apk --no-cache add build-base WORKDIR /src RUN wget http://dovecot.org/nightly/dovecot-latest.tar.gz && tar -zxvf dovecot-latest.tar.gz && cd dovecot-0.0.0-* && ./configure && make install && cd .. RUN wget http://dovecot.org/nightly/imaptest/imaptest-latest.tar.gz && tar -zxvf imaptest-latest.tar.gz && cd dovecot-0.0-imaptest-0.0.0-* && ./configure --with-dovecot=$(ls -d ../dovecot-0.0.0-*) && make install diff --git a/Dockerfile.moximaptest b/Dockerfile.moximaptest new file mode 100644 index 0000000..a3d2fad --- /dev/null +++ b/Dockerfile.moximaptest @@ -0,0 +1,11 @@ +FROM golang:1-alpine AS build +WORKDIR /build +COPY . . +RUN GOPROXY=off CGO_ENABLED=0 go build -trimpath + +# Using latest may break at some point, but will hopefully be convenient most of the time. +FROM alpine:latest +WORKDIR /mox +COPY --from=build /build/mox /bin/mox + +CMD ["/bin/mox", "serve"] diff --git a/Makefile b/Makefile index a5c1af0..0875217 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,6 @@ integration-start: # run from within "make integration-start" integration-test: CGO_ENABLED=0 go test -tags integration - go tool cover -html=cover.out -o cover.html imaptest-build: -MOX_UID=$$(id -u) MOX_GID=$$(id -g) docker-compose -f docker-compose-imaptest.yml build --no-cache mox diff --git a/README.md b/README.md index 015dd3b..1ae1003 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,9 @@ Verify you have a working mox binary: Note: Mox only compiles/works on unix systems, not on Plan 9 or Windows. +You can also run mox with docker image "moxmail/mox" on hub.docker.com, with +tags like "latest", "0.0.1", etc. See docker-compose.yml in this repository. + # Quickstart diff --git a/docker-compose-imaptest.yml b/docker-compose-imaptest.yml index 88579e6..a967b95 100644 --- a/docker-compose-imaptest.yml +++ b/docker-compose-imaptest.yml @@ -1,7 +1,9 @@ version: '3.7' services: mox: - build: . + build: + context: . + dockerfile: Dockerfile.moximaptest user: ${MOX_UID}:${MOX_GID} volumes: - ./testdata/imaptest/data:/mox/data @@ -9,7 +11,8 @@ services: - ./testdata/imaptest/domains.conf:/mox/domains.conf - ./testdata/imaptest/imaptest.mbox:/mox/imaptest.mbox working_dir: /mox - command: sh -c 'echo testtest | ./mox setaccountpassword mjl@mox.example && ./mox serve' + tty: true # For job control + command: sh -c 'export MOXCONF=mox.conf; set -m; mox serve & sleep 1; echo testtest | mox setaccountpassword mjl@mox.example; fg' healthcheck: test: netstat -nlt | grep ':1143 ' interval: 1s diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8fc4568 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +# Before launching mox, run the quickstart to create config files: +# +# MOX_UID=0 MOX_GID=0 docker-compose run mox mox quickstart you@yourdomain.example +# +# After following the instructions, start mox as the newly created mox user: +# +# MOX_UID=$(id -u mox) MOX_GID=$(id -g mox) docker-compose up + +version: '3.7' +services: + mox: + # Replace latest with the version you want to run. + image: moxmail/mox:latest + user: ${MOX_UID}:${MOX_GID} + environment: + - MOX_DOCKER=... # Quickstart won't try to write systemd service file. + # Mox needs host networking because it needs access to the IPs of the + # machine, and the IPs of incoming connections for spam filtering. + network_mode: 'host' + command: sh -c "umask 007 && exec mox serve" + volumes: + - ./config:/mox/config + - ./data:/mox/data + working_dir: /mox + restart: on-failure + healthcheck: + test: netstat -nlt | grep ':25 ' + interval: 1s + timeout: 1s + retries: 10 diff --git a/quickstart.go b/quickstart.go index 3c69d16..0b3b4a3 100644 --- a/quickstart.go +++ b/quickstart.go @@ -8,7 +8,6 @@ import ( "log" "net" "os" - "os/user" "path/filepath" "runtime" "sort" @@ -345,7 +344,8 @@ This likely means one of two things: dc := config.Dynamic{} sc := config.Static{DataDir: "../data"} - os.MkdirAll(sc.DataDir, 0770) + dataDir := "data" // ../data is relative to config/ + os.MkdirAll(dataDir, 0770) sc.LogLevel = "info" sc.Hostname = hostname.Name() sc.ACME = map[string]config.ACME{ @@ -491,7 +491,7 @@ This likely means one of two things: if err != nil { fatalf("open account: %s", err) } - cleanupPaths = append(cleanupPaths, sc.DataDir, filepath.Join(sc.DataDir, "accounts"), filepath.Join(sc.DataDir, "accounts", username), filepath.Join(sc.DataDir, "accounts", username, "index.db")) + cleanupPaths = append(cleanupPaths, dataDir, filepath.Join(dataDir, "accounts"), filepath.Join(dataDir, "accounts", username), filepath.Join(dataDir, "accounts", username, "index.db")) password := pwgen() if err := acc.SetPassword(password); err != nil { @@ -534,34 +534,36 @@ and permissions. `) - userName := "root" - groupName := "root" - if u, err := user.Current(); err != nil { - log.Printf("get current user: %v", err) - } else { - userName = u.Username - if g, err := user.LookupGroupId(u.Gid); err != nil { - log.Printf("get current group: %v", err) - } else { - groupName = g.Name - } - } - fmt.Printf(`Assuming the mox binary is in the current directory, and you will run mox under -user name "mox", and the admin user is the current user, the following command -sets the correct permissions: + if os.Getenv("MOX_DOCKER") == "" { + fmt.Printf(`Assuming the mox binary is in the current directory, and you will run mox under +user name "mox", and the admin user is the current user, the following commands +set the correct permissions: sudo useradd -d $PWD mox - sudo chown %s:mox . mox - sudo chown -R mox:%s config data + sudo chown $(id -nu):mox . mox + sudo chown -R mox:$(id -ng) config data sudo chmod 751 . sudo chmod 750 mox sudo chmod -R u=rwX,g=rwX,o= config data sudo chmod g+s $(find . -type d) -`, userName, groupName) +`) + } else { + fmt.Printf(`Assuming you will run mox under user name "mox", and the admin user is the +current user, the following commands set the correct permissions: - // For now, we only give service config instructions for linux. - if runtime.GOOS == "linux" { + sudo useradd -d $PWD mox + sudo chown $(id -nu):mox . + sudo chown -R mox:$(id -ng) config data + sudo chmod 751 . + sudo chmod -R u=rwX,g=rwX,o= config data + sudo chmod g+s $(find . -type d) + +`) + } + + // For now, we only give service config instructions for linux when not running in docker. + if runtime.GOOS == "linux" && os.Getenv("MOX_DOCKER") == "" { pwd, err := os.Getwd() if err != nil { log.Printf("current working directory: %v", err)