Skip to content

Repository files navigation

Portfolio

Dioxus fullstack (SSR + hydration) portfolio served by Axum.

Release Build License Rust

What this is

The source of https://tim-schoenle.de, as one Rust workspace.

apps/web is a single crate with two feature-selected builds. The server build is a native Axum binary that renders every route. The web build is a WASM bundle that hydrates it in the browser. The Dioxus CLI produces both.

The configuration tables below are generated, not written. They come out of the Rust types that load the configuration, as do config.example.toml and the contract document the image publishes about itself, so renaming a field corrects all three in the commit that renames it.

Quick start

docker run --rm -p 8080:8080 timschoenle/portfolio:v2.10.0

Then open http://localhost:8080. The runtime image is FROM scratch: one statically linked binary, the client bundle beside it, and the configuration contract at /config/contract.json. It runs as 1001:1001 and needs nothing writable.

Table of contents

Features

  • Every route renders on the server and then hydrates. Per-route <head> metadata and JSON-LD ship in the first response, and the locale is negotiated from request headers before the document is serialised, so nothing arrives in the wrong language and gets swapped a moment later.
  • EN and DE throughout, including both legal pages and both resumes. translation_key_sets_match in crates/data fails the build when the two translation files disagree on a key, which otherwise shows up as one English string in a German page rather than as an error.
  • The resumes are typeset during the build. Typst lays out one A4 page per language, scaling the type down and re-typesetting until the content fits, and each PDF carries a SHA-256 fingerprint that the contact card shows.
  • The Content-Security-Policy is built per response. The server hashes the inline scripts that document actually carries and reserves a nonce for the script Cloudflare injects at the edge, so script-src needs no 'unsafe-inline'.
  • /licenses lists every crate the client and the server link, the licence it ships under, and the verbatim text of every licence file found. cargo-about produces it during the image build, and an unlisted licence fails that build.
  • The project list is fetched from the GitHub API at build time, with archived, blacklisted and year-stale repositories dropped, then embedded into the binary by build.rs.
  • Cmd+K opens a command palette with fuzzy search and keyboard navigation. The stack section is a skill radar with per-skill tooltips and category filtering.

Installation

Docker

docker pull timschoenle/portfolio:v2.10.0

Both architectures are pushed as one manifest list, so docker pull resolves the right image per node. The push carries an SBOM and max-mode provenance and is signed with cosign. Pin by digest in production. The Helm chart does.

Helm

helm repo add timschoenle https://timschoenle.github.io/helm-charts
helm install portfolio timschoenle/portfolio

The chart is in TimSchoenle/helm-charts and each release bumps it to the new image digest.

From source

rustup target add wasm32-unknown-unknown
cargo install --locked dioxus-cli cargo-about
git clone https://github.com/TimSchoenle/Portfolio.git
cd Portfolio

Node.js is needed for the Tailwind step, and just runs every recipe CI runs.

Usage

Two artefacts have to exist before the web build, because apps/web/build.rs embeds both. Without them it substitutes empty defaults and the pages render their empty state.

cargo run -p resume-generator -- apps/web/generated   # resume PDFs, fingerprints, social card
just licenses                                         # third-party inventory for /licenses
cd apps/web && npm ci && npm run build:css
dx serve --platform web                               # SSR + hydration on http://localhost:8080

Run the checks CI runs, in one recipe:

just verify   # fmt, lint, test

Refresh the project list from the GitHub API:

PORTFOLIO_GITHUB__TOKEN_FILE=/run/secrets/gh_token \
  cargo run --release -p update-repos -- apps/web/repos.json

Without a token the run still works, against the anonymous rate limit. docs/PROJECT_DATA.md has the filtering rules and the caching.

Configuration

Values are resolved in five layers, each overriding the one above it. The last three are mutually exclusive per key: a key supplied by two of them fails the boot rather than letting one win, because a stale environment variable shadowing a rotated mounted secret keeps the process running on the old credential.

  1. Defaults — the serde defaults of each typed block.
  2. TOML at $PORTFOLIO_CONFIG — a file, or every *.toml inside it when it names a directory.
  3. EnvironmentPORTFOLIO_-prefixed variables, __ for nesting.
  4. Secrets directory at $PORTFOLIO_SECRETS_DIR — one file per key, named after it (github__token). This is what a Kubernetes Secret volume mounts.
  5. File indirectionPORTFOLIO_<KEY>_FILE=/path names a file holding the value.

An empty value counts as unset in every layer, because container platforms routinely inject KEY= for a declared-but-unset variable.

Server

The two variables the loader reads before any layer exists, then every key apps/web loads. Each environment spelling also accepts a _FILE suffix naming a file that holds the value.

Variable Role Default Purpose
PORTFOLIO_CONFIG config config.toml Names the TOML layer: a file, or a directory whose *.toml files are all merged in name order.
PORTFOLIO_SECRETS_DIR secrets dir Names a directory of key-named files — a mounted Kubernetes Secret volume. Each file supplies the key its name spells.
TOML Type Environment Default Flags Purpose
assets.dist_dir PathBuf PORTFOLIO_ASSETS__DIST_DIR public Directory holding the dx bundle output, relative to the working directory.
csp.hash_inline_scripts bool PORTFOLIO_CSP__HASH_INLINE_SCRIPTS true Hash every inline <script> in the document being served instead of admitting all inline script with 'unsafe-inline'.
csp.cloudflare.script_nonce bool PORTFOLIO_CSP__CLOUDFLARE__SCRIPT_NONCE true Reserve a per-response nonce in script-src for the script Cloudflare injects at the edge.
csp.cloudflare.turnstile bool PORTFOLIO_CSP__CLOUDFLARE__TURNSTILE false Admit https://challenges.cloudflare.com in script-src and frame-src, for a Turnstile widget.
csp.cloudflare.web_analytics bool PORTFOLIO_CSP__CLOUDFLARE__WEB_ANALYTICS false Admit the Cloudflare Web Analytics beacon and the endpoint it reports to.
isr.cache_dir PathBuf PORTFOLIO_ISR__CACHE_DIR unset (ISR off; the image sets /tmp/isr) Writable directory rendered HTML is cached into. Unset or empty disables ISR.
isr.ttl_secs u64 PORTFOLIO_ISR__TTL_SECS 0 (permanent) Revalidation interval in seconds. Zero means a permanent cache.
sentry.enabled bool PORTFOLIO_SENTRY__ENABLED false Initialise the Sentry client. false installs no client, no panic hook, no tracing layer and no HTTP middleware, so every other key here is inert and nothing leaves the process.
sentry.dsn SecretString PORTFOLIO_SENTRY__DSN unset secret Ingest URL, https://<key>@<host>/<project>.
sentry.environment String PORTFOLIO_SENTRY__ENVIRONMENT unset (production in a release build, development otherwise) Environment tag on every event.
sentry.release String PORTFOLIO_SENTRY__RELEASE unset (portfolio@ and the built version) Release tag on every event.
sentry.server_name String PORTFOLIO_SENTRY__SERVER_NAME unset Host tag on every event.
sentry.sample_rate f32 PORTFOLIO_SENTRY__SAMPLE_RATE 1 Fraction of captured events actually sent, 0.01.0.
sentry.traces_sample_rate f32 PORTFOLIO_SENTRY__TRACES_SAMPLE_RATE 0 Fraction of request traces recorded, 0.01.0.
sentry.capture_level SentryLevel: off | error | warn | info | debug | trace PORTFOLIO_SENTRY__CAPTURE_LEVEL error Least severe tracing level reported as a Sentry issue.
sentry.breadcrumb_level SentryLevel: off | error | warn | info | debug | trace PORTFOLIO_SENTRY__BREADCRUMB_LEVEL info Least severe tracing level kept as a breadcrumb, the trail attached to the next issue.
sentry.max_breadcrumbs usize PORTFOLIO_SENTRY__MAX_BREADCRUMBS 100 How many breadcrumbs one event carries.
sentry.attach_stacktraces bool PORTFOLIO_SENTRY__ATTACH_STACKTRACES true Attach a stack trace to events that carry none of their own.
sentry.send_default_pii bool PORTFOLIO_SENTRY__SEND_DEFAULT_PII false Send personally identifying data with every event: the client IP, the full request header set (Cookie included) and the resolved user.
sentry.http_transactions bool PORTFOLIO_SENTRY__HTTP_TRANSACTIONS true Record one Sentry transaction per request, named by the matched route rather than by the URI — so /api/repos/{name} does not become one transaction name per repository.
sentry.span_attributes bool PORTFOLIO_SENTRY__SPAN_ATTRIBUTES false Copy tracing span fields onto the Sentry span as attributes.
sentry.debug bool PORTFOLIO_SENTRY__DEBUG false Print the SDK's own diagnostics to stderr. For proving a DSN works, not for running.

