A Python Flask app for our wedding
Having a good few years of Python experience I decided to set myself the challenge of building my own web app from scratch for our wedding. Specifically a way of managing RSVPs and a source of information for guests. I ended up learning a lot more than I initially thought I would, such as domains, nginx web hosting, docker compose, email APIs and much much more.
- Flask and various flask addons including Flask-WTForms and Flask-SQLAlchemy.
- SQLite for RSVP responses and invitees.
- Docker and Docker Compose for running the multi-container application.
- Nginx and Gunicorn to handle incoming requests and set up the web server respectively.
- SendGrid and the SendGridAPIClient for sending emails.
- HTML, CSS, Bootstrap and Jinja for front-end development.
- Digital Ocean droplet Ubuntu virtual machine.
The app can be launched using the native Flask web server. While this isn't suitable for a production application, it is useful for testing.
-
Clone this repository.
git clone https://github.com/Lewis-Gallagher/wedding-website.git
-
Create a python virtual environment and install the requirements from within the project directory.
python3 -m venv venv pip install -r requirements.txt
-
Create a
.env
file containing environmental variables for the app:- FLASK_APP - the name of the python file that loads the app.
- SENDGRID_API_KEY - A SendGrid API key for sending emails.
- SECRET_KEY - A secret key for CSRF protection to use the FLask-WTF FlaskForms.
- MAIL_DEFAULT_SENDER - The email address which will send emails to invitees.
For example:
FLASK_APP=wedding-website.py SENDGRID_API_KEY="********" SECRET_KEY="example-secret-key" MAIL_DEFAULT_SENDER=lewis@nplgwedding.com
N.B. The
FLASK_APP
variable is required. The app will still run without the bottom three variables, however the RSVP form and confirmation emails will not be operational. -
Launch the app via Flask
flask run
-
The website requires a Ubuntu server with a small number of prerequisites which are detailed in the droplet setup file.
-
Clone this repository.
git clone https://github.com/Lewis-Gallagher/wedding-website.git
-
Set up environmental variables for the service.
- SENDGRID_API_KEY - A SendGrid API key for sending emails.
- SECRET_KEY - A secret key for CSRF protection to use the FLask-WTF FlaskForms.
- MAIL_DEFAULT_SENDER - The email address which will send emails to invitees.
A
.env
file should be created in the project directory in the following format:SENDGRID_API_KEY="********" SECRET_KEY="example-secret-key" MAIL_DEFAULT_SENDER=lewis@nplgwedding.com
The flask service in the
docker compose yaml
file will use theenv_file: .env
argument to assign variables inside the.env
file when the container is launched.N.B. This isn't the most secure method, as anybody with root access to the droplet can inspect the container while it's running to view any set environmental variables. An alternative would be to use docker secrets, however, this requires setting up docker as a swarm service.
-
Launch the app profile of the docker service from within the project directory (i.e. in the same directory as the
docker-compose.yml
file):sudo docker compose up --profile app -d
The certbot certificates require renewal every 90 days. I haven't figured out how to enforce automatic renweal of certificates so for now these commands are run within the virutal machine hosting the app.
sudo docker compose --profile certbot run certbot renew --cert-name nplgwedding.com --force-renewal
sudo docker compose --profile certbot run certbot renew --cert-name www.nplgwedding.com --force-renewal
The Docker service may need to be restarted after these commands are run.
The app uses an SQLite database as it is lightweight and traffic is expected to be very low. This is interacted with via the Flask SQLAlchemy package.
The database contains one table: Guest
. The Guest
table contains attendee information including a unique ID, name, contact information, dietary requirements and an optional message.
Due to the relatively static nature of the site and the reality that most guests will only visti it once to submit their RSVPs, I felt a login service was unnecessary. This does mean that literally anyone can submit an RSVP to the site, regardless if they were invited to the wedding or not. The database is protected from SQL injections, and therefore the main consideration is potential malicious spam submitions.
Database backups are important to not erroneously lose attendee information. This is achieved manually every time the database is manually accessed by running the sq3backup.sh script:
cd db/
. sq3backup.sh app.db
This creates a backup file of the database, labelled with the date and time, within the db/bak/
directory.
The database can be restored with:
dbfile=bak/20221228-143151.app.db.sq3
sqlite3 app.db ".restore $dbfile"
Finally, the database can be exported to csv format with:
sqlite3 -header -csv app.db "SELECT * from Guest;" > guests.csv
I am using a Digital Ocean droplet running Ubuntu 20.04 to host the website. A small amount of set up is required to get things up and running. See droplet-setup.md for details.
The service consists of two images. The first is built from the Dockerfile
in the project directory, which contains the source code for the app. The second pulls the nginx container from Dockerhub.
From there the service is run with docker compose in detached mode (-d
) from the project directory, containing the docker-compose.yml
file:
sudo docker compose --profile app up -d
The status of each container can be viewed with:
sudo docker ps
Docker compose logs can be inspected by running the following command from the project directory, containing the docker-compose.yml
file.
sudo docker compose logs
Which will output a list of all running containers
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6bc904c67b99 nginx:1.23 "/docker-entrypoint.…" 10 hours ago Up 21 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp nginx
51fb1a63d09f wedding-website-flask "gunicorn --bind 0.0…" 10 hours ago Up 21 minutes flask
If stopped for any reason, the Docker service will automatically attempt to restart unless it is explicitly stopped, via the restart: unless-stopped
argument. So in the event of the droplet being rebooted, or the app crashing, the Docker service will restart automatically without the need for manual intervention.