# syntax=docker/dockerfile:1

# One image, holding both halves of the app.
#
# v1 shipped the backend as an image and the frontend as an `rsync` of `dist/`
# over SSH, run by hand from a laptop -- so the two could be, and were, out of
# step with each other, and the audit marked that as the one thing to do
# differently (F76). Here they are built from the same commit into the same
# image and can only be deployed together. The frontend is not served from
# here: it is lifted out of this image onto the directory Caddy already serves,
# which is why it sits at a known path.
#
# The frontend addresses the API as `/api/...`, relative, so this artefact is
# the same everywhere and nothing is baked in per environment. v1 compiled the
# API's absolute URL into the WASM, which is the other half of why its frontend
# deploy was a separate manual step.

# ---------------------------------------------------------------- frontend --
FROM rust:1.98-slim-bookworm AS web

RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates curl \
 && rm -rf /var/lib/apt/lists/*

# Node from the official image rather than Debian's, which ships npm 9.
# Tailwind v4's `oxide` is a native binary chosen by an optional dependency
# keyed on platform *and* libc, and npm 9 does not understand the `libc` field
# -- so it silently installs no binding at all and the build dies on
# `Cannot find module '@tailwindcss/oxide-linux-x64-gnu'`. Copied from a pinned
# image rather than piped from a setup script into a shell, so which Node this
# is stays a line in this file.
COPY --from=node:22-bookworm-slim /usr/local/bin/node /usr/local/bin/node
COPY --from=node:22-bookworm-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
 && ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx

# Trunk as a released binary rather than `cargo install trunk`, which builds it
# from source and takes longer than everything else here put together.
ARG TRUNK_VERSION=0.21.14
RUN curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \
  | tar -xzC /usr/local/bin trunk

RUN rustup target add wasm32-unknown-unknown

WORKDIR /app

# Tailwind runs from Trunk's pre-build hook as `npx --prefix ../..`, so the
# packages have to be at the workspace root. Copied before the source so a
# change to the Rust does not reinstall them.
COPY package.json package-lock.json ./
RUN npm ci

COPY . .

WORKDIR /app/crates/runway-web
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    trunk build --release

# ----------------------------------------------------------------- backend --
FROM rust:1.98-slim-bookworm AS server

WORKDIR /app
COPY . .

# The binary is copied out of the cache mount because a cache mount is not part
# of the layer: whatever is written there is gone by the time the next stage
# looks. This is also why there is no dummy-source dance -- the cache does what
# that trick was inventing.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release --locked -p runway-server \
 && cp target/release/runway-server /runway-server

# ----------------------------------------------------------------- runtime --
FROM debian:bookworm-slim AS runtime

# TLS is rustls with its roots compiled in, so there is no OpenSSL here and
# nothing to keep patched. `tzdata` is not optional: this is a calendar, and
# recurrence over a DST boundary is decided by the zone database.
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates tzdata \
 && rm -rf /var/lib/apt/lists/*

COPY --from=server /runway-server /usr/local/bin/runway-server
# Where the updater looks for the frontend. Nothing serves it from inside this
# container.
COPY --from=web /app/crates/runway-web/dist /srv/dist

# Migrations are compiled into the binary by `sqlx::migrate!` and run on
# startup, and the database is created if it is missing -- so there is no
# entrypoint script, no `sqlx-cli` in the image, and nothing that can decide to
# carry on after a failed migration the way v1's start.sh did.
# The mount point, created here rather than left to the volume. SQLite creates
# the database *file* if it is missing but not the directory holding it, so an
# image without this starts only when something happens to have mounted
# something at /db, and fails with "unable to open database file" when nothing
# has -- which says nothing about what is actually wrong.
RUN mkdir -p /db

ENV RUNWAY_DATABASE_URL=sqlite:///db/runway.db \
    RUNWAY_BIND=0.0.0.0:3000

EXPOSE 3000
CMD ["runway-server"]
