diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d229810 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.env +target +data +web/dist +web/node_modules +README.md +Makefile +Dockerfile +.dockerignore +compose*.yml diff --git a/.gitignore b/.gitignore index 81212f1..85dda3e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ # secrets .env + +# host-specific compose overlay +compose.override.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c894f9f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# syntax=docker/dockerfile:1 + +# 1. Frontend. Built first so a Rust-only change does not re-run bun. +FROM oven/bun:1-alpine AS web +WORKDIR /web +COPY web/package.json web/bun.lock ./ +RUN bun install --frozen-lockfile +COPY web/ ./ +RUN bun run build + +# 2. Backend. rust-embed bakes web/dist into the binary in release mode, +# so the result is a single self-contained executable. +FROM rust:1-slim-trixie AS build +WORKDIR /app +COPY Cargo.toml Cargo.lock ./ +COPY src/ ./src/ +COPY migrations/ ./migrations/ +COPY --from=web /web/dist/ ./web/dist/ +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/app/target \ + cargo build --release --locked \ + && cp target/release/narl-diary /usr/local/bin/narl-diary + +# 3. Runtime. SQLite is compiled into the binary, so nothing else is needed. +FROM debian:trixie-slim +RUN useradd --system --uid 10001 --create-home --home-dir /home/diary diary \ + && mkdir -p /data \ + && chown diary:diary /data +COPY --from=build /usr/local/bin/narl-diary /usr/local/bin/narl-diary +USER diary +WORKDIR /home/diary +VOLUME ["/data"] +ENV DIARY_BIND=0.0.0.0:4242 \ + DIARY_DATA_DIR=/data +EXPOSE 4242 +ENTRYPOINT ["/usr/local/bin/narl-diary"] diff --git a/README.md b/README.md index e31773a..7623fe9 100644 --- a/README.md +++ b/README.md @@ -92,3 +92,22 @@ Everything except the two share routes requires the session cookie. Put a TLS-terminating reverse proxy in front of it, point it at `DIARY_BIND`, and set `DIARY_SECURE_COOKIE=1`. Nothing else is needed: no database server, no runtime dependencies, no build tools on the host. + +### Docker + +```sh +cp .env.example .env # set DIARY_USER, DIARY_PASSWORD, DIARY_SECRET +docker compose up -d --build +``` + +The image builds the frontend with bun and the binary with cargo, then ships +only the binary on `debian:trixie-slim` (~86 MB), running as an unprivileged +user. `DIARY_BIND` and `DIARY_DATA_DIR` are forced in `compose.yml`, so the +values in `.env` do not have to change; everything else is read from `.env`. +Database and uploads live in the `diary-data` volume and survive recreation. + +By default the port is published on `127.0.0.1:4242` only. To serve it through +traefik instead, copy `compose.override.yml.example` to `compose.override.yml` +and adjust the host — it joins the external `narl` network, drops the published +port, and routes to container port 4242. Set `DIARY_SECURE_COOKIE=1` in `.env` +once it is served over HTTPS. diff --git a/compose.override.yml.example b/compose.override.yml.example new file mode 100644 index 0000000..7fdd7f7 --- /dev/null +++ b/compose.override.yml.example @@ -0,0 +1,18 @@ +# Copy to compose.override.yml on the host that runs traefik. +services: + diary: + ports: !override [] + networks: + - narl + - internal_net + labels: + - "traefik.enable=true" + - "traefik.docker.network=narl" + - "traefik.http.routers.diary.rule=Host(`diary.narl.io`)" + - "traefik.http.routers.diary.entrypoints=websecure" + - "traefik.http.routers.diary.tls.certresolver=https_resolver" + - "traefik.http.services.diary.loadbalancer.server.port=4242" + +networks: + narl: + external: true diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..372c1cd --- /dev/null +++ b/compose.yml @@ -0,0 +1,33 @@ +services: + diary: + build: . + image: narl-diary:latest + restart: unless-stopped + env_file: + - .env + environment: + # These win over anything in .env: inside the container the app must + # listen on all interfaces and keep its state on the volume. + DIARY_BIND: 0.0.0.0:4242 + DIARY_DATA_DIR: /data + volumes: + - diary-data:/data + networks: + - internal_net + ports: + # Localhost only. Drop this in the override when a reverse proxy + # reaches the container over its own network instead. + - "127.0.0.1:4242:4242" + healthcheck: + # No extra tooling in the image, so probe the port with bash directly. + test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/4242"] + interval: 30s + timeout: 3s + start_period: 5s + retries: 3 + +volumes: + diary-data: + +networks: + internal_net: