A tiny bit of context
So every now and then I give introductory courses to embedded systems security. To keep the students engaged, I mix in a CTF: most practical cases are actually challenges that earn points. At the end of the course, the team that has gained the most points wins a prize.
In the first iterations of the course, the students were asked to call me upon so that I could check their answer. The main issue with this approach is its lack of fairness: if two teams find the right answer at the same time, who wins is arbitrarily determined by which raised hand I saw first.
To solve this issue, I decided to deploy a CTF management web portal. After some research, I decided to use CTFd 
. The platform is open source, but the developers also offer managed services. As I have a very low volume of users (between 8 and 15), I decided to self-host it.
My usual goto is CentOS 7.0. However, having never set up a Python Web Server before, I chose to follow the developers recommendations and used Ubuntu 18.04. Since my VPS does not run on Docker but on LXC, using a prebuilt image was not an option.
This is done starting from a fresh, fully updated Ubuntu 18.04. CTFd will be ran through gunicorn to serve the Python web service using a low privilege user.
The CTFd GitHub Wiki actually talks about using uwsgi, but I could not get it to work properly (challenge submissions failed) and the developers recommended that I use gunicorn instead.
Install the dependencies:
# apt install git build-essential libffi-dev python3-pip gunicorn
Create the user and prepare the folders to be used for the logs:
# adduser ctfd
# mkdir /var/log/CTFd
# chown ctfd. /var/log/CTFd
Clone the GitHub repository and install the necessary Python dependencies:
# su - ctfd
$ git clone https://github.com/CTFd/CTFd.git
$ cd CTFd
$ pip3 install -r requirements.txt
You can now create a systemd unit that will spawn gunicorn and run CTFd (largely based on 
# cat /etc/systemd/system/ctfd.service
Description = CTFd
After = network.target
PermissionsStartOnly = true
PIDFile = /run/CTFd.pid
User = ctfd
Group = ctfd
WorkingDirectory = /home/ctfd/CTFd
ExecStartPre = /bin/mkdir -p /run/CTFd
ExecStartPre = /bin/chown -R ctfd. /run/CTFd
ExecStart = /usr/local/bin/gunicorn 'CTFd:create_app()' --bind '0.0.0.0:8000' --workers 2 --worker-class 'gevent' --pid /run/CTFd/CTFd.pid --access-logfile "/var/log/CTFd/access.log" --error-logfile "/var/log/CTFd/error.log"
ExecReload = /bin/kill -s HUP $MAINPID
ExecStop = /bin/kill -s TERM $MAINPID
PrivateTmp = true
WantedBy = multi-user.target
Enable and start the CTFd unit:
# systemctl daemon-reload
# systemctl enable ctfd.service
# systemctl start ctfd.service
You now have a CTFd instance running on port 8000! I recommend that you put a reverse proxy (e.g. Nginx) in front of it to do static content caching and SSL, put that’s up to you.