1 Commits
Author SHA1 Message Date
sneak bead5134c2 Add docker-compose.yml for deploying upaas (closes #223)
Check / check (pull_request) Skipped
The compose file builds the image from this repo, mounts the Docker
socket and HOST_DATA_DIR (passed to upaas as UPAAS_HOST_DATA_DIR),
reads settings from .env, and restarts unless stopped. The port is
published on 127.0.0.1 only, for a TLS-terminating proxy in front, and
UPAAS_PLAINTEXT_HTTP is left unset. A healthcheck against /health uses
the runtime image's busybox wget.

The README's plain-HTTP Compose example becomes a short deploy section
that points at the file. .env is added to .dockerignore so settings are
not copied into the build.

Model: opus-5-5
2026-09-28 09:28:36 +00:00
5 changed files with 17 additions and 96 deletions
+14 -24
View File
@@ -192,23 +192,16 @@ This ensures the main branch always contains clean, tested, working code.
Environment variables: Environment variables:
| Variable | Description | Default | | Variable | Description | Default |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `PORT` | HTTP listen port. `UPAAS_PORT` is also read and wins when both are set. | 8080 | | `PORT` | HTTP listen port | 8080 |
| `UPAAS_DATA_DIR` | Directory for the SQLite database, session key, builds and deployment logs. Deploys need it to be an absolute path unless `UPAAS_HOST_DATA_DIR` is set. | `./data` (the Docker image sets `/var/lib/upaas`) | | `UPAAS_DATA_DIR` | Data directory for SQLite and keys | `./data` (local dev only — use absolute path for Docker) |
| `UPAAS_HOST_DATA_DIR` | Host path of `UPAAS_DATA_DIR`, needed when upaas runs in a container so the bind mounts it passes to Docker point at the right host directory. When set, it must be absolute, or upaas refuses to start. | the value of `UPAAS_DATA_DIR` | | `UPAAS_HOST_DATA_DIR` | Host path for DATA_DIR (when running in container) | _(none — must be set to an absolute path)_ |
| `UPAAS_DOCKER_HOST` | Docker daemon address | unix:///var/run/docker.sock | | `UPAAS_DOCKER_HOST` | Docker socket path | unix:///var/run/docker.sock |
| `UPAAS_PLAINTEXT_HTTP` | Set when µPaaS is reached over plain HTTP (no TLS-terminating proxy in front) so CSRF origin checks use `http://`. Leave unset behind a TLS-terminating reverse proxy. | false | | `UPAAS_PLAINTEXT_HTTP` | Set when µPaaS is reached over plain HTTP (no TLS-terminating proxy in front) so CSRF origin checks use `http://`. Leave unset behind a TLS-terminating reverse proxy. | false |
| `UPAAS_DEBUG` | Enable debug logging. Also sends the session cookie without the `Secure` flag. | false | | `DEBUG` | Enable debug logging | false |
| `UPAAS_SENTRY_DSN` | Read but not used: upaas sends nothing to Sentry | "" | | `SENTRY_DSN` | Sentry error reporting DSN | "" |
| `UPAAS_METRICS_USERNAME` | When set, `/metrics` is served behind basic auth with this username. When unset, there is no `/metrics`. | "" | | `METRICS_USERNAME` | Basic auth for /metrics | "" |
| `UPAAS_METRICS_PASSWORD` | Basic auth password for `/metrics` | "" | | `METRICS_PASSWORD` | Basic auth for /metrics | "" |
| `UPAAS_MAINTENANCE_MODE` | Only shown as `maintenanceMode` in the `/health` response; it blocks nothing | false |
| `UPAAS_SESSION_SECRET` | Key that signs the session and CSRF cookies. When unset, a random key is generated once and kept in `$UPAAS_DATA_DIR/session.key`. | "" |
| `UPAAS_CORS_ORIGINS` | Comma-separated origins allowed to make cross-origin requests with cookies. When unset, no CORS headers are sent. | "" |
The Docker client also reads the standard `DOCKER_API_VERSION`,
`DOCKER_CERT_PATH` and `DOCKER_TLS_VERIFY` variables; `UPAAS_DOCKER_HOST`, which
has a default, always overrides `DOCKER_HOST`.
## Running with Docker ## Running with Docker
@@ -236,18 +229,15 @@ settings from a `.env` file next to it, which needs at least:
HOST_DATA_DIR=/srv/upaas/data HOST_DATA_DIR=/srv/upaas/data
``` ```
Other settings from [Configuration](#configuration) go in the same file, except Other settings from [Configuration](#configuration) go in the same file. Then
`PORT`, `UPAAS_PORT` and `UPAAS_DATA_DIR`: the compose file sets both port run `docker compose up -d` from the repo root; `docker compose ps` shows the
settings to 8080 and `UPAAS_DATA_DIR` to `/var/lib/upaas`, overriding `.env`, to
match its port mapping, healthcheck and data directory mount. Then run
`docker compose up -d` from the repo root; `docker compose ps` shows the
container as healthy once `/health` answers. container as healthy once `/health` answers.
**Important**: `HOST_DATA_DIR` **must** be an **absolute path** on the host. It **Important**: `HOST_DATA_DIR` **must** be an **absolute path** on the host. It
is bind-mounted into the container and passed as `UPAAS_HOST_DATA_DIR` so that is bind-mounted into the container and passed as `UPAAS_HOST_DATA_DIR` so that
Docker bind mounts during builds resolve correctly, because the Docker daemon Docker bind mounts during builds resolve correctly. Relative paths (e.g.
resolves paths on the host, not in the container. upaas refuses to start when `./data`) will break container builds because the Docker daemon resolves paths
`UPAAS_HOST_DATA_DIR` is a relative path such as `./data`. relative to the host, not the container.
The port is published on `127.0.0.1:8080` only, for a TLS-terminating reverse The port is published on `127.0.0.1:8080` only, for a TLS-terminating reverse
proxy in front of it. Leave `UPAAS_PLAINTEXT_HTTP` unset behind that proxy. proxy in front of it. Leave `UPAAS_PLAINTEXT_HTTP` unset behind that proxy.
+1 -19
View File
@@ -20,28 +20,10 @@ regress.
# Completed Steps # Completed Steps
- 2026-09-29: `docker-compose.yml` now sets `UPAAS_PORT` to 8080 as well as
`PORT`, since upaas reads `UPAAS_PORT` first and a `UPAAS_PORT` in `.env` made
it listen away from the port mapping and healthcheck; the README's Compose
section names both (#230).
- 2026-09-29: The README Configuration table now lists every setting upaas
reads, adding `UPAAS_MAINTENANCE_MODE`, `UPAAS_SESSION_SECRET` and
`UPAAS_CORS_ORIGINS`, and gives the real default and effect of each:
`UPAAS_PORT` wins over `PORT`, `UPAAS_HOST_DATA_DIR` falls back to
`UPAAS_DATA_DIR`, `UPAAS_DEBUG` drops the session cookie's `Secure` flag, and
`UPAAS_SENTRY_DSN` is not used (#229).
- 2026-09-28: The README Configuration table now names `UPAAS_DEBUG`,
`UPAAS_SENTRY_DSN`, `UPAAS_METRICS_USERNAME` and `UPAAS_METRICS_PASSWORD`, the
names upaas actually reads (the unprefixed names it listed were ignored), and
the `UPAAS_HOST_DATA_DIR` row refers to `UPAAS_DATA_DIR` (#224).
- 2026-09-28: Added `docker-compose.yml` for deploying upaas: settings from - 2026-09-28: Added `docker-compose.yml` for deploying upaas: settings from
`.env`, the port published on `127.0.0.1` only for a TLS proxy in front, and a `.env`, the port published on `127.0.0.1` only for a TLS proxy in front, and a
healthcheck against `/health`; the README's plain-HTTP Compose example is healthcheck against `/health`; the README's plain-HTTP Compose example is
replaced by a short deploy section. upaas now refuses to start when replaced by a short deploy section (#223).
`UPAAS_HOST_DATA_DIR` is set to a relative path (#223).
- 2026-09-23: Apps are now built with BuildKit, so the stages of a multi-stage - 2026-09-23: Apps are now built with BuildKit, so the stages of a multi-stage
build stay in Docker's size-limited build cache instead of piling up as build stay in Docker's size-limited build cache instead of piling up as
-8
View File
@@ -7,14 +7,6 @@ services:
# Every line of .env is passed to upaas as an environment variable. # Every line of .env is passed to upaas as an environment variable.
env_file: .env env_file: .env
environment: environment:
# Override any PORT or UPAAS_PORT in .env, so upaas listens where the
# port mapping and healthcheck below expect it. Both are set because
# upaas reads UPAAS_PORT first.
PORT: "8080"
UPAAS_PORT: "8080"
# Overrides any UPAAS_DATA_DIR in .env, so the database stays on the
# HOST_DATA_DIR mount below instead of inside the container.
UPAAS_DATA_DIR: /var/lib/upaas
# The Docker daemon resolves app bind mounts on the host, so upaas must # The Docker daemon resolves app bind mounts on the host, so upaas must
# know the host path of its data directory. # know the host path of its data directory.
UPAAS_HOST_DATA_DIR: ${HOST_DATA_DIR:?set HOST_DATA_DIR in .env to an absolute host path} UPAAS_HOST_DATA_DIR: ${HOST_DATA_DIR:?set HOST_DATA_DIR in .env to an absolute host path}
-10
View File
@@ -32,12 +32,6 @@ const (
filePermissions = 0o600 filePermissions = 0o600
) )
// errHostDataDirNotAbsolute is returned when UPAAS_HOST_DATA_DIR is set to a
// relative path, which the Docker daemon cannot resolve for app bind mounts.
var errHostDataDirNotAbsolute = errors.New(
"UPAAS_HOST_DATA_DIR must be an absolute path",
)
// Params contains dependencies for Config. // Params contains dependencies for Config.
type Params struct { type Params struct {
fx.In fx.In
@@ -130,10 +124,6 @@ func buildConfig(log *slog.Logger, params *Params) (*Config, error) {
dataDir := viper.GetString("DATA_DIR") dataDir := viper.GetString("DATA_DIR")
hostDataDir := viper.GetString("HOST_DATA_DIR") hostDataDir := viper.GetString("HOST_DATA_DIR")
if hostDataDir != "" && !filepath.IsAbs(hostDataDir) {
return nil, fmt.Errorf("%w, got %q", errHostDataDirNotAbsolute, hostDataDir)
}
if hostDataDir == "" { if hostDataDir == "" {
hostDataDir = dataDir hostDataDir = dataDir
} }
-33
View File
@@ -1,33 +0,0 @@
package config //nolint:testpackage // tests unexported buildConfig
import (
"errors"
"log/slog"
"testing"
)
func TestBuildConfigRejectsRelativeHostDataDir(t *testing.T) {
t.Setenv("UPAAS_HOST_DATA_DIR", "./data")
setupViper("upaas")
_, err := buildConfig(slog.Default(), &Params{})
if !errors.Is(err, errHostDataDirNotAbsolute) {
t.Fatalf("expected errHostDataDirNotAbsolute, got %v", err)
}
}
func TestBuildConfigHostDataDirDefaultsToDataDir(t *testing.T) {
t.Setenv("UPAAS_DATA_DIR", "./data")
t.Setenv("UPAAS_HOST_DATA_DIR", "")
t.Setenv("UPAAS_SESSION_SECRET", "test-secret")
setupViper("upaas")
cfg, err := buildConfig(slog.Default(), &Params{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.HostDataDir != "./data" {
t.Errorf("expected HostDataDir ./data, got %q", cfg.HostDataDir)
}
}