Skip to content

Commit 3ba30e8

Browse files
feat(builder): declare uv.lock git sources for the caller to mirror (#103)
* build(deps): pin core to the git source downloads change Pins github.com/codefly-dev/core to the open change that adds DockerBuildRecipe.git_source_downloads (contract v6), by pseudo-version, until it is released; the pin moves to the core tag, together with the shared workflow in ci.yml, after the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(builder): declare uv.lock git sources for the caller to mirror A git dependency that needs a credential resolves on the host and fails in the builder stage, which has none. The recipe now declares every git commit uv.lock pins (git_source_downloads, mirror context "uvsources"): the caller fetches them on its own machine before the build and supplies one bare mirror per repository, and the builder stage maps each locked URL onto its mirror with url.<mirror>.insteadOf before the unchanged uv sync --frozen. The declared repository is the lock's URL up to its query, which is the string uv hands git, so the insteadOf match is exact. The workspace root's lock is read for a workspace member. A lock with no git sources renders the Dockerfile and plan contract it rendered before, and a plain docker build without the context fetches from the network as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(builder): leave http and file git sources to the build's own fetch Only https and ssh git sources can be mirrored. A package locked to any other git URL was refused, which would have broken a project that builds today. It is now left out of the declaration, with a warning naming the package (not the URL), and the build fetches it from the network as before. A locked git source without a commit is still refused: uv always records one, so that lock is malformed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(deps): repin core to the exported git source checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(builder): declare and render only the git sources core accepts A locked source core refuses (a credential over https, percent-encoding, "@" in the path, a scheme other than https or ssh) was declared and rendered into the Dockerfile before core refused the plan, failing a build that works today. Each source is now checked with core's own GitSourceRepositoryProblem first: a refused one is neither declared nor rendered, and a warning names the package and core's reason, never the URL; the build fetches it itself, as before. A commit that is not a full object id (IsGitSourceCommit) still fails, naming the package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(builder): append the mirror mappings to inherited git config The sync step set GIT_CONFIG_COUNT and numbered its insteadOf mappings from 0, replacing any git configuration a base image or an earlier ENV had put in the environment. The mappings are now appended after ${GIT_CONFIG_COUNT:-0}, and the count is raised to cover them. Values stay single-quoted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(builder): pin the Dockerfile frontend when git sources are declared The sync step's RUN --mount comes from the Dockerfile frontend, so a recipe that declares git sources pins it, with the same digest the Go agent's recipe pins. A recipe without git sources renders as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(builder): keep the mirror mappings from capturing or losing to other git config git's insteadOf rewrites every URL that begins the configured one, so a declared https://h/acme/mono also rewrote an undeclared locked https://h/acme/mono@v1 onto its mirror, failing a build that worked. A declarable source whose URL begins an undeclared locked source's URL is now left out as well (warned, fetched by the build), until no such capture remains. Two declared URLs, one beginning the other, stay declared: git applies the longest match. An inherited rewrite of the same URL came first and won over the appended mirror mapping. The mappings now take entries 0..N-1 and the inherited entries move after them, read by constructed name and never evaluated. The inherited count is read as git reads it (a leading "+" and leading zeros, so 01, +1 and 08 work); a count git would reject is left alone for git to reject. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(builder): append the mirror mappings only after a plain inherited count Moving inherited git config after the mirror mappings could not win precedence anyway (git breaks insteadOf ties by the order the destination base was registered, so a system or global rewrite still wins) and added edge cases of its own. The mappings are appended after the inherited entries again, without reading or moving them. They are used only when GIT_CONFIG_COUNT is unset, empty, or a plain count of at most four digits once one leading "+" and leading zeros are dropped; otherwise the git configuration is left alone, the build log says the sources are fetched without the mirror, and the build runs as it does today. An image whose own git configuration (system, global or environment) already rewrites a declared repository's URL keeps its rewrite: git prefers it, and the build fetches that source as it does without a mirror. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(deps): pin core to v0.19.6, which carries the git source declaration (#832) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: run the reusable service CI from core v0.19.6, the linked core's tag Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Saurabh Maheshwari <242053479+saurabhmaheshwari-obin@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent fb66a62 commit 3ba30e8

10 files changed

Lines changed: 789 additions & 10 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ concurrency:
1212

1313
jobs:
1414
ci:
15-
uses: codefly-dev/core/.github/workflows/go-service-ci.yml@v0.19.4
15+
uses: codefly-dev/core/.github/workflows/go-service-ci.yml@v0.19.6
1616
with:
1717
# The Python runtime tests drive `uv` on the host (dependency sync,
1818
# pytest); it isn't on the runner by default.

‎AGENTS.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ within reach.
6767
## Build and test
6868

6969
Derived from `.github/workflows/ci.yml`, which delegates to core's shared
70-
`go-service-ci.yml`, pinned to the linked core's tag (`@v0.19.4`; move it with
70+
`go-service-ci.yml`, pinned to the linked core's tag (`@v0.19.6`; move it with
7171
the core bump), and adds a `race` job of its own. Go comes from `go.mod`
72-
(1.27.2). All of it runs locally:
72+
(1.27.3). All of it runs locally:
7373

