Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.git
node_modules
**/node_modules
**/test
**/tests
36 changes: 36 additions & 0 deletions .github/workflows/docker-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: docker-check

on:
pull_request:
paths:
- Dockerfile
- .dockerignore
- package-lock.json
- .github/workflows/docker-*.yml
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build
uses: docker/build-push-action@v6
with:
context: .
push: false
load: true
tags: indiekit:check
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Run the image
run: docker run --rm --user 4242:4242 -e HOME=/nonexistent indiekit:check node packages/indiekit/bin/cli.js --version
63 changes: 63 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: docker-image

on:
workflow_call:
inputs:
ref:
description: Git ref to build
required: true
type: string
tags:
description: Newline-separated full image tags
required: true
type: string
version:
description: Value for org.opencontainers.image.version
required: true
type: string

jobs:
build:
runs-on: ubuntu-latest
if: github.repository == 'getindiekit/indiekit'
concurrency:
group: docker-${{ inputs.version }}
cancel-in-progress: false
timeout-minutes: 60
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ inputs.tags }}
provenance: false
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ inputs.version }}
org.opencontainers.image.licenses=MIT
cache-from: type=gha
cache-to: type=gha,mode=max
60 changes: 60 additions & 0 deletions .github/workflows/docker-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: docker-preview

on:
workflow_dispatch:
inputs:
ref:
description: Branch, tag or commit to build
required: true
type: string
default: main
channel:
description: Channel name for the moving tag, e.g. preview-v1 or rc
required: true
type: string

jobs:
tags:
runs-on: ubuntu-latest
if: github.repository == 'getindiekit/indiekit'
permissions:
contents: read
outputs:
tags: ${{ steps.derive.outputs.tags }}
version: ${{ steps.derive.outputs.version }}
steps:
- name: Checkout ref
uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}

- name: Validate channel and derive tags
id: derive
env:
CHANNEL: ${{ inputs.channel }}
run: |
set -euo pipefail
[[ "$CHANNEL" =~ ^[a-z0-9][a-z0-9.-]*$ ]] || { echo "channel must match ^[a-z0-9][a-z0-9.-]*$"; exit 1; }
for reserved in latest beta; do
[ "$CHANNEL" = "$reserved" ] && { echo "channel '$CHANNEL' is reserved for the release workflow"; exit 1; }
done
short=$(git rev-parse --short=7 HEAD)
image=ghcr.io/${{ github.repository }}
echo "version=$CHANNEL-$short" >> "$GITHUB_OUTPUT"
{
echo 'tags<<EOF'
echo "$image:$CHANNEL"
echo "$image:$CHANNEL-$short"
echo 'EOF'
} >> "$GITHUB_OUTPUT"

image:
needs: tags
uses: ./.github/workflows/docker-image.yml
with:
ref: ${{ inputs.ref }}
tags: ${{ needs.tags.outputs.tags }}
version: ${{ needs.tags.outputs.version }}
permissions:
contents: read
packages: write
59 changes: 59 additions & 0 deletions .github/workflows/docker-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: docker-release

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: Release tag to build, e.g. v1.0.0-beta.29
required: true
type: string

jobs:
tags:
runs-on: ubuntu-latest
if: github.repository == 'getindiekit/indiekit'
permissions:
contents: read
outputs:
ref: ${{ steps.derive.outputs.ref }}
tags: ${{ steps.derive.outputs.tags }}
version: ${{ steps.derive.outputs.version }}
steps:
- name: Derive image tags from the release version
id: derive
env:
TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: |
set -euo pipefail
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]] || { echo "not a version tag: $TAG"; exit 1; }
image=ghcr.io/${{ github.repository }}
version="${TAG#v}"
echo "ref=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
{
echo 'tags<<EOF'
echo "$image:$version"
if [[ "$version" == *-* ]]; then
# Prerelease: the identifier is the channel (1.0.0-beta.29 -> beta).
channel="${version#*-}"; channel="${channel%%.*}"
echo "$image:$channel"
else
echo "$image:latest"
echo "$image:${version%%.*}"
echo "$image:${version%.*}"
fi
echo 'EOF'
} >> "$GITHUB_OUTPUT"

image:
needs: tags
uses: ./.github/workflows/docker-image.yml
with:
ref: ${{ needs.tags.outputs.ref }}
tags: ${{ needs.tags.outputs.tags }}
version: ${{ needs.tags.outputs.version }}
permissions:
contents: read
packages: write
37 changes: 37 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Official Indiekit runtime image.
#
# The whole workspace is installed, so every official package (endpoints, post
# types, presets, stores, syndicators) resolves by name from a mounted
# indiekit.config.js. Third-party plugins are not included.
#
# Run it with your configuration and content store mounted at /app, as your
# own user so files written to ./content belong to you:
# docker run -p 3000:3000 \
# -v ./indiekit.config.js:/app/indiekit.config.js:ro \
# -v ./content:/app/content \
# --user "$(id -u):$(id -g)" \
# -e PUBLICATION_URL -e MONGO_URL -e SECRET -e PASSWORD_SECRET \
# ghcr.io/getindiekit/indiekit
ARG NODE_VERSION=24
FROM node:${NODE_VERSION}-alpine

WORKDIR /app
ENV NODE_ENV=production

# Run as the unprivileged user the base image provides, so files written to a
# mounted content store are not owned by root.
RUN chown node:node /app
USER node

# The workspace manifests live inside packages/ and helpers/, so the whole
# tree is copied before install; a source change therefore reinstalls
# dependencies. Acceptable for a release image.
COPY --chown=node:node package.json package-lock.json ./
COPY --chown=node:node helpers ./helpers
COPY --chown=node:node packages ./packages
# --ignore-scripts: the root postinstall is husky, a development hook. Native
# modules (sharp) ship prebuilt binaries and need no install script.
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force

EXPOSE 3000
CMD ["node", "packages/indiekit/bin/cli.js", "serve"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ A [MongoDB](https://www.mongodb.com) database is optional, but required for the

## Get started

Learn how to [set up an Indiekit server](docs/get-started.md) and view an [example server configuration](https://github.com/getindiekit/example-config).
Learn how to [set up an Indiekit server](docs/get-started.md) and view an [example server configuration](https://github.com/getindiekit/example-config). A container image is published on each release at `ghcr.io/getindiekit/indiekit`; see [docs/docker.md](docs/docker.md).

## Documentation website

Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const sidebar = [
text: "Introduction",
items: [
{ text: "Get started", link: "/get-started" },
{ text: "Run in a container", link: "/docker" },
{ text: "How Indiekit works", link: "/introduction" },
{ text: "Core concepts", link: "/concepts" },
{ text: "Sponsors", link: "/sponsors" },
Expand Down
106 changes: 106 additions & 0 deletions docs/docker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Run Indiekit in a container

An image with every official plugin is published at `ghcr.io/getindiekit/indiekit`. You supply a configuration file, a content store and a MongoDB database; the image supplies everything else.

## Quick start with Docker Compose

Create `indiekit.config.js` (see [Configuration](/configuration/)) and a `compose.yml`:

```yaml
services:
indiekit:
image: ghcr.io/getindiekit/indiekit:beta
restart: unless-stopped
user: "1000:1000"
ports:
- "3000:3000"
volumes:
- ./indiekit.config.js:/app/indiekit.config.js:ro
- ./content:/app/content
environment:
PUBLICATION_URL: http://localhost:3000
MONGO_URL: mongodb://mongo:27017/indiekit
SECRET: ${SECRET}
PASSWORD_SECRET: ${PASSWORD_SECRET}
depends_on:
mongo:
condition: service_healthy
mongo:
image: mongo:8
restart: unless-stopped
volumes:
- mongo:/data/db
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
volumes:
mongo:
```

Replace `1000:1000` with your own user and group ids (`id -u` and `id -g`), so files written to `./content` belong to you.

Create the store directory yourself, so it is owned by you:

```sh
mkdir content
```

Write a `.env` file beside `compose.yml` with your secrets:

```sh
printf "SECRET='…'\nPASSWORD_SECRET='…'\n" > .env
```

`SECRET` is any long random string; `PASSWORD_SECRET` comes from the "Set your password" section below.

Then:

```sh
docker compose up -d
```

Indiekit listens on port 3000. Put a reverse proxy with HTTPS in front of it for a public site.

## What the image expects

Run the container as your own uid:gid so the mounted store is writable; the default `node` user (1000) only works when your uid is 1000.

| Path or variable | Purpose |
|---|---|
| `/app/indiekit.config.js` | Your configuration. Mounted read-only. |
| `/app/content` | The directory for `@indiekit/store-file-system`, if you use it. |
| `PUBLICATION_URL` | Your site’s URL. |
| `MONGO_URL` | MongoDB connection string. |
| `SECRET`, `PASSWORD_SECRET` | See [Get started](/get-started). |
| `PORT` | Optional, defaults to 3000. |

Any plug-ins you configure may need their own secrets; pass them the same way.

Any official plugin named in your configuration is already installed. Third-party plugins are not included in the image; build your own image on top of this one to add them.

> [!WARNING]
> `PUBLICATION_URL` must resolve from inside the container: signing in makes the server fetch its own URL to complete IndieAuth. On a laptop that means the published host port must equal the container port (`3000:3000` with `PUBLICATION_URL=http://localhost:3000`); behind a reverse proxy, the public URL must be reachable from the container.

## Set your password

1. Start the stack without `PASSWORD_SECRET` set.
2. Visit `/auth/new-password` at your `PUBLICATION_URL`.
3. Enter the password you want to use and copy the generated value.
4. Set `PASSWORD_SECRET` to that value and restart the stack.

See [Get started](/get-started) for the `$` quoting rules that apply to this value in `compose.yml`.

## Tags

The first image appears with the first release after this lands; until then no tag exists.

| Tag | Built from | When |
|---|---|---|
| `1.0.0-beta.29` | The release tag | Each GitHub release |
| `beta` | The newest prerelease | Each GitHub release while Indiekit is in beta |
| `latest`, `1`, `1.0` | The newest stable release | From the first stable release onward |
| `<channel>`, `<channel>-<sha>` | Any ref, on request | When a maintainer publishes a preview |

Images are built for `linux/amd64` and `linux/arm64`.
2 changes: 2 additions & 0 deletions docs/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ where `directory_name` is the name of the directory within which you want to sav

This is the fastest way to set up a new Indiekit server from scratch.

Prefer containers? See [Run Indiekit in a container](/docker).

> Alternatively, you can clone this [example configuration](https://github.com/getindiekit/example-config) repository on GitHub and manually edit the values in the configuration file.

The wizard will walk you through every step of configuring a new server, and will ask you questions about:
Expand Down
Loading