# Run a Cashu Mint

Install a CDK Cashu mint (cdk-mintd) from the official prebuilt binary, backed by your own LND node, with Postgres, Redis, a hardened systemd service, and a Cloudflare Tunnel to put it online. Part of the New Mint walkthrough.

The mint. This is the service that issues and redeems ecash using your Lightning node as a backend
for deposits and withdrawals. Mints are elegant in their operational simplicity, once you've gotten
the setup right.

This is the full walkthrough. It uses [CDK](https://github.com/cashubtc/cdk), the Cashu
Development Kit, whose `cdk-mintd` is a production-minded mint daemon in Rust. You
install its official prebuilt binary, point it at your LND node, store its data in the
Postgres you set up for Lightning, and expose it on the internet with a Cloudflare Tunnel.

<Aside type="note" title="Why not Nutshell?">
  This guide uses CDK. `cdk-mintd` is the best choice for a mint in
  {new Date().getFullYear()}. It has a more mature RPC and a richer feature set than
  [Nutshell](https://github.com/cashubtc/nutshell), the reference Python mint.
</Aside>

## What this step covers

- **`cdk-mintd` installed from its official prebuilt binary**, checksum-verified and placed as a system binary.
- **An isolated `cdk-mintd` service user** that reads your LND node's TLS
  certificate and macaroon.
- **Postgres and Redis** wired in: Postgres for the mint database
  (the same instance your LND node uses) and Redis for the response cache.
- **A hardened systemd service** so the mint starts on boot and restarts on
  failure.
- **A public mint URL** served through a Cloudflare Tunnel, with TLS terminated at
  Cloudflare's edge.

## Requirements

- **A running LND node** from the [Lightning step](/new-mint/lightning-node/),
  reachable on its gRPC port (`10009` by default), with its TLS certificate and
  macaroons on this machine.
- **PostgreSQL installed and running.** The Lightning step sets this up if you
  followed its tip to choose the Postgres backend. You reuse that instance here.
- **A domain.** Registered with any registrar. The mint is exposed through a Cloudflare
  Tunnel, which routes a hostname on your domain to it.
- **A free Cloudflare account.** Add your domain to it and point the domain's
  nameservers at Cloudflare, so the tunnel can manage the hostname's DNS.

<Aside type="caution" title="Postgres comes from the Lightning step">
  This guide assumes you took MiniBolt's **PostgreSQL** path for LND, not the default
  bbolt backend, so Postgres is already running on `127.0.0.1:5432` with the `admin`
  role. If you ran LND on bbolt, install Postgres now with MiniBolt's
  [PostgreSQL guide](https://minibolt.minibolt.info/bonus-guides/system/postgresql)
  before continuing.
</Aside>

<Aside type="note" title="Other ways to serve your mint">
  A public hostname is all the mint needs; how you provide it is up to you. A static IP
  with port forwarding, or another tunnel service, work just as well. This guide uses a
  Cloudflare Tunnel because it's a solid free, no-KYC option that needs no static IP or
  open ports.
</Aside>

## Preparations

### Install Redis

`cdk-mintd` caches deterministic responses (NUT-19) so a retried mint, swap, or melt
returns the same result instead of being processed twice. Backing that cache with
Redis keeps it across restarts.

* With user `admin`, install Redis and enable it to start on boot

```bash
sudo apt update
sudo apt install redis-server
sudo systemctl enable redis-server
```

* Confirm Redis is listening on `127.0.0.1` only, reachable from this machine and not the network

```bash
sudo ss -tulpn | grep 6379
```

**Example** of expected output:

```
tcp   LISTEN 0   511   127.0.0.1:6379   0.0.0.0:*   users:(("redis-server",pid=1234,fd=6))
```

### Create the mint database

Reuse the Postgres instance your LND node runs on, but give the mint its own role
instead of the shared `admin` one. Create a `cdk_mintd` role and a database it owns.

* With user `admin`, create the role and its database, choosing a strong password

```bash
sudo -u postgres psql -c "CREATE ROLE cdk_mintd WITH LOGIN PASSWORD 'your-mint-db-password';"
sudo -u postgres createdb -O cdk_mintd cdk_mintd
```

<Aside type="note" title="Pick a real password">
  Replace `your-mint-db-password` with one you generate, and use the same value in the
  connection URL below. The mint owns this database; Orchard later connects with a
  separate read-only role (see the [Orchard step](/new-mint/orchard/)).
</Aside>

## Installation

### Create the cdk-mintd user & group

For improved security, create a dedicated `cdk-mintd` user to run the mint. A
dedicated user limits the damage if the daemon is ever compromised: an attacker is
confined to this user's permissions and cannot reach the rest of the machine.

* With user `admin`, create the `cdk-mintd` user and group

```bash
sudo adduser --disabled-password --gecos "" cdk-mintd
```

* Add the `cdk-mintd` user to the `lnd` group, so it can read your Lightning node's
  TLS certificate and macaroon

```bash
sudo usermod -aG lnd cdk-mintd
```

### Give the mint access to your LND credentials

`cdk-mintd` authenticates to LND with a TLS certificate and an `admin` macaroon. As a
member of the `lnd` group, the `cdk-mintd` user has read-only access to those files;
symlink the LND data directory into its home so the
daemon finds them under `~/.lnd`.

* Change to the `cdk-mintd` user

```bash
sudo su - cdk-mintd
```

* Link the LND data directory into the `cdk-mintd` home

```bash
ln -s /data/lnd /home/cdk-mintd/.lnd
```

* Check that the symbolic link was created correctly

```bash
ls -la /home/cdk-mintd/.lnd
```

**Example** of expected output:

```
lrwxrwxrwx 1 cdk-mintd cdk-mintd 9 Jun 19 12:00 /home/cdk-mintd/.lnd -> /data/lnd
```

* Come back to the `admin` user

```bash
exit
```

* Make the LND data directories browsable by the group, and allow the group to read
  the `admin.macaroon`

```bash
sudo chmod -R g+X /data/lnd/data/
sudo chmod g+r /data/lnd/data/chain/bitcoin/mainnet/admin.macaroon
```

<Aside type="note" title="Why the admin macaroon">
  A mint must pay invoices to settle melts, so a read-only macaroon is not enough.
  This guide uses LND's `admin.macaroon`, matching CDK's own example. To grant less,
  bake a custom macaroon limited to invoice and payment permissions and point the
  config at that instead (see [Extras](#extras-optional)).
</Aside>

### Download and install cdk-mintd

CDK publishes a static binary for each release, built with the Postgres and Redis
features this guide uses, so there is nothing to compile.

* With user `admin`, download the release binary for your architecture and its checksum
  file

<Code
  lang="bash"
  code={`cd /tmp
VERSION=${latestCdkVersion}
ARCH=$(uname -m)
wget https://github.com/cashubtc/cdk/releases/download/v$VERSION/cdk-mintd-$VERSION-$ARCH
wget https://github.com/cashubtc/cdk/releases/download/v$VERSION/SHA256SUMS`}
/>

* Verify the download against the checksum published with the release

```bash
sha256sum --check --ignore-missing SHA256SUMS
```

**Example** of expected output:

<Code code={`cdk-mintd-${latestCdkVersion}-x86_64: OK`} />

* Install the binary to a system path, confirm the version, and remove the downloads

```bash
sudo install -m 0755 cdk-mintd-$VERSION-$ARCH /usr/local/bin/cdk-mintd
cdk-mintd --version
rm cdk-mintd-$VERSION-$ARCH SHA256SUMS
```

**Example** of expected output:

<Code code={`cdk-mintd ${latestCdkVersion}`} />

### Create the data directory

The mint keeps its configuration and seed in a work directory. Following the same
layout, put it under `/data`.

* With user `admin`, create the work directory and give it to the `cdk-mintd` user

```bash
sudo mkdir -p /data/cdk-mintd
sudo chown -R cdk-mintd:cdk-mintd /data/cdk-mintd
```

### Generate the mint seed

`cdk-mintd` derives the mint's signing keys from a standard BIP-39 seed phrase.
The mint signs with it on every operation, so the seed lives here on the server.
CDK does not generate one for you, so make a fresh 24 word phrase with the BIP-39 reference
implementation, which Ubuntu packages.

* With user `admin`, install the generator and print a fresh phrase

```bash
sudo apt install python3-mnemonic
python3 -c "from mnemonic import Mnemonic; print(Mnemonic('english').generate(strength=256))"
```

**Example** of expected output (yours will differ, and you must use your own):

```
army van defense carry jealous true garbage claim echo media make ...
```

* Write the phrase down and keep an offline copy before going further. You paste it
  into `config.toml` in the next step, but the server should not be the only place it
  lives: if the disk dies, that offline copy is half of how you restore the mint.

<Aside type="danger" title="The seed is half of your backup">
  This phrase controls every keyset the mint issues, and it never changes. Have a
  backup plan for this seed. The seed alone cannot rebuild a mint:
  recovery needs the seed **together with** a recent copy of the mint's database (see
  [Backup and restore](/orchard/mint/#database)). Lose the seed and the mint
  can no longer sign; leak it and someone else can.
</Aside>

## Configuration

* Change to the `cdk-mintd` user and create the config file in the work directory

```bash
sudo su - cdk-mintd
nano /data/cdk-mintd/config.toml
```

* Paste the following, replacing the placeholders. Set `url` to the public hostname
  you will route through the tunnel below, paste your seed into `mnemonic`, and use
  the LND macaroon path for your network (`mainnet` shown). Save and exit

```toml title="/data/cdk-mintd/config.toml"
[info]
url = "https://mint.yourdomain.com/"
listen_host = "127.0.0.1"
listen_port = 8085
mnemonic = "your twelve or twenty-four word seed phrase here"

[info.http_cache]
backend = "redis"
ttl = 60
tti = 60
key_prefix = "mintd"
connection_string = "redis://127.0.0.1:6379"

[mint_info]
name = "Your Mint"
description = "Sovereign bank in cyberspace"
contact_email = "you@yourdomain.com"

[database]
engine = "postgres"

[database.postgres]
url = "postgresql://cdk_mintd:your-mint-db-password@127.0.0.1:5432/cdk_mintd"
tls_mode = "disable"

[ln]
ln_backend = "lnd"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000

[lnd]
address = "https://localhost:10009"
cert_file = "/home/cdk-mintd/.lnd/tls.cert"
macaroon_file = "/home/cdk-mintd/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"

[mint_management_rpc]
enabled = true
address = "127.0.0.1"
port = 8086
```

* Come back to the `admin` user

```bash
exit
```

<Aside type="note" title="Managing the mint through Orchard">
  With `[mint_management_rpc]` enabled, the mint reads `[mint_info]` from this file only
  on first start. After that, info can be managed over the
  gRPC by Orchard, not by editing `config.toml`. It runs insecurely here because Orchard
  shares this machine, and the port is reachable only from it. If Orchard runs on a
  different host, secure it with
  [mutual TLS](#secure-the-management-api-with-mutual-tls) instead.
</Aside>

### Create systemd service

Now ensure the mint starts as a service so that it is always running, restarts on
failure, and comes back after a reboot.

* With user `admin`, create the service file

```bash
sudo nano /etc/systemd/system/cdk-mintd.service
```

* Paste the following configuration. Save and exit

```ini title="/etc/systemd/system/cdk-mintd.service"
# Orchard: systemd unit for cdk-mintd
# /etc/systemd/system/cdk-mintd.service

[Unit]
Description=CDK Cashu mint daemon
Requires=lnd.service postgresql.service redis-server.service
After=lnd.service postgresql.service redis-server.service

[Service]
ExecStart=/usr/local/bin/cdk-mintd --work-dir /data/cdk-mintd

User=cdk-mintd
Group=cdk-mintd

# Process management
####################
Restart=on-failure
RestartSec=30
Type=simple

# Hardening Measures
####################
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true

[Install]
WantedBy=multi-user.target
```

* Reload systemd so it sees the new unit

```bash
sudo systemctl daemon-reload
```

* Enable autoboot **(optional)**

```bash
sudo systemctl enable cdk-mintd
```

* Prepare `cdk-mintd` monitoring by the systemd journal. You can exit monitoring at
  any time with `Ctrl-C`

```bash
journalctl -fu cdk-mintd
```

## Run

To keep an eye on the mint as it starts, open a second terminal, connect to the node,
and log in as `admin`.

* With user `admin`, start the service

```bash
sudo systemctl start cdk-mintd
```

Watch the journal in the other terminal until it settles.

### Validation

* Ensure the mint is listening on its local `8085` port

```bash
sudo ss -tulpn | grep 8085
```

**Example** of expected output:

```
tcp   LISTEN 0   1024   127.0.0.1:8085   0.0.0.0:*   users:(("cdk-mintd",pid=5678,fd=10))
```

* Ask the mint for its public info from the machine itself. It returns a JSON
  document describing your mint

```bash
curl http://127.0.0.1:8085/v1/info
```

**Example** of expected output (truncated):

<Code code={`{"name":"Your Mint","version":"cdk-mintd/${latestCdkVersion}","description":"Sovereign bank in cyberspace","contact":[...],"nuts":{...}}`} />

<Aside type="tip" title="Mint is up">
  A JSON response from `/v1/info` means the mint is running and serving its API.
  Next step is to make the mint publicly reachable.
</Aside>

## Put your mint online with a Cloudflare Tunnel

Wallets reach a mint over HTTPS, so the mint needs a public hostname. A Cloudflare
Tunnel makes one outbound connection from this machine to Cloudflare's network and
forwards traffic to the mint's local port — your home IP is never exposed, and there is
no router port to forward.

Follow MiniBolt's
[Cloudflare Tunnel guide](https://minibolt.minibolt.info/bonus-guides/networking/cloudflare-tunnel)
to install `cloudflared`, authenticate it to your Cloudflare account, create a tunnel,
and run it as a `systemd` service. It is the same `cloudflared` setup MiniBolt uses for
its other services, with checksum-verified downloads and screenshots for the Cloudflare
dashboard steps. Two things are specific to the mint:

* **Use your mint's hostname throughout.** Wherever the guide uses `subdomain.domain.com`
  — in the `cloudflared tunnel route dns` command and in the tunnel config — use the same
  hostname you set as `url` in `config.toml`, for example `mint.yourdomain.com`.
* **Route it to the mint's local port.** In the tunnel's `config.yml`, replace the
  guide's example ingress rules with a single rule for the mint, keeping the
  `http_status:404` rule last:

```yaml
ingress:
  # Cashu mint
  - hostname: mint.yourdomain.com
    service: http://localhost:8085
  - service: http_status:404
```

With the tunnel running, ask your public mint for its info from any machine. You should
get the same JSON as the local check, now over HTTPS:

```bash
curl https://mint.yourdomain.com/v1/info
```

<Aside type="caution" title="Cloudflare sees your traffic">
  A Cloudflare Tunnel hides your home IP, but Cloudflare terminates TLS at its edge and
  can read or modify your mint's traffic. That's the trade-off for the convenience.
</Aside>

<Aside type="tip" title="Mint is online">
  Congrats! Your mint now answers on `https://mint.yourdomain.com/` and settles
  through your own Lightning node. The last step is handing it to Orchard.
</Aside>

## Extras (optional)

- **Mint information.** `name`, `description`, `contact_email`, `motd`, and `icon_url`
  in `[mint_info]` are what wallets show before someone trusts your mint. Keep them
  accurate. Once Orchard is connected you manage these from the dashboard; see
  [Mint information](/orchard/mint/#info).
- **Fees and limits.** Set `input_fee_ppk` under `[info]` to charge a per-input fee,
  `fee_percent` and `reserve_fee_min` under `[lnd]` to tune the Lightning fee reserve,
  and `max_inputs` / `max_outputs` under `[limits]` to cap transaction size. Once Orchard
  is connected, you can change the input fee by rotating to a new keyset; see
  [Keysets](/orchard/mint/#keysets).
- **A least-privilege macaroon.** Instead of `admin.macaroon`, bake an LND macaroon
  limited to invoice and offchain permissions and point `macaroon_file` at it, so a
  leaked mint credential cannot touch the rest of your node.
- **Other ways to expose the mint.** The Tunnel is the easy option and hides your IP.
  Alternatives: a VPS relay you control (also hides your IP, no Cloudflare in the path),
  or a forwarded port from home (simplest, but exposes your IP). Tor won't work as wallets
  can't reach a `.onion` mint.

### Secure the management API with mutual TLS

The management gRPC runs insecurely in the main setup because Orchard shares this machine
and the port is reachable only from the machine itself. If Orchard runs on a different host, or you want
authentication once the connection leaves this machine, give the interface mutual TLS instead.
`cdk-mintd` does not generate these certificates: you create a small private CA that
signs one certificate for the mint and one for Orchard.

* As the `cdk-mintd` user, create the TLS directory the mint reads and generate the
  certificates

```bash
sudo su - cdk-mintd
mkdir -p /data/cdk-mintd/tls && cd /data/cdk-mintd/tls

# A private CA that signs both the mint (server) and Orchard (client)
openssl genpkey -algorithm RSA -out ca.key
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -subj "/CN=cdk-mintd CA" -out ca.pem

# The mint's server certificate, valid for the local address Orchard dials
openssl genpkey -algorithm RSA -out server.key
openssl req -new -key server.key -subj "/CN=cdk-mintd" -out server.csr
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 3650 -sha256 \
  -extfile <(printf "subjectAltName=IP:127.0.0.1,DNS:localhost") -out server.pem

# Orchard's client certificate, signed by the same CA
openssl genpkey -algorithm RSA -out client.key
openssl req -new -key client.key -subj "/CN=orchard" -out client.csr
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 3650 -sha256 -out client.pem

rm server.csr client.csr
exit
```

The mint reads `server.pem`, `server.key`, and `ca.pem` from this directory. Because the
directory now exists with certificates in it, `cdk-mintd` serves the management gRPC over
mutual TLS on its next start, and any client must present a certificate signed by your CA.

* With user `admin`, restart the mint to pick up the certificates

```bash
sudo systemctl restart cdk-mintd
```

Give Orchard the three client files so it can connect: `ca.pem` to verify the mint, plus
`client.pem` and `client.key` as its own identity. To return to running insecurely, stop
the mint, remove `/data/cdk-mintd/tls`, and start it again.

## Upgrade

* With user `admin`, stop the service

```bash
sudo systemctl stop cdk-mintd
```

* Back up the mint database before upgrading, so you can roll back if the new version
  misbehaves. Orchard reads the database straight from Postgres, so the backup works with
  the mint stopped — and stopping first gives a clean snapshot. See
  [Backup and restore](/orchard/mint/#database).

* Download the new release binary and its checksum for your architecture (replace the
  version with the release you want)

<Code
  lang="bash"
  code={`cd /tmp
VERSION=${latestCdkVersion}
ARCH=$(uname -m)
wget https://github.com/cashubtc/cdk/releases/download/v$VERSION/cdk-mintd-$VERSION-$ARCH
wget https://github.com/cashubtc/cdk/releases/download/v$VERSION/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS`}
/>

* Replace the binary, clean up, and start the service again

```bash
sudo install -m 0755 cdk-mintd-$VERSION-$ARCH /usr/local/bin/cdk-mintd
rm cdk-mintd-$VERSION-$ARCH SHA256SUMS
sudo systemctl start cdk-mintd
```

## Uninstall

<Aside type="danger" title="This removes the mint">
  Warning: this section removes the mint, its data, and its public route. Only run
  these commands if you intend to uninstall.
</Aside>

### Uninstall service

* With user `admin`, stop and disable the service, then remove the unit

```bash
sudo systemctl stop cdk-mintd
sudo systemctl disable cdk-mintd
sudo rm /etc/systemd/system/cdk-mintd.service
sudo systemctl daemon-reload
```

### Delete user & group

* Delete the `cdk-mintd` user. Do not worry about the
  `userdel: cdk-mintd mail spool (/var/mail/cdk-mintd) not found` message

```bash
sudo userdel -rf cdk-mintd
```

### Drop the database

* Drop the mint database

```bash
sudo -u postgres dropdb cdk_mintd
```

### Remove the binary and data

* Remove the work directory and the installed binary

```bash
sudo rm -rf /data/cdk-mintd
sudo rm /usr/local/bin/cdk-mintd
```

### Remove the public route

* Delete the mint's ingress entry from `/home/admin/.cloudflared/config.yml`, remove
  the DNS record from your Cloudflare dashboard, and delete the tunnel if it is no
  longer used. Use the name you gave it (`cloudflared tunnel list` shows it)

```bash
cloudflared tunnel delete <NAME>
```

## Port reference

| Port | Protocol | Use |
| --- | --- | --- |
| 8085 | TCP (localhost) | Mint HTTP API, forwarded by the tunnel |
| 8086 | TCP (localhost) | Management gRPC, used by Orchard |
| 6379 | TCP (localhost) | Redis response cache |
| 5432 | TCP (localhost) | Postgres, shared with LND |

The mint has no inbound public port. Wallets reach it through the Cloudflare Tunnel's
outbound connection.