7474
```bash
7575
go build ./... # CI: build job
@@ -126,6 +126,7 @@ anything from it.
126126
| `runtime.go` | Init/Start/Stop/Destroy, uvicorn supervision, hot reload, the lock order |
127127
| `builder.go` | `Create` scaffolding, OpenAPI endpoint loading, Docker + kustomize recipes |
128128
| `uvsources.go` | the assembled build context: `[tool.uv.sources]` paths leaving the project |
129+
| `uvgitsources.go` | the git commits `uv.lock` pins, declared for the caller to mirror into the build |
129130
| `probes.go`, `probe.py` | endpoint health translated to Kubernetes probes / a local probe |
130131
| `templates/` | **what ships** — embedded via `go:embed`, rendered on `Create` |
131132
| `base/` | a checked-in sample service, embedded nowhere |
@@ -158,6 +159,13 @@ into `templates/factory/code/pyproject.toml.tmpl` yourself.
158159
same relative path resolves there; that plan claims the whole tree
159160
(`RECIPE_INVENTORY_SCOPE_TREE`), the Dockerfile-only one claims just what it
160161
emitted. Never rewrite `pyproject.toml` or `uv.lock` to make a path resolve.
162+
A git source is declared in the recipe (`git_source_downloads`) by the URL
163+
exactly as `uv.lock` records it up to its query, which is what uv hands git;
164+
the caller fetches it with its own credentials and the Dockerfile maps that
165+
URL onto the mirror; a normalized URL would miss it silently. A source core
166+
refuses, or whose URL begins an undeclared locked one (insteadOf is a
167+
prefix match), is not declared; a warning says why. An image whose own git
168+
config already rewrites a declared URL keeps that rewrite (git prefers it).
161169
- **The runtime image is pinned.** `docker-image` rejects a bare name and
162170
`:latest`. The default `ghcr.io/codefly-dev/python` companion is built from
163171
core and pinned by the digest of its multi-platform index — prefer it. Its tag

‎builder.go‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ type DockerTemplating struct {
192192
// IgnorePatterns are the dockerignore entries of a recipe that copies the
193193
// service tree whole.
194194
IgnorePatterns []string
195+
// GitSources are the repositories of the git commits the lock pins, which
196+
// the builder stage reads from the caller's mirrors (uvgitsources.go).
197+
GitSources []uvGitMirror
195198
}
196199

197200
func (s *Builder) BuildCapabilities(context.Context, *builderv0.BuildCapabilitiesRequest) (*builderv0.BuildCapabilitiesResponse, error) {
@@ -248,11 +251,25 @@ func (s *Builder) Build(ctx context.Context, req *builderv0.BuildRequest) (*buil
248251
if err != nil {
249252
return s.Base.Builder.BuildError(err)
250253
}
254+
gitSources, unmirrored, err := lockedGitSources(uvLockPath(s.Location, s.SourceLocation, membership))
255+
if err != nil {
256+
return s.Base.Builder.BuildError(err)
257+
}
258+
for _, source := range unmirrored {
259+
// The URL is not logged: a lock may still carry a credential in it.
260+
s.Wool.Warn("git source is not mirrored: the image build fetches it itself",
261+
wool.Field("package", source.Package), wool.Field("reason", source.Reason))
262+
}
263+
var options []services.RecipeOption
264+
if len(gitSources) > 0 {
265+
options = append(options, services.WithGitSourceDownloads(gitSources...))
266+
}
251267
docker := DockerTemplating{
252268
Builder: runtimeImage.FullName(),
253269
Components: components,
254270
UVImage: uvImage,
255271
ProjectWorkdir: defaultProjectWorkdir,
272+
GitSources: uvGitMirrors(gitSources),
256273
}
257274
if assembled != nil {
258275
docker.ServicePrefix = assembled.ServicePrefix
@@ -272,19 +289,23 @@ func (s *Builder) Build(ctx context.Context, req *builderv0.BuildRequest) (*buil
272289
// Only the Dockerfile and its ignore were written, and the application's
273290
// inputs are the service's own tree: the plan claims what it emitted and
274291
// the caller builds the service directory.
275-
return s.Base.Builder.SingleImageBuildResponse(req, image, emitted)
292+
return s.Base.Builder.SingleImageBuildResponse(req, image, emitted, options...)
276293
}
277294
// The whole destination was assembled by this build, so the plan claims all
278295
// of it and the caller builds the assembled tree rather than the service
279296
// directory — which is what RECIPE_INVENTORY_SCOPE_TREE declares.
280-
plan, err := services.BuildDockerBuildPlan(outputDir, []*builderv0.DockerBuildRecipe{{
297+
recipe := &builderv0.DockerBuildRecipe{
281298
Name: "app",
282299
Dockerfile: "Dockerfile",
283300
Context: ".",
284301
Dockerignore: dockerignoreOf(emitted),
285302
Image: image,
286303
Platforms: services.RecipeBuildPlatforms(),
287-
}})
304+
}
305+
for _, option := range options {
306+
option(recipe)
307+
}
308+
plan, err := services.BuildDockerBuildPlan(outputDir, []*builderv0.DockerBuildRecipe{recipe})
288309
if err != nil {
289310
return s.Base.Builder.BuildError(err)
290311
}

‎go.mod‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
module github.com/codefly-dev/service-python-fastapi
22

3-
go 1.27.2
3+
go 1.27.3
44

55
require (
6-
github.com/codefly-dev/core v0.19.4
6+
github.com/codefly-dev/core v0.19.6
77
github.com/codefly-dev/service-python v0.0.66
88
github.com/pelletier/go-toml/v2 v2.4.3
99
github.com/stretchr/testify v1.12.1

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
4343
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
4444
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
4545
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
46-
github.com/codefly-dev/core v0.19.4 h1:vUvaFrWkB8B3ksQY/qlgBTVj3jMNhdEpJca8aofaals=
47-
github.com/codefly-dev/core v0.19.4/go.mod h1:0uX/7lTBVaGV0RI2Mb3mpEr/PPdx/jZqXHym/U8FVDI=
46+
github.com/codefly-dev/core v0.19.6 h1:ZkCChK/BioK+B7zZ0fp+/rXDCy0k+lsdFqHjyc5xFIg=
47+
github.com/codefly-dev/core v0.19.6/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A=
4848
github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ=
4949
github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg=
5050
github.com/codefly-dev/service-python v0.0.66 h1:Y74QvKAYMaOnZcsfRxuD9dpQo3//7Mjo12RYELyrp5o=

‎templates/builder/Dockerfile.tmpl‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
{{if .GitSources -}}
2+
# syntax=docker/dockerfile:1@sha256:ecfaec9ed6d810b56388c508f4121597bfbba70d41a6dfeee4d8cad5f295fc32
3+
# The git commits uv.lock pins (git_source_downloads in the recipe), fetched by
4+
# the caller on its own machine, with its own credentials, before any image
5+
# build, and supplied here as the build context of this name: one bare mirror
6+
# per repository. Without a caller supplying it, the stage is empty and uv
7+
# fetches them from the network as it always did.
8+
FROM scratch AS uvsources
9+
10+
{{end -}}
111
# The builder image — installs the venv via uv, then we copy it into the runtime.
212
FROM {{.Builder}} as builder
313

@@ -43,7 +53,30 @@ COPY {{.Context}} {{.Image}}
4353
ENV UV_PROJECT_ENVIRONMENT=/app/.venv \
4454
UV_LINK_MODE=copy \
4555
UV_PYTHON_INSTALL_DIR=/opt/uv/python
56+
{{- if .GitSources}}
57+
#
58+
# With the caller's mirrors, git reads each locked repository from its mirror
59+
# instead of the URL the lock records (uv hands git that URL unchanged), so no
60+
# credential is needed, and a commit missing from the mirror fails here rather
61+
# than prompting for one. The mappings are appended after the git configuration
62+
# the environment already carries, when its GIT_CONFIG_COUNT is a plain count;
63+
# otherwise the configuration is left alone and the sources are fetched as
64+
# without a mirror. An image whose own git configuration (system, global or
65+
# environment) already rewrites a declared repository's URL keeps its rewrite:
66+
# git prefers it, and the build fetches that source as it does without a mirror.
67+
RUN --mount=type=bind,from=uvsources,target=/uvsources \
68+
if [ -n "$(ls -A /uvsources)" ]; then \
69+
n=${GIT_CONFIG_COUNT:-0}; n=${n#+}; \
70+
case $n in ''|*[!0-9]*) n= ;; *) while [ "${n#0}" != "$n" ] && [ -n "${n#0}" ]; do n=${n#0}; done; [ ${#n} -le 4 ] || n= ;; esac; \
71+
if [ -z "$n" ]; then echo "inherited GIT_CONFIG_COUNT is not a plain count; git sources are fetched without the mirror"; \
72+
else export GIT_TERMINAL_PROMPT=0{{range $i, $source := .GitSources}} \
73+
"GIT_CONFIG_KEY_$((n+{{$i}}))=url.file:///uvsources/{{$source.Path}}.insteadOf" "GIT_CONFIG_VALUE_$((n+{{$i}}))="'{{$source.Repository}}'{{end}} \
74+
GIT_CONFIG_COUNT=$((n+{{len .GitSources}})); fi; \
75+
fi && \
76+
mkdir -p /opt/uv/python && uv sync --frozen --no-dev --no-install-project --no-editable
77+
{{- else}}
4678
RUN mkdir -p /opt/uv/python && uv sync --frozen --no-dev --no-install-project --no-editable
79+
{{- end}}
4780

4881

4982
# Runtime
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# syntax=docker/dockerfile:1@sha256:ecfaec9ed6d810b56388c508f4121597bfbba70d41a6dfeee4d8cad5f295fc32
2+
# The git commits uv.lock pins (git_source_downloads in the recipe), fetched by
3+
# the caller on its own machine, with its own credentials, before any image
4+
# build, and supplied here as the build context of this name: one bare mirror
5+
# per repository. Without a caller supplying it, the stage is empty and uv
6+
# fetches them from the network as it always did.
7+
FROM scratch AS uvsources
8+
9+
# The builder image — installs the venv via uv, then we copy it into the runtime.
10+
FROM BUILDER_IMAGE as builder
11+
12+
# uv is part of what the recipe has to state. A base image carries whichever uv
13+
# it happened to be built with, so a project written against a newer `[tool.uv]`
14+
# key resolves on the developer's machine and fails inside the builder stage
15+
# with a TOML parse error naming a field that uv there does not know. Copying a
16+
# pinned uv in makes the tool that reads pyproject.toml and uv.lock a declared
17+
# input of the recipe instead of a property of the base image.
18+
COPY --from=UV_IMAGE /uv /usr/local/bin/uv
19+
20+
WORKDIR /app
21+
22+
COPY code/pyproject.toml code/uv.lock ./
23+
24+
# Install deps to a project-local .venv. --frozen installs the locked resolution
25+
# as it stands instead of re-resolving it at build time. --no-editable installs
26+
# workspace members and path sources as packages in the venv rather than links
27+
# to builder-stage sources, which the runtime stage does not carry.
28+
#
29+
# A project whose requires-python the base image's Python does not satisfy gets
30+
# the interpreter uv installs for it, as on the host. It is installed where the
31+
# runtime stage copies it from, so the venv's interpreter exists there too.
32+
ENV UV_PROJECT_ENVIRONMENT=/app/.venv \
33+
UV_LINK_MODE=copy \
34+
UV_PYTHON_INSTALL_DIR=/opt/uv/python
35+
#
36+
# With the caller's mirrors, git reads each locked repository from its mirror
37+
# instead of the URL the lock records (uv hands git that URL unchanged), so no
38+
# credential is needed, and a commit missing from the mirror fails here rather
39+
# than prompting for one. The mappings are appended after the git configuration
40+
# the environment already carries, when its GIT_CONFIG_COUNT is a plain count;
41+
# otherwise the configuration is left alone and the sources are fetched as
42+
# without a mirror. An image whose own git configuration (system, global or
43+
# environment) already rewrites a declared repository's URL keeps its rewrite:
44+
# git prefers it, and the build fetches that source as it does without a mirror.
45+
RUN --mount=type=bind,from=uvsources,target=/uvsources \
46+
if [ -n "$(ls -A /uvsources)" ]; then \
47+
n=${GIT_CONFIG_COUNT:-0}; n=${n#+}; \
48+
case $n in ''|*[!0-9]*) n= ;; *) while [ "${n#0}" != "$n" ] && [ -n "${n#0}" ]; do n=${n#0}; done; [ ${#n} -le 4 ] || n= ;; esac; \
49+
if [ -z "$n" ]; then echo "inherited GIT_CONFIG_COUNT is not a plain count; git sources are fetched without the mirror"; \
50+
else export GIT_TERMINAL_PROMPT=0 \
51+
"GIT_CONFIG_KEY_$((n+0))=url.file:///uvsources/c9fa8363b851c2643d648edad1c8c7892904f717385511fe4771422d0817a55e.git.insteadOf" "GIT_CONFIG_VALUE_$((n+0))="'https://git.example.invalid/acme/mono' \
52+
"GIT_CONFIG_KEY_$((n+1))=url.file:///uvsources/d2b987abe743521016c33e82dba28324380629b9d2544f9e5213ee4299a3af6c.git.insteadOf" "GIT_CONFIG_VALUE_$((n+1))="'ssh://git@git.example.invalid:2222/acme/tools.git' \
53+
GIT_CONFIG_COUNT=$((n+2)); fi; \
54+
fi && \
55+
mkdir -p /opt/uv/python && uv sync --frozen --no-dev --no-install-project --no-editable
56+
57+
58+
# Runtime
59+
FROM BUILDER_IMAGE as runtime
60+
61+
WORKDIR /app
62+
63+
# UID pinned to match runAsUser in the kustomize Deployment: Kubernetes'
64+
# runAsNonRoot check rejects a container whose image user is non-numeric.
65+
RUN adduser -D -u 1000 appuser
66+
USER appuser
67+
68+
ENV VIRTUAL_ENV=/app/.venv \
69+
PATH="/app/.venv/bin:$PATH"
70+
71+
COPY --from=builder /opt/uv/python /opt/uv/python
72+
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
73+
74+
COPY --chown=appuser service.codefly.yaml .
75+
76+
77+
COPY --chown=appuser code/src code/src
78+
79+
80+
81+
82+
WORKDIR /app/code
83+
84+
# uvicorn's logging configuration and the filter it names (uvicorn_logging.go):
85+
# health probes are dropped from the access log, everything else is as uvicorn
86+
# writes it. The builder writes both into the project before the build.
87+
COPY --chown=appuser code/uvicorn-log-config.json code/codefly_uvicorn_logging.py ./
88+
89+
# Default REST port (overridden by codefly at runtime).
90+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080", "--log-config", "uvicorn-log-config.json"]
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# The builder image — installs the venv via uv, then we copy it into the runtime.
2+
FROM BUILDER_IMAGE as builder
3+
4+
# uv is part of what the recipe has to state. A base image carries whichever uv
5+
# it happened to be built with, so a project written against a newer `[tool.uv]`
6+
# key resolves on the developer's machine and fails inside the builder stage
7+
# with a TOML parse error naming a field that uv there does not know. Copying a
8+
# pinned uv in makes the tool that reads pyproject.toml and uv.lock a declared
9+
# input of the recipe instead of a property of the base image.
10+
COPY --from=UV_IMAGE /uv /usr/local/bin/uv
11+
12+
WORKDIR /app
13+
14+
COPY code/pyproject.toml code/uv.lock ./
15+
16+
# Install deps to a project-local .venv. --frozen installs the locked resolution
17+
# as it stands instead of re-resolving it at build time. --no-editable installs
18+
# workspace members and path sources as packages in the venv rather than links
19+
# to builder-stage sources, which the runtime stage does not carry.
20+
#
21+
# A project whose requires-python the base image's Python does not satisfy gets
22+
# the interpreter uv installs for it, as on the host. It is installed where the
23+
# runtime stage copies it from, so the venv's interpreter exists there too.
24+
ENV UV_PROJECT_ENVIRONMENT=/app/.venv \
25+
UV_LINK_MODE=copy \
26+
UV_PYTHON_INSTALL_DIR=/opt/uv/python
27+
RUN mkdir -p /opt/uv/python && uv sync --frozen --no-dev --no-install-project --no-editable
28+
29+
30+
# Runtime
31+
FROM BUILDER_IMAGE as runtime
32+
33+
WORKDIR /app
34+
35+
# UID pinned to match runAsUser in the kustomize Deployment: Kubernetes'
36+
# runAsNonRoot check rejects a container whose image user is non-numeric.
37+
RUN adduser -D -u 1000 appuser
38+
USER appuser
39+
40+
ENV VIRTUAL_ENV=/app/.venv \
41+
PATH="/app/.venv/bin:$PATH"
42+
43+
COPY --from=builder /opt/uv/python /opt/uv/python
44+
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
45+
46+
COPY --chown=appuser service.codefly.yaml .
47+
48+
49+
COPY --chown=appuser code/src code/src
50+
51+
52+
53+
54+
WORKDIR /app/code
55+
56+
# uvicorn's logging configuration and the filter it names (uvicorn_logging.go):
57+
# health probes are dropped from the access log, everything else is as uvicorn
58+
# writes it. The builder writes both into the project before the build.
59+
COPY --chown=appuser code/uvicorn-log-config.json code/codefly_uvicorn_logging.py ./
60+
61+
# Default REST port (overridden by codefly at runtime).
62+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080", "--log-config", "uvicorn-log-config.json"]

0 commit comments

Comments
 (0)