-
Notifications
You must be signed in to change notification settings - Fork 0
Installation
English · Tiếng Việt · 中文
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.mdthere 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.
| 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
|
| 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.
- Debian or Ubuntu on
amd64orarm64, withsudoaccess. - Outbound HTTPS to
lyquyduong.github.io(the apt repo) and togithub.com/objects.githubusercontent.com(nothing is fetched from there directly by apt, but the same network path is needed later for self-update checks).
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 updateThe 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.
sudo apt install skimmailThis 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.
sudo systemctl status skimmail
curl http://localhost:8080/healthzA 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).
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 --rotateThe --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.
sudo apt purge skimmail # stops the service, removes the binary, the
# systemd unit and /etc/default/skimmailapt 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 updateBack up first (Settings ▸ Backup) if there's any chance you'll want the data back.
- Docker Engine (any reasonably current version; the image is multi-arch
linux/amd64+linux/arm64, selected automatically).
docker run -d --name skimmail -p 8080:8080 -v skimmail-data:/data \
-e AUTH_MODE=passphrase ghcr.io/lyquyduong/skimmail:latestName 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.
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.
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 --rotateNo --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.)
docker rm -f skimmail
docker rmi ghcr.io/lyquyduong/skimmail:latestYour 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 Engine + the
docker composeplugin (Compose v2; thedocker-composev1 binary works too but is not what these commands assume).
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 -dThis 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.
docker compose ps
curl http://localhost:8080/healthz # {"status":"ok","version":"1.17.0"}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 --rotatedocker compose down # stops and removes the container
rm docker-compose.yml # the file you downloadedYour data survives in the named volume. To delete everything (⚠ irreversible):
docker compose down -v-
linux/amd64,linux/arm64,darwin/amd64ordarwin/arm64. - No database, no container runtime, and no package manager needed — SQLite is built into the binary.
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.gzThat 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 ./skimmailSet 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.
curl http://localhost:8080/healthz # {"status":"ok","version":"1.17.0"}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 inLost the code? Mint a replacement, naming the same DATA_DIR you started it
with:
./skimmail --data-dir ./skimmail-data claim-code --rotateStop 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 — irreversibleIf you wired up your own systemd unit or other supervisor, remove that too.
- 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