diff --git a/.dockerignore b/.dockerignore index 1269488..4f8233a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,3 @@ data +db-data +.env diff --git a/.env.mariadb b/.env.mariadb new file mode 100644 index 0000000..21e328e --- /dev/null +++ b/.env.mariadb @@ -0,0 +1,38 @@ + +# General Configuration +WRITEFREELY_BIND_PORT=8080 +WRITEFREELY_BIND_HOST=0.0.0.0 +WRITEFREELY_SITE_NAME="My Blog" +WRITEFREELY_SITE_DESCRIPTION="My fancy blog" + +# Database Configuration +MARIADB_USER=writefreely +MARIADB_PASSWORD=changeme +MARIADB_DATABASE=writefreely +MARIADB_ROOT_PASSWORD=changeme + +WRITEFREELY_DATABASE_DATABASE=mysql +WRITEFREELY_DATABASE_USERNAME=${MARIADB_USER} +WRITEFREELY_DATABASE_PASSWORD=${MARIADB_PASSWORD} +WRITEFREELY_DATABASE_NAME=${MARIADB_DATABASE} +WRITEFREELY_DATABASE_HOST=writefreely-db +WRITEFREELY_DATABASE_PORT=3306 + + +# Application Settings +WRITEFREELY_HOST= +WRITEFREELY_SINGLE_USER=true +WRITEFREELY_OPEN_REGISTRATION=false +WRITEFREELY_MIN_USERNAME_LEN=4 +WRITEFREELY_MAX_BLOG=4 +WRITEFREELY_FEDERATION=true +WRITEFREELY_PUBLIC_STATS=true +WRITEFREELY_PRIVATE=false +WRITEFREELY_LOCAL_TIMELINE=true +WRITEFREELY_USER_INVITES= + +# Writefreely Users +WRITEFREELY_ADMIN_USER=admin +WRITEFREELY_ADMIN_PASSWORD=changeme +WRITEFREELY_WRITER_USER= +WRITEFREELY_WRITER_PASSWORD= diff --git a/.env.sqlite b/.env.sqlite new file mode 100644 index 0000000..5e8292a --- /dev/null +++ b/.env.sqlite @@ -0,0 +1,31 @@ + +# General Configuration +WRITEFREELY_BIND_PORT=8080 +WRITEFREELY_BIND_HOST=0.0.0.0 +WRITEFREELY_SITE_NAME="My Blog" +WRITEFREELY_SITE_DESCRIPTION="My fancy blog" + +# Database Configuration +WRITEFREELY_DATABASE_DATABASE=sqlite3 +WRITEFREELY_SQLITE_FILENAME=./writefreely.db +WRITEFREELY_DATABASE_USERNAME=writefreely +WRITEFREELY_DATABASE_PASSWORD=changeme +WRITEFREELY_DATABASE_NAME=writefreely + +# Application Settings +WRITEFREELY_HOST= +WRITEFREELY_SINGLE_USER=true +WRITEFREELY_OPEN_REGISTRATION=false +WRITEFREELY_MIN_USERNAME_LEN=4 +WRITEFREELY_MAX_BLOG=4 +WRITEFREELY_FEDERATION=true +WRITEFREELY_PUBLIC_STATS=true +WRITEFREELY_PRIVATE=false +WRITEFREELY_LOCAL_TIMELINE=true +WRITEFREELY_USER_INVITES= + +# Writefreely Users +WRITEFREELY_ADMIN_USER=admin +WRITEFREELY_ADMIN_PASSWORD=changeme +WRITEFREELY_WRITER_USER= +WRITEFREELY_WRITER_PASSWORD= diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index bdd8e12..b4b6758 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,88 +1,63 @@ name: Docker -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - on: - schedule: - - cron: '37 1 * * *' + workflow_dispatch: push: branches: [ main ] - # Publish semver tags as releases. tags: [ 'v*.*.*' ] - pull_request: - branches: [ main ] + schedule: + - cron: '37 1 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true env: - # Use docker.io for Docker Hub if empty - REGISTRY: ghcr.io - # github.repository as / - IMAGE_NAME: ${{ github.repository }} - + WRITEFREELY_VERSION: v0.15.1 + DOCKERHUB_REPO: jrasanen/writefreely jobs: build: - runs-on: ubuntu-latest permissions: contents: read packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. id-token: write steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v4 - # Workaround: https://github.com/docker/build-push-action/issues/461 - - name: Setup Docker buildx - uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf - - # Login against a Docker registry except on PR - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' - uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GH_TOKEN }} - - name: Log in to Docker Hub - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - uses: actions/cache@v4 with: - images: | - jrasanen/writefreely - ghcr.io/${{ github.repository }} + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- - - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a - with: - context: . - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Build and push Docker images - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + - name: Build and Push + uses: docker/build-push-action@v5 with: context: . + file: ./Dockerfile push: true - tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + WRITEFREELY_VERSION=${{ env.WRITEFREELY_VERSION }} + tags: | + ${{ env.DOCKERHUB_REPO }}:${{ env.WRITEFREELY_VERSION }} + ${{ env.DOCKERHUB_REPO }}:latest + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 82f0c3a..b1ee384 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /data/ +/db-data/ +/.env diff --git a/Dockerfile b/Dockerfile index 88eb37e..9572f3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,38 +1,30 @@ -## Writefreely Docker image -## Copyright (C) 2019, 2020 Gergely Nagy -## -## This program is free software: you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation, either version 3 of the License, or -## (at your option) any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with this program. If not, see . - -ARG GOLANG_VERSION=1.21 +ARG GOLANG_VERSION=1.22 # Build image FROM golang:${GOLANG_VERSION}-alpine as build -ARG WRITEFREELY_VERSION=v0.14.0 -ARG WRITEFREELY_FORK=writeas/writefreely +LABEL org.opencontainers.image.source="https://github.com/writefreely/writefreely" +LABEL org.opencontainers.image.description="WriteFreely is a clean, minimalist publishing platform made for writers. Start a blog, share knowledge within your organization, or build a community around the shared act of writing." -RUN apk add --update nodejs npm make g++ git sqlite-dev +ARG WRITEFREELY_VERSION=v0.15.1 +ARG WRITEFREELY_FORK=writefreely/writefreely + +RUN apk -U upgrade \ + && apk add --no-cache nodejs npm make g++ git sqlite-dev \ + && npm install -g less less-plugin-clean-css \ + && mkdir -p /go/src/github.com/writefreely/writefreely RUN npm install -g less less-plugin-clean-css -RUN go get -u github.com/jteeuwen/go-bindata/... RUN mkdir -p /go/src/github.com/${WRITEFREELY_FORK} RUN git clone https://github.com/${WRITEFREELY_FORK}.git /go/src/github.com/${WRITEFREELY_FORK} -b ${WRITEFREELY_VERSION} WORKDIR /go/src/github.com/${WRITEFREELY_FORK} ENV GO111MODULE=on +ENV NODE_OPTIONS=--openssl-legacy-provider + RUN make build \ && make ui + RUN mkdir /stage && \ cp -R /go/bin \ /go/src/github.com/${WRITEFREELY_FORK}/templates \ @@ -44,9 +36,15 @@ RUN mkdir /stage && \ mv /stage/cmd/writefreely/writefreely /stage # Final image -FROM alpine:3.18 +FROM alpine:3.19 + +ARG WRITEFREELY_UID=1000 +ARG WRITEFREELY_GID=1000 + +RUN apk -U upgrade && apk add --no-cache openssl ca-certificates + +RUN addgroup -g ${WRITEFREELY_GID} -S writefreely && adduser -u ${WRITEFREELY_UID} -S -G writefreely writefreely -RUN apk add --no-cache openssl ca-certificates COPY --from=build --chown=daemon:daemon /stage /writefreely COPY bin/writefreely-docker.sh /writefreely/ @@ -54,4 +52,9 @@ WORKDIR /writefreely VOLUME /data EXPOSE 8080 +RUN chown -R writefreely:writefreely /writefreely + +USER writefreely + ENTRYPOINT ["/writefreely/writefreely-docker.sh"] + diff --git a/README.md b/README.md index 6ebc4a5..2311d4a 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,143 @@ -# writefreely-docker +# WriteFreely Docker Build -[![Build Status](https://ci.madhouse-project.org/api/badges/algernon/writefreely-docker/status.svg?branch=master)](https://ci.madhouse-project.org/algernon/writefreely-docker) -[![Docker Image](https://img.shields.io/badge/docker-latest-blue?style=flat-square)](https://hub.docker.com/r/algernon/writefreely) -[![Source](https://img.shields.io/badge/source-git-brightgreen?style=flat-square)](https://git.madhouse-project.org/algernon/writefreely-docker) - -This is a [Docker][docker] image for [WriteFreely][writefreely], set up in a way -that makes it easier to deploy it in production, including the initial setup step. - - [docker]: https://www.docker.com/ - [writefreely]: https://github.com/writeas/writefreely - -## Overview - -The image is set up to use SQLite for the database, it does not support MySQL -out of the box - but you can always provide your own `config.ini`. The config -file, the database, and the generated keys are all stored on the single volume -the image uses, mounted on `/data`. - -The primary purpose of the image is to provide a single-step setup and upgrade -experience, where the initial setup and any upgrades are handled by the image -itself. As such, the image will create a default `config.ini` unless one already -exists, with reasonable defaults. It will also run database migrations, and save -a backup before doing so (which it will delete, if no migrations were -necessary). +This project builds a Docker image for [WriteFreely](https://github.com/writefreely/writefreely), a minimalist, privacy-focused, and federated blogging platform. The image is uses on Alpine Linux. ## Getting started To get started, the easiest way to test it out is running the following command: -```shell +```bash docker run -p 8080:8080 -it --rm -v /some/path/to/data:/data \ - algernon/writefreely + jrasanen/writefreely ``` -Then point your browser to `http://localhost:8080`, and you should see -WriteFreely up and running. +Then point your browser to http://localhost:8080, and you should see WriteFreely up and running. ## Setup -The image will perform an initial setup, unless the supplied volume already -contains a `config.ini`. Settings can be tweaked via environment variables, of -which you can find a list below. Do note that these environment variables are -*only* used for the initial setup as of this writing! If a configuration file -already exists, the environment variables will be blissfully ignored. +The image will perform an initial setup, unless the supplied volume already contains a config.ini. Settings can be tweaked via environment variables, of which you can find a list below. Do note that these environment variables are only used for the initial setup as of this writing! If a configuration file already exists, the environment variables will be blissfully ignored. ### Environment variables -- `WRITEFREELY_BIND_HOST` and `WRITEFREELY_BIND_PORT` determine the host and port WriteFreely will bind to. Defaults to `0.0.0.0` and `8080`, respectively. -- `WRITEFREELY_SITE_NAME` is the site title one wants. Defaults to "A Writefreely blog". -- `WRITEFREELY_SINGLE_USER`, `WRITEFREELY_OPEN_REGISTRATION`, - `WRITEFREELY_MIN_USERNAME_LEN`, `WRITEFREELY_MAX_BLOG`, - `WRITEFREELY_FEDERATION`, `WRITEFREELY_PUBLIC_STATS`, `WRITEFREELY_PRIVATE`, - `WRITEFREELY_LOCAL_TIMELINE`, and `WRITEFREELY_USER_INVITES` all correspond to - the similarly named `config.ini` settings. See the [WriteFreely docs][wf:docs] - for more information about them. +The following variables will be used to construct the `config.ini` on first start. After it has been configured, you can edit it on the volume. - [wf:docs]: https://writefreely.org/docs/latest/admin/config +## General Configuration + +- **`WRITEFREELY_BIND_PORT`**: Specifies the port on which the WriteFreely server will listen. Defaults to `8080`. +- **`WRITEFREELY_BIND_HOST`**: Defines the host IP to bind to. Defaults to `0.0.0.0`. +- **`WRITEFREELY_SITE_NAME`**: Sets the name of your WriteFreely site. Used to identify the site in federation. +- **`WRITEFREELY_SITE_DESCRIPTION`**: Provides a short description of your site. This description may be used in federated networks. + +## Database Configuration + +- **`WRITEFREELY_DATABASE_DATABASE`**: Specifies the type of database used, such as `mysql` or `sqlite3`. +- **`WRITEFREELY_SQLITE_FILENAME`**: (Optional) DB filename if `sqlite3` detabase is selected. Defaults to `/data/writefreely.db`. +- **`WRITEFREELY_DATABASE_USERNAME`**: The username for the database. +- **`WRITEFREELY_DATABASE_PASSWORD`**: The password for the database. +- **`WRITEFREELY_DATABASE_NAME`**: The name of the database to connect to. +- **`WRITEFREELY_DATABASE_HOST`**: The hostname or IP address of the database server. +- **`WRITEFREELY_DATABASE_PORT`**: The port number on which the database server is running. + +## Application Settings + +- **`WRITEFREELY_HOST`**: The full URL where the site will be accessible. +- **`WRITEFREELY_SINGLE_USER`**: Set to `true` to run the instance as a single-user blog, otherwise `false`. +- **`WRITEFREELY_OPEN_REGISTRATION`**: Whether or not anyone can register via the landing page +- **`WRITEFREELY_MIN_USERNAME_LEN`**: The minimum length for usernames. +- **`WRITEFREELY_MAX_BLOG`**: Maximum number of blogs a single user can create under one account +- **`WRITEFREELY_FEDERATION`**: Whether or not federation via ActivityPub is enabled +- **`WRITEFREELY_PUBLIC_STATS`**: DWhether or not usage stats are made public via NodeInfo +- **`WRITEFREELY_PRIVATE`**: Set to `true` to make the site private. +- **`WRITEFREELY_LOCAL_TIMELINE`**: Whether or not the instance reader (and the Public option on blogs) is enabled +- **`WRITEFREELY_USER_INVITES`**: Who is allowed to send user invites, if anyone. A blank value disables invites for all users. Valid choices: empty, user, or admin + +## Writefreely Users + +- **`WRITEFREELY_ADMIN_USER`**: Administrator user name. In single user instances is editor too. +- **`WRITEFREELY_ADMIN_PASSWORD`**: Administrator password + +### Volumes + +* `/data`: Directory where WriteFreely stores its data, including database files and configuration. + +### Using Docker Compose + +You can use Docker Compose to set up WriteFreely with different database configurations. The configuration files are already included in this repository. Follow the steps below to start the services. + +#### Clone the Repository + +First, clone this repository: + +```bash +git clone https://github.com/yourusername/writefreely-docker.git +cd writefreely-docker +``` + +#### Prepare the Data Directory + +Create the data directory and assign the appropriate permissions: + +```bash +mkdir data +sudo chown 1000:1000 data +``` + +#### Configure the Environment + +Before starting the services, you need to copy the appropriate .env file and edit it to configure the environment variables, especially the passwords. + +##### For MariaDB + +Copy the .env.mariadb file to .env: + +```bash +cp .env.mariadb .env +``` + +##### For SQLite + +Copy the .env.sqlite file to .env: + +```bash +cp .env.sqlite .env +``` + +Then, edit the .env file to set the appropriate values for your environment: + +```bash +nano .env +``` + +Ensure to set secure passwords and other necessary configuration options. + +#### Start the Services + +##### MariaDB + +To use the **MariaDB** configuration, run: + +```bash +docker-compose -f docker-compose.mariadb.yaml up +``` + +##### SQLite + +To use the **SQLite** configuration, run: + +```bash +docker-compose -f docker-compose.sqlite3.yaml up +``` + +### Building the Image + +If you want to build the image yourself, clone this repository and run the following command inside the repository's directory: + +```bash +docker build -t yourusername/writefreely . +``` + +Replace `yourusername` with your Docker Hub username or a suitable image name. + +### Contributing + +Contributions are welcome! Please fork this repository and submit pull requests for any enhancements or bug fixes. diff --git a/bin/writefreely-docker.sh b/bin/writefreely-docker.sh index 21079ac..da78a9f 100755 --- a/bin/writefreely-docker.sh +++ b/bin/writefreely-docker.sh @@ -20,61 +20,204 @@ set -e cd /data WRITEFREELY=/writefreely/writefreely +attempts=0 +max_attempts=5 + +log() { + echo "$(date '+%Y/%m/%d %H:%M:%S') $1" +} + +validate_url() { + URL="$1" + if echo "$URL" | grep -Eq "^https?://[a-zA-Z0-9._-]+"; then + return 0 # Success + else + return 1 # Failure + fi +} + +retry_command() { + local cmd=$1 + attempts=0 + until $cmd; do + attempts=$((attempts+1)) + if [ $attempts -ge $max_attempts ]; then + log "Failed to execute '$cmd' after $attempts attempts." + return 1 + fi + log "Retrying '$cmd' ($attempts/$max_attempts)..." + sleep 5 + done + return 0 +} + +initialize_database() { + log "Initializing database..." + if ! retry_command "${WRITEFREELY} --init-db"; then + log "Initialization of database failed. Removing config.ini." + rm ./config.ini + exit 1 + fi +} + +generate_keys() { + log "Generating keys..." + ${WRITEFREELY} --gen-keys +} + +create_admin_user() { + if [ -n "$WRITEFREELY_ADMIN_USER" ]; then + ${WRITEFREELY} user create --admin ${WRITEFREELY_ADMIN_USER}:${WRITEFREELY_ADMIN_PASSWORD} + log "Created admin user ${WRITEFREELY_ADMIN_USER}" + else + log "Admin user not defined" + exit 1 + fi +} + +create_writer_user() { + if [ -n "$WRITEFREELY_WRITER_USER" ]; then + ${WRITEFREELY} user create ${WRITEFREELY_WRITER_USER}:${WRITEFREELY_WRITER_PASSWORD} + log "Created writer user ${WRITEFREELY_WRITER_USER}" + fi +} + +validate_url "$WRITEFREELY_HOST" || { + log "Error: $WRITEFREELY_HOST is not a valid URL. It must start with http:// or https:// and be followed by a valid hostname." + exit 1 +} if [ -e ./config.ini ] && [ -e ./keys/email.aes256 ]; then - ${WRITEFREELY} -migrate - exec ${WRITEFREELY} + log "Migration required. Running migration..." + ${WRITEFREELY} -migrate + exec ${WRITEFREELY} fi if [ -e ./config.ini ]; then - ${WRITEFREELY} -init-db - ${WRITEFREELY} -gen-keys - exec ${WRITEFREELY} + initialize_database + generate_keys + create_admin_user + create_writer_user + exec ${WRITEFREELY} fi WRITEFREELY_BIND_PORT="${WRITEFREELY_BIND_PORT:-8080}" WRITEFREELY_BIND_HOST="${WRITEFREELY_BIND_HOST:-0.0.0.0}" WRITEFREELY_SITE_NAME="${WRITEFREELY_SITE_NAME:-A Writefreely blog}" +WRITEFREELY_SITE_DESCRIPTION="${WRITEFREELY_SITE_DESCRIPTION:-My Writefreely blog}" cat >./config.ini <