Skip to content
This repository was archived by the owner on Jul 9, 2024. It is now read-only.

Commit f7ecf1e

Browse files
committed
Dockerize Airseeker
1 parent a722394 commit f7ecf1e

File tree

8 files changed

+1030
-29
lines changed

8 files changed

+1030
-29
lines changed

README.md

+51-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,51 @@
1-
# airseeker
1+
# Airseeker
2+
3+
A tool to update a beacons with signed responses from Airnode's gateway
4+
5+
# Installation
6+
7+
```sh
8+
yarn install
9+
```
10+
11+
## Build
12+
13+
```sh
14+
yarn build
15+
```
16+
17+
## Usage
18+
19+
```sh
20+
yarn start
21+
```
22+
23+
### Running with process mamnager
24+
25+
You can use [PM2](https://pm2.keymetrics.io/) process manager to run Airseeker. PM2 is also used in the
26+
[Dockerized](#docker) version.
27+
28+
```sh
29+
# Starting Airseeker
30+
yarn pm2:start
31+
# PM2 status
32+
yarn pm2:status
33+
# Logs
34+
yarn pm2:logs
35+
# Stopping Airseeker
36+
yarn pm2:stop
37+
```
38+
39+
## Docker
40+
41+
The container is running the Airseeker with the [PM2](https://pm2.keymetrics.io/) process manager and running a cronjob
42+
taking care of log rotation with [logrotate](https://linux.die.net/man/8/logrotate). We're using a default
43+
[generated logrotate configuration from PM2](https://pm2.keymetrics.io/docs/usage/log-management/#setting-up-a-native-logrotate).
44+
45+
### Build
46+
47+
```sh
48+
yarn docker:build
49+
```
50+
51+
Resulting image is named `api3/airseeker`.

docker/Dockerfile

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
ARG build=local
2+
3+
# Environment
4+
FROM node:14.17-alpine3.14 AS environment
5+
6+
ENV name="airseeker" \
7+
appDir="/app" \
8+
buildDir="/build"
9+
ENV packageDir="${buildDir}/package"
10+
11+
# Build preparation
12+
FROM environment AS preparation
13+
14+
WORKDIR ${buildDir}
15+
16+
RUN apk add --update --no-cache git
17+
18+
# Source preparation - local
19+
FROM preparation as sourcelocal
20+
21+
COPY . ${buildDir}
22+
23+
# Source preparation - git
24+
FROM preparation as sourcegit
25+
26+
ARG branch=main
27+
ARG repository=https://github.com/api3dao/airseeker.git
28+
29+
RUN git clone --single-branch --branch ${branch} ${repository} ${buildDir}
30+
31+
# Production dependencies
32+
FROM source${build} AS deps
33+
34+
RUN yarn install --production --no-optional --ignore-scripts
35+
36+
# Production source code
37+
FROM source${build} AS build
38+
39+
RUN yarn install && \
40+
yarn build && \
41+
yarn pack && \
42+
mkdir -p ${packageDir} && \
43+
tar -xf *.tgz -C ${packageDir} --strip-components 1
44+
45+
# Result image
46+
FROM environment
47+
48+
ENV cronjob="/etc/cron.d/logrotate"
49+
50+
WORKDIR ${appDir}
51+
52+
LABEL application=${name} \
53+
description="A tool to update a beacons with signed responses from Airnode's gateway"
54+
55+
COPY --from=deps ${buildDir}/node_modules ./node_modules
56+
COPY --from=build ${packageDir} .
57+
COPY docker/entrypoint.sh /entrypoint.sh
58+
COPY docker/logrotate-crontab ${cronjob}
59+
60+
# Create Airseeker user
61+
RUN addgroup -S ${name} && \
62+
adduser -h ${appDir} -s /bin/false -S -D -H -G ${name} ${name} && \
63+
chown -R ${name} ${appDir} && \
64+
# Install pm2
65+
yarn global add pm2 && \
66+
# Install dependencies
67+
apk add --update --no-cache logrotate su-exec && \
68+
# Configure logrotate
69+
rm /etc/logrotate.d/* && \
70+
pm2 logrotate -u ${name} && \
71+
sed -i "s;/root;${appDir};g" /etc/logrotate.d/pm2-${name} && \
72+
# Enable logrotate cronjob
73+
chmod +x ${cronjob} && \
74+
crontab -u ${name} ${cronjob}
75+
76+
ENTRYPOINT ["/entrypoint.sh"]

docker/entrypoint.sh

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
3+
# Starting cron (as root) to run logrotate (as non-root) once a day
4+
crond
5+
6+
# Starting pm2 under non-root user
7+
# Using `exec` to pass signals from shell to the binary
8+
exec su-exec ${name}:${name} pm2-runtime /app/ecosystem.config.js --env production

docker/logrotate-crontab

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0 2 * * * logrotate -s /app/.pm2/logrotate.status /etc/logrotate.conf

ecosystem.config.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* eslint-disable functional/immutable-data */
2+
module.exports = {
3+
apps: [
4+
{
5+
name: 'airseeker',
6+
script: './dist/src/main.js',
7+
kill_timeout: 10000,
8+
env: {
9+
NODE_ENV: 'development',
10+
},
11+
env_production: {
12+
NODE_ENV: 'production',
13+
},
14+
},
15+
],
16+
};

package.json

+10
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,26 @@
44
"version": "1.0.0",
55
"description": "A tool to update a beacons with signed responses from Airnode's gateway",
66
"main": "./dist/src/main.js",
7+
"files": [
8+
"dist",
9+
"ecosystem.config.js"
10+
],
711
"scripts": {
812
"build": "yarn clean && yarn compile",
913
"clean": "rimraf -rf ./dist *.tgz",
1014
"compile": "tsc -p tsconfig.json",
15+
"docker:build": "docker build -t api3/airseeker:latest -f docker/Dockerfile .",
1116
"format": "yarn prettier",
1217
"format:fix": "yarn prettier:fix",
1318
"lint": "yarn format && yarn eslint",
1419
"lint:fix": "yarn format:fix && yarn eslint:fix",
1520
"eslint": "eslint . --ext .js,.ts",
1621
"eslint:fix": "eslint . --ext .js,.ts --fix",
1722
"postinstall": "husky install",
23+
"pm2:logs": "yarn pm2 logs",
24+
"pm2:start": "yarn pm2 start ecosystem.config.js",
25+
"pm2:status": "yarn pm2 status",
26+
"pm2:stop": "yarn pm2 delete ecosystem.config.js",
1827
"prettier": "prettier --check \"./**/*.{js,ts,md,yml,json}\"",
1928
"prettier:fix": "prettier --write \"./**/*.{js,ts,md,yml,json}\" --ignore-unknown",
2029
"start": "ts-node src/main.ts",
@@ -32,6 +41,7 @@
3241
"eslint-plugin-jest": "^26.1.2",
3342
"husky": "^7.0.4",
3443
"jest": "^27.5.1",
44+
"pm2": "^5.2.0",
3545
"prettier": "^2.6.0",
3646
"rimraf": "^3.0.2",
3747
"ts-jest": "^27.1.3",

src/main.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,35 @@
1-
console.log('main process');
1+
const DATA_FETCH_FREQUENCY = 5000;
2+
const BEACON_UPDATE_FREQUENCY = 10_000;
3+
4+
let stopSignalReceived = false;
5+
6+
const handleStopSignal = (signal: string) => {
7+
console.log(`Signal ${signal} received`);
8+
console.log('Stopping Airseeeker...');
9+
// Let the process wait for the last cycles instead of killing it immediately
10+
stopSignalReceived = true;
11+
};
12+
13+
const fetchData = async () => {
14+
console.log('Fetching data');
15+
if (!stopSignalReceived) {
16+
setTimeout(() => {
17+
fetchData();
18+
}, DATA_FETCH_FREQUENCY);
19+
}
20+
};
21+
22+
const updateBeacons = async () => {
23+
console.log('Updating beacons');
24+
if (!stopSignalReceived) {
25+
setTimeout(() => {
26+
updateBeacons();
27+
}, BEACON_UPDATE_FREQUENCY);
28+
}
29+
};
30+
31+
fetchData();
32+
updateBeacons();
33+
34+
process.on('SIGINT', handleStopSignal);
35+
process.on('SIGTERM', handleStopSignal);

0 commit comments

Comments
 (0)