mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
chore: add Dockerfile, docker-compose, and Docker hosting docs (#12)
* chore: add Dockerfile, docker-compose, and Docker docs - Multi-stage Dockerfile: Rust builder stage + debian:bookworm-slim runtime with Chromium, Node/single-file-cli, Python venv (yt-dlp + twitter-api-client) - docker-compose.yml: wires ARCHIVR_BIND, config volume, and persistent data volume - docker/config.example.toml: annotated TOML template for Docker deployments - docs/README.md: add Hosting with Docker section; add ARCHIVR_BIND and ARCHIVR_STATIC_DIR to the Environment Variables reference * fix: address code review issues with Docker setup - .gitignore: whitelist Dockerfile, docker-compose.yml, docker/ so they are actually tracked (the * catch-all was silently dropping them) - Dockerfile: build and ship the archivr CLI alongside archivr-server so users can run `archivr init` inside the container on first setup - docker/config.example.toml: fix archive_path to point at the .archivr subdirectory that archivr init creates (not the parent directory), which is what read_archive_paths expects - docs/README.md: replace the bare mkdir quickstart step with `archivr init`, explain why mkdir is insufficient; add a callout that auth_db_path must be set explicitly to a writable path when the config mount is read-only * fix: address second round of Docker review issues Chromium sandbox (P2): - singlefile.rs: add ARCHIVR_CHROME_ARGS env var (space-separated flags appended to Chromium's --browser-args JSON array); Dockerfile sets it to --no-sandbox because Chromium refuses to start as root without it Store-path outside volume (P1): - README: pass explicit absolute store-path as the second positional arg to `archivr init` so the blob store lands on /data instead of the container layer (CLI default is ./.archivr/store, resolved from cwd, which is / with no WORKDIR set) ENTRYPOINT vs CMD (P2): - Dockerfile: switch from ENTRYPOINT to CMD so `docker compose run archivr archivr init …` overrides the full command instead of being appended to the server invocation ffmpeg missing (P2): - Dockerfile: add ffmpeg to the apt-get install block (required by yt-dlp --merge-output-format mp4 for bestvideo+bestaudio streams) Node version (P2): - Dockerfile: replace Debian bookworm's nodejs (18.x) with Node 20 via the NodeSource setup script (single-file-cli declares engines.node >=20) Build context secrets (P2): - Add .dockerignore excluding config/ and docker/ from the build context so runtime secrets (e.g. twitter-cookies.txt) are never sent to the builder - Whitelist .dockerignore in .gitignore docs: - README: document ARCHIVR_CHROME_ARGS in the Environment Variables section * fix: third round of Docker review issues Rust toolchain (P1): - Dockerfile: bump builder from rust:1.87 to rust:1.88; time@0.3.51, time-core@0.1.9, and time-macros@0.2.30 (present in Cargo.lock) all require MSRV 1.88, so the real cargo build --release step was failing single-file-cli wait mode (P2): - singlefile.rs: replace --browser-wait-until=networkidle2 with networkAlmostIdle; the single-file-cli option only accepts InteractiveTime/networkIdle/networkAlmostIdle/load/domContentLoaded (verified in options.js); networkidle2 is a Puppeteer concept that the CLI does not recognise, causing silent fallback to the earliest state and incomplete captures. networkAlmostIdle is the closest equivalent (<=2 open connections, matching Puppeteer's networkidle2 semantics) Build context size (P3): - .dockerignore: add target/, frontend/node_modules/, frontend/dist/; these can reach 1.4G+ after a local dev build and are never read by the Dockerfile, so sending them to the builder wastes time and memory
This commit is contained in:
parent
685b6cc7ea
commit
2414acf0df
7 changed files with 279 additions and 8 deletions
15
.dockerignore
Normal file
15
.dockerignore
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Exclude runtime config and data directories from the Docker build context.
|
||||||
|
# The config/ directory may contain secrets (e.g. twitter-cookies.txt) that are
|
||||||
|
# only needed at runtime via a volume mount — they must never reach the builder.
|
||||||
|
config/
|
||||||
|
docker/
|
||||||
|
|
||||||
|
# Development and VCS noise
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Generated build outputs — can be 1.4G (target/) and 243M (node_modules/)
|
||||||
|
# after a local dev build; exclude them to keep the build context small.
|
||||||
|
target/
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -19,6 +19,12 @@
|
||||||
!ARCHIVR-MENTAL-MODEL.md
|
!ARCHIVR-MENTAL-MODEL.md
|
||||||
!NEXT.md
|
!NEXT.md
|
||||||
|
|
||||||
|
!Dockerfile
|
||||||
|
!.dockerignore
|
||||||
|
!docker-compose.yml
|
||||||
|
!docker/
|
||||||
|
!docker/**
|
||||||
|
|
||||||
!modules/
|
!modules/
|
||||||
!modules/**
|
!modules/**
|
||||||
|
|
||||||
|
|
|
||||||
106
Dockerfile
Normal file
106
Dockerfile
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Stage 1 – Build the Rust server and CLI binaries
|
||||||
|
###############################################################################
|
||||||
|
FROM rust:1.88-slim-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Layer the dependency build separately for better cache reuse.
|
||||||
|
# Stub out every crate so Cargo can resolve and compile all dependencies
|
||||||
|
# before we copy the real source.
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY crates/archivr-core/Cargo.toml crates/archivr-core/Cargo.toml
|
||||||
|
COPY crates/archivr-server/Cargo.toml crates/archivr-server/Cargo.toml
|
||||||
|
COPY crates/archivr-cli/Cargo.toml crates/archivr-cli/Cargo.toml
|
||||||
|
|
||||||
|
RUN mkdir -p \
|
||||||
|
crates/archivr-core/src \
|
||||||
|
crates/archivr-server/src \
|
||||||
|
crates/archivr-cli/src \
|
||||||
|
&& touch crates/archivr-core/src/lib.rs \
|
||||||
|
&& echo 'fn main() {}' > crates/archivr-server/src/main.rs \
|
||||||
|
&& echo 'fn main() {}' > crates/archivr-cli/src/main.rs \
|
||||||
|
&& cargo build --release -p archivr-server -p archivr-cli || true
|
||||||
|
|
||||||
|
# Build the real binaries; touch source files to force Cargo to relink.
|
||||||
|
COPY crates/ crates/
|
||||||
|
RUN touch \
|
||||||
|
crates/archivr-core/src/lib.rs \
|
||||||
|
crates/archivr-server/src/main.rs \
|
||||||
|
crates/archivr-cli/src/main.rs \
|
||||||
|
&& cargo build --release -p archivr-server -p archivr-cli
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Stage 2 – Runtime image
|
||||||
|
###############################################################################
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
# Runtime dependencies:
|
||||||
|
# chromium used by single-file-cli for full-page archiving
|
||||||
|
# nodejs (20+) runtime for single-file-cli (requires Node >=20; Debian
|
||||||
|
# bookworm ships 18, so we install from the NodeSource repo)
|
||||||
|
# ffmpeg required by yt-dlp to merge separate audio/video streams
|
||||||
|
# (e.g. YouTube bestvideo+bestaudio format selection)
|
||||||
|
# python3 + pip + venv twitter scraper
|
||||||
|
# ca-certificates outbound HTTPS from the server and NodeSource HTTPS
|
||||||
|
# libssl3 OpenSSL linked by the Rust binary
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
chromium \
|
||||||
|
nodejs \
|
||||||
|
ffmpeg \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
libssl3 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install single-file-cli globally so `single-file` is on PATH.
|
||||||
|
RUN npm install -g single-file-cli
|
||||||
|
|
||||||
|
# Install yt-dlp and twitter-api-client into an isolated venv to avoid
|
||||||
|
# conflicts with Debian's system Python packages.
|
||||||
|
RUN python3 -m venv /opt/archivr-venv \
|
||||||
|
&& /opt/archivr-venv/bin/pip install --no-cache-dir \
|
||||||
|
yt-dlp \
|
||||||
|
twitter-api-client
|
||||||
|
|
||||||
|
# Server and CLI binaries (CLI is needed to run `archivr init` on first setup)
|
||||||
|
COPY --from=builder /build/target/release/archivr-server /usr/local/bin/archivr-server
|
||||||
|
COPY --from=builder /build/target/release/archivr /usr/local/bin/archivr
|
||||||
|
|
||||||
|
# Pre-built frontend assets (already compiled; no Vite build step needed)
|
||||||
|
COPY crates/archivr-server/static/ /usr/share/archivr-server/static/
|
||||||
|
|
||||||
|
# Twitter scraper script
|
||||||
|
COPY vendor/twitter/scrape_user_tweet_contents.py \
|
||||||
|
/usr/local/lib/archivr/scrape_user_tweet_contents.py
|
||||||
|
|
||||||
|
# Wire up env vars that the server (and archivr-core) read at runtime.
|
||||||
|
# ARCHIVR_BIND and ARCHIVR_TWITTER_CREDENTIALS_FILE are intentionally left
|
||||||
|
# unset here — set them in docker-compose.yml or at `docker run` time.
|
||||||
|
ENV ARCHIVR_STATIC_DIR=/usr/share/archivr-server/static \
|
||||||
|
ARCHIVR_CHROME=/usr/bin/chromium \
|
||||||
|
ARCHIVR_SINGLE_FILE=/usr/local/bin/single-file \
|
||||||
|
ARCHIVR_TWEET_PYTHON=/opt/archivr-venv/bin/python3 \
|
||||||
|
ARCHIVR_TWEET_SCRAPER=/usr/local/lib/archivr/scrape_user_tweet_contents.py \
|
||||||
|
ARCHIVR_YT_DLP=/opt/archivr-venv/bin/yt-dlp \
|
||||||
|
ARCHIVR_CHROME_ARGS=--no-sandbox
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Expects the TOML config at /config/archivr-server.toml (mount a volume).
|
||||||
|
# Copy docker/config.example.toml as a starting point.
|
||||||
|
# Using CMD (not ENTRYPOINT) so `docker compose run archivr archivr init …`
|
||||||
|
# can override the whole command for first-time archive initialisation.
|
||||||
|
CMD ["archivr-server", "/config/archivr-server.toml"]
|
||||||
|
|
@ -68,18 +68,30 @@ fn save_with(
|
||||||
// without a writable user-data-dir. Using a subdirectory of temp_dir
|
// without a writable user-data-dir. Using a subdirectory of temp_dir
|
||||||
// keeps it isolated and it gets cleaned up with the rest of the temp dir.
|
// keeps it isolated and it gets cleaned up with the rest of the temp dir.
|
||||||
let chrome_data_dir = temp_dir.join("chrome-data");
|
let chrome_data_dir = temp_dir.join("chrome-data");
|
||||||
let browser_args = format!(
|
// Build the browser-args JSON array. Start with the flags always required,
|
||||||
"[\"--disable-web-security\",\"--user-data-dir={}\"]",
|
// then append any extra flags from ARCHIVR_CHROME_ARGS (space-separated).
|
||||||
chrome_data_dir.display()
|
// Docker containers running as root need "--no-sandbox" here because
|
||||||
);
|
// Chromium refuses to start as root without it.
|
||||||
|
let mut chrome_flags = vec![
|
||||||
|
"--disable-web-security".to_string(),
|
||||||
|
format!("--user-data-dir={}", chrome_data_dir.display()),
|
||||||
|
];
|
||||||
|
if let Ok(extra) = std::env::var("ARCHIVR_CHROME_ARGS") {
|
||||||
|
chrome_flags.extend(extra.split_whitespace().filter(|s| !s.is_empty()).map(str::to_string));
|
||||||
|
}
|
||||||
|
let quoted: Vec<String> = chrome_flags
|
||||||
|
.iter()
|
||||||
|
.map(|f| format!("\"{}\"", f.replace('\\', "\\\\").replace('"', "\\\"")))
|
||||||
|
.collect();
|
||||||
|
let browser_args = format!("[{}]", quoted.join(","));
|
||||||
|
|
||||||
let out = Command::new(single_file)
|
let out = Command::new(single_file)
|
||||||
.arg(url)
|
.arg(url)
|
||||||
.arg(&out_file)
|
.arg(&out_file)
|
||||||
.arg(format!("--browser-executable-path={chrome}"))
|
.arg(format!("--browser-executable-path={chrome}"))
|
||||||
.arg("--browser-headless")
|
.arg("--browser-headless")
|
||||||
.arg("--browser-wait-until=networkidle2")
|
.arg("--browser-wait-until=networkAlmostIdle")
|
||||||
// Extra delay after networkidle2: Cloudflare Fonts injects @font-face
|
// Extra delay after networkAlmostIdle: Cloudflare Fonts injects @font-face
|
||||||
// CSS after HTML parse, so the font hook needs more time to see it.
|
// CSS after HTML parse, so the font hook needs more time to see it.
|
||||||
.arg("--browser-wait-delay=2000")
|
.arg("--browser-wait-delay=2000")
|
||||||
// Realistic UA: some origins block headless Chrome's default UA string.
|
// Realistic UA: some origins block headless Chrome's default UA string.
|
||||||
|
|
|
||||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
services:
|
||||||
|
archivr:
|
||||||
|
build: .
|
||||||
|
image: archivr-server:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
# Listen on all interfaces inside the container so the port mapping works.
|
||||||
|
ARCHIVR_BIND: "0.0.0.0:8080"
|
||||||
|
# Uncomment and set this to enable Twitter/X archiving.
|
||||||
|
# The file must be accessible inside the container (e.g. in the config volume).
|
||||||
|
# ARCHIVR_TWITTER_CREDENTIALS_FILE: /config/twitter-cookies.txt
|
||||||
|
volumes:
|
||||||
|
# Mount a directory containing archivr-server.toml as read-only config.
|
||||||
|
# Copy docker/config.example.toml to ./config/archivr-server.toml to start.
|
||||||
|
- ./config:/config:ro
|
||||||
|
# Persistent volume for the auth database and archive directories.
|
||||||
|
# The paths inside must match archive_path values in your TOML config.
|
||||||
|
- archivr-data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
archivr-data:
|
||||||
32
docker/config.example.toml
Normal file
32
docker/config.example.toml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# archivr-server.toml — example configuration for Docker deployment.
|
||||||
|
#
|
||||||
|
# Copy this file to ./config/archivr-server.toml (next to docker-compose.yml),
|
||||||
|
# edit it to suit your setup, then run:
|
||||||
|
#
|
||||||
|
# docker compose up -d
|
||||||
|
#
|
||||||
|
# The bind address defaults to 127.0.0.1:8080. When running under Docker,
|
||||||
|
# set ARCHIVR_BIND=0.0.0.0:8080 in the environment (docker-compose.yml does
|
||||||
|
# this already) — the env var takes precedence over the value below.
|
||||||
|
|
||||||
|
# bind = "0.0.0.0:8080"
|
||||||
|
|
||||||
|
# Path to the server-level authentication database.
|
||||||
|
# Must be on a persistent volume so it survives container restarts.
|
||||||
|
auth_db_path = "/data/archivr-auth.sqlite"
|
||||||
|
|
||||||
|
# Define one or more archives.
|
||||||
|
# archive_path must point to the .archivr directory created by `archivr init`,
|
||||||
|
# on the persistent data volume (/data by default).
|
||||||
|
# Initialize each archive before starting the server (see Docker quickstart step 2).
|
||||||
|
|
||||||
|
[[archives]]
|
||||||
|
id = "main"
|
||||||
|
label = "Main Archive"
|
||||||
|
archive_path = "/data/archives/main/.archivr"
|
||||||
|
|
||||||
|
# Add more archives as needed:
|
||||||
|
# [[archives]]
|
||||||
|
# id = "videos"
|
||||||
|
# label = "Videos"
|
||||||
|
# archive_path = "/data/archives/videos/.archivr"
|
||||||
|
|
@ -191,6 +191,69 @@ services.archivr-server = {
|
||||||
Initialise them with `archivr init` first, then `chown -R archivr:archivr /srv/archivr`.
|
Initialise them with `archivr init` first, then `chown -R archivr:archivr /srv/archivr`.
|
||||||
|
|
||||||
|
|
||||||
|
### Hosting with Docker
|
||||||
|
|
||||||
|
A `Dockerfile` and `docker-compose.yml` are provided for self-hosting without Nix.
|
||||||
|
|
||||||
|
**Quickstart**
|
||||||
|
|
||||||
|
1. Copy the example config and edit it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir config
|
||||||
|
cp docker/config.example.toml config/archivr-server.toml
|
||||||
|
# edit config/archivr-server.toml — set archive id, label, and archive_path
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Initialize each archive on the persistent data volume before the first start.
|
||||||
|
The image includes the `archivr` CLI for this purpose:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose run --rm archivr archivr init /data/archives/main /data/archives/main/.archivr/store --name "Main Archive"
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `/data/archives/main/.archivr/` with the metadata the server requires.
|
||||||
|
A bare `mkdir` is not enough — the server reads `name` and `store_path` files that
|
||||||
|
only `archivr init` writes.
|
||||||
|
|
||||||
|
3. Start the server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://localhost:8080`.
|
||||||
|
|
||||||
|
**Volumes**
|
||||||
|
|
||||||
|
| Mount | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `./config` (read-only) | Directory containing `archivr-server.toml` |
|
||||||
|
| `archivr-data` named volume | Auth database (`/data/archivr-auth.sqlite`) and archive directories |
|
||||||
|
|
||||||
|
> **Important:** `auth_db_path` must be set explicitly in `archivr-server.toml` to a
|
||||||
|
> path on the writable data volume (e.g. `/data/archivr-auth.sqlite`). If left unset,
|
||||||
|
> the server defaults to writing the auth database next to the config file — which is
|
||||||
|
> on the read-only `/config` mount and will fail. The example config sets this correctly.
|
||||||
|
|
||||||
|
**Twitter/X archiving**
|
||||||
|
|
||||||
|
Supply a cookies file inside the config volume and set `ARCHIVR_TWITTER_CREDENTIALS_FILE` in `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
ARCHIVR_TWITTER_CREDENTIALS_FILE: /config/twitter-cookies.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Building the image locally**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t archivr-server .
|
||||||
|
```
|
||||||
|
|
||||||
|
The image compiles the Rust binary in a separate build stage so only the runtime
|
||||||
|
dependencies (Chromium, Node.js, Python) land in the final layer.
|
||||||
|
|
||||||
### Supported Shorthand Inputs
|
### Supported Shorthand Inputs
|
||||||
|
|
||||||
- YouTube video/short media:
|
- YouTube video/short media:
|
||||||
|
|
@ -219,15 +282,29 @@ Initialise them with `archivr init` first, then `chown -R archivr:archivr /srv/a
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
|
- `ARCHIVR_BIND`
|
||||||
|
- Optional.
|
||||||
|
- Overrides the bind address from the TOML config. Useful in Docker where you need
|
||||||
|
`0.0.0.0:8080` without editing the config file. Default: `127.0.0.1:8080`.
|
||||||
|
- `ARCHIVR_STATIC_DIR`
|
||||||
|
- Optional.
|
||||||
|
- Path to the directory of pre-built frontend assets served by the web UI.
|
||||||
|
Set automatically by the Nix wrapper and the Docker image. When running from
|
||||||
|
source with `cargo run`, falls back to `crates/archivr-server/static`.
|
||||||
- `ARCHIVR_YT_DLP`
|
- `ARCHIVR_YT_DLP`
|
||||||
- Optional.
|
- Optional.
|
||||||
- Overrides the `yt-dlp` binary used for YouTube, X media posts, Instagram, Facebook, TikTok, Reddit, and Snapchat downloads.
|
- Overrides the `yt-dlp` binary used for YouTube, X media posts, Instagram, Facebook, TikTok, Reddit, and Snapchat downloads.
|
||||||
- `ARCHIVR_SINGLE_FILE`
|
- `ARCHIVR_SINGLE_FILE`
|
||||||
- Optional.
|
- Optional.
|
||||||
- Overrides the `single-file` binary used for web page archiving. When installed through Nix, this is set automatically to the Nixpkgs `single-file-cli` binary.
|
- Overrides the `single-file` binary used for web page archiving. Set automatically by the Nix wrapper and the Docker image.
|
||||||
- `ARCHIVR_CHROME`
|
- `ARCHIVR_CHROME`
|
||||||
- Optional.
|
- Optional.
|
||||||
- Overrides the Chromium/Chrome executable passed to `single-file` via `--browser-executable-path`. When installed through Nix, this is set automatically to the Nixpkgs `chromium` binary. Default: `chromium`.
|
- Overrides the Chromium/Chrome executable passed to `single-file` via `--browser-executable-path`. Set automatically by the Nix wrapper and the Docker image. Default: `chromium`.
|
||||||
|
- `ARCHIVR_CHROME_ARGS`
|
||||||
|
- Optional.
|
||||||
|
- Space-separated extra flags appended to Chromium's `--browser-args`. The Docker
|
||||||
|
image sets this to `--no-sandbox` because Chromium refuses to run as root without
|
||||||
|
it. Leave unset when running natively (Nix, Linux desktop).
|
||||||
- `ARCHIVR_TWITTER_CREDENTIALS_FILE`
|
- `ARCHIVR_TWITTER_CREDENTIALS_FILE`
|
||||||
- Required for tweet/thread scraping inputs such as `tweet:ID` and `x:thread:ID`.
|
- Required for tweet/thread scraping inputs such as `tweet:ID` and `x:thread:ID`.
|
||||||
- Must point to a cookies file for the vendored scraper.
|
- Must point to a cookies file for the vendored scraper.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue