A Docker node is two containers, Postgres and the node, started by Docker Compose from three files in a directory of their own. Nothing is compiled on your machine.
- Docker Engine or Docker Desktop with Compose 2.24 or later, on x86-64 or arm64.
bashandcurl, for the./stugaoperator commands.
curl -fsSL https://github.com/stuga-dev/stuga/releases/latest/download/install.sh | bashThis makes a stuga directory, downloads the release's compose.yml, env.example and stuga
into it, writes .env with this machine's address on the network as PUBLIC_ORIGIN and a random
database password, starts the stack and waits until the node serves. It ends by printing the link
that sets the node up, with its setup code in it. Continue with Getting started.
Options go after bash -s --: --dir <dir>, --version <1.2.3>, --port <n>, --origin <url>
when this machine's address is not the one other devices use, and --local-only to serve this
machine only.
By hand, the same steps are:
mkdir stuga && cd stuga
for f in compose.yml env.example stuga; do
curl -fsSLO "https://github.com/stuga-dev/stuga/releases/latest/download/$f"
done
chmod +x stuga
cp env.example .env
echo "PUBLIC_ORIGIN=http://192.168.1.50:8787" >> .env # this machine's address on the network
./stuga db-password # a random database password, in .env
docker compose up -dFor a specific release, use releases/download/v<version>/ in place of releases/latest/download/.
Until the node serves, it answers with a page that says it is starting. ./stuga status reports
ready yes once it serves, and the setup link while nobody has claimed the node.
| Path | Holds |
|---|---|
compose.yml |
The stack, with its three images pinned to one release. An upgrade replaces it, so do not edit it. |
.env |
Your settings and the database password, readable only by you. Compose reads it, and passes it to the node. |
stuga |
The operator commands. |
compose.rollback.yml |
Only while the stack runs another release than compose.yml names, after a restore or a failed upgrade: keeps the node, Postgres and remote access on it until the next upgrade, through COMPOSE_FILE in .env. See Roll back. |
data/node/ |
The node's data directory (DATA_DIR): document snapshots, media, per-actor SQLite stores, the signing key and secrets. |
the stuga_pgdata volume |
The Postgres cluster. docker volume inspect stuga_pgdata shows where Docker keeps it. |
the stuga_remote volume |
What the node and the remote access connector share: its settings, credential and socket. The node writes it again, so it is not backed up. |
backups/ |
Backups: the node's daily ones, the one it takes before an upgrade, and ./stuga backup's. |
Postgres is on the internal stuga_db network, which only the node joins: it publishes no port and
has no route out.
The node's log is docker compose logs node, Postgres's is docker compose logs postgres, and the
remote access connector's is docker compose logs remote. Add -f to follow one.
Put settings in .env, then run docker compose up -d. A docker compose restart keeps the old
environment. Any node variable in Configuration can go in .env, except the
ones the stack sets itself. compose.yml sets DATABASE_URL, DATA_DIR, BIND, PORT,
BACKUP_DIR and the remote access hints (STUGA_REMOTE_DIR, STUGA_CONNECTOR_REQUEST,
STUGA_CONNECTOR_STATUS, STUGA_REMOTE_GID, STUGA_REMOTE_CONNECTOR_UID) inside the container, and
a value for them in .env has no effect there. Remote access is off until .env sets
STUGA_REMOTE_SERVICE, which switches the rest on. The image sets PG_BIN and the packaging hints, so leave
those out too.
These variables belong to the stack, not to the node:
| Variable | Default | |
|---|---|---|
HOST_BIND |
0.0.0.0 |
The host address Docker publishes the node on. 127.0.0.1 keeps it to this machine; this machine's network address keeps it to your network. |
HOST_PORT |
8787 |
The host port Docker publishes the node on. PUBLIC_ORIGIN must name it. |
STUGA_VOLUME_NAME |
stuga_pgdata |
The Postgres volume. |
COMPOSE_PROJECT_NAME |
stuga |
The Compose project. Give a second stack on the same host its own project, volume and port. |
BACKUP_DIR |
./backups |
Where backups go, the node's own and ./stuga's. |
POSTGRES_PASSWORD |
stuga |
The database password. install.sh and ./stuga upgrade write a random one when .env has none. |
Postgres keeps its own copy of POSTGRES_PASSWORD, so changing it in .env alone locks the node
out. To change it, delete the line and run docker compose stop node, ./stuga db-password and
docker compose up -d.
To add to the stack, for example another volume, put the change in compose.override.yml and set
COMPOSE_FILE=compose.yml:compose.override.yml in .env, so that ./stuga uses the same files
as docker compose. While the stack runs another release than compose.yml names
(Roll back), COMPOSE_FILE also names compose.rollback.yml: leave it there.
The node is published on every address of the machine, and only whoever has its setup code can
claim it (Getting started). PUBLIC_ORIGIN is the address
invite links carry; install.sh sets it to this machine's address on the network. Reserve that
address on your router so it does not change. To keep the node to this machine, set
HOST_BIND=127.0.0.1, or to your network, set it to this machine's network address.
Network access explains what each way of reaching the node protects.
On a machine with a public address, such as a VPS, the default HOST_BIND puts the node's plain
http port on the internet. The node takes no password there from outside its network
(Passwords over plain http), but sessions and
pages still cross in the clear. Set HOST_BIND=127.0.0.1 and open the node through an SSH tunnel
from your computer:
ssh -L 8787:127.0.0.1:8787 <server>Then open http://localhost:8787 to claim the node and sign in. On a public address the install
script prints the setup link this way, and ./stuga reset-password prints the same tunnel with its
link. To reach the node from anywhere without a tunnel, turn on
remote access, or serve HTTPS as below. While remote access is on,
./stuga status says so when HOST_BIND still publishes the port on every address.
For HTTPS, put a reverse proxy in front of the node, as
Network access describes, or let the node serve TLS itself: mount the
certificate directory into the node container in compose.override.yml, set TLS_CERT_DIR to its
path inside the container, and set PUBLIC_ORIGIN to the https address. The container's health
check and ./stuga then reach the node over https.
Members and agents can have the node fetch a URL, such as an image to upload. The node refuses
private and reserved addresses, but inside its container it doesn't know the server's own public
address, and a connection from the server to itself passes the provider's firewall. Keep other
services on the server to 127.0.0.1 and ::1, or deny Docker's networks their ports in the
server's firewall (DOCKER-USER for published ones), for IPv4 and IPv6.
./stuga runs the node's own stuga-node commands in a one-off container of the image that
created the node's container (for a restore, the backup's release), and stops and starts the node
around the commands that need that.
What each command does, and its exit codes, are described in Operations.
| Command | |
|---|---|
./stuga status |
Version, the release that last served the data, schema, whether the node is serving or why it refuses the data, the last backup, the remote access connector's state, and the setup link while nobody has claimed the node. |
./stuga backup |
Stops the node, takes a verified backup, and starts the node again. |
./stuga verify <backup> |
Checks that a backup is whole and restorable here. |
./stuga list |
Backups, and what restores kept beside the data. |
./stuga restore [--yes] <backup> |
Pulls the images of the release that served the backup's data, verifies the backup while the node still serves, asks you to type its name, restores it, and starts the stack on that release. |
./stuga upgrade [<version>] |
Fetches the newest release's files (or the named one's), pulls its images unless they are already on the machine, starts them and waits until the node serves. The node backs up before it upgrades. |
./stuga reset-password <username> |
Prints a password reset link. The node must be running. |
./stuga media-scan [--reclaim] [--empty-trash[=<days>]] [--grace-hours=<hours>] |
Reports or reclaims media no document uses. The node must be running. |
./stuga db-password |
Gives the database a random password and writes it to .env, when .env has none. The node must be stopped. |
A <backup> is a name in BACKUP_DIR, or a path inside it. To restore a backup taken elsewhere,
copy its directory into backups/ first.
The commands wait for a starting node for as long as it shows progress. Two shell variables bound
that wait: STUGA_READY_QUIET_SECONDS (default 600) and STUGA_READY_MAX_SECONDS (default 7200).
A restore keeps what it replaced. Once you are satisfied, remove both copies. ./stuga list names
them:
docker compose exec postgres psql -U stuga -d postgres -c 'DROP DATABASE "stuga_replaced_<stamp>"'
sudo rm -rf data/node.replaced-<stamp>Settings → This node → About names a newer version once the node knows of one (Operations).
Read the release's Upgrade notes first. Then:
./stuga upgrade # the newest release
./stuga upgrade 1.2.3 # or a given oneIt downloads that release's compose.yml, env.example and stuga over the ones you have (your
.env is not touched), pulls the new images, starts them and waits until the node serves. The new
version backs up the database before it changes anything, and answers with a page that says so
meanwhile. ./stuga upgrade ends by printing the version change and the restore command that
undoes it; if the new version does not serve, it prints the command for the node's log and the
restore command for the backup that version took, or, when it stopped before that backup and
changed nothing, stops it and keeps the stack on the previous release as Roll back
describes. It refuses a release older than the one that last served the data, downloaded or put
there by hand, with the node running or stopped: going back is a restore. Compare the new
env.example with your .env for settings the release adds. When .env has no POSTGRES_PASSWORD, the upgrade stops the
node and gives the database a random one first.
A NAS app store that updates the images does the same without ./stuga: the node backs up before it
upgrades, whatever started it. A NAS app or an image set to an older release than the one that
served the data refuses it and changes nothing.
A node that cannot reach ghcr.io upgrades from images carried to it. On a machine that is online,
fetch the release's three files and save its three images for the node's architecture (linux/arm64
or linux/amd64):
V=1.2.3
for f in compose.yml env.example stuga; do
curl -fsSLO "https://github.com/stuga-dev/stuga/releases/download/v$V/$f"
done
for image in stuga-node stuga-postgres stuga-remote; do
docker pull --platform linux/arm64 "ghcr.io/stuga-dev/$image:$V"
done
docker save -o "stuga-$V-images.tar" "ghcr.io/stuga-dev/stuga-node:$V" \
"ghcr.io/stuga-dev/stuga-postgres:$V" "ghcr.io/stuga-dev/stuga-remote:$V"Carry the three files and the archive to the node. There, load the images, put the three files over
the ones in the Stuga directory, and upgrade as usual. With a compose.yml naming a newer version
than the one that served the data, ./stuga upgrade uses it as it is and downloads nothing:
docker load -i stuga-1.2.3-images.tar
chmod +x stuga
./stuga upgradeThe backup, the wait and the way back are the same as with a connection. Keep the previous version's
images until you are satisfied with the new one: a roll back starts the stack on them, and
docker image prune -a would remove them. A first install works the same way, with
./stuga db-password and docker compose up -d in place of ./stuga upgrade.
Stuga 0.1 releases were previews, and 0.2 starts with new data. On a node that runs 0.1,
./stuga upgrade puts 0.1's files back and changes nothing. To move:
- In Stuga 0.1, export each workspace: Settings → This workspace → General → Export workspace.
- In the Stuga directory, remove the stack and its database:
docker compose down -v. The directory'sdata/andbackups/are 0.1's; remove the directory once you no longer need them. - Install the new release into a new directory, set it up, invite your colleagues, and import each archive with Create a workspace → Start with → Import (Workspace archive).
Restore the backup the new version took before it upgraded, which ./stuga upgrade names and
Settings → This node → Backups shows with its command:
./stuga restore <backup>The stack comes back on the release whose data the backup holds: the node, Postgres and remote
access, whose images the restore pulls before it stops anything. Keep compose.yml: it names the
newer images, but an older release's compose.yml does not know the database password. The
restore writes compose.rollback.yml, which pins all three images to the older release, and adds
it to COMPOSE_FILE in .env, so docker compose up -d keeps the stack there too.
The next ./stuga upgrade takes compose.rollback.yml out of .env and deletes it before it
starts the newer release.
A node started on an older release than the one that served its data, by hand, by a NAS app or by
install.sh --version, refuses the data and changes nothing: its page and log say what to do, and
./stuga status says why. ./stuga upgrade <version>, with the version that served the data,
puts that release back.
In the Stuga directory:
docker compose downThis removes the containers. Your data stays in data/, backups/ and the stuga_pgdata volume, and
the stuga_remote volume stays too. To delete them and the images too, run
docker compose down -v --rmi all and remove the directory.
The node writes its files as root, so on Linux that needs sudo rm -rf.