Host your own file transfers on Windows, macOS or Linux.
Version 1.1.0 · joaofariapost.com
Post Transfer combines a browser portal with a desktop transfer client. You choose where your files live: an internal disk, an attached drive, or a mounted network share. The computer hosting the server must remain powered on and connected while other people transfer files.
This is a manual source installation guide. Copy the commands for
your operating system, one block at a time. Replace values marked
YOUR_USERNAME and transfer.example.com before
using them. No automatic server installer is included.
| Goal | Install |
|---|---|
| Connect to a server someone else operates | Desktop client only; go to section 9 |
| Use your own files on one computer | SFTPGo and the web portal; sections 2–6 |
| Share files with people outside your network | Also configure a domain and HTTPS; section 8 |
| Keep the portal running after closing Terminal/PowerShell | Follow section 7 for your OS |
SFTPGo manages accounts, storage access and sharing permissions. The portal supplies the João Faria interface. Caddy supplies HTTPS for public access. The desktop application is a client; it does not turn a disk into a server by itself.
These examples describe a new installation. On an existing server, first back up its configuration and check which ports and services are already in use. Do not replace an existing SFTPGo database, host keys or Caddy configuration with the examples below.
Extract this package so that server,
desktop and config are immediately inside the
installation folder:
| Operating system | Installation folder used below |
|---|---|
| Windows | C:\PostTransfer |
| macOS | ~/PostTransfer |
| Linux | /opt/post-transfer |
Use Node.js 22 or a newer supported LTS release. This source was checked with Node.js 22 and the SFTPGo 2.7.5 API. Check compatibility before upgrading a working deployment to a new major release.
C:\PostTransfer. Confirm
that C:\PostTransfer\server\server.js exists; avoid an
extra nested folder after extraction.caddy.exe in
C:\PostTransfer.Open a new PowerShell window and check:
node --version
npm.cmd --version
Get-Service SFTPGoUsing npm.cmd avoids PowerShell script execution-policy
errors. Run service and firewall commands below in PowerShell as
Administrator.
With Homebrew installed:
brew install node@22 sftpgo caddy
export PATH="$(brew --prefix node@22)/bin:$PATH"
node --version
npm --version
brew info sftpgoUse the export PATH command in each new terminal used
for installation. The launch-agent example later uses an absolute Node
path, so it does not depend on the interactive shell PATH.
Install Node.js 22 or a newer supported LTS release using the official instructions. For an
unattended service, use a system-wide Node binary, or set the service’s
ExecStart to a stable absolute path accessible to its
account. A private nvm installation under your home
directory is not suitable for the example service account.
Install the downloaded SFTPGo .deb matching your CPU
from the official
releases. In the folder containing that one package:
sudo apt install ./sftpgo_*.deb
sudo systemctl enable --now sftpgo
node --version
npm --version
command -v nodeFor other distributions, use the corresponding SFTPGo installation instructions. Public HTTPS also requires Caddy, installed using its official package instructions.
The examples use these ports:
| Component | Listen address | Port |
|---|---|---|
| SFTPGo administration and API | 127.0.0.1 |
8080 |
| SFTPGo WebDAV | 127.0.0.1 |
10081 |
| Post Transfer portal | 127.0.0.1 |
18090 |
| Caddy public website, if enabled | Public interface | 80 and 443 |
| SFTP desktop connections, if enabled | Your chosen interface | 2022 |
Keep the backend HTTP ports private. Use 10081, not 10080: Node’s Fetch implementation blocks port 10080.
Find and back up the active SFTPGo configuration. Typical paths are:
C:\ProgramData\SFTPGo\sftpgo.json. Confirm the
active directory with
Get-CimInstance Win32_Service -Filter "Name='SFTPGo'" | Select-Object PathName.$(brew --prefix)/etc/sftpgo/sftpgo.json./etc/sftpgo/sftpgo.json. Check
systemctl cat sftpgo for overrides.Open the active configuration in a text editor with permission to save it. On a standard new Windows installation, the following commands make a backup and open Notepad:
$p="C:\ProgramData\SFTPGo\sftpgo.json"
Copy-Item -LiteralPath $p -Destination "$p.backup-$(Get-Date -Format yyyyMMdd-HHmmss)"
notepad.exe $pOn macOS, use
sudo nano "$(brew --prefix)/etc/sftpgo/sftpgo.json"; on
Linux, use sudo nano /etc/sftpgo/sftpgo.json, after copying
the file to a backup. In nano, save with Ctrl+O, Enter, then exit with
Ctrl+X.
Edit the existing JSON file. Within its first
httpd.bindings object, set port to
8080, address to 127.0.0.1, and
enable_https to false. Keep
enable_web_admin, enable_web_client and
enable_rest_api enabled. Within its first
webdavd.bindings object, set port to
10081, address to 127.0.0.1, and
enable_https to false. Preserve every other
setting. Do not paste a second JSON document into the file; change only
these fields in their existing sections.
For desktop SFTP access, set the first sftpd.bindings
port to 2022. Use address 127.0.0.1 for
same-computer use, or 0.0.0.0 to accept remote IPv4
connections, with firewall access limited appropriately. These are field
edits, not a replacement configuration. See the SFTPGo configuration
reference.
Restart SFTPGo after editing. Restarting interrupts its active transfers:
Windows:
Restart-Service SFTPGo
curl.exe -I http://127.0.0.1:8080/web/admin/login
curl.exe -I http://127.0.0.1:10081/macOS, using the packaged Homebrew service:
sudo brew services restart sftpgo
curl -I http://127.0.0.1:8080/web/admin/login
curl -I http://127.0.0.1:10081/Linux:
sudo systemctl restart sftpgo
curl -I http://127.0.0.1:8080/web/admin/login
curl -I http://127.0.0.1:10081/A WebDAV 401 Unauthorized response is expected without
credentials: it confirms that the authenticated endpoint is listening.
Connection refused means the service or binding still needs
attention.
On the server computer, open http://127.0.0.1:8080/web/admin/. Complete the initial administrator setup if prompted. Save that administrator password securely. If you see a login page instead, use the administrator already configured on that instance; there is no default password supplied by this package.
After signing in, open the user-management area, choose Users, and add a user:
/, following the
recommendations below.http://127.0.0.1:8080/web/client/ before installing the
portal. Confirm it can list and download a small file.Create a separate SFTPGo user for transferring files; administrator credentials are not a transfer account. The native SFTPGo administration pages keep their own interface; the Post Transfer theme applies to the included portal and desktop app.
Choose the local filesystem storage provider and set the user’s home directory to an existing folder, for example:
| Storage | Example home directory |
|---|---|
| Windows internal or attached disk | D:\TransferData\ClientA |
| Windows network share | \\NAS\Projects\ClientA |
| macOS disk | /Users/Shared/TransferData/ClientA |
| macOS mounted drive | /Volumes/Projects/ClientA |
| Linux disk or mounted share | /srv/transfer/ClientA |
The SFTPGo operating-system service account needs read access, plus write access for uploads, to this directory and access through its parent directories. For example, on a new Linux package installation:
sudo install -d -o sftpgo -g sftpgo -m 0750 /srv/transfer/ClientADo not change ownership of an existing shared project tree blindly. On Windows, a network share normally needs a dedicated service account with both share and filesystem permissions. A drive mapped in your own desktop session may not exist for a service; use a UNC path. On macOS/Linux, ensure the network volume mounts before the service needs it. Do not put SMB passwords in the portal configuration.
Give the transfer user only the permissions needed under
/: list and download; add upload and create directories
when required. Add rename/delete only when intended. Enable HTTP, WebDAV
and SFTP access as needed in the user’s protocol restrictions. For staff
who create delivery links, allow public sharing in SFTPGo’s user
settings; disable it for clients who should not create shares.
The /studio and /client pages are different
interfaces, not independent security roles. SFTPGo permissions enforce
access to files and sharing. When first-login password changes are
required, complete them in the native Web Client if a particular portal
screen cannot handle that flow.
The packaged Homebrew SFTPGo service runs with elevated privileges;
grant explicit user permissions rather than *, and do not
grant chmod/chown to clients. For a dedicated
deployment, configure an unprivileged account with access only to its
data. SFTPGo
service-account guidance.
Set-Location C:\PostTransfer\server
Copy-Item .env.example .env
npm.cmd ci --omit=dev
if ($LASTEXITCODE -ne 0) { throw "Dependency installation failed" }
node --env-file=.env server.jscd "$HOME/PostTransfer/server"
cp .env.example .env
npm ci --omit=dev && node --env-file=.env server.jsExtract the package into /opt/post-transfer, with your
installation account owning it while dependencies are installed.
Then:
cd /opt/post-transfer/server
cp .env.example .env
npm ci --omit=dev && node --env-file=.env server.jsThe default .env contains:
HOST=127.0.0.1
PORT=18090
SFTPGO_URL=http://127.0.0.1:8080
WEBDAV_URL=http://127.0.0.1:10081
PUBLIC_URL=http://localhost:18090
COOKIE_SECURE=false
Only copy .env.example on the first installation;
preserve .env during updates. Match the two backend URLs to
the actual ports you configured.
Leave the terminal open while testing. Open
http://localhost:18090/ on that same computer. Use
/studio to create deliveries, or /client to
upload and download with a transfer account. The native SFTPGo client is
available locally at
http://127.0.0.1:8080/web/client/.
/studio; open its link in a
private browser window and test it.At this stage, localhost links work only on the same
computer. They cannot be sent to external clients. The desktop
application’s public-link feature requires an HTTPS delivery URL; use
section 8 before testing that feature.
Press Ctrl+C in the portal’s test terminal before starting a service on the same port. Service installation is manual and optional. Do not run two copies against port 18090.
Download WinSW-x64.exe version 2.12.0 from the official
release and put it in C:\PostTransfer. Use the matching
XML file supplied in config:
Set-Location C:\PostTransfer
Copy-Item WinSW-x64.exe PostTransferPortal.exe
Copy-Item config\PostTransferPortal.xml PostTransferPortal.xml
New-Item -ItemType Directory -Path C:\PostTransfer\logs -Force
icacls C:\PostTransfer /grant "*S-1-5-19:(OI)(CI)RX"
icacls C:\PostTransfer\logs /grant "*S-1-5-19:(OI)(CI)M"
& .\PostTransferPortal.exe install
if ($LASTEXITCODE -ne 0) { throw "Service installation failed" }
Start-Service PostTransferPortal
Get-Service PostTransferPortal
curl.exe -I http://127.0.0.1:18090/The XML runs Node as LocalService, with automatic startup and restart
on process failure. If Node is installed somewhere other than
C:\Program Files\nodejs\node.exe, edit
<executable> first. It does not need filesystem
access to the transferred files; SFTPGo performs those operations. Keep
the installation directory writable only by administrators and the log
directory writable by the service. These examples follow WinSW’s v2 XML
format.
If installation reports that the service already exists, inspect
Get-Service PostTransferPortal rather than repeatedly
installing it. Logs are in C:\PostTransfer\logs.
This launch agent survives closing Terminal and starts when your user logs in. It is not a boot-time server before login. Prevent the Mac from sleeping during transfers.
mkdir -p "$HOME/Library/LaunchAgents" "$HOME/PostTransfer/logs"
cp "$HOME/PostTransfer/config/com.joaofariapost.posttransfer.plist" "$HOME/Library/LaunchAgents/"
command -v node
echo "$HOME"
open -e "$HOME/Library/LaunchAgents/com.joaofariapost.posttransfer.plist"In the file, replace the Node placeholder with the full path printed
above, and every /Users/YOUR_USERNAME with the printed home
path. XML paths must be absolute; ~ and $HOME
do not expand there. Save, then:
plutil -lint "$HOME/Library/LaunchAgents/com.joaofariapost.posttransfer.plist"
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.joaofariapost.posttransfer.plist"
curl -I http://127.0.0.1:18090/Logs are in ~/PostTransfer/logs. If already loaded,
restart with
launchctl kickstart -k "gui/$(id -u)/com.joaofariapost.posttransfer".
A dedicated Mac server requiring operation before login needs a
LaunchDaemon configured for a dedicated account, with persistent storage
mounts; that deployment is outside this login-service example.
For a new installation, create a dedicated account and give it read access to the application. This example assumes the account does not already exist:
sudo useradd --system --home-dir /opt/post-transfer --shell /usr/sbin/nologin posttransfer
sudo chown -R root:posttransfer /opt/post-transfer/server
sudo chmod -R g+rX,o-rwx /opt/post-transfer/server
sudo chmod 640 /opt/post-transfer/server/.env
sudo cp /opt/post-transfer/config/post-transfer.service /etc/systemd/system/
command -v node
sudo nano /etc/systemd/system/post-transfer.serviceSet ExecStart to the actual system-wide Node path if it
is not /usr/bin/node. Then:
sudo systemctl daemon-reload
sudo systemctl enable --now post-transfer
sudo systemctl status post-transfer --no-pager
curl -I http://127.0.0.1:18090/Read logs with
sudo journalctl -u post-transfer -n 50 --no-pager. The
system account needs no direct access to the shared files.
Use your own subdomain, such as transfer.example.com.
The branding website is not a hosted transfer endpoint provided with
this package.
.env: set
PUBLIC_URL=https://transfer.example.com using your real
domain and COOKIE_SECURE=true. Keep
HOST=127.0.0.1.Use config/Caddyfile.example, replacing its domain. It
proxies the portal and the native SFTPGo client/API. Administration
pages remain local; administer from the server or an SSH tunnel.
transfer.example.com {
@backend path /web/client /web/client/* /static/* /api/v2/*
handle @backend {
reverse_proxy 127.0.0.1:8080
}
handle {
reverse_proxy 127.0.0.1:18090
}
}
Caddy manages HTTPS certificates for eligible public names when the required network access is available. Keep its certificate storage persistent. Caddy operating guidance.
Copy the example to C:\PostTransfer\Caddyfile and edit
the domain. Add this global block at its very beginning so the service
uses a known writable certificate directory:
{
storage file_system {
root C:/PostTransfer/caddy-data
}
}
Then, in Administrator PowerShell:
Set-Location C:\PostTransfer
New-Item -ItemType Directory -Path C:\PostTransfer\caddy-data -Force
icacls C:\PostTransfer\caddy-data /inheritance:r /grant:r "*S-1-5-18:(OI)(CI)F" "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-19:(OI)(CI)M"
& .\caddy.exe validate --config C:\PostTransfer\Caddyfile --adapter caddyfile
if ($LASTEXITCODE -ne 0) { throw "Caddy configuration invalid" }
Copy-Item WinSW-x64.exe PostTransferCaddy.exe
Copy-Item config\PostTransferCaddy.xml PostTransferCaddy.xml
& .\PostTransferCaddy.exe install
if ($LASTEXITCODE -ne 0) { throw "HTTPS service installation failed" }
New-NetFirewallRule -DisplayName "Post Transfer HTTPS" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 80,443 -Program C:\PostTransfer\caddy.exe
Restart-Service PostTransferPortal
Start-Service PostTransferCaddy
Get-Service PostTransferPortal,PostTransferCaddyFor a new Caddy installation, copy the edited example to
$(brew --prefix)/etc/Caddyfile. Preserve an existing file
and merge your site block if Caddy already serves other sites.
caddy validate --config "$(brew --prefix)/etc/Caddyfile" --adapter caddyfile
sudo brew services start caddy
launchctl kickstart -k "gui/$(id -u)/com.joaofariapost.posttransfer"Only start after validation succeeds. Allow inbound connections to Caddy if macOS asks. The Homebrew Caddy service stores state under its configured Homebrew data directory; do not delete it. The portal login-agent limitation still applies, even if Caddy runs at boot. Homebrew Caddy service definition.
With Caddy installed from its official system package, add the edited
site block to /etc/caddy/Caddyfile. Then:
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl restart post-transfer
sudo systemctl enable --now caddy
sudo systemctl reload caddyOnly continue after successful validation. Allow inbound TCP 80/443
in your host and provider firewalls. If UFW is already your firewall,
use sudo ufw allow 80/tcp and
sudo ufw allow 443/tcp. Do not expose backend ports 8080,
10081 or 18090.
Open the real HTTPS address from a different connection, such as a
phone on mobile data. Verify a login, file download, upload if allowed,
and a new public delivery link. Existing links containing
localhost need to be copied again with the public domain.
Do not publish a deployment that only works with certificate
verification disabled.
The desktop source is included under desktop. It can
connect to a separately installed SFTPGo server or open an HTTPS public
delivery link. You do not need to install the portal on every client’s
computer.
Open a terminal in desktop, then run:
Windows:
Set-Location C:\PostTransfer\desktop
npm.cmd ci
if ($LASTEXITCODE -ne 0) { throw "Dependency installation failed" }
npm.cmd startmacOS:
cd "$HOME/PostTransfer/desktop"
npm ci && npm startLinux, in the extracted desktop source folder as a regular desktop user:
npm ci && npm startElectron needs a graphical desktop session and the OS libraries documented in Electron’s development prerequisites. Do not run the desktop app as root or disable its sandbox to work around installation problems.
For account login, enter the server hostname or IPv4 address (no
https:// prefix), SFTP port, username, password and the
server’s verified SHA256 host-key fingerprint. Get the fingerprint from
the server administrator through a trusted channel. The administrator
can use ssh-keygen -lf on the active SFTPGo host public-key
file and supply its SHA256:... value; locate the active
keys through the SFTPGo configuration. Do not accept an unverified
fingerprint from a remote scan as proof of identity.
For remote SFTP, permit and forward TCP 2022 to SFTPGo if that is the port configured in section 3. SFTP does not travel through the HTTPS Caddy proxy. Public-link downloads use HTTPS port 443 and do not require an SFTP account.
To produce local application packages, build on the matching operating system. These commands do not use GitHub artifact storage:
Windows portable application:
npm.cmd run dist -- --win portable --x64 --publish nevermacOS Apple Silicon (use --x64 on an Intel build
host):
npm run dist -- --mac dmg zip --arm64 --publish neverLinux x64:
npm run dist -- --linux AppImage deb --x64 --publish neverOutputs are in desktop/dist. Linux build hosts may need
additional packaging tools. Source archives are not executable
downloads: build the applications before advertising Windows, Mac or
Linux binaries on your website. Sign/notarize public builds using your
own publisher credentials where appropriate; none are included here.
Transfer speed is displayed in Mbps or Gbps. Transferred amounts and file sizes use byte units. A percentage and remaining-time estimate require a known total size; streams without that metadata display indeterminate progress. Estimates settle as data arrives and are not a guaranteed completion time. Browser folder downloads without ZIP require the directory-picker API, so support varies by browser.
Back up the portal .env, SFTPGo
configuration/database/host keys, Caddy configuration/certificate data
and the actual transferred files. The application source is not a backup
of your storage.
Update during a quiet period: stop your portal service, preserve
.env, replace application source, run
npm ci --omit=dev inside server, then start
the service and repeat section 6. Restarting the portal invalidates its
in-memory login sessions. The service’s restart policy handles process
failures; it is not an end-to-end uptime monitor.
| Symptom | Check |
|---|---|
WebDAV 401 in an unauthenticated curl test |
Expected; retry through the portal with a transfer user |
ECONNREFUSED |
Backend running, WebDAV enabled, matching port 10081 |
Fetch reports bad port |
Replace 10080 with 10081 in both backend and .env |
502 Bad Gateway |
Portal/backend listener, configured proxy address and logs |
| Login works but files return 403 | User home permissions, protocol restrictions, required password change |
| Public link works only on the server | Wrong PUBLIC_URL, DNS, port forwarding, firewall or
CGNAT |
| HTTPS fails to start | Port conflict, DNS, certificate challenge reachability, service storage permissions |
| Network folder unavailable after logout/reboot | Service-account access and persistent mount |
| Missing ETA or percentage | Unknown total length or no data yet; inspect transfer state and bytes |
| Low speed | Compare the same file and client through local backend, local portal and public HTTPS; check disk, CPU, network and configured user limits |
Native services avoid the Docker Desktop network path used in some deployments, but no fixed throughput is promised. Performance depends on the complete storage and network route.
This package includes source and configuration examples, not a
remotely managed service. SFTPGo, Node.js, Electron and Caddy remain
independent third-party projects. Preserve their license notices when
distributing builds. See THIRD-PARTY-NOTICES.md.