Serve multiple Django sites from one cloud server
I was a long time user of Heroku
, but after they announced the removal of all of their free plans, I have started to look to move all of my side projects to other hosts. I don't begrudge them for wanting to focus on paying customers, but most of my side projects have very low traffic so their pricing tiers don't make much sense for me anymore.
Initially I moved most of my sites to render. It is a delightful hosting platform very similar to Heroku, although you'll need to translate any custom buildpacks
into the render.yml
file, so it's not quite as simple a translation as I would like. However, render
is easy to use and has a nice UI to provision services. I still have a few sites hosted there, mostly for their managed Postgres database support. I even have a checklist I use to deploy sites to render
.
However, for low traffic sites I have increasingly become enamored with CapRover
after Tobi-De suggested it to me. CapRover
can be hosted on lots of hosting platforms, but I have primarily used it on Digital Ocean. If you sign up for Digital Ocean you will get $200 in free credits (and full disclosure, I get a little credit for my hosting costs as well).
Self-hosting CapRover
Benefits
One major pro of using CapRover
is that you can use one server to host multiple sites at once. On Heroku or render
each "service" will host one site by default. Free tiers will typically limit you to one free site, even for very low traffic sites. Sometimes services in the free tier will be "put to sleep" as well. On CapRover
, I can host multiple Django apps on a $6/month server and they are fast and reliable.
Another pro is that CapRover
has a long list of one-click installs for open source software. This includes backend services like Postgres
, mysql
, and redis
. Complete packages are also available like analytics tracking (umami
and ackee
), error monitoring (glitchtip
), or uptime monitoring (Uptime Kuma
) are all one click away.
Drawbacks
The major con is that support is mainly through GitHub Issues. However, the CapRover
documentation is pretty good -- I did have to re-read some sections a few times to understand what to do, but overall it seems pretty solid. However, hopefully this tutorial lets you avoid some of the learning curve I went through.
One other con is that I have noticed a short time where my sites are unavailable while new code is getting deployed. According to this comment CapRover
does support zero-downtime deploys. For low traffic sites I'm not worried about a few seconds of downtime, but it is something to keep in mind.
Other options
dokku is another popular option for a self-hosted PaaS and is similar to CapRover
, but it doesn't have an admin web UI. It does support buildpacks
so if you are transferring from Heroku
that might be helpful.
How to create a server in the ☁️
Sign up
There are lots of options for servers in the cloud, however the easiest one I have used with CapRover
is Digital Ocean. Sign up for an account to get started.
Create a project
A project
holds related servers together. To make things easier, I just have one project that I put everything in.
Create a server
Digital Ocean calls their cloud servers droplets
(which is sort of adorable come to think of it).
- Click the green Create button in the top navigation header
- Click Droplets
- Under Choose an image, click the Marketplace tab
- Search for "caprover" in the Search keyword text box
- Click CapRover in the search results that shows up
- Under Choose a plan, click Basic
- In the CPU options radio button, click Regular with SSD
- Click the $6/mo option; feel free to choose a more expensive option if you want -- you will be able to change this option later on if you want
- Under Chose a datacenter region, select a region close to you; I selected New York
- Under Authentication, create an SSH key with
ssh-keygen
or1Password
and copy the output of the public key in - Click the Monitoring checkbox under Select additional options
- Put in a memorable name for your new server in the cloud in the text box under Choose a hostname
- Click Create Droplet
It might take a few minutes, but eventually there will be an ipv4
address on the droplet
page. With that you can start setting up CapRover
.
At this point, you can access CapRover
by going to droplet
IP address and a port of 3000, e.g. if your IP address was 123.456.789.123, then you could go to 123.456.789.123:3000. The default login is "captain42", but I suggest continuing on. The CapRover
setup will prompt you to change the password in the next step.
CapRover
setup
Subdomain
You can use any domain name for the CapRover
instance. It doesn't need to be the site that you want CapRover
to host. For example, let's say you have two domains, boats-r-us.com
and boats-and-totes.com
. Both will be Django
projects that are deployed using CapRover
. You could use either of those domains for the CapRover
admin UI or another domain. For example, you might want to host CapRover
on boat-lovers.com
. It's up to you to decide for your situation.
You can do this in any DNS provider, but I tend to use Cloudflare
. Other DNS providers should be a similar process.
- Log into Cloudflare
- Click on the domain name you want to use for the
CapRover
admin UI, e.g.boat-lovers.com
- Click on DNS in the sidebar
- Click on Add record
- Make sure that Type is "A"
- Decide on a unique subdomain which will be the third-level domain where all
CapRover
services will be created -- this will require a wildcard fourth-level A name to be added; e.g.apps
could be your third-level domain name forboat-lovers.com
so an app namedboats-r-us
would live atboats-r-us.apps.boat-lovers.com
a. Add the subdomain with the fourth-level wildcard to the Name text box; e.g. "*.apps" - Put the
ipv4
address for thedroplet
into the IPv4 address text box - Make sure the the Proxy status is un-checked, i.e. it is grey and says DNS only
- Click Save
Server setup
Run the following in your source code directory on your local machine.
npm install -g caprover
caprover serversetup
- Follow the prompts, but make sure to use the third-level domain for your "root domain", e.g.
apps.boat-lovers.com
- Go to
captain.apps.boat-lovers.com
and login with the password you set to access theCapRover
admin UI
Deploy Django code
My docker-python-poetry-django
repository has the files needed for CapRover
to work correctly.
captain-definition
: tellsCapRover
where to find theDockerfile
Dockerfile
: multi-stage definition for how the server should be setup to run the Django site; it installs dependencies viapoetry
, runs thebin/post_compile
script for Django-specific management commands, and serves the site viagunicorn
bin/post_compile
: script to runcollectstatic
,migrate
, etc.; can be customized for your particular application
Make sure to update
ALLOWED_HOSTS
in your Django settings file to include the correct fourth-level domain name, e.g.ALLOWED_HOSTS = ['boats-r-us.apps.boat-lovers.com']
.
caprover deploy
from source code directory and follow the prompts- Go the the
CapRover
admin UI and click on Apps and then your app name - Click on the Deployment tab at the top
- Click on View Build Logs if necessary and watch for errors
- Go to your app's URL via the fourth-level domain name
Public domain name setup
Now that your app works, you can provide a better public URL for people to access the site. A "naked" domain won't have a "www" in front of it. You can also create a rule in Cloudflare
to redirect one type of domain to the other.
first-level domain name
The first level domain can be either "naked" or a third-level domain name. A "naked" domain only has a first level domain name and a TLD, e.g. boat-lovers.com
.
Naked
This will alias boat-lovers.com
to boats-r-us.apps.boat-lovers.com
.
- Log into Cloudflare
- Click on the domain name you want to use, e.g.
boat-lovers.com
- Click on DNS in the sidebar
- Click on Add record
- Make sure that Type is "A"
- Put "@" in for the Name
- Put the droplet
ipv4
address into the IPv4 address text box - Make sure the the Proxy status is checked
- Click Save
www
This will alias www.boat-lovers.com
to boats-r-us.apps.boat-lovers.com
.
- Log into Cloudflare
- Click on the domain name you want to use, e.g.
boat-lovers.com
- Click on DNS in the sidebar
- Click on Add record
- Make sure that Type is "CNAME"
- Put "www" in for the Name
- Put the app's URL into the Target text box
- Make sure the the Proxy status is checked
- Click Save
Add the domain name
- Log into your
CapRover
instance, e.g.captain.apps.boat-lovers.com
- Click on Apps in the sidebar
- Click on the name of your app in the list
- In the HTTP Settings tab, underneath of Your app is publicly available at:, add either the naked domain or the "www" domain (or both), e.g.
boats-r-us.com
orwww.boats-r-us.com
- Click Connect New Domain
- Once all domains are added, click Enable HTTPS for each one (even though this seems unnecessary since
Cloudflare
provides free SSL as well)
Automatic deploys after pushing to GitHub
Once manual deploys are working, you can set up CapRover
to deploy a new version of your code every time you push to a specific branch in your repository.
Generate the keys for deploys
Generate a private key and public key in the same directory as your code.
ssh-keygen -t ed25519 -C "skiff@boat-lovers.com" -f ./deploykey -q -N ""
A private key (named deploykey
) and a public key (deploykey.pub
) will be created in the current directory.
Add the public key to GitHub
- Create a deploy key in GitHub by going to
https://github.com/USERNAME/REPOSITORY_NAME/settings/keys/new
- Give the key a Title (e.g. "CapRover")
- Paste the contents of
deploykey.pub
into the Key text field - Do not check Allow write access unless you have a good reason to
- Click Add key
Add the private key to CapRover
Go to the Deployment tab for your app in CapRover
. Scroll down to Method 3: Deploy from Github/Bitbucket/Gitlab.
- Type your GitHub repo into Repository
- Put the branch you want to automatically deploy into Branch (typically either "main" or "master")
- Paste the contents of the
deploykey
file into the text field underneath of Or, instead of username/password, use SSH Key: - Make sure there is a blank line at the bottom of the pasted contents otherwise GitHub won't validate it
- Click Save & Update
- The text box above Repository should now have a long URL inside of it; copy that into your clipboard
Add the webhook to GitHub
- Go to https://github.com/USERNAME/REPOSITORY/settings/hooks/new and paste the generated URL from the last step into the Payload URL text box; leave the rest of the settings as-is
- Click Add webhook
Now when you push commits to the branch you specified:
- GitHub calls the webhook
CapRover
pulls the new code from GitHubCapRover
builds the DockerfileCapRover
deploys the site
Custom Dockerfile per app
For one of my side projects, devmarks.io, there is a web site and a worker process. They share the same code. So, I have two apps in CapRover
. One named devmarks-web
and one named devmarks-worker
. Typically my Dockerfile
calls a script that runs collectstatic
and a few other management commands and then runs gunicorn
. However, for my worker app I don't need collectstatic
to run and instead of gunicorn
I would like to start my worker process.
CapRover
allows per-app changes to the captain-definition
file. So, for devmarks-worker
I created a captain-definition-worker
file that references a new Dockerfile-worker
file. The only difference in Dockerfile-worker
is the CMD
statement at the end which calls python manage.py worker
.
- Go to your app in the
CapRover
admin UI - Click the Deployment tab
- Scroll to the bottom to the captain-definition Relative Path textbox
- Click
Edit
- Type in
./captain-definition-worker
and click Save & Update
Now when CapRover
starts up the container it will use captain-definition-worker
and Dockerfile-worker
to start up my worker process.
Cron jobs
I have a few cron jobs that I need to run that are currently Django management commands. I spent a while researching how to run these cron jobs in Docker, but kept getting turned around when thinking about them.
Cron + Docker = The Easiest Job Scheduler You’ll Ever Create seems useful if your cron jobs are independent of your code. Chadburn is a one-click install for CapRover
and seemingly helps manage cron tasks, but has the same limitations since it is contained with its own container. However, my cron jobs need to either 1) be in the same Docker instance as my code, or 2) be able to call into the Docker container with my code.
Setting up cron inside my Docker container with my source code would mean that every instance would run the cron jobs, potentially duplicating the cron jobs when more than one instance of my app was running. That didn't seem ideal.
The option I am leaning toward is to specify the cron jobs in the droplet that contains all of my containers. It feels a little messy, but I will document them in my code repository to try to mitigate that. This solution works because from the droplet I can call into any CapRover
app I want.
docker exec -it $(docker ps --filter name=srv-captain--APP_NAME -q) python manage.py MANAGEMENT_COMMAND
Troubleshooting CapRover
At one point, I broke the CapRover
admin UI because of an SSL issue. I ended up looking through the logs and restarting the CapRover
instance to fix it.
Logging into the droplet
Log into Digital Ocean
and click the Console button for your droplet
.
View logs
docker service logs srv-captain--APP_NAME --since 60m --follow
Restart CapRover
docker service update captain-captain --force
See all running apps
docker ps
Run Django management command
Get the Docker
container id from the ps
command above and use it with docker exec
.
docker exec -it CONTAINER_ID python manage.py MANAGEMENT_COMMAND
Or to run a command by the CapRover
app name.
docker exec -it $(docker ps --filter name=srv-captain--APP_NAME -q) python manage.py MANAGEMENT_COMMAND
Domain Verification Failed - Error 1107
Enabling HTTPS for a domain is usually painless, but one time I kept getting a validation error for an extended period. I double-checked that the new IP and domain were set properly in Cloudflare multiple times and waited a few hours, but it never worked. However, you can skip domain verification if needed.
- Log into Digital Ocean
- Go to your droplet
- Click on the Console button
- Copy the following into the terminal and pretty Enter
echo "{\"skipVerifyingDomains\":\"true\"}" > /captain/data/config-override.json
docker service update captain-captain --force
5. Wait a few minutes for CapRover
to restart
6. Try to enable HTTPS for your app again and hopefully it will work
Conclusion
Hopefully this has been helpful for anyone who wants to host a Django site (or a few!) relatively inexpensively. Just a reminder if you sign up for Digital Ocean with my referral code you will get $200 in free credits (and my undying appreciation!).
More resources, documentation, and details
Related Content
Hi, I'm Adam 👋
I've been a backend programmer for ~20 years in a variety of different languages before I discovered Python 10 years ago and never looked back.
alldjango
includes all the hard-won experience I've gained over the years building production-scale Django websites.
Feel free to reach out to me on Mastodon or make a GitHub Issue with questions, comments, or bitter invectives.
All code is licensed as MIT.