Add docker-compose.yml for deploying upaas (closes #223)
Check / check (pull_request) Skipped
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 PORT is pinned to 8080 so .env cannot move upaas off the port mapping and the healthcheck, which uses the runtime image's busybox wget. upaas now refuses to start when UPAAS_HOST_DATA_DIR is set to a relative path; when unset it still falls back to the data directory. The README's plain-HTTP Compose example becomes a short deploy section that points at the file. .env is added to .dockerignore. Model: opus-5-5
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
.git
|
||||
.env
|
||||
bin/
|
||||
.editorconfig
|
||||
.vscode/
|
||||
|
||||
@@ -219,39 +219,29 @@ This recipe serves plain HTTP, so `UPAAS_PLAINTEXT_HTTP=true` is required for
|
||||
setup and every other form to pass the CSRF origin check. Behind a
|
||||
TLS-terminating reverse proxy, drop that line.
|
||||
|
||||
### Docker Compose
|
||||
### Deploying with Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
upaas:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${HOST_DATA_DIR}:/var/lib/upaas
|
||||
environment:
|
||||
- UPAAS_HOST_DATA_DIR=${HOST_DATA_DIR}
|
||||
# Set when serving plain HTTP (no TLS-terminating proxy); drop behind one
|
||||
- UPAAS_PLAINTEXT_HTTP=true
|
||||
# Optional: uncomment to enable debug logging
|
||||
# - DEBUG=true
|
||||
# Optional: Sentry error reporting
|
||||
# - SENTRY_DSN=https://...
|
||||
# Optional: Prometheus metrics auth
|
||||
# - METRICS_USERNAME=prometheus
|
||||
# - METRICS_PASSWORD=secret
|
||||
[`docker-compose.yml`](docker-compose.yml) builds the image from this repo and
|
||||
runs it with the Docker socket and the data directory mounted. It reads its
|
||||
settings from a `.env` file next to it, which needs at least:
|
||||
|
||||
```bash
|
||||
HOST_DATA_DIR=/srv/upaas/data
|
||||
```
|
||||
|
||||
**Important**: You **must** set `HOST_DATA_DIR` to an **absolute path** on the
|
||||
host before running `docker compose up`. This value is bind-mounted into the
|
||||
container and passed as `UPAAS_HOST_DATA_DIR` so that Docker bind mounts during
|
||||
builds resolve correctly. Relative paths (e.g. `./data`) will break container
|
||||
builds because the Docker daemon resolves paths relative to the host, not the
|
||||
container.
|
||||
Other settings from [Configuration](#configuration) go in the same file, except
|
||||
`PORT`: the compose file sets it to 8080, overriding `.env`, to match its port
|
||||
mapping and healthcheck. Then run `docker compose up -d` from the repo root;
|
||||
`docker compose ps` shows the container as healthy once `/health` answers.
|
||||
|
||||
Example: `HOST_DATA_DIR=/srv/upaas/data docker compose up -d`
|
||||
**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
|
||||
Docker bind mounts during builds resolve correctly, because the Docker daemon
|
||||
resolves paths on the host, not in the container. upaas refuses to start when
|
||||
`UPAAS_HOST_DATA_DIR` is a relative path such as `./data`.
|
||||
|
||||
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.
|
||||
|
||||
Apps are built with BuildKit, so the stages of a multi-stage build are kept in
|
||||
Docker's build cache rather than as untagged images. Docker Engine 28.2 and
|
||||
|
||||
@@ -20,6 +20,12 @@ regress.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 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
|
||||
healthcheck against `/health`; the README's plain-HTTP Compose example is
|
||||
replaced by a short deploy section. upaas now refuses to start when
|
||||
`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
|
||||
build stay in Docker's size-limited build cache instead of piling up as
|
||||
untagged images; build progress is still written to the deployment log as
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Runs upaas. Put settings in .env next to this file; see "Deploying with
|
||||
# Docker Compose" in README.md.
|
||||
services:
|
||||
upaas:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
# Every line of .env is passed to upaas as an environment variable.
|
||||
env_file: .env
|
||||
environment:
|
||||
# Overrides any PORT in .env, so upaas listens where the port mapping
|
||||
# and healthcheck below expect it.
|
||||
PORT: "8080"
|
||||
# The Docker daemon resolves app bind mounts on the host, so upaas must
|
||||
# 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}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${HOST_DATA_DIR:?set HOST_DATA_DIR in .env to an absolute host path}:/var/lib/upaas
|
||||
# Loopback only, for a TLS-terminating reverse proxy in front. Leave
|
||||
# UPAAS_PLAINTEXT_HTTP unset behind that proxy.
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -32,6 +32,12 @@ const (
|
||||
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.
|
||||
type Params struct {
|
||||
fx.In
|
||||
@@ -124,6 +130,10 @@ func buildConfig(log *slog.Logger, params *Params) (*Config, error) {
|
||||
dataDir := viper.GetString("DATA_DIR")
|
||||
hostDataDir := viper.GetString("HOST_DATA_DIR")
|
||||
|
||||
if hostDataDir != "" && !filepath.IsAbs(hostDataDir) {
|
||||
return nil, fmt.Errorf("%w, got %q", errHostDataDirNotAbsolute, hostDataDir)
|
||||
}
|
||||
|
||||
if hostDataDir == "" {
|
||||
hostDataDir = dataDir
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user