Skip to content

Installation

SkimMail docs edited this page Sep 17, 2026 · 8 revisions

English · Tiếng Việt · 中文

Installation

SkimMail ships four ways: an apt package, a Docker image, a docker-compose file that pulls that image, and a binary tarball. Pick one — they are independent installs of the same single build (SQLite + PostgreSQL + MySQL + S3 support built in; WireGuard egress is a plugin installed later from Settings ▸ Plugins). There is no "lite" edition to choose between.

Building from source is not something you can do with the public repository. SkimMail is licensed under the Business Source License 1.1: the source lives in a private repository, and only compiled binaries are distributed. github.com/lyquyduong/skimmail — the public repo — holds exactly three files (README.md, README.vi.md, README.zh.md), no source tree. If you have access to the private repository, README.dev.md there covers building from source, dev mode and VS Code setup; there is nothing equivalent to hand you here, and no recipe below will work against the public repo.

Supported platforms

Install method OS / architecture
apt (.deb) Debian/Ubuntu, amd64, arm64
Docker / docker-compose Linux container, amd64, arm64 (multi-arch image)
Binary tarball linux/amd64, linux/arm64, darwin/amd64, darwin/arm64

Choosing a method

You want… Use
A normal Debian/Ubuntu server, managed by systemd, updated with apt upgrade apt
A container host, or you already run everything in Docker Docker or docker-compose
A single self-updating binary with no package manager and no container runtime Binary tarball

Every method prints the same one-time claim code at first start and requires it before the first sign-in — see Security for why. The exact command to read it differs per method and is given in each section below.


apt (Debian / Ubuntu)

Prerequisites

  • Debian or Ubuntu on amd64 or arm64, with sudo access.
  • Outbound HTTPS to lyquyduong.github.io (the apt repo) and to github.com/objects.githubusercontent.com (nothing is fetched from there directly by apt, but the same network path is needed later for self-update checks).

Add the repository and its signing key

curl -fsSL https://lyquyduong.github.io/skimmail/apt/skimmail.gpg \
  | sudo tee /usr/share/keyrings/skimmail.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/skimmail.gpg] https://lyquyduong.github.io/skimmail/apt stable main" \
  | sudo tee /etc/apt/sources.list.d/skimmail.list
sudo apt update

The key you just fetched is an ed25519 OpenPGP key that signs the repository's Release file (apt-ftparchive + gpg, built by the release pipeline); apt refuses the repo if the signature doesn't check out, so there is nothing further to verify by hand.

Install

sudo apt install skimmail

This creates a dedicated system user (skimmail, no login shell), registers and starts skimmail.service, and prints a short banner with the claim-code command. Configuration lives in /etc/default/skimmail (a systemd EnvironmentFile — see Configuration); data lives in /var/lib/skimmail.

Verify it started

sudo systemctl status skimmail
curl http://localhost:8080/healthz

