Skip to content

Implemented Containerization of the Lyra Webapp #157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 63 commits into from
Jun 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
29c17c3
giving up for the day
henrycatalinismith Feb 1, 2025
279e863
Fixing the docker setup
xela1601 Feb 2, 2025
aa6ad88
adding gitlab public key to known host
xela1601 Feb 2, 2025
01cd797
moving config out of the docker image for security reasons
xela1601 Feb 2, 2025
87d0ad0
clone projects into projects folder in docker and run git config
amerharb Feb 15, 2025
83f84ef
several small improvements:
xela1601 Feb 15, 2025
9d04a5d
cleanup dockerignore and handling store.json
xela1601 Mar 1, 2025
8919a65
lint ts files
amerharb Apr 12, 2025
899e6ee
Merge branch 'main' into issue-156/merge-from-main
WULCAN Apr 20, 2025
3865b5e
add hint for ssh key mounting in docker-compose
xela1601 Apr 24, 2025
731b62d
remove trailing space from readme
xela1601 Apr 24, 2025
ea21b60
adjust dockerignore to ignore more stuff
xela1601 Apr 24, 2025
ac6aabe
update package-lock.json
xela1601 Apr 24, 2025
2757f52
add comment for webappAbsPath
xela1601 Apr 24, 2025
e1701a0
use new location for lyra store mounted into container within docker-…
xela1601 Apr 24, 2025
092644c
Merge pull request #186 from zetkin/issue-156/merge-from-main
xela1601 Apr 24, 2025
f2eadb2
added workflow to build and push container image
xela1601 Feb 15, 2025
44b7c25
adjusted and moved documentation
xela1601 Feb 15, 2025
0c105ae
add empty line
amerharb Apr 12, 2025
f53fc7b
Merge pull request #161 from zetkin/feature/container-registry-support
xela1601 Apr 24, 2025
4d8794b
add docs for the usage of github container registry (ghcr) and new gi…
xela1601 Apr 24, 2025
4a792e7
Merge remote-tracking branch 'origin/main' into issue-156/merge-from-…
WULCAN Apr 27, 2025
8529edd
add error handling of lyra-store loading from disk
xela1601 Apr 27, 2025
2729fee
add hint about lyra-store location for mounting into container
xela1601 Apr 27, 2025
02e035d
extended documentation about docker volume mounts in readme
xela1601 Apr 27, 2025
848a2cb
Merge pull request #189 from zetkin/issue-156/merge-from-main
henrycatalinismith May 25, 2025
a2ff94a
Remove hyphen from docker-compose
henrycatalinismith May 25, 2025
97e8659
Add extra paragraph
henrycatalinismith May 25, 2025
bc0b0aa
Remove GPG-related change to commit behaviour
henrycatalinismith May 25, 2025
5ec7e77
Revert error handling to omit potentially sensitive info
henrycatalinismith May 25, 2025
fbf93e9
Remove user controlled input from log message
WULCAN May 25, 2025
570945e
Log after validation
WULCAN May 25, 2025
f206aae
Remove log Line
WULCAN May 25, 2025
390961a
Remove languageName from log record
WULCAN May 25, 2025
6983820
Hex encode value before logging
WULCAN May 25, 2025
8cc8db7
Hex encode project name in project page too
WULCAN May 25, 2025
607027a
update next.js to resolve critical vulnerability
xela1601 May 27, 2025
460298d
updated browserlist via npx update-browserslist-db@latest
xela1601 May 27, 2025
f46a57c
fix docker paths within README.md
xela1601 May 27, 2025
1e781b5
add outputFileTracingRoot & document standalone monorepo caveats
xela1601 May 27, 2025
5206e0b
use node 22 in dockerfile
xela1601 May 27, 2025
e052c82
adjust project_path reference in README
xela1601 May 27, 2025
786feed
make serverConfig's host optional with default value of github.com
xela1601 May 27, 2025
6cf337a
adjust logic of cloneIfNotExist method to allow cloning into empty di…
xela1601 May 27, 2025
98f24ff
Merge pull request #194 from zetkin/issue-156/log-injection
xela1601 May 28, 2025
c15aa47
make messagesPath in lyraConfig private again
xela1601 May 28, 2025
f497ccb
fix: avoid leaking absolute paths in getProjectsConfigPath
xela1601 May 28, 2025
0b98c65
fix next.config.js outputFileTracingRoot
xela1601 Jun 5, 2025
96731da
move `--check` to before the script, to make sure and clear that it's…
xela1601 Jun 5, 2025
71cf555
fix issue about cloning repo in case of empty repository directory
xela1601 Jun 5, 2025
6f8f194
add warning that repo must not have the same value for two different …
xela1601 Jun 5, 2025
8d6a8ac
document in README that encrypted private keys are not supported
xela1601 Jun 5, 2025
f629d0b
fix wrong path of lyra store in README
xela1601 Jun 5, 2025
cd600f9
simplify exception handling within Store.loadFromDisk
xela1601 Jun 6, 2025
375c169
adjust comment about access rights of private ssh key
xela1601 Jun 6, 2025
34f74be
remove unnecessary comment in README about specific ssh key for lyra …
xela1601 Jun 6, 2025
a5a3b5b
log hint about why update translation failed
xela1601 Jun 6, 2025
3d09692
remove log statement about not empty and cloned repo
xela1601 Jun 6, 2025
255964f
replace buildx with build in github workflow as buildx it is not need…
xela1601 Jun 6, 2025
50be058
Remove docker/setup-docker-action
WULCAN Jun 6, 2025
b2a5a32
Reformat with prettier
WULCAN Jun 6, 2025
f4953fb
Merge pull request #204 from zetkin/issue-156/prettier
amerharb Jun 6, 2025
80fd05a
Merge pull request #203 from zetkin/issue-156/remove-action
xela1601 Jun 6, 2025
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
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
webapp/.next
webapp/node_modules
webapp/coverage
webapp/store.json

