Setting up Docker on the server

Install Docker Engine and the Compose v2 plugin that sfp server requires as its default container runtime.

Docker is the default container engine for sfp-server. sfp server init checks two host commands before it writes anything: <code>docker --version</code> and <code>docker compose version</code>. The second check means the Docker Compose v2 plugin is required — Docker Engine alone, or a legacy v1 standalone docker-compose, is not enough.

Prerequisites:

  • Ubuntu (24.04 recommended) or Debian instance with sudo privileges
  • SSH access to the instance
  • curl available on the host
  • An arm64 host is unsupported without penalty: every pinned image in the stack is linux/amd64, so an arm64 host runs all of them under emulation — see 5.

[1] Install Docker Engine and the Compose v2 plugin

The Ubuntu package docker-compose-plugin installs the Compose v2 subcommand that sfp-server invokes as docker compose. Install it in the same transaction as the engine:

# Add Docker's official apt repository (one-time)
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install the engine, CLI, and the Compose v2 plugin
sudo apt-get update
sudo apt-get install -y \
    docker-ce \
    docker-ce-cli \
    containerd.io \
    docker-buildx-plugin \
    docker-compose-plugin

# Start Docker and enable it on boot
sudo systemctl enable --now docker
sudo systemctl status docker
Terminal output of the apt repository setup and Docker Engine install on Ubuntu, ending with the Docker daemon active
Installing Docker Engine with the Compose v2 plugin on Ubuntu.

[2] Allow your deploy user to run Docker

sfp-server runs docker and docker compose as your deploy user — the user that executes sfp server init. If that user is not in the docker group, the commands fail with permission errors:

sudo groupadd -f docker
sudo usermod -aG docker "$USER"
newgrp docker        # apply the group in the current shell; or disconnect and reconnect SSH
docker ps            # must run without sudo

Membership of the docker group is root-equivalent: group members can read and write the Docker socket and therefore execute anything as root on the host. Add only trusted users.

Terminal output of adding the current user to the docker group, followed by a successful docker ps as a non-root user
Adding the deploy user to the docker group and verifying passwordless docker ps.

[3] Verify the install

Run the two commands sfp-server itself probes, plus a smoke container:

docker --version          # e.g. Docker version 27.4.1, build ...
docker compose version    # MUST print "Docker Compose version v2.x" — v1 is not accepted
docker run hello-world    # downloads and runs a test image
Terminal output of docker --version and docker compose version both succeeding, then docker run hello-world whose output shows the container ran
Verifying the engine and the Compose v2 plugin; docker compose version must print v2.

[4] What sfp server init checks

At the start of sfp server init, the CLI runs <code>docker --version</code> and <code>docker compose version</code> on the execution target (local, or the remote host selected with --ssh-host) and fails with an actionable error if either is missing. No other Docker-side prerequisite is probed. The engine choice is persisted in the tenant's .env as SFP_CONTAINER_ENGINE=docker; later sfp server commands re-read that file, so the engine is resolved without re-passing the flag. This section describes CLI behavior rather than a screen, so it carries no capture.


[5] arm64 hosts — every pinned image is linux/amd64

The self-hosted stack pins platform: linux/amd64 on every service, and the third-party images are pinned to their amd64 index digests. This is true for Docker and Podman alike — the engine renderer changes image references and mount labels, never the platform. On an arm64 host the engine pulls the amd64 child images and runs every container under x86 emulation, which materially slows the stack (especially Postgres, the message queue, and the workers). Prefer x86-64 VMs/instances for a self-hosted sfp-server, or use Supabase Cloud modes where the database is hosted elsewhere. This consequence is a property of the stack, not of a capture, so the section is marked conceptual.


[6] Rootless Docker

The lifecycle is tested against a rootful daemon. The optional OpenTelemetry monitoring collector binds the Docker socket at /var/run/docker.sock and reads container logs at /var/lib/docker/containers; with rootless Docker those paths live under the user's runtime/data directories (${XDG_RUNTIME_DIR}/podman/... is Podman's analogue; rootless Docker uses ${XDG_RUNTIME_DIR}/docker.sock), so that optional mount would not resolve. There is no separate rootless code path in the lifecycle — unlike Podman, where the renderer adapts mounts and SELinux labels for you. If you need rootless, use Podman (see Podman Support). This is a configuration comparison, so the section is marked conceptual.


Post-Installation Optional Steps

The Compose v2 plugin from 1 is the standard path on Ubuntu/Debian. On hosts without Docker's packages — in practice RHEL/Fedora where the Podman provider needs a Compose v2 backend — install the standalone v2 binary and point podman compose at it. This is also the install path referenced by Podman Support << << << 3.

curl -fsSL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) \
  -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
podman compose version     # must print "Docker Compose version v2.x"

On SELinux/rpm-based hosts, register the binary with Podman by adding it to ~/.config/containers/containers.conf:

[engine]
compose_providers=["/home/<user>/.docker/cli-plugins/docker-compose"]
Terminal output of downloading the standalone docker-compose v2 binary, then podman compose version reporting Docker Compose version v2.x
Installing the standalone Compose v2 binary as the backend for podman compose.

Bound container log size

The stack's containers write json-file logs, and the optional monitoring collector tails them at /var/lib/docker/containers. Bounding the driver keeps host disk usage predictable:

sudo tee /etc/docker/daemon.json <<EOF
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
EOF
sudo systemctl restart docker
Terminal output of writing the daemon.json log limits and restarting the Docker daemon
Bounding container log size with daemon.json.

[7] Troubleshooting

SymptomCauseFix
docker compose fails, but docker-compose --version worksOnly the legacy v1 standalone binary (or nothing) is installedInstall the Compose v2 plugin: sudo apt-get install -y docker-compose-plugin, then verify docker compose version prints v2
permission denied while trying to connect to the Docker daemon socketDeploy user not in the docker groupsudo usermod -aG docker $USER and re-login/newgrp docker (see [2])
Cannot connect to the Docker daemonDaemon not running, or socket path wrongsudo systemctl enable --now docker; sudo journalctl -u docker.service for the cause
Service starts on an arm64 host but is very slowEvery pinned image is linux/amd64 running under emulationPrefer an x86-64 host, or use Supabase Cloud modes (see [5])

This table captures no single surface, so the section is marked conceptual.


Where to next

On this page