Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
# ── Required ──────────────────────────────────────────────

# fmsg domain name this host serves
# - fmsgd will uses TCP 4930 on fmsg.<FMSG_DOMAIN>
# - fmsg-webapi uses HTTPS 443 on fmsgapi.<FMSG_DOMAIN>
FMSG_DOMAIN=example.com

# Email address for Let's Encrypt certificate registration
CERTBOT_EMAIL=

# HMAC secret used to validate JWT tokens for fmsg-webapi
# Prefix with base64: to supply a base64-encoded key (e.g. base64:c2VjcmV0)
FMSG_API_JWT_SECRET=changeme

# Per-service database passwords (used by application services)
FMSGD_WRITER_PGPASSWORD=changeme
FMSGID_WRITER_PGPASSWORD=changeme

# ── Optional (defaults shown) ────────────────────────────

# GIT_SSL_NO_VERIFY=false
# FMSG_PORT=4930
# FMSGID_PORT=8080
# GIN_MODE=release
# FMSG_SKIP_DOMAIN_IP_CHECK=false
# PGUSER=postgres
98 changes: 98 additions & 0 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Quickstart - Setting up an fmsg host with fmsg-docker

This quickstart gets the docker compose stack from this repository up and running on your server. TLS provisioning is included and an HTTPS API is exposed so you can start sending and receiving fmsg messages for your domain. TCP port 4930 is also exposed for fmsg host-to-host communication.