A healthy instance answers {"status":"ok","version":"1.17.0"}. Live logs: sudo journalctl -u skimmail -f (a rotating file also exists at /var/log/skimmail/skimmail.log, viewable and configurable from Settings ▸ Logs once you're signed in).

First run: the claim code

Open http://<server>:8080. Before it lets you create the first account or passphrase, SkimMail asks for the one-time claim code it printed at first boot:

sudo journalctl -u skimmail | grep 'claim code'

sudo is not optional here. Without it, journalctl silently hides another user's unit (the service runs as the skimmail system user, not you) and prints a hint instead of the code — this was reported against 1.9.1 by people who ran the command without sudo, saw nothing useful, and assumed the code had never been printed.

Lost the code, or the log has rotated past it? Mint a replacement — the old one stops working the moment you do:

sudo -u skimmail skimmail --data-dir /var/lib/skimmail claim-code --rotate

The --data-dir /var/lib/skimmail is required. This command does not read /etc/default/skimmail — that file is a systemd EnvironmentFile, handed to the process only when systemd itself starts the unit, never to an interactively-invoked shell command. Without --data-dir, DATA_DIR falls back to its default of ./data, resolved against whatever directory you happened to be standing in — which either fails on a permission error, or (worse) silently creates a throwaway database there and prints a perfectly-valid-looking code the real, running server will never accept. Since 1.9.1 the command refuses to run at all against a directory that holds no instance, so a wrong --data-dir now fails loudly instead of doing that.

Uninstall

sudo apt purge skimmail        # stops the service, removes the binary, the
                                # systemd unit and /etc/default/skimmail

apt purge does not delete your mail — nothing in SkimMail ever has. The package's own removal message tells you exactly where it is. If you actually want it gone:

sudo rm -rf /var/lib/skimmail  # ⚠ deletes every account, message and the
                                #   encryption master key — irreversible
sudo rm -rf /var/log/skimmail  # log file + rotated backups
sudo userdel skimmail          # the system user created at install
sudo rm -f /etc/apt/sources.list.d/skimmail.list /usr/share/keyrings/skimmail.gpg
sudo apt update

Back up first (Settings ▸ Backup) if there's any chance you'll want the data back.


Docker

Prerequisites

  • Docker Engine (any reasonably current version; the image is multi-arch linux/amd64 + linux/arm64, selected automatically).

Run it

docker run -d --name skimmail -p 8080:8080 -v skimmail-data:/data \
  -e AUTH_MODE=passphrase ghcr.io/lyquyduong/skimmail:latest

Name the container skimmail — the commands below assume it. The image sets DATA_DIR=/data and LISTEN_ADDR=:8080 itself; skimmail-data is a named volume so your data survives docker rm. To pin a specific release instead of latest, use the v-prefixed tag, e.g. ghcr.io/lyquyduong/skimmail:v1.17.0.

Verify it started

docker ps                              # shows skimmail as Up
curl http://localhost:8080/healthz     # {"status":"ok","version":"1.17.0"}

The image is distroless (no shell, no package manager inside it) — docker exec skimmail sh will not work. docker exec skimmail /skimmail <command> works, because that runs the SkimMail binary directly rather than a shell; you'll use exactly that form below.

First run: the claim code

docker logs skimmail 2>&1 | grep 'claim code'

Docker's log driver captures everything the single process in the container writes, so — unlike the apt/journalctl case — there's no separate-user permission wrinkle here; you don't need sudo for this command on a normal Docker install (only for the journalctl case above).

Lost the code? Mint a replacement:

docker exec skimmail /skimmail claim-code --rotate

No --data-dir needed here: the image already sets DATA_DIR=/data, so docker exec — which runs inside the container's own environment — sees it automatically. (Contrast this with the apt case above, where the equivalent environment file is never handed to an interactive shell.)

Uninstall

docker rm -f skimmail
docker rmi ghcr.io/lyquyduong/skimmail:latest

Your data is untouched — it lives in the skimmail-data volume, not in the container. To delete it too (⚠ irreversible):

docker volume rm skimmail-data

Docker Compose

Prerequisites

  • Docker Engine + the docker compose plugin (Compose v2; the docker-compose v1 binary works too but is not what these commands assume).

Get the compose file and start it

No source checkout needed — the compose file is published alongside every release:

curl -fsSLO https://lyquyduong.github.io/skimmail/docker-compose.yml
docker compose up -d

This pulls ghcr.io/lyquyduong/skimmail:latest, publishes port 8080, and creates a named volume for /data. To pin a version: SKIMMAIL_VERSION=v1.17.0 docker compose up -d. Uncomment BASE_URL, AUTH_MODE, or DB_DRIVER/DATABASE_URL in the file before the first start if you want Postgres or MySQL from day one — see Configuration for what changing the database after data exists actually requires.

Verify it started

docker compose ps
curl http://localhost:8080/healthz     # {"status":"ok","version":"1.17.0"}

First run: the claim code

docker compose logs skimmail 2>&1 | grep 'claim code'

Use docker compose logs <service>, addressed by the service name (skimmail, as named in the compose file) — not docker logs skimmail. Compose does not name the container skimmail: with the published file's name: skimmail project name and a service also called skimmail, the actual container is skimmail-skimmail-1, so a bare docker logs skimmail fails with "No such container". docker compose logs looks the service up by its compose-file name regardless, which is why it's the form to use here.

Lost the code? Mint a replacement the same way, through the service:

docker compose exec skimmail /skimmail claim-code --rotate

Uninstall

docker compose down            # stops and removes the container
rm docker-compose.yml          # the file you downloaded

Your data survives in the named volume. To delete everything (⚠ irreversible):

docker compose down -v

Binary tarball

Prerequisites

  • linux/amd64, linux/arm64, darwin/amd64 or darwin/arm64.
  • No database, no container runtime, and no package manager needed — SQLite is built into the binary.

Download and run

Get the asset matching your OS and architecture from GitHub Releases — the name follows the pattern skimmail_<version>_<os>_<arch>.tar.gz, e.g. for the current release on Linux/amd64:

curl -fsSLO https://github.com/lyquyduong/skimmail/releases/download/v1.17.0/skimmail_v1.17.0_linux_amd64.tar.gz
tar xzf skimmail_v1.17.0_linux_amd64.tar.gz

That extracts a single executable, already named for its own version and platform (e.g. skimmail_v1.17.0_linux_amd64) and already marked executable — no chmod +x needed. Rename it if you'd rather type something shorter:

mv skimmail_v1.17.0_linux_amd64 skimmail
DATA_DIR=./skimmail-data ./skimmail

Set DATA_DIR explicitly (as above) rather than relying on the default ./data — the default is relative to whatever directory you happen to launch it from, which matters the moment you start it from somewhere else (a cron job, a different terminal, your own systemd unit) and get a second, empty instance by accident.

There is no bundled service definition for this path — wire the binary into whatever supervises long-running processes on your system (your own systemd unit on Linux, launchd on macOS, a process manager, or a plain nohup …/screen/tmux session for a quick trial). A minimal systemd unit needs little more than a User=, an ExecStart= pointing at the binary with an explicit --data-dir, and Restart=on-failure; nothing in this path installs one for you automatically the way the apt package does.

Verify it started

curl http://localhost:8080/healthz     # {"status":"ok","version":"1.17.0"}

First run: the claim code

The binary prints its log to stdout, so read whatever is capturing that (your terminal, a systemd unit's journalctl, a log file your process manager writes):

journalctl -u <your-unit-name> | grep 'claim code'   # if you wired up your own systemd unit
# or just watch the terminal you started it in

Lost the code? Mint a replacement, naming the same DATA_DIR you started it with:

./skimmail --data-dir ./skimmail-data claim-code --rotate

Uninstall

Stop the process (however you started or supervised it), then delete the binary and its data directory:

rm ./skimmail
rm -rf ./skimmail-data          # ⚠ deletes every account, message and the
                                 #   encryption master key — irreversible

If you wired up your own systemd unit or other supervisor, remove that too.


Next

  • Configuration — the three-plane config model, and every environment variable.
  • Security — the claim code in depth, the screen lock, trusted proxies, and what encryption at rest actually covers.

SkimMail · skimmail@base101.app · 2026-09-17 · commit 97c7846

Clone this wiki locally