**/.DS_Store
**/npm-debug.log*
**/.env*.local
**/.vercel
**/*.tsbuildinfo
**/next-env.d.ts
39 changes: 39 additions & 0 deletions .github/workflows/build-and-push-image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Build and Push Docker Image

on:
push:
tags:
- '*.*.*'
- '*.*.*-rc.*'
Comment on lines +6 to +7
Copy link
Collaborator

@WULCAN WULCAN Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern *.*.*-rc.* is already covered by the pattern *.*.* which allows any string as long it contains to dots.

Let's remove the second pattern or rewrite this to more specific patterns in a separate issue #201 rather than delaying merge futher. You can resolve this conversation when you have read it.


jobs:
build-and-push:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v3
Copy link
Collaborator

@WULCAN WULCAN Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version 3 of actions/checkout has not been maintained for a while. I don't think there's any known severe issue with using v3 so we can update to v4 in a separate merge request. I created a separate issue #202 to track that instead of delaying merge further. You can resolve this conversation when you have read it.


- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
Copy link
Collaborator

@WULCAN WULCAN Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version 2 of docker/login-action is not maintained. I created a separate issue #202 for updating to v3 instead of delaying this merge request further. You can resolve this conversation when you have read it.

with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract version from tag
id: extract_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV

- name: Validate version
run: |
if [[ ! "${{ env.VERSION }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc.[0-9]+)?$ ]]; then
echo "Invalid tag version: ${{ env.VERSION }}. Ensure to respect semantic versioning."
exit 1
fi

- name: Build and tag image
run: docker build -t ghcr.io/zetkin/lyra:${{ env.VERSION }} .

- name: Push image
run: docker push ghcr.io/zetkin/lyra:${{ env.VERSION }}
49 changes: 49 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
##################################################
# 1) BUILD STAGE
##################################################
FROM node:22-alpine AS builder

WORKDIR /app

# Copy only the necessary files for install
COPY package.json package-lock.json ./
COPY webapp/package.json webapp/

# Install dependencies in a single layer to leverage caching
# npm ci is better for reproducible builds than npm install
RUN npm ci

# Copy the rest of the source code
COPY webapp webapp

RUN npm --workspace webapp run build

##################################################
# 2) RUNTIME STAGE
##################################################
FROM node:22-alpine AS runner

RUN apk add --no-cache git openssh && \
addgroup -g 1001 nodejs && \
adduser -G nodejs -u 1001 -D nodeuser

WORKDIR /app

# Copy over the production build from builder stage
COPY --from=builder /app/webapp/.next/standalone ./
COPY --from=builder /app/webapp/.next/static ./webapp/.next/static

RUN mkdir -p /home/nodeuser/.ssh && \
mkdir -p /lyra-projects && \
chown -R nodeuser:nodejs /home/nodeuser/.ssh && \
chown -R nodeuser:nodejs /app && \
chown -R nodeuser:nodejs /lyra-projects

COPY known_hosts /home/nodeuser/.ssh/known_hosts

# Switch to non-root user
USER nodeuser


EXPOSE 3000
CMD ["sh", "-c", "git config --global user.email \"$GIT_USER_EMAIL\" && git config --global user.name \"$GIT_USER_NAME\" && node webapp/server.js"]
249 changes: 249 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# Lyra web app

Lyra is a translation management system that integrates with
source code repositories of internationalized applications.

- Lyra can extract messages and translations
from source code repositories.
- Lyra is an editor for translating messages.
- Lyra can create pull requests to merge updated
translations into the source code repository.

> [!WARNING]
> A party who controls a source code repository branch
> which Lyra is set up to translate will also have
> a lot of control over the Lyra process. That control
> probably includes reading files from the operating system
> and possibly includes arbitrary code exection.

For each repository Lyra is set up to translate,
Lyra needs control over a file system directory.

> [!WARNING]
> A party who controls the contents of
> a local repository directory will also have
> a lot of control over the Lyra process, probably
> including arbitrary code execution.

## Setup

1. Install npm
2. Install dependencies: `npm install`

### Visual Studio Code

If you are using Visual Studio Code, there are a few more optional steps
you can take to setup your develop environment.

#### Prettier

Install the recommended extension `Prettier - Code formatter`.

#### PlantUML

1. Install the extension `PlantUML` (Id: `jebbs.plantuml`).
2. Follow its documentation for setting up its requirments.

## Running in development

In the root folder (outside webapp) create file `./config/projects.yaml`
with example content:

```yaml
projects:
- name: example-unique-name
base_branch: main
project_path: . # relative path of project from repository root
owner: amerharb
repo: zetkin.app.zetkin.org
host: github.com
github_token: << github token >>
```

⚠️ Note that `repo` must not have the same value for two different projects.

Multiple projects are supported, and they're all stored within the `lyra-projects` folder on the same level as the lyra repository itself.

The project repository (client repository) will be cloned locally (if it does not exist yet) and needs to have a lyra configuration file
`lyra.yml` or `lyra.yaml` in the root of the repository.
This lyra configuration file looks like this:

```yaml
projects:
- path: . # relative path to project in repo
messages:
format: ts
path: src # relative path of messages folder relative from above project path
translations:
path: src/locale # relative path of translations folder relative from above project path
languages: # list of language codes supported in the project
- sv
- de
base_branch: main # optional default to 'main'
```

Start the server with `npm run dev` in `webapp`.

Open the URL provided in the terminal to view projects.

Click on projects to view project pages
or click on languages to view translation pages.

## Dependencies

Lyra runs on Node and likely uses its HTTP-server to serve requests
from Internet. This HTTP server will process data before any
authentication or validation: it is fully exposed. A vulnerability
in Node can have a severe impact of Lyra, including compromise of
GitHub authentication tokens, and through those, compromise of at
least the targeted repositories.

https://nodejs.org/

Under Node, Lyra runs on the framework Next.js, while not as exposed,
or as privileged as Node, it still processes data before authentication
and a lot of that data has not been validated by any other program before
being processed by Next.js. The framework does not have quite as much
privileges as Node but once we are confident in Node being fully patched
and known vulnerabilities being mitigated, we should do the same for Next.js

https://nextjs.org/

We have several other direct dependencies that are used by Lyra but
a vulnerability in any of these will most likely be less severe than
a vulnerability in Node or in Next.

Our direct dependencies pull in more transivite dependencies. Some of
these could be more sensitive than our direct dependencies but we will
likely never have enough resources to analyze them all. With some luck,
the projects producing our direct dependencies takes some responsibility
for their dependencies. Keeping our direct dependencies patched reduces
the risk somewhat of us being affected by public vulnerabilities in our
transitive dependencies.

Lyra's build and test programs have even more dependencies. These will
not typically have access to production data or production credentials,
but they will very likely have access to very powerful developer
credentials. Tracking published vulnerabilities in all these is beyond
all hope and feasibility but we can try to keep them somewhat up to date.

## Standalone build (monorepo)

We enable Next.js **standalone output** to keep Docker images tiny.
Because this package lives in a workspace, we must widen dependency tracing.

### `next.config.js`

```js
const path = require('path');

/** @type {import('next').NextConfig} */
module.exports = {
output: 'standalone',
experimental: {
// trace files from the repository root
outputFileTracingRoot: path.join(__dirname, '../..'),
},
};
```

### Gotchas

* Place every module that is **imported at runtime** in this package’s `dependencies` (not `devDependencies`).
Example: `typescript` required by `src/utils/readTypedMessages.ts`.

* After upgrading **Next.js** or adding workspace packages, check the bundle:

```bash
npm run build
node --check webapp/.next/standalone/lyra/webapp/server.js
```

### References

* Next.js docs – “output › Caveats”
[https://nextjs.org/docs/13/app/api-reference/next-config-js/output#caveats](https://nextjs.org/docs/13/app/api-reference/next-config-js/output#caveats)

* GitHub discussion – deep `webapp/server.js` path (#72436)
[https://github.com/vercel/next.js/discussions/72436](https://github.com/vercel/next.js/discussions/72436)

* GitHub issue – public assets missing in monorepo builds (#33895)
[https://github.com/vercel/next.js/issues/33895](https://github.com/vercel/next.js/issues/33895)


## Docker setup

To run Lyra in a docker container, you need to build the Docker image using the [`Dockerfile`](./Dockerfile) in the root of this repository.
The [`docker-compose.yaml`](./docker-compose.yaml) file in the root of this repository can be used to build the image and run the image as a container in one command:
```shell
$ docker compose up
```

or in a detached mode:
```shell
$ docker compose up -d
```

### Docker volume mounts

#### SSH key
Note that in order for the running docker container to be able to interact with the client repository,
you need to mount a private SSH key of a user with access to the repository into the Docker container.
Currently, this is achieved by mounting the private SSH key at `~/.ssh/id_rsa` into the container at
`/home/nodeuser/.ssh/id_rsa`.
If your SSH key is located elsewhere on your local machine, you will need to adjust the path in the
[docker-compose.yaml](./docker-compose.yaml) file accordingly.
⚠️ Note that encrypted private keys are not supported for now.

When mounting the SSH key into the container, the file ownership and permissions from your local system are preserved.
Since the container runs as the nodeuser user (UID 1001), but the mounted key is owned by your local user,
it’s important to ensure that the SSH key has the correct permissions.
SSH requires that private keys are not accessible by others.
To avoid permission issues, you should set the permissions of your private key to 600 on your local machine:

```bash
$ chmod 600 ~/.ssh/id_rsa
```

This ensures that the private key is only readable by the owner, which is sufficient for SSH to accept it inside the
container.

File permissions for the private key and lyra-store.json on your local machine also need to allow access for a user ID of 1001.

#### Lyra Store

Before running the container, ensure the file `lyra-store.json` exists on the host system.
This file is the store for lyra projects and is mounted into the container via docker volume mounts.
You can copy this via `cp ./webapp/store.json ~/lyra-store.json` to this location or just change it to use [`webapp/store.json`](./webapp/store.json).

### Release a new container image

The GitHub Actions workflow [`build-and-push-image.yaml`](.github/workflows/build-and-push-image.yaml) is designed to
automate the process of building, tagging, and pushing a Docker image to the GitHub Container Registry (ghcr.io)
whenever a new tag is pushed to the repository.
The tags must follow semantic versioning, while release candidates are supported as well.

Do not forget to document your changes within the [`CHANGELOG.md`](./webapp/CHANGELOG.md) file and adjusting the version within the [`./webapp/package.json`](./webapp/package.json) file.

### Use built image from the container registry

In case you want to use the already built image that is pushed to the GitHub Container Registry, you can adjust the [
`docker-compose.yaml`](docker-compose.yaml) file as follows (replace `latest` with the version of your preference):

```diff
services:
lyra:
container_name: lyra
- build:
- context: .
+ image: ghcr.io/zetkin/lyra:latest
ports:
- "3000:3000"
environment:
- [email protected]
- GIT_USER_NAME="Lyra Translator Bot"
volumes:
- ~/.ssh/id_github:/home/nodeuser/.ssh/id_rsa:ro
- ~/lyra-store.json:/app/webapp/store.json
- ./config:/app/config
```
16 changes: 16 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
services:
lyra:
container_name: lyra
build:
context: .
ports:
- "3000:3000"
environment:
- [email protected]
- GIT_USER_NAME="Lyra Translator Bot"
volumes:
# note: adjust the path to the ssh key you want to use to access the git repositories
- ~/.ssh/id_rsa:/home/nodeuser/.ssh/id_rsa:ro
# note: adjust the path to the lyra-store-file you want to use
- ~/lyra-store.json:/app/webapp/store.json
- ./config:/app/config
3 changes: 3 additions & 0 deletions known_hosts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
Loading