- HTML 53.9%
- Python 41.7%
- JavaScript 2.7%
- CSS 1.2%
- Dockerfile 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| backend | ||
| frontend | ||
| infra | ||
| .containerignore | ||
| .gitignore | ||
| .python-version | ||
| manage.py | ||
| package-lock.json | ||
| package.json | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
Overview
Development
The project is managed using podman.
Virtual Machine
If running on MacOS, Windows, or a Linux distro that ships without systemd
(e.g. Void Linux), you'll need to install a podman virtual machine to continue.
Even if this is not the case, it is recommended to use a VM anyways for both
consistency and security.
Depending on your OS, you will need some hypervisor software installed. Refer to the podman-machine documentation for details. Once setup, run:
# Linux
$ podman machine init --volume "$PWD:/home/core/dovely" dovely
# MacOS
$ podman machine init --volume "$PWD:/var/home/core/dovely" dovely
and then
$ podman machine start dovely
In some cases, the above commands may complain about a few additional packages that may be missing.
The first that may be missing is gvproxy.
Download the latest release for your architecture. You will want to rename the
download to just gvproxy and set its executable bit on. Then move to one of:
/usr/local/libexec/podman/usr/local/lib/podman/usr/libexec/podman/usr/lib/podman
or a custom location which you can specify in $XDG_CONFIG_HOME/containers/containers.conf
like so:
[engine]
helper_binaries_dir=['/path/to/gvisor']
The second that may be missing is virtiofsd.
Download the latest release and move the resulting binary somewhere in your
PATH.
Quadlets
Podman provides a containerized systemd-like interface via quadlets.
We use this functionality for services our Django server depend on, e.g. the
database, cache, etc. To turn on, install the files in quadlets/ into the VM
and start the services:
$ podman machine ssh dovely
> cd dovely
> podman quadlet install infra/dovely* infra/dev/dovely*
> systemctl --user start dovely-pod
By booting up just the pod, all containers associated with the pod will automatically turn on. Refer to the documentation for help on writing unit files.
Procrastinate
The dovely-procrastinate.container quadlet will continually fail until we create
an image it can pull. First add the secrets the quadlet relies on, e.g.
$ printf "$VALUE" | podman secret create secret_key -
Then build the base image:
$ podman build --file infra/$MODE/Containerfile --tag django .
Note, we use the production image even in development. It gives a change to test the production Containerfile and the procrastinate instance should rarely need updating.
Environment
You will need to create a .env file locally with at the following contents:
DEBUG=1
POSTMARK_API_KEY=
SECRET_KEY=django-insecure-msa*o7ov4jw2+hmfib9-!#nm=_%n6=^29xt=*yf2l=4o5!fm6y
UMAMI_SCRIPT=http://localhost:3000/script.js
UMAMI_WEBSITE_ID=
Asset Management
Our frontend development is largely driven by the following libraries:
Before continuing, it is strongly recommended to install nvm.
It is assumed you are running node v24.12.0 (npm v11.6.2). Do so by running:
$ nvm install 24.12.0
$ nvm use 24.12.0
You can produce the required CSS and JavaScript files by running:
$ npm install
$ npm run build
This will produce an index.css and index.js file in the backend/assets
directory. Note the assets directory is specified as an extra search directory
for static files in our Django application.
We also augmented the default Django template system with django-cotton which simplifies component construction. For ease of development, you will want to continually scan the template files for new class names. You can do so by running the following in parallel to the development server command:
$ npm run dev
The above may require other packages for watching files, e.g. watchman or
similar.
Static Files
Static files are handled by Whitenoise.
No additional configuration is necessary outside of running the collectstatic
command at some point.
Media Files
For now, we just use the local filesystem to store media files. This is
done using a volume named django-media, shared by nginx and our django
instances.
Database
Before starting up the development server, you need to add the user the server will run as. SSH into the VM and then run:
$ podman exec -it systemd-dovely-postgres psql -U postgres
> CREATE USER django WITH PASSWORD 'django';
> GRANT CONNECT ON DATABASE dovely TO django;
> GRANT pg_read_all_data TO django;
> GRANT pg_write_all_data TO django;
Development Server
To run the Django development server, first build the image described by
Containerfile. Then start a new container based on this image, attaching it
to the systemd pod started up in the previous section. Assuming you are still
SSH'ed into the VM, you can run:
$ infra/attach dev uv run manage.py runserver 0.0.0.0:8000
Once running, the development server can be accessed at http://localhost:8000.
Testing
To run tests, first SSH into the VM. Afterward, you can run the following:
$ infra/attach dev uv run manage.py collectstatic --noinput
$ infra/attach dev uv run manage.py test
Analytics
We use Umami for basic analytics. Access to the Umami
dashboard by navigating to http://localhost:3000. The default username and
password is admin and umami respectively.
When first starting, add a new website with form fields/values:
- Name:
dovely - Domain:
http://localhost:8000
Afterward, update the UMAMI_WEBSITE_ID field in your local .env file with
the newly created ID.
Deployment
We are hosted on Hetzner. This section assumes you are SSH'ed into the server instance and have run commands corresponding to the previous instructions (though not in a VM).
Unlike in development, our server runs in rootful mode. This ensures systemd processes are always running, simplifies certain volume mounting expectations, and in general makes our life a bit easier.
Our servers run Fedora since it comes pre-installed with a recent enough version of podman that supports quadlets. The first time you log into the machine, you will want to run the following commands:
$ sudo dnf update
$ sudo dnf install certbot git podman
You will also want to register an SSH key with GitHub so you can then clone this repository.
Many of the steps used to initially turn up the server mirror those in Development. Order doesn't strictly matter since quadlets automatically retry on failure, but following the remaining sections in order is probably most seamless.
Ignore instructions related to the
.envfile. This is not used in production.
Django Server
We deploy our Django server as a quadlet in production. First, add the necessary secrets, one for each environment variable the application expects to have available.
This also installs the necessary secrets for our procrastinate worker since those are a subset of the server's secrets.
Then build a new image and install the corresponding quadlet along with all the others:
$ podman build --file infra/$MODE/Containerfile --tag django .
$ podman quadlet install infra/dovely* infra/$MODE/dovely*
$ systemctl restart dovely-pod
Database
Follow the instructions in the previous Database step.
You should also run any pending migrations like so:
$ infra/attach $MODE uv run manage.py migrate
Analytics
Follow the instructions in the previous Analytics step.
Reverse Proxy
We use the NGINX reverse proxy to serve multiple backends through a single port, namely the Django backend and Umami. To configure the proxy, run the following:
$ podman cp infra/$MODE/nginx.conf systemd-dovely-nginx:/etc/nginx/conf.d/dovely.conf
$ systemctl restart dovely-nginx
Use the
$MODE/nginx.bootstrap.conffile when first bootstrapping the server. Update to the environment specific configuration oncecertbotis setup.
SSL Certificates
We employ Let's Encrypt to issue us SSL
certificates. Using email josh@getdovely.com, generate new certificates by
running the following:
$ mkdir -p $HOME/.local/state/data
$ certbot certonly --webroot \
--webroot-path $HOME/.local/state/data \
--domain getdovely.com \
--domain www.getdovely.com \
--domain umami.getdovely.com
This will generate a few certificates we can now copy into our bound volume:
$ cp /etc/letsencrypt/live/getdovely.com/fullchain.pem /root/.local/state/cert
$ cp /etc/letsencrypt/live/getdovely.com/privkey.pem /root/.local/state/cert
You can renew certificates with just the following:
$ certbot renew
Copy the new live certificates into the same location as above and restart the NGINX server:
$ systemctl restart dovely-nginx