To learn more about fmsg, see the documentation repository: (fmsg)[https://github.com/markmnl/fmsg].

Read the (README.md)[https://github.com/markmnl/fmsg-docker] of this repo for more about settings and environment being used in this quickstart.

## Requirements

1. A domain you control, e.g `example.com`
2. A server with a public IP and
1. TCP port `4930` open to the internet (fmsg TLS)
2. TCP port `443` open to the internet (fmsg-webapi HTTPS)
3. TCP port `80` open to the internet (only first start - required for initial Let's Encrypt certificate issuance)
3. Docker and Docker Compose

## Steps

### 0. Server Setup

Clone this repository to the server and make sure docker is running.
```
git clone https://github.com/markmnl/fmsg-docker.git
```

### 1. Configure DNS

Create A (or AAAA if your public IP is IPv6) DNS records to resolve to your server IP for:

1. `fmsg.<your-domain>`
2. `fmsgapi.<your-domain>`

_NOTE_ Ensure DNS is kept up-to-date with your server's IP so you can send and receive messages!

### 2. Configure FMSG

Copy the example env file:

```sh
cp .env.example compose/.env
```

Edit `compose/.env` and set at least:

```env
FMSG_DOMAIN=example.com
CERTBOT_EMAIL=
FMSG_API_JWT_SECRET=<secret>
FMSGD_WRITER_PGPASSWORD=<strong-password>
FMSGID_WRITER_PGPASSWORD=<strong-password>
```

_NOTE_
* FMSG_DOMAIN is the domain part of fmsg addresses e.g. in `@user@example.com` would be `example.com`. This server you are setting up is located at the subdomain `fmsg.<your-domain>` but addresses will be at `<your-domain>`, you should only specify `<your-domain>` for FMSG_DOMAIN here.
* CERTBOT_EMAIL is an email address supplied to [Let's Encrypt](https://letsencrypt.org/) for e.g. TLS expiry warnings.
* For all secrets and passwords env vars create your own.

Start the stack for the first time from `compose/` and pass the one-time init passwords on the command line (keep these secret, keep them safe):

(might require sudo)

```sh
cd compose
PGPASSWORD=<postgres-password> \
FMSGD_READER_PGPASSWORD=<strong-password> \
FMSGID_READER_PGPASSWORD=<strong-password> \
docker compose up -d
```

If `fmsgd` is running and port `4930` is reachable on `fmsg.<your domain>`, the host is up.

On first start, certbot will request Let's Encrypt TLS certificates for `fmsg.<your-domain>` and `fmsgapi.<your-domain>`. If certificate issuance fails (e.g. the domains do not resolve to the server or port 80 is blocked), the stack will not start. Certificates are persisted in a Docker volume and reused on subsequent starts. Once certificates are issued port 80 is no longer needed until certificates need to be renewed - usually 90 days.


## Next Steps

### Add Users

Create users (message stores, analoguous to mailboxes) by placing a CSV file in the `fmsgid_data` volume at `/opt/fmsgid/data/addresses.csv`. The format is:

```csv
address,display_name,accepting_new,limit_recv_size_total,limit_recv_size_per_msg,limit_recv_size_per_1d,limit_recv_count_per_1d,limit_send_size_total,limit_send_size_per_msg,limit_send_size_per_1d,limit_send_count_per_1d
@alice@example.com,Alice,true,102400000,10240,102400,1000,102400000,10240,102400,1000
```

You can copy it into the volume with:

```sh
docker compose cp addresses.csv fmsgid:/opt/fmsgid/data/addresses.csv
docker compose restart fmsgid
```

### Connect a Client

* Connect a client such as [fmsg-cli](https://github.com/markmnl/fmsg-cli) to `fmsgapi.<your-domain>` configured with your `FMSG_API_JWT_SECRET` to send and retrieve messages.

_NOTE_ Anyone with `FMSG_API_JWT_SECRET` can mint tokens for your `fmsgapi.<your-domain>` for any user e.g. `@alice@<your-domain>`.
41 changes: 34 additions & 7 deletions compose/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
services:

certbot:
image: certbot/certbot
restart: "no"
entrypoint: ["/bin/sh", "/entrypoint.sh"]
environment:
FMSG_DOMAIN: ${FMSG_DOMAIN}
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
volumes:
- letsencrypt:/etc/letsencrypt
- ../docker/certbot/entrypoint.sh:/entrypoint.sh:ro
ports:
- "80:80"

postgres:
image: postgres:18-alpine
restart: unless-stopped
Expand All @@ -26,18 +39,20 @@ services:
dockerfile: Dockerfile
args:
FMSGID_REF: ${FMSGID_REF:-main}
GIT_SSL_NO_VERIFY: ${GIT_SSL_NO_VERIFY:-}
CACHEBUST: ${CACHEBUST:-}
restart: unless-stopped
environment:
GIN_MODE: ${GIN_MODE:-release}
FMSGID_PORT: ${FMSGID_PORT:-8080}
FMSGID_CSV_FILE: /opt/fmsgid/data/addresses.csv
PGHOST: postgres
PGPORT: 5432
PGDATABASE: fmsgid
PGUSER: fmsgid_writer
PGPASSWORD: ${FMSGID_WRITER_PGPASSWORD}
PGSSLMODE: disable
volumes:
- fmsgid_data:/opt/fmsgid/data
depends_on:
postgres:
condition: service_healthy
Expand All @@ -48,7 +63,6 @@ services:
dockerfile: Dockerfile
args:
FMSGD_REF: ${FMSGD_REF:-main}
GIT_SSL_NO_VERIFY: ${GIT_SSL_NO_VERIFY:-}
CACHEBUST: ${CACHEBUST:-}
restart: unless-stopped
environment:
Expand All @@ -57,8 +71,8 @@ services:
FMSG_ID_URL: http://fmsgid:${FMSGID_PORT:-8080}
FMSG_SKIP_DOMAIN_IP_CHECK: ${FMSG_SKIP_DOMAIN_IP_CHECK:-false}
FMSG_SKIP_AUTHORISED_IPS: ${FMSG_SKIP_AUTHORISED_IPS:-false}
FMSG_TLS_CERT: ${FMSG_TLS_CERT:-}
FMSG_TLS_KEY: ${FMSG_TLS_KEY:-}
FMSG_TLS_CERT: /etc/letsencrypt/live/fmsg.${FMSG_DOMAIN}/fullchain.pem
FMSG_TLS_KEY: /etc/letsencrypt/live/fmsg.${FMSG_DOMAIN}/privkey.pem
PGHOST: postgres
PGPORT: 5432
PGDATABASE: fmsgd
Expand All @@ -67,9 +81,12 @@ services:
PGSSLMODE: disable
volumes:
- fmsg_data:/opt/fmsg/data
- letsencrypt:/etc/letsencrypt:ro
ports:
- "${FMSG_PORT:-4930}:4930"
depends_on:
certbot:
condition: service_completed_successfully
postgres:
condition: service_healthy
fmsgid:
Expand All @@ -81,7 +98,6 @@ services:
dockerfile: Dockerfile
args:
FMSG_WEBAPI_REF: ${FMSG_WEBAPI_REF:-main}
GIT_SSL_NO_VERIFY: ${GIT_SSL_NO_VERIFY:-}
CACHEBUST: ${CACHEBUST:-}
restart: unless-stopped
environment:
Expand All @@ -94,14 +110,25 @@ services:
PGDATABASE: fmsgd
PGUSER: fmsgd_writer
PGPASSWORD: ${FMSGD_WRITER_PGPASSWORD}
FMSG_TLS_CERT: /etc/letsencrypt/live/fmsgapi.${FMSG_DOMAIN}/fullchain.pem
FMSG_TLS_KEY: /etc/letsencrypt/live/fmsgapi.${FMSG_DOMAIN}/privkey.pem
FMSG_DATA_DIR: /opt/fmsg/data
PGSSLMODE: disable
volumes:
- fmsg_data:/opt/fmsg/data
- letsencrypt:/etc/letsencrypt:ro
ports:
- "443:443"
depends_on:
- fmsgd
- fmsgid
certbot:
condition: service_completed_successfully
fmsgd:
condition: service_started
fmsgid:
condition: service_started

volumes:
postgres_data:
fmsg_data:
fmsgid_data:
letsencrypt:
44 changes: 44 additions & 0 deletions docker/certbot/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/sh
set -e

: "${FMSG_DOMAIN:?FMSG_DOMAIN is required}"
: "${CERTBOT_EMAIL:?CERTBOT_EMAIL is required}"

FMSGD_DOMAIN="fmsg.${FMSG_DOMAIN}"
WEBAPI_DOMAIN="fmsgapi.${FMSG_DOMAIN}"

# Skip issuance if both certificates already exist
if [ -d "/etc/letsencrypt/live/${FMSGD_DOMAIN}" ] && \
[ -d "/etc/letsencrypt/live/${WEBAPI_DOMAIN}" ]; then
echo "Certificates for ${FMSGD_DOMAIN} and ${WEBAPI_DOMAIN} already exist, skipping."
exit 0
fi

echo "Requesting certificate for ${FMSGD_DOMAIN} ..."
certbot certonly \
--standalone \
--non-interactive \
--agree-tos \
--email "${CERTBOT_EMAIL}" \
-d "${FMSGD_DOMAIN}"

echo "Requesting certificate for ${WEBAPI_DOMAIN} ..."
certbot certonly \
--standalone \
--non-interactive \
--agree-tos \
--email "${CERTBOT_EMAIL}" \
-d "${WEBAPI_DOMAIN}"

# certbot creates private keys as root:root 0600. The application
# containers run as an unprivileged user so the keys must be readable.
chmod 0644 "/etc/letsencrypt/live/${FMSGD_DOMAIN}/privkey.pem" \
"/etc/letsencrypt/live/${WEBAPI_DOMAIN}/privkey.pem"
chmod 0755 /etc/letsencrypt/live \
/etc/letsencrypt/archive \
"/etc/letsencrypt/live/${FMSGD_DOMAIN}" \
"/etc/letsencrypt/live/${WEBAPI_DOMAIN}" \
"/etc/letsencrypt/archive/${FMSGD_DOMAIN}" \
"/etc/letsencrypt/archive/${WEBAPI_DOMAIN}"

echo "Certificates issued successfully."
7 changes: 1 addition & 6 deletions docker/fmsg-webapi/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
FROM golang:1.25 AS builder

ARG FMSG_WEBAPI_REF=main
ARG GIT_SSL_NO_VERIFY=
ARG CACHEBUST

WORKDIR /build

RUN if [ "$GIT_SSL_NO_VERIFY" = "true" ]; then \
git config --global http.sslVerify false; \
export GOINSECURE='*' GONOSUMDB='*' GONOSUMCHECK='*' GOPROXY=direct; \
fi && \
git clone --branch "$FMSG_WEBAPI_REF" --depth 1 https://github.com/markmnl/fmsg-webapi.git . && \
RUN git clone --branch "$FMSG_WEBAPI_REF" --depth 1 https://github.com/markmnl/fmsg-webapi.git . && \
cd src && \
go build -o fmsg-webapi .

Expand Down
10 changes: 4 additions & 6 deletions docker/fmsgd/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
FROM golang:1.25 AS builder

ARG FMSGD_REF=main
ARG GIT_SSL_NO_VERIFY=
ARG CACHEBUST

WORKDIR /build

RUN if [ "$GIT_SSL_NO_VERIFY" = "true" ]; then \
git config --global http.sslVerify false; \
export GOINSECURE='*' GONOSUMDB='*' GONOSUMCHECK='*' GOPROXY=direct; \
fi && \
git clone --branch "$FMSGD_REF" --depth 1 https://github.com/markmnl/fmsgd.git . && \
RUN git clone --branch "$FMSGD_REF" --depth 1 https://github.com/markmnl/fmsgd.git . && \
cd src && \
go build -o fmsgd .

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*

RUN useradd -r -s /bin/false fmsg

WORKDIR /opt/fmsgd
Expand Down
7 changes: 1 addition & 6 deletions docker/fmsgid/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
FROM golang:1.25 AS builder

ARG FMSGID_REF=main
ARG GIT_SSL_NO_VERIFY=
ARG CACHEBUST

WORKDIR /build

RUN if [ "$GIT_SSL_NO_VERIFY" = "true" ]; then \
git config --global http.sslVerify false; \
export GOINSECURE='*' GONOSUMDB='*' GONOSUMCHECK='*' GOPROXY=direct; \
fi && \
git clone --branch "$FMSGID_REF" --depth 1 https://github.com/markmnl/fmsgid.git . && \
RUN git clone --branch "$FMSGID_REF" --depth 1 https://github.com/markmnl/fmsgid.git . && \
cd src && \
go build -o fmsgid .

Expand Down
21 changes: 20 additions & 1 deletion test/docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,24 @@

services:

certbot:
entrypoint: ["true"]
restart: "no"
ports: !override []
profiles: ["certbot"]

fmsgd:
environment:
FMSG_TLS_CERT: /opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.crt
FMSG_TLS_KEY: /opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.key
FMSG_TLS_INSECURE_SKIP_VERIFY: "true"
volumes:
- ../test/.tls:/opt/fmsg/tls:ro
depends_on: !override
postgres:
condition: service_healthy
fmsgid:
condition: service_started
networks:
default:
fmsg-test:
Expand All @@ -26,7 +37,15 @@ services:
- fmsg.${FMSG_DOMAIN}

fmsg-webapi:
ports:
environment:
FMSG_TLS_CERT: ""
FMSG_TLS_KEY: ""
depends_on: !override
fmsgd:
condition: service_started
fmsgid:
condition: service_started
ports: !override
- "${FMSG_WEBAPI_HOST_PORT:-8081}:${FMSG_API_PORT:-8000}"

networks:
Expand Down
Loading
Loading