Add multi-site docker configuration
This commit is contained in:
parent
bc8b805b2f
commit
e5a6270d9c
|
@ -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
|
48
README.md
48
README.md
|
@ -2,17 +2,49 @@
|
|||
|
||||
## 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
|
||||
|
||||
```sh
|
||||
cd timer-repo/
|
||||
git pull
|
||||
podman build -t timer .
|
||||
|
||||
cat > .config/containers/systemd/timer.container <<EOF
|
||||
[Container]
|
||||
ContainerName=timer
|
||||
Image=timer:latest
|
||||
|
||||
Environment=PORT=5050
|
||||
Environment=HALLS="/hall-a:Hall A;/hall-b:Hall B;/test-hall:TEST"
|
||||
Environment=BASIC_AUTH="user:password"
|
||||
|
||||
PublishPort=127.0.0.1:5050:5050
|
||||
|
||||
Network=INSERT_THE_NETWORK
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart timer
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
`docker-compose up --build`
|
||||
|
||||
## Based on Defcon
|
||||
|
||||
### Defcon
|
||||
|
|
131
app.js
131
app.js
|
@ -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,27 +47,42 @@ 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';
|
||||
|
||||
server.listen(port, host, function() {
|
||||
console.log("Express server listening on %j in %s mode", server.address(), app.settings.env);
|
||||
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()};
|
||||
});
|
||||
|
||||
var stopwatch = new Stopwatch();
|
||||
console.log("Configuring halls:", halls)
|
||||
|
||||
stopwatch.on('tick:stopwatch', function(time) {
|
||||
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) {
|
||||
stopwatch.on('reset:stopwatch', function(time) {
|
||||
io.sockets.emit('time', { time: time });
|
||||
});
|
||||
});
|
||||
|
||||
stopwatch.start();
|
||||
// stopwatch.start(); // we probably don't want autostart on server start
|
||||
|
||||
io.sockets.on('connection', function (socket) {
|
||||
io.sockets.on('connection', function (socket) {
|
||||
io.sockets.emit('time', { time: stopwatch.getTime() });
|
||||
|
||||
socket.on('click:start', function () {
|
||||
|
@ -66,53 +104,62 @@ io.sockets.on('connection', function (socket) {
|
|||
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 });
|
||||
});
|
||||
router.get('/', function(req, res) {
|
||||
res.render('index', { title: title, prefix: prefix });
|
||||
});
|
||||
|
||||
|
||||
const control = express.Router();
|
||||
const control = express.Router();
|
||||
|
||||
control.get('/', function(req, res) {
|
||||
res.render('control', { title: process.env.TITLE });
|
||||
});
|
||||
control.post('/reset/', function (req, res) {
|
||||
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) {
|
||||
});
|
||||
control.post('/reset-short/', function (req, res) {
|
||||
stopwatch.resetShort();
|
||||
res.send("OK");
|
||||
});
|
||||
control.post('/start-from-reset/', function (req, res) {
|
||||
});
|
||||
control.post('/start-from-reset/', function (req, res) {
|
||||
stopwatch.reset();
|
||||
stopwatch.start();
|
||||
res.send("OK");
|
||||
});
|
||||
control.post('/start-from-reset-short/', function (req, res) {
|
||||
});
|
||||
control.post('/start-from-reset-short/', function (req, res) {
|
||||
stopwatch.resetShort();
|
||||
stopwatch.start();
|
||||
res.send("OK");
|
||||
});
|
||||
control.post('/start/', function (req, res) {
|
||||
});
|
||||
control.post('/start/', function (req, res) {
|
||||
stopwatch.start();
|
||||
res.send("OK");
|
||||
});
|
||||
control.post('/stop/', function (req, res) {
|
||||
});
|
||||
control.post('/stop/', function (req, res) {
|
||||
stopwatch.stop();
|
||||
res.send("OK");
|
||||
});
|
||||
control.post('/zero/', function (req, res) {
|
||||
});
|
||||
control.post('/zero/', function (req, res) {
|
||||
stopwatch.zero();
|
||||
res.send("OK");
|
||||
});
|
||||
|
||||
router.use('/control', control);
|
||||
|
||||
app.use(prefix, router);
|
||||
});
|
||||
|
||||
app.use('/control', control);
|
||||
server.listen(port, host, function() {
|
||||
console.log("Express server listening on %j in %s mode", server.address(), app.settings.env);
|
||||
});
|
||||
|
||||
if (halls.length != 1 && halls[0]["prefix"] != "") {
|
||||
app.get('/', function(req, res) {
|
||||
res.render("list", { layout: false, halls: halls });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -11,6 +11,6 @@
|
|||
"underscore": "^1.13.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "0.6.x"
|
||||
"node": "23.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<div id="wrapper">
|
||||
<div class="wrapper">
|
||||
<div id="countdown"></div>
|
||||
</div>
|
||||
|
|
|
@ -8,20 +8,20 @@
|
|||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![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'>
|
||||
<link href='//fonts.googleapis.com/css?family=Black+Ops+One' rel='stylesheet' type='text/css'>
|
||||
|
||||
</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>
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue