# Deploy with Docker Compose

> Start Sluice and Postgres on one host with Docker Compose, keep the master key, and upgrade the stack.

This guide shows you how to run Sluice and Postgres on one host with Docker Compose. The stack fits a trial, a laptop or a small server. For a cluster, see [Deploy on Kubernetes with Helm](/how-to/deploy-on-kubernetes/).

The stack has two services:

| Service | Image | Notes |
|---|---|---|
| `postgres` | `postgres:17-alpine` | The user, the password and the database are `sluice`. The data stays in the named volume `postgres-data`. |
| `sluice` | `sluice-uv` | Starts after Postgres is healthy. Tasks run on the process executor inside this container. The image has `bash`, `uv`, Python 3.12 and `bun`. |

Sluice itself needs no volume. It keeps all state in Postgres, and the default storage driver `postgres` also keeps file contents, logs and artifacts there.

## Start the stack

The compose file uses the released image from GitHub Container Registry. It needs no checkout.

<Steps>

1. Download the compose file into an empty directory:

   ```sh
   mkdir sluice/ && cd sluice/
   curl -fsSLO https://raw.githubusercontent.com/alternayte/sluice/main/deploy/compose/compose.yml
   ```

2. Set the two required variables. Compose stops when one of them is empty.

   ```sh
   export SLUICE_BOOTSTRAP_ADMIN_PASSWORD='change-me-now-1'
   export SLUICE_MASTER_KEYS="k1:$(openssl rand -base64 32)"
   ```

3. Start the stack:

   ```sh
   docker compose up -d
   ```

4. Open [http://localhost:8080](http://localhost:8080). Sign in as `admin@local.test` with the password of step 2.

</Steps>

The image is `ghcr.io/alternayte/sluice-uv:latest`. Pin a release with `SLUICE_IMAGE`, for example `SLUICE_IMAGE=ghcr.io/alternayte/sluice-uv:0.2.1`.

To run your own build, run `just build-images` in a checkout of the repository. Then start the stack with `SLUICE_IMAGE=sluice-uv:dev docker compose -f deploy/compose/compose.yml up -d`.

Check that the server is ready. The answer lists the checks `database`, `master_keys`, `migrations` and `storage`:

```sh
curl -s http://localhost:8080/readyz
```

## Set the variables

The compose file reads these variables from the shell or from a `.env` file next to it:

| Variable | Default | Notes |
|---|---|---|
| `SLUICE_BOOTSTRAP_ADMIN_PASSWORD` | none | Required. The password of the first admin. |
| `SLUICE_MASTER_KEYS` | none | Required. The keys that encrypt the builtin secrets. |
| `SLUICE_BOOTSTRAP_ADMIN_EMAIL` | `admin@local.test` | The email of the first admin. |
| `SLUICE_PUBLIC_URL` | `http://localhost:8080` | The URL that browsers use. |
| `SLUICE_PORT` | `8080` | A variable of the compose file only: the host port. Sluice does not read it. |
| `SLUICE_IMAGE` | `ghcr.io/alternayte/sluice-uv:latest` | A variable of the compose file only: the image of the `sluice` service. |
| `POSTGRES_PASSWORD` | `sluice` | The password of the Postgres user. |

To set more server variables, add them under `environment` of the `sluice` service. [Environment variables](/reference/env/) lists all of them.

## Keep the master key

The command in the steps creates a new master key each time. Sluice encrypts each builtin secret with the first key of `SLUICE_MASTER_KEYS`. When the key of a stored secret is missing, `/readyz` fails with `master_key_missing`. Store the value in a `.env` file next to the compose file:

```sh
printf 'SLUICE_MASTER_KEYS=%s\n' "$SLUICE_MASTER_KEYS" >> .env
printf 'SLUICE_BOOTSTRAP_ADMIN_PASSWORD=%s\n' "$SLUICE_BOOTSTRAP_ADMIN_PASSWORD" >> .env
```

Keep a copy of the key outside the host. Without the key, Sluice cannot read the builtin secrets of a database backup. To replace a key, see [Rotate the master key](/operations/rotate-the-master-key/).

## Understand the bootstrap admin

Sluice creates the first admin from `SLUICE_BOOTSTRAP_ADMIN_EMAIL` and `SLUICE_BOOTSTRAP_ADMIN_PASSWORD` only when the `users` table is empty. Later starts do not change users. A new bootstrap password thus has no effect after the first start.

After the first sign-in, change the password on **Settings → Profile**. Then create one user for each person on **Settings → Users**.

## Put TLS in front

Sluice serves plain HTTP. To serve it on a domain, put a reverse proxy with TLS in front of port 8080. Then set `SLUICE_PUBLIC_URL` to the `https://` URL that browsers use. Sluice uses this URL for the same-origin check of the UI, the `Secure` flag of the session cookie, and the webhook URLs.

<Aside type="caution">
When `SLUICE_PUBLIC_URL` and the host in the browser differ, every change from the UI fails with `403 csrf_failed`.
</Aside>

## Stop, remove and upgrade

| Command | Effect |
|---|---|
| `docker compose stop` | Stops the containers. The data stays. |
| `docker compose down` | Removes the containers. The volume `postgres-data` stays. |
| `docker compose down -v` | Removes the containers and the volume with all data. |

To upgrade, back up the database first. Then set `SLUICE_IMAGE` to the new release, or keep `latest`, and start the stack again:

```sh
docker compose pull
docker compose up -d
```

The server applies the new database migrations at start. See [Upgrade](/operations/upgrade/) and [Back up and restore](/operations/back-up-and-restore/).

## Related pages

- [Run your first flow](/tutorials/run-your-first-flow/)
- [Deploy on Kubernetes with Helm](/how-to/deploy-on-kubernetes/)
- [Harden a deployment](/operations/harden-a-deployment/)
- [Security model](/concepts/security-model/)
