GoodMem
ReferenceSecurity

Local TLS with `mkcert`

Overview of use of `mkcert` for TLS with a local CA.

Local TLS with mkcert

When it comes to GoodMem, we consider TLS at the service boundary a baseline responsibility, not an optional enhancement. While users can always opt into plaintext with --tls-disabled and set up their own service boundary, we want to make default, TLS-enabled operation as frictionless as possible. GoodMem does support self-signed certificates today, but those come with their own problems in browsers and applications that treat them as low trust. With the CLI tool mkcert, users can host a local certificate authority (CA) to develop within TLS without those problems.

GoodMem with mkcert

mkcert is a vetted tool for making locally-trusted certificates for development environments. If you've already got mkcert installed on your machine, or you'll be using another similar tool, feel free to skip ahead. Otherwise, let's get started.

Installing mkcert

mkcert may be installed via package managers, pre-built binaries, or building from source. See its documentation for more details: https://github.com/FiloSottile/mkcert

Running mkcert

Once installed, mkcert needs to create a local certificate authority (CA) like so:

$ mkcert -install

On success, you should see a message like this:

Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️

Then creating the local certificates is as simple as:

$ mkcert goodmem.test localhost 127.0.0.1 ::1

On success, you should see a message like the following:

Created a new certificate valid for the following names 📜
 - "goodmem.test"
 - "localhost"
 - "127.0.0.1"
 - "::1"

The certificate is at "./goodmem.test+3.pem" and the key at "./goodmem.test+3-key.pem" ✅

Among the options provided by mkcert, you may want to use the -cert-file FILE and -key-file FILE flags. For example:

$ mkcert -key-file secret/file/here/key.pem -cert-file secret/file/there/cert.pem goodmem.test

Running goodmem with TLS Certificates

At this point, you now have two options to tell GoodMem about your new certificates. You can pass these either as CLI flags or as environment variables, as described in TLS Configuration. In both cases, the file options are paired: passing one file requires you to pass the other as well.

Environment variables

As is idiomatic with Docker containers, the GoodMem Docker image expects its configuration variables to be passed in via environment variables. In particular, the TLS files should be specified as:

Environment variableDescription
GOODMEM_TLS_CERT_FILEPEM certificate/chain for user-supplied TLS (requires GOODMEM_TLS_KEY_FILE).
GOODMEM_TLS_KEY_FILEPEM private key for user-supplied TLS (GOODMEM_TLS_CERT_FILE`).
GOODMEM_TLS_TRUST_CERT_FILEReserved for future client-auth support; currently unused for inbound TLS.

CLI Flags

Alternatively, you may pass CLI flags to the GoodMem server directly:

Server flagDescription
--tls-cert-filePEM certificate/chain for user-supplied TLS (requires --tls-key-file).
--tls-key-filePEM private key for user-supplied TLS (requires --tls-cert-file).
--tls-trust-cert-fileReserved for future client-auth support; currently unused for inbound TLS.

Switch an existing self-signed install to mkcert

If you already installed a local-docker GoodMem profile with the default self-signed TLS setup, you can switch that install in place to a locally trusted mkcert certificate.

  1. Stop the current install:

    goodmem system stop
  2. Find the install directory:

    goodmem profile current

    For local-docker installs, the output includes an Install Path. You can also use goodmem profile current --output json and read install_path.

  3. Change into that install directory:

    cd <install path>
  4. Edit .env and remove the GOODMEM_TLS_SELF_SIGNED_HOSTNAMES line if it is present.

  5. Add these lines to .env:

    GOODMEM_TLS_CERT_FILE=/run/secrets/goodmem_tls_cert
    GOODMEM_TLS_KEY_FILE=/run/secrets/goodmem_tls_key
  6. Generate a certificate and key with mkcert for every hostname or IP address clients will use. For example:

    mkcert goodmem.test localhost 127.0.0.1 ::1
  7. Create a directory for the copied TLS files:

    mkdir -p certs
  8. Move the generated certificate and key into the install directory and lock down the key permissions:

    mv <path to generated cert> certs/cert.pem && chmod 644 certs/cert.pem
    mv <path to generated key> certs/key.pem && chmod 640 certs/key.pem
  9. Edit docker-compose.yml.

    Under the server service, add:

        secrets:
          - goodmem_tls_cert
          - goodmem_tls_key

    At the top level of the file, add:

    secrets:
      goodmem_tls_cert:
        file: <install path>/certs/cert.pem
      goodmem_tls_key:
        file: <install path>/certs/key.pem

    Replace <install path> with the absolute path from step 2.

  10. Edit install-config.json and set:

"tls_mode": "custom"
  1. Start the install again:
goodmem system start

Validation

And that's it! You now have everything you need to run GoodMem with CA-signed certificates. You may validate your server is using the certificates with the following:

$ curl -v --cacert "$(mkcert -CAROOT)/rootCA.pem" https://127.0.0.1:8080/readyz
$ # OR
$ curl -v --cacert "$(mkcert -CAROOT)/rootCA.pem" https://localhost:8080/readyz

You should see a --verbose response with the TLS handshake and more details:

Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: /path/to/mkcert/rootCA.pem
*  CApath: /etc/ssl/certs
* ...
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: O=mkcert development certificate; OU=root@[...]
*  start date: Jan  1 00:00:00 2026 GMT
*  expire date: Apr  1 00:00:00 2028 GMT
*  subjectAltName: host "localhost" matched cert's "localhost"
*  issuer: O=mkcert development CA; OU=root@[...]; CN=mkcert root@[...]
*  SSL certificate verify ok.
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /readyz HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> 
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 30
< Server: Jetty(11.0.26)
< 
* Connection #0 to host localhost left intact
{"ready":true,"state":"READY"}

Note: this would not work without a local CA because "no one can get a universally valid certificate for localhost".

With mkcert or similar tools, such a local CA can be easily set up on your developer's machine to more closely reflect the CA your production deployment relies on. Happy hacking! :)