diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..ef99e5a5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +testdata/ +cmd/maddy/maddy +maddy +tests/maddy.cover diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 369360a8..dae44f21 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -85,16 +85,16 @@ jobs: path: '~/maddy-x86_64-linux-musl.tar.zst' if-no-files-found: error docker-builder: - name: "Build Docker image" - needs: build-and-test + name: "Build & push Docker image" + needs: build-and-test # Upload if: github.ref_type == 'tag' runs-on: ubuntu-latest steps: - - name: Set up QEMU + - name: "Set up QEMU" uses: docker/setup-qemu-action@v1 with: platforms: arm64 - - name: Set up Docker Buildx + - name: "Set up Docker Buildx" id: buildx uses: docker/setup-buildx-action@v1 - name: "Login to Docker Hub" @@ -108,13 +108,24 @@ jobs: registry: "ghcr.io" username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: "Generate container metadata" + uses: docker/metadata-action@v4 + with: + images: | + foxcpp/maddy + ghcr.io/foxcpp/maddy + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + labels: | + org.opencontainers.image.title=Maddy Mail Server + org.opencontainers.image.documentation=https://maddy.email/docker/ + org.opencontainers.image.url=https://maddy.email - name: "Build and push" uses: docker/build-push-action@v2 with: context: . - platforms: linux/amd64,linux/arm64/v8 + platforms: linux/amd64,linux/arm64 push: true - tags: | - foxcpp/maddy:${{ github.ref_name }} - ghcr.io/foxcpp/maddy:${{ github.ref_name }} - + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.mkdocs.yml b/.mkdocs.yml index 189eba55..9c352e80 100644 --- a/.mkdocs.yml +++ b/.mkdocs.yml @@ -19,6 +19,7 @@ nav: - multiple-domains.md - upgrading.md - seclevels.md + - docker.md - Reference manual: - reference/modules.md - reference/global-config.md diff --git a/Dockerfile b/Dockerfile index a94efaf1..c760ec09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,30 @@ FROM golang:1.17-alpine AS build-env -RUN set -ex ;\ - apk upgrade --no-cache --available ;\ - apk add --no-cache bash git build-base +RUN set -ex && \ + apk upgrade --no-cache --available && \ + apk add --no-cache build-base WORKDIR /maddy -ADD go.mod go.sum ./ -ENV LDFLAGS -static + +COPY go.mod go.sum ./ RUN go mod download -ADD . ./ -RUN mkdir -p /pkg/data -COPY maddy.conf /pkg/data/maddy.conf -# Monkey-patch config to use environment. -RUN sed -Ei 's!\$\(hostname\) = .+!$(hostname) = {env:MADDY_HOSTNAME}!' /pkg/data/maddy.conf -RUN sed -Ei 's!\$\(primary_domain\) = .+!$(primary_domain) = {env:MADDY_DOMAIN}!' /pkg/data/maddy.conf -RUN sed -Ei 's!^tls .+!tls file /data/tls_cert.pem /data/tls_key.pem!' /pkg/data/maddy.conf -RUN ./build.sh --builddir /tmp --destdir /pkg/ --tags docker build install +COPY . ./ +RUN mkdir -p /pkg/data && \ + cp maddy.conf.docker /pkg/data/maddy.conf && \ + ./build.sh --builddir /tmp --destdir /pkg/ --tags docker build install -FROM alpine:3.15.0 +FROM alpine:3.16.0 LABEL maintainer="fox.cpp@disroot.org" LABEL org.opencontainers.image.source=https://github.com/foxcpp/maddy -RUN set -ex ;\ - apk upgrade --no-cache --available ;\ +RUN set -ex && \ + apk upgrade --no-cache --available && \ apk --no-cache add ca-certificates COPY --from=build-env /pkg/data/maddy.conf /data/maddy.conf COPY --from=build-env /pkg/usr/local/bin/maddy /pkg/usr/local/bin/maddyctl /bin/ EXPOSE 25 143 993 587 465 VOLUME ["/data"] -ENTRYPOINT ["/bin/maddy", "-config", "/data/maddy.conf", "run"] +ENTRYPOINT ["/bin/maddy", "-config", "/data/maddy.conf"] +CMD ["run"] diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 00000000..36b18c32 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,75 @@ +# Docker + +Official Docker image is available from Docker Hub. + +It expects configuration file to be available at /data/maddy.conf. + +If /data is a Docker volume, then default configuration will be placed there +automatically. If it is used, then MADDY_HOSTNAME, MADDY_DOMAIN environment +variables control the host name and primary domain for the server. TLS +certificate should be placed in /data/tls/fullchain.pem, private key in +/data/tls/privkey.pem + +DKIM keys are generated in /data/dkim_keys directory. + +## Image tags + +- `latest` - A latest stable release. May contain breaking changes. +- `X.Y` - A specific feature branch, it is recommended to use these tags to + receive bugfixes without the risk of feature-related regressions or breaking + changes. +- `X.Y.Z` - A specific stable release + +## Ports + +All standard ports, as described in maddy docs. + +- `25` - SMTP inbound port. +- `465`, `587` - SMTP Submission ports +- `993`, `143` - IMAP4 ports + +## Volumes + +`/data` - maddy state directory. Databases, queues, etc are stored here. You +might want to mount a named volume there. The main configuration file is stored +here too (`/data/maddy.conf`). + +## Management utility + +To run management commands, create a temporary container with the same +/data directory and put the command after the image name, like this: + +``` +docker run --rm -it -v maddydata:/data foxcpp/maddy:0.6.0 creds create foxcpp@maddy.test +docker run --rm -it -v maddydata:/data foxcpp/maddy:0.6.0 imap-acct create foxcpp@maddy.test +``` + +Use the same image version as the running server. Things may break badly +otherwise. + +Note that, if you modify messages using maddyctl while the server is running - +you must ensure that /tmp from the server is accessible for the management +command. One way to it is to run it using `docker exec` instead of `docker run`: +``` +docker exec -it container_name_here maddy creds create foxcpp@maddy.test +``` + +## TL;DR + +``` +docker volume create maddydata +docker run \ + --name maddy \ + -e MADDY_HOSTNAME=mx.maddy.test \ + -e MADDY_DOMAIN=maddy.test \ + -v maddydata:/data \ + -p 25:25 \ + -p 143:143 \ + -p 587:587 \ + -p 993:993 \ + foxcpp/maddy:0.6 +``` + +It will fail on first startup. Copy TLS certificate to /data/tls/fullchain.pem +and key to /data/tls/privkey.pem. Run the server again. Finish DNS configuration +(DKIM keys, etc) as described in [tutorials/setting-up/](tutorials/setting-up/). \ No newline at end of file diff --git a/maddy.conf b/maddy.conf index 788c9d6c..3e285a32 100644 --- a/maddy.conf +++ b/maddy.conf @@ -1,12 +1,9 @@ -## Maddy Mail Server - default configuration file (2021-08-16) +## Maddy Mail Server - default configuration file (2022-06-18) # Suitable for small-scale deployments. Uses its own format for local users DB, # should be managed via maddyctl utility. # # See tutorials at https://maddy.email for guidance on typical # configuration changes. -# -# See manual pages (also available at https://maddy.email) for reference -# documentation. # ---------------------------------------------------------------------------- # Base variables diff --git a/maddy.conf.docker b/maddy.conf.docker new file mode 100644 index 00000000..fd04e1ac --- /dev/null +++ b/maddy.conf.docker @@ -0,0 +1,182 @@ +## Maddy Mail Server - default configuration file (2022-06-18) +## This is the copy of maddy.conf with changes necessary to run it in Docker. +# Suitable for small-scale deployments. Uses its own format for local users DB, +# should be managed via maddyctl utility. +# +# See tutorials at https://maddy.email for guidance on typical +# configuration changes. + +# ---------------------------------------------------------------------------- +# Base variables + +$(hostname) = {env:MADDY_HOSTNAME} +$(primary_domain) = {env:MADDY_DOMAIN} +$(local_domains) = $(primary_domain) + +tls file /data/tls/fullchain.pem /data/tls/privkey.pem + +# ---------------------------------------------------------------------------- +# Local storage & authentication + +# pass_table provides local hashed passwords storage for authentication of +# users. It can be configured to use any "table" module, in default +# configuration a table in SQLite DB is used. +# Table can be replaced to use e.g. a file for passwords. Or pass_table module +# can be replaced altogether to use some external source of credentials (e.g. +# PAM, /etc/shadow file). +# +# If table module supports it (sql_table does) - credentials can be managed +# using 'maddyctl creds' command. + +auth.pass_table local_authdb { + table sql_table { + driver sqlite3 + dsn credentials.db + table_name passwords + } +} + +# imapsql module stores all indexes and metadata necessary for IMAP using a +# relational database. It is used by IMAP endpoint for mailbox access and +# also by SMTP & Submission endpoints for delivery of local messages. +# +# IMAP accounts, mailboxes and all message metadata can be inspected using +# imap-* subcommands of maddyctl utility. + +storage.imapsql local_mailboxes { + driver sqlite3 + dsn imapsql.db +} + +# ---------------------------------------------------------------------------- +# SMTP endpoints + message routing + +hostname $(hostname) + +table.chain local_rewrites { + optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3" + optional_step static { + entry postmaster postmaster@$(primary_domain) + } + optional_step file /etc/maddy/aliases +} + +msgpipeline local_routing { + # Insert handling for special-purpose local domains here. + # e.g. + # destination lists.example.org { + # deliver_to lmtp tcp://127.0.0.1:8024 + # } + + destination postmaster $(local_domains) { + modify { + replace_rcpt &local_rewrites + } + + deliver_to &local_mailboxes + } + + default_destination { + reject 550 5.1.1 "User doesn't exist" + } +} + +smtp tcp://0.0.0.0:25 { + limits { + # Up to 20 msgs/sec across max. 10 SMTP connections. + all rate 20 1s + all concurrency 10 + } + + dmarc yes + check { + require_mx_record + dkim + spf + } + + source $(local_domains) { + reject 501 5.1.8 "Use Submission for outgoing SMTP" + } + default_source { + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + reject 550 5.1.1 "User doesn't exist" + } + } +} + +submission tls://0.0.0.0:465 tcp://0.0.0.0:587 { + limits { + # Up to 50 msgs/sec across any amount of SMTP connections. + all rate 50 1s + } + + auth &local_authdb + + source $(local_domains) { + check { + authorize_sender { + prepare_email &local_rewrites + user_to_email identity + } + } + + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + modify { + dkim $(primary_domain) $(local_domains) default + } + deliver_to &remote_queue + } + } + default_source { + reject 501 5.1.8 "Non-local sender domain" + } +} + +target.remote outbound_delivery { + limits { + # Up to 20 msgs/sec across max. 10 SMTP connections + # for each recipient domain. + destination rate 20 1s + destination concurrency 10 + } + mx_auth { + dane + mtasts { + cache fs + fs_dir mtasts_cache/ + } + local_policy { + min_tls_level encrypted + min_mx_level none + } + } +} + +target.queue remote_queue { + target &outbound_delivery + + autogenerated_msg_domain $(primary_domain) + bounce { + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + reject 550 5.0.0 "Refusing to send DSNs to non-local addresses" + } + } +} + +# ---------------------------------------------------------------------------- +# IMAP endpoints + +imap tls://0.0.0.0:993 tcp://0.0.0.0:143 { + auth &local_authdb + storage &local_mailboxes +}