Add multi-site docker configuration

This commit is contained in:
Tocho Tochev 2024-10-27 14:47:03 +02:00
parent bc8b805b2f
commit 1026d71f3a
13 changed files with 236 additions and 121 deletions

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM node:23-alpine
RUN mkdir /app
WORKDIR /app
COPY package.json /app
RUN npm install
COPY models/ /app/models/
COPY views/ /app/views/
COPY public/ /app/public/
COPY app.js /app/
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=5050
ENV TITLE="Hall X"
ENV PREFIX=
#ENV HALLS="/hall-a:Hall A;/hall-b:Hall B"
CMD node app.js

View File

@ -1 +0,0 @@
web: node app.js

View File

@ -2,17 +2,29 @@
## Deployment
Clone to `/opt/timer`.
Put `openfest-timer@.service` in `/etc/systemd/system`.
Requires NodeJS and npm.
### Configuration
```bash
systemctl daemon-reload
npm install
systemctl enable --now openfest-timer@hall-a.service
systemctl enable --now openfest-timer@hall-b.service
Configuration is done by environmental variables:
```yaml
HALLS: "/hall-a:Hall A;/hall-b:Hall B"
TITLE: "Hall A" # not in use if HALLS is set to non-empty
PREFIX: /hall-a # not in use if HALLS is set to non-empty
BASIC_AUTH: "admin:password" # user:plaintext-password
```
### Prod Deployment
```
cd timer-repo/
git pull
podman build -t timer .
```
### Local Development
`docker-compose up --build`
## Based on Defcon
### Defcon

223
app.js
View File

@ -4,17 +4,40 @@ const
server = require('http').createServer(app),
Stopwatch = require('./models/stopwatch'),
errorHandler = require('errorhandler'),
expressLayouts = require('express-ejs-layouts'),
io = require('socket.io')(server, {});
expressLayouts = require('express-ejs-layouts');
const auth = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.set("WWW-Authenticate", "Basic realm='timer'").status(401).send('Unauthorized');
}
else {
const authTokens = authHeader.split(' ');
if (authTokens[0] === 'Basic') {
const credentials = Buffer.from(authTokens[1], 'base64').toString('utf8');
if (credentials !== process.env.BASIC_AUTH) { // this is obviously not the most secure way, but we don't care
return res.status(401).send('Unauthorized');
}
else {
next();
}
} else {
return res.status(401).send('Unauthorized');
}
}
};
// Configuration
if (process.env.BASIC_AUTH) {
app.use(auth);
}
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(expressLayouts);
app.use(express.static(__dirname + '/public'));
if (process.env.NODE_ENV === 'development') {
app.use(errorHandler({ dumpExceptions: true, showStack: true }));
}
@ -24,95 +47,119 @@ if (process.env.NODE_ENV === 'production') {
}
// Use the port that Heroku provides or default to 5000
// Use the port and host
var port = process.env.PORT || 5000;
var host = process.env.HOST || '0.0.0.0';
var prefix = process.env.PREFIX || '';
const halls = (process.env.HALLS || ((process.env.PREFIX || "") + ":" + (process.env.TITLE || "TIMER"))).replace("; ?$", "").split(';').map(tuple => {
const parts = tuple.split(':');
return {"prefix": parts[0].trim(), "title": parts[1].trim()};
});
console.log("Configuring halls:", halls)
halls.map(hall => {
const title = hall["title"]
const prefix = hall["prefix"]
const router = express.Router();
const io = require('socket.io')(server, {path: prefix + '/socket.io'})
router.use(expressLayouts);
router.use(express.static(__dirname + '/public'));
var stopwatch = new Stopwatch();
stopwatch.on('tick:stopwatch', function(time) {
io.sockets.emit('time', { time: time });
});
stopwatch.on('reset:stopwatch', function(time) {
io.sockets.emit('time', { time: time });
});
// stopwatch.start(); // we probably don't want autostart on server start
io.sockets.on('connection', function (socket) {
io.sockets.emit('time', { time: stopwatch.getTime() });
socket.on('click:start', function () {
stopwatch.start();
});
socket.on('click:stop', function () {
stopwatch.stop();
});
socket.on('click:zero', function () {
stopwatch.zero();
});
socket.on('click:reset', function () {
stopwatch.reset();
});
socket.on('click:resetShort', function () {
stopwatch.resetShort();
});
});
router.get('/', function(req, res) {
res.render('index', { title: title, prefix: prefix });
});
const control = express.Router();
control.get('/', function(req, res) {
res.render('control', { title: title, prefix: prefix });
});
control.post('/reset/', function (req, res) {
stopwatch.reset();
res.send("OK");
});
control.post('/reset-short/', function (req, res) {
stopwatch.resetShort();
res.send("OK");
});
control.post('/start-from-reset/', function (req, res) {
stopwatch.reset();
stopwatch.start();
res.send("OK");
});
control.post('/start-from-reset-short/', function (req, res) {
stopwatch.resetShort();
stopwatch.start();
res.send("OK");
});
control.post('/start/', function (req, res) {
stopwatch.start();
res.send("OK");
});
control.post('/stop/', function (req, res) {
stopwatch.stop();
res.send("OK");
});
control.post('/zero/', function (req, res) {
stopwatch.zero();
res.send("OK");
});
router.use('/control', control);
app.use(prefix, router);
});
server.listen(port, host, function() {
console.log("Express server listening on %j in %s mode", server.address(), app.settings.env);
});
var stopwatch = new Stopwatch();
stopwatch.on('tick:stopwatch', function(time) {
io.sockets.emit('time', { time: time });
});
stopwatch.on('reset:stopwatch', function(time) {
io.sockets.emit('time', { time: time });
});
stopwatch.start();
io.sockets.on('connection', function (socket) {
io.sockets.emit('time', { time: stopwatch.getTime() });
socket.on('click:start', function () {
stopwatch.start();
if (halls.length != 1 && halls[0]["prefix"] != "") {
app.get('/', function(req, res) {
res.render("list", { layout: false, halls: halls });
});
}
socket.on('click:stop', function () {
stopwatch.stop();
});
socket.on('click:zero', function () {
stopwatch.zero();
});
socket.on('click:reset', function () {
stopwatch.reset();
});
socket.on('click:resetShort', function () {
stopwatch.resetShort();
});
});
// configure title
//app.use(function (req, res, next) {
// res.locals.title = process.env.TITLE || 'Timer'
//});
app.get('/', function(req, res) {
res.render('index', { title: process.env.TITLE });
});
const control = express.Router();
control.get('/', function(req, res) {
res.render('control', { title: process.env.TITLE });
});
control.post('/reset/', function (req, res) {
stopwatch.reset();
res.send("OK");
});
control.post('/reset-short/', function (req, res) {
stopwatch.resetShort();
res.send("OK");
});
control.post('/start-from-reset/', function (req, res) {
stopwatch.reset();
stopwatch.start();
res.send("OK");
});
control.post('/start-from-reset-short/', function (req, res) {
stopwatch.resetShort();
stopwatch.start();
res.send("OK");
});
control.post('/start/', function (req, res) {
stopwatch.start();
res.send("OK");
});
control.post('/stop/', function (req, res) {
stopwatch.stop();
res.send("OK");
});
control.post('/zero/', function (req, res) {
stopwatch.zero();
res.send("OK");
});
app.use('/control', control);

13
docker-compose.yml Normal file
View File

@ -0,0 +1,13 @@
services:
timer:
build:
dockerfile: Dockerfile
context: .
environment:
HOST: 0.0.0.0
#TITLE: "Hall A"
#PREFIX: /hall-a
#HALLS: "/hall-a:Hall A;/hall-b:Hall B"
#BASIC_AUTH: "admin:password"
ports:
- 127.0.0.1:5050:5050

View File

@ -1,13 +0,0 @@
[Unit]
Description=OpenFest Timer (%i)
After=network.target
[Service]
EnvironmentFile=/opt/timer/%i.env
Type=simple
Restart=on-failure
WorkingDirectory=/opt/timer
ExecStart=/usr/bin/env node app.js
[Install]
WantedBy=multi-user.target

View File

@ -11,6 +11,6 @@
"underscore": "^1.13.6"
},
"engines": {
"node": "0.6.x"
"node": "23.0"
}
}

View File

@ -1,10 +1,21 @@
#wrapper {
.wrapper {
width: 95%;
height: 100%;
margin: 30vh 14vw;
}
.wrapper #countdown {
font-family: 'Black Ops One', cursive;
font-size: 15vw;
}
.control-wrapper {
width: 475px;
height: 171px;
margin: 100px auto;
}
#countdown {
.control-wrapper #countdown {
font-family: 'Black Ops One', cursive;
font-size: 90px;
}

View File

@ -1,5 +1,5 @@
//var socket = io.connect(window.location.hostname);
var socket = io();
// a bit hackish way to find the path but it will get the job done
var socket = io({"path": window.location.pathname.replace(/(\/control\/?|\/)?$/, "") + "/socket.io"});
socket.on('time', function (data) {
$('#countdown').html(data.time);

View File

@ -1,4 +1,4 @@
<div id="wrapper">
<div class="control-wrapper">
<h1><%= title %></h1>
<div id="countdown"></div>
<button id="start" class="thoughtbot">Start</button>

View File

@ -1,3 +1,3 @@
<div id="wrapper">
<div class="wrapper">
<div id="countdown"></div>
</div>

View File

@ -12,7 +12,7 @@
<![endif]-->
<!-- styles -->
<link href="/css/main.css" rel="stylesheet">
<link href="<%= prefix %>/css/main.css" rel="stylesheet">
<!-- fonts -->
<link href='http://fonts.googleapis.com/css?family=Black+Ops+One' rel='stylesheet' type='text/css'>
@ -20,8 +20,8 @@
</head>
<body>
<%- body %>
<script src="/socket.io/socket.io.js"></script>
<script src="/js/libs/jquery.js"></script>
<script src="/js/main.js"></script>
<script src="<%= prefix %>/socket.io/socket.io.js"></script>
<script src="<%= prefix %>/js/libs/jquery.js"></script>
<script src="<%= prefix %>/js/main.js"></script>
</body>
</html>

21
views/list.ejs Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Timer</title>
<meta name="description" content="">
<meta name="author" content="">
</head>
<body>
<ul>
<% for (const hall of halls) { %>
<li>
<a href="<%= hall["prefix"] %>"><%= hall["title"] %></a>
<ul>
<a href="<%= hall["prefix"] %>/control">Control <%= hall["title"] %></a>
</ul>
</li>
<% } %>
</ul>
</body>
</html>