Builder

What update-repos loads. It runs during the image build and exits. The server never reads these keys, so a deployment needs no GitHub token. That is why there are two tables and not one.

TOML Type Environment Default Flags Purpose
github.username String PORTFOLIO_GITHUB__USERNAME unset (the site's own CONFIG.github_username) User whose repositories to list.
github.token SecretString PORTFOLIO_GITHUB__TOKEN unset secret Bearer token lifting the GitHub API rate limit.
github.repos Vec<String> PORTFOLIO_GITHUB__REPOS [] (every active repository the user owns) Explicit repository set, bypassing the "every active repository" listing and its filtering.

There are two secrets in the workspace, and each should arrive as a file rather than as an environment variable: /proc/<pid>/environ, a crash dump and docker inspect all carry the environment, and child processes inherit it.

github.token is the builder's, and the Docker build already mounts it as a BuildKit secret and hands update-repos the path. sentry.dsn is the server's, read only when error reporting is switched on — supply it as PORTFOLIO_SENTRY__DSN_FILE=/path, or as sentry__dsn in a mounted Secret volume.

IP, PORT and RUST_LOG sit outside this namespace deliberately. They are the Dioxus toolchain's contract with the binary, and it keeps reading them itself.

config.example.toml carries every key at its default, commented out, and is rendered from the same payload as these tables.

Operations

Probes

Endpoint Alias Purpose
GET /api/health Health report with the current UTC time
GET /api/health/live GET /livez Liveness — the process is running; failure restarts the container
GET /api/health/ready GET /readyz Readinessindex.html under assets.dist_dir (default public/) is present and servable; failure removes the pod from the Service endpoints

All probe responses are no-store. Readiness returns 503 until the assets are there.

Runtime posture

The server reads its bundle directory and writes to stdout. It runs as numeric non-root 1001:1001, so runAsNonRoot verifies statically, and it satisfies the restricted Pod Security Standard with a read-only root filesystem, no privilege escalation, every capability dropped and seccompProfile: RuntimeDefault.

Incremental static regeneration is the one thing that wants a writable path. The image points the cache at /tmp/isr and bakes an empty one owned by the runtime user; where that path is not writable, every request is rendered fresh instead.

Error reporting

Optional, off, and the only thing that would give the process an outbound connection. Set sentry.enabled with a DSN and the server installs a Sentry client, a panic hook, a tracing layer feeding it, and one transaction per request named by the matched route. Enabled without a usable DSN it refuses to boot, rather than reporting into a dashboard whose emptiness looks like a healthy site.

Two keys decide how much goes with it. sentry.traces_sample_rate is 0, so switching reporting on does not also switch performance data on. sentry.send_default_pii is false and should stay there: on, every event carries the client IP and the full request header set, Cookie included.

While Sentry is off, RUST_LOG and the log format are the Dioxus toolchain's as before; while it is on, the server installs the subscriber itself — same variable, same default verbosity, plus the layer that reports.

Compatibility

Supported
Rust 1.97 (edition 2024)
Platforms linux/amd64, linux/arm64
Pod Security Standard restricted

Documentation

Document Purpose
Architecture Six packages in one Cargo workspace: two libraries every binary reads, three binaries, and a placeholder at the root that exists so release-please has a version to move.
Deployment What the image contains, how it is built reproducibly, what it publishes about its own configuration, and where the chart that runs it lives.
Project data How apps/web/repos.json is fetched, filtered, cached and embedded, and what the projects section reads out of it.
Security posture The Content-Security-Policy the server builds per response, the headers around it, and what the process needs from the filesystem it runs on.
docs/config.contract.json

Contributing

CONTRIBUTING.md has the development setup, the commit convention and the checks a pull request has to pass. Note what the licence grants before you fork: reading the source and running it locally, and nothing about redistributing or deploying it.

Several files here are generated, this one included. Each says so in its opening lines, and an edit made to the output instead of to the template is replaced by the next render.

Security

Do not open a public issue for a vulnerability. SECURITY.md has the reporting route and the supported versions.

License

LicenseRef-Proprietary. Viewing the source and running it locally for personal, non-commercial evaluation is granted. Copying, modifying, redistributing, deploying and training on it are not. LICENSE has the terms. The bundled fonts keep their own: Inter and Liberation Sans are both under the SIL Open Font License, in apps/resume-generator/fonts/.

About

Dioxus fullstack (SSR + hydration) portfolio served by Axum.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages