Skip to content

Server Installation

This content is for the 1.0.0 version. Switch to the latest version for up-to-date documentation.

This page covers installing PocketCmd Server, the first run that pairs your first device, the background service it installs and how to upgrade or remove it. The server is a single binary, pcmd. Pre-built binaries exist for Linux and macOS on amd64 and arm64.

Install and run it as the regular user who should own the server, not as root: the service and its data in ~/.pocketcmd belong to that user.

Terminal window
curl -fsSL https://pocketcmd.com/install.sh | sh -s -- --api 1

--api takes the API version of your app. The app shows this command with its own version under Teams › Set up a new server and in Add Server › Import from Computer, so copying it from there always gets you a matching server. The server’s major version is its API version, so --api 1 installs the newest stable 1.x.y release. Without --api the script installs the latest release, which may belong to a newer API than your app speaks. See Versions and compatibility.

The script:

  1. Detects the OS (Linux or Darwin) and architecture (x86_64/amd64 or arm64/aarch64) and refuses anything else. In a Rosetta shell on an Apple silicon Mac it installs the native arm64 build.
  2. Picks the release: the newest vN.x.y for --api N, otherwise the latest release of pocketcmd/pocketcmd-server.
  3. Downloads pocketcmd_<os>_<arch>.tar.gz and checksums.txt from that GitHub release.
  4. Verifies the archive’s SHA-256 checksum and aborts on a mismatch.
  5. If cosign is installed, verifies the keyless signature on checksums.txt and aborts if it fails. Without cosign this step is skipped.
  6. Installs pcmd to /usr/local/bin (using sudo if the directory is not writable) or, failing that, to ~/.local/bin, and warns if that directory is not on your PATH.

The script reads these environment variables. Set them on the sh side of the pipe:

Variable Effect
PCMD_API Same as --api.
PCMD_VERSION Release tag to install, for example v1.0.0. Overrides --api. Default: the latest release.
PCMD_INSTALL_DIR Target directory for the binary.
PCMD_BASE_URL A mirror directory containing the archives and checksums.txt. Bypasses GitHub.
PCMD_REPO owner/name to install from. Default: pocketcmd/pocketcmd-server.
PCMD_COSIGN_IDENTITY Certificate identity regex for the cosign check.
Terminal window
curl -fsSL https://pocketcmd.com/install.sh | PCMD_VERSION=v1.0.0 sh

Release archive. Download the archive for your platform from the releases page, check it against checksums.txt and put the pcmd binary from it on your PATH. Pick a release whose major version matches your app’s API version. To verify a release by hand:

Terminal window
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-identity-regexp 'https://github.com/pocketcmd/pocketcmd-server/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
checksums.txt
gh attestation verify pocketcmd_linux_amd64.tar.gz --repo pocketcmd/pocketcmd-server

Go. Requires Go 1.26 or later. A go install build reports its version as 1.0.0-dev; only release builds carry the real version, commit and build date.

Terminal window
go install github.com/pocketcmd/pocketcmd-server/cmd/pcmd@latest

From source.

Terminal window
git clone https://github.com/pocketcmd/pocketcmd-server.git
cd pocketcmd-server
make build # ./pcmd for the current platform
make test # go test ./...
make all # pcmd-{darwin,linux}-{amd64,arm64}

The SQLite driver is pure Go, so the binary builds with CGO_ENABLED=0 and has no runtime dependencies.

Run the binary with no arguments:

Terminal window
pcmd

On systemd and macOS it refuses to run as root or under sudo. Log in as the user who should own the server instead.

On the first run it:

  1. Asks which network interface the server should listen on, but only when the machine has more than one address and you did not pass --host. It offers the IPv4 addresses first, then global IPv6 addresses. There are no other prompts.
  2. Creates the server’s TLS identity certificate in ~/.pocketcmd/tls/.
  3. Starts the HTTPS server on port 8767 (or --port) and prints a QR code plus the same pocketcmd:// URL after Or copy:. When more than one address exists, it lists them all.
  4. Waits up to five minutes for a device to pair (--pair-timeout; 0 waits forever). Each code is valid for 60 seconds and works once; when one expires, the command prints The pairing code expired. Scan the new code: and a new QR code.
  5. After pairing, writes ~/.pocketcmd/config.json with the port, bind address and advertised address, so later commands reuse them. Then it starts the server the way you chose. In background mode it writes the service definition, stops its own listener so the service can bind the port, starts the service and checks /api/health for up to 10 seconds. With --foreground it keeps running until you press Ctrl+C.

The first device to pair becomes the team owner. On a phone or tablet, open Teams and choose Set up a new server to scan the code. The desktop apps take the URL through Teams › Paste URL. See QR Code Pairing.

The QR code is drawn with block characters on a local terminal. Over SSH (SSH_TTY set) or in a locale that is not UTF-8, it is drawn in plain ASCII instead. On a terminal too narrow for the code, only the URL is printed. --no-qr always prints only the URL, and --qr-invert swaps dark and light for a dark terminal without color support.

On a headless machine, pass --non-interactive: the interface prompt is skipped, the server listens on the first detected address, and the command waits for a device. Scan the code or paste the URL from another machine. The prompt is also skipped when stdin or stderr is not a terminal.

If no device pairs in time, or you press Ctrl+C, the command exits without writing config.json. Run pcmd again to start over with a new code; the TLS identity created in step 2 is reused.

Once the first device has paired, add further devices and people with pcmd invite or from the app.

The server listens on one network interface: the one picked on the first run, or the first detected address with --non-interactive. It saves that address, and the QR code carries it. It never listens on every interface unless you ask for it. To pick an interface yourself, pass --host with an IP address of this machine, or 0.0.0.0 (:: to include IPv6) for every interface:

Terminal window
pcmd --host 10.8.0.1 # a VPN interface only
pcmd --host 127.0.0.1 # loopback only, reach it through an SSH tunnel
pcmd --host 0.0.0.0 # every interface

Every data endpoint requires mutual TLS and a session token, whichever interface the server listens on.

Reaching the server from outside your network

Section titled “Reaching the server from outside your network”

The QR code carries the address your devices connect to. By default that is the address the server listens on. --advertise-ip puts a different IP address or hostname in the code, and the first run saves it; pcmd invite, pcmd sync and pcmd rotate-server-cert reuse the saved address unless you pass another. IPv6 addresses are bracketed in the URLs.

  • A VPS whose public IP is on its network interface: pick that interface on the first run and allow TCP 8767 in the firewall.

  • Behind NAT (a home router, or a cloud VM that only sees a private address): forward TCP 8767 to the interface the server listens on, and put the public address in the QR code before you pair. A dynamic-DNS name keeps working when your home IP changes:

    Terminal window
    pcmd --advertise-ip myhome.example.net
Platform Init system Service definition Process output
Linux systemd (user) ~/.config/systemd/user/pocketcmd.service journal (journalctl --user -u pocketcmd)
Linux OpenRC /etc/init.d/pocketcmd, added to the default runlevel /var/log/pocketcmd.log
macOS launchd ~/Library/LaunchAgents/com.pocketcmd.server.plist ~/.pocketcmd/server.log

On Linux the installer uses systemd when systemctl is on the PATH, otherwise OpenRC when /sbin/openrc-run exists. Every service runs pcmd --foreground -p <port> --host <host> with the port and host from the run that installed it, and restarts it if it exits. The server’s own structured log is kept in state.db and read with pcmd logs.

  • systemd: the unit restarts the server 5 seconds after an exit and is sandboxed: the file system is read-only except ~/.pocketcmd, /tmp is private and privilege escalation is blocked. Custom metric commands run inside the same sandbox. A user service stops when you log out unless lingering is enabled; enable it with loginctl enable-linger $USER. pcmd status shows whether it is on.
  • OpenRC: OpenRC has no per-user services, so installing writes a system service and needs sudo pcmd install. The service runs as the user who installed it, with that user’s ~/.pocketcmd, so neither the server nor its metric commands run as root.
  • launchd: the agent has RunAtLoad and KeepAlive set, so it starts when you log in and is restarted if it exits.

With no supported init system, the service install fails and the first run keeps the server running in the foreground. Run pcmd --foreground under the supervisor of your choice.

pcmd install refuses on a host where no device has paired yet, because the service would print pairing codes nobody sees. Run pcmd first, or pass --allow-unpaired.

Manage the service with:

Terminal window
pcmd status # running or stopped, PID, port, paired devices, mode, linger, paths
pcmd stop # stop the service, keep it installed
pcmd start # start it again and wait for /api/health
pcmd restart # stop and start; rewrites the definition only if it changed
pcmd uninstall # stop and remove the service definition
pcmd install # write the service definition and start it

In background mode, running pcmd also installs the service if it is missing and starts it if it is stopped, then prints its status. pcmd status exits with 0 when the service runs, 2 when it is stopped and 3 when none is installed.

There is no official container image. The Dockerfiles in the repository’s docker/ directory build the end-to-end test harness and are not meant for deployment. To run the server in your own container:

  • Run pcmd --foreground as the container’s process, as a non-root user.
  • Persist $HOME/.pocketcmd on a volume. It holds the server identity, and losing it means every device pairs again.
  • Publish port 8767/tcp.
  • Pass --advertise-ip with the host’s LAN address. The container’s own address is usually not reachable from your phone.
  • Start the first run with --non-interactive, then scan the ASCII QR code or copy the Or copy: URL from the container’s output.
Terminal window
pcmd --version
pcmd status
curl -k https://127.0.0.1:8767/api/health

/api/health is the only unauthenticated liveness endpoint and is rate-limited to 60 requests per minute per IP address. It answers 503 with "status":"degraded" when the server cannot reach state.db. -k is needed because the server certificate is self-signed.

Run the install script again with your app’s API version, then restart the service so it runs the new binary:

Terminal window
pcmd backup
curl -fsSL https://pocketcmd.com/install.sh | sh -s -- --api 1
pcmd restart

Paired devices, users and sync data are kept; they live in ~/.pocketcmd, not in the binary. Releases follow semantic versioning: breaking changes to the CLI, the config file or the wire protocol only ship in a new major version, which is also a new API version. state.db migrates forward when the new binary starts, and an older binary refuses to open data written by a newer one, so keep the backup until the upgrade has worked.

pcmd uninstall removes the service definition only. To remove everything:

Terminal window
pcmd uninstall
rm "$(command -v pcmd)"
rm -rf ~/.pocketcmd

Deleting ~/.pocketcmd destroys the server identity, the team and every sync record stored on this server. Devices that were paired keep their local data.

Platform Architectures Service
macOS amd64, arm64 launchd agent
Linux amd64, arm64 systemd user service or OpenRC
Linux without an init system amd64, arm64 none, run with --foreground
Windows n/a not supported