# Front build stage # # Приложение собирается до бинарника: вшивание требует готового каталога. # Имя этого образа — единственное; шаг набора проверок берёт его отсюда же, # чтобы версия сборочного окружения не жила вторым числом в Taskfile.yml. # # Образ на glibc, а не на alpine, и разница здесь не в весе: musl шлёт запросы # `A` и `AAAA` разом и ждёт **оба** ответа, а DNS-сервер, который на `AAAA` # молчит, оставляет его без адреса вовсе — при живом `A`. `npm` на такой отказ # уходит в повторы с нарастающей паузой на каждом пакете, и сборка не краснеет, # а **висит**. Библиотека glibc довольствуется полученным `A` и собирает. # Ступень сборочная: в готовый образ её слои не едут, и лишний вес остаётся # ценой одной сборки, а не размером выкладки. FROM docker.io/library/node:24 AS front-build WORKDIR /web # Зависимости ставятся из файла замка командой, которая его не правит: # иначе собранное в образе перестаёт совпадать с собранным в наборе проверок. COPY web/package.json web/package-lock.json ./ RUN npm ci COPY web/ ./ RUN npm run build # Build stage FROM docker.io/library/golang:1.26-alpine AS build-env # Сборочных зависимостей нет: хранилище ходит в SQLite через modernc.org/sqlite, # и CGO больше не требуется. # Set up the working directory WORKDIR /app # Copy go mod files COPY go.mod go.sum ./ # Download dependencies RUN go mod download # Copy source code COPY . . # Собранное приложение приезжает ступенью выше: в дереве сборки его нет, # а вшивание без него отдаёт бинарник, который отвечает «приложение не собрано». COPY --from=front-build /web/embed/dist ./web/embed/dist # Build the application # Собирается одна точка входа из cmd/, а не весь пакет: соседний cmd/devtools — # оснастка разработчика (подставной прокси), и в образе ей делать нечего. RUN CGO_ENABLED=0 go build -o transcriber ./cmd/transcriber # ---------------- # Production stage # ---------------- FROM docker.io/library/alpine:latest LABEL maintainer="anton@vakhrushev.me" # Install runtime dependencies RUN apk --no-cache add \ ca-certificates \ ffmpeg \ && rm -rf /var/cache/apk/* # Create user and group RUN addgroup -S -g 1000 transcriber && \ adduser -S -H -D \ -h /data/transcriber \ -s /bin/sh \ -u 1000 \ -G transcriber \ transcriber # Set environment variables ENV USER=transcriber # Create necessary directories RUN mkdir -p /data && chown -R transcriber:transcriber /data RUN mkdir -p /config && chown -R transcriber:transcriber /config # Set working directory WORKDIR /app # Copy the binary from build stage COPY --from=build-env /app/transcriber . # Copy entrypoint script COPY docker/entrypoint.sh /usr/bin/entrypoint RUN chmod 755 /usr/bin/entrypoint # Set user # # Числом, а не именем: имя разрешает в идентификатор сам образ, и хост, которому # нужно понять владельца файлов в смонтированном каталоге, разрешить его не # может. Числа те же, что заданы выше при заведении пользователя (`-u 1000`, # `-g 1000`), поэтому владелец файлов не меняется — меняется только запись. USER 1000:1000 EXPOSE 8080 # Set entrypoint and default command # ENTRYPOINT ["/usr/bin/entrypoint"] CMD ["./transcriber"]