Tutorial

In this tutorial, you will deploy a Django project or a PocketBase binary (in binary mode) to a Linux server running Ubuntu using the fujin CLI.

First, make sure you follow the installation instructions and have the fujin command available globally in your shell.

Prerequisites

Linux Box

fujin has no strict requirements on the virtual private server (VPS) you choose, apart from the fact that it must be running a recent version of Ubuntu or a Debian-based system. I’ve mainly run my tests with various versions of Ubuntu: 20.04, 22.04, and 24.04. Other than that, use the best option for your app or the cheapest option you can find and make sure you have root access to the server.

Domain name

You can get one from popular registrars like namecheap or godaddy. If you just need something to test this tutorial, you can use sslip, which is what I’ll be using here.

If you’ve bought a new domain, create an A record to point to the server IP address.

Python package

Note

If you are deploying a binary or self-contained executable, skip to the next section

Project Setup

If you are deploying a Python project with fujin, you need your project to be packaged and ideally have an entry point. We will be using Django as an example here, but the same steps can be applied to any other Python project, and you can find examples with more frameworks in the examples folder on GitHub.

Let’s start by installing and initializing a simple Django project.

uv tool install django
django-admin startproject bookstore
cd bookstore
uv init --package .
uv add django gunicorn

The uv init --package command makes your project mostly ready to be used with fujin. It initializes a packaged application using uv, meaning the app can be packaged and distributed (e.g: via PyPI) and defines an entry point, which are the two requirements of fujin.

This is the content you’ll get in the pyproject.toml file, with the relevant parts highlighted.

fujin.toml
 1[project]
 2name = "bookstore"
 3version = "0.1.0"
 4description = "Add your description here"
 5readme = "README.md"
 6authors = [
 7    { name = "Tobi", email = "tobidegnon@proton.me" }
 8]
 9requires-python = ">=3.12"
10dependencies = [
11    "django>=5.1.3",
12    "gunicorn>=23.0.0",
13]
14
15[project.scripts]
16bookstore = "bookstore:main"
17
18[build-system]
19requires = ["hatchling"]
20build-backend = "hatchling.build"

The build-system section is what allows us to build our project into a wheel file (Python package format), and the project.scripts defines a CLI entry point for our app. This means that if our app is installed (either with pip install or uv tool install, for example), there will be a bookstore command available globally on our system to run the project.

Note

If you are installing it in a virtual environment, then there will be a file .venv/bin/bookstore that will run this CLI entry point. This is what fujin expects internally. When it deploys your Python project, it sets up and installs a virtual environment in the app directory in a .venv folder and expects this entry point to be able to run commands with the fujin app exec <command> command.

Currently, our entry point will run the main function in the src/bookstore/__init__.py file. Let’s change that.

rm -r src
mv manage.py bookstore/__main__.py

We first remove the src folder, as we won’t use that since our Django project will reside in the top-level bookstore folder. I also recommend keeping all your Django code in that folder, including new apps, as this makes things easier for packaging purposes. Then we move the manage.py file to the bookstore folder and rename it to __main__.py. This enables us to do this:

uv run bookstore migrate # equivalent to python manage.py migrate if we kept the manage.py file

Now to finish, update the scripts section in your pyproject.toml file.

fujin.toml
[project.scripts]
bookstore = "bookstore.__main__:main"

Now the CLI that will be installed with your project will do the job of the manage.py file. To test this out, run the following commands:

uv sync # needed because we updated the scripts section
source .venv/bin/activate
bookstore runserver

fujin init

Now that our project is ready, run fujin init at the root of it.

Here’s what you’ll get:

fujin.toml
app = "bookstore"
build_command = "uv build && uv pip compile pyproject.toml -o requirements.txt"
distfile = "dist/bookstore-{version}-py3-none-any.whl"
requirements = "requirements.txt"
release_command = "bookstore migrate"
installation_mode = "python-package"

[webserver]
upstream = "unix//run/bookstore.sock"
type = "fujin.proxies.caddy"

[processes]
web = ".venv/bin/gunicorn bookstore.wsgi:application --bind unix//run/bookstore.sock"

[aliases]
shell = "server exec --appenv -i bash"

[host]
user = "root"
domain_name = "bookstore.com"
envfile = ".env.prod"

Update the host section; it should look something like this, but with your server IP:

fujin.toml
[host]
domain_name = "SERVER_IP.sslip.io"
user = "root"
envfile = ".env.prod"

Caution

Make sure to replace SERVER_IP with the actual IP address of your server.

Create a .env.prod file at the root of your project; it can be an empty file for now. The only requirement is that the file should exist. Update your bookstore/settings.py with the changes below:

settings.py
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = ["SERVER_IP.sslip.io"]

With the current setup, we should already be able to deploy our app with the fujin up command, but static files won’t work. Let’s make some changes.

Update bookstore/settings.py with the changes below:

settings.py
118STATIC_URL = "static/"
119STATIC_ROOT = "./staticfiles"

The last line means that when the collectstatic command is run, the files will be placed in a staticfiles directory in the current directory.

Now let’s update the fujin.toml file to run collectstatic before the app is started and move these files to the folder where our web server can read them:

...
release_command = "bookstore migrate && bookstore collectstatic --no-input && sudo rsync --mkpath -a --delete staticfiles/ /var/www/bookstore/static/"
...

[webserver]
...
statics = { "/static/*" = "/var/www/bookstore/static/" }

Note

If your server has a version of rsync that does not have the --mkpath option, you can update the rsync part to create the folder beforehand:

&& sudo mkdir -p /var/www/bookstore/static/ && sudo rsync -a --delete staticfiles/ /var/www/bookstore/static/"

Now move to the create user section for the next step.

Binary

This mode is intended for self-contained executables, for example, with languages like Golang or Rust that can be compiled into a single file that is shipped to the server. You can get a similar feature in Python with tools like pyapp and pex. For this tutorial, we will use pocketbase, a Go backend that can be run as a standalone app.

mkdir pocketbase
cd pocketbase
touch .env.prod
curl -LO https://github.com/pocketbase/pocketbase/releases/download/v0.22.26/pocketbase_0.22.26_linux_amd64.zip
fujin init --profile binary

With the instructions above, we will download a version of Pocketbase to run on Linux from their GitHub release and initialize a new fujin configuration in binary mode. Now update the fujin.toml file with the changes below:

fujin.toml
 1app = "pocketbase"
 2version = "0.22.26"
 3build_command = "unzip pocketbase_0.22.26_linux_amd64.zip"
 4distfile = "pocketbase"
 5release_command = "pocketbase migrate"
 6installation_mode = "binary"
 7
 8[webserver]
 9upstream = "localhost:8090"
10type = "fujin.proxies.caddy"
11
12[processes]
13web = "pocketbase serve --http 0.0.0.0:8090"
14
15[aliases]
16shell = "server exec --appenv -i bash"
17
18[host]
19domain_name = "SERVER_IP.sslip.io"
20user = "root"
21envfile = ".env.prod"

Caution

Make sure to replace SERVER_IP with the actual IP address of your server.

Create User

Currently, we have the user set to root in our fujin.toml file and fujin might work with the root user, but I’ve noticed some issues with it, so I highly recommend creating a custom user. For that, you’ll need the root user with SSH access set up on the server. Then you’ll run the command fujin server create-user with the username you want to use. You can, for example, use fujin as the username.

create-user example
fujin server create-user fujin

This will create a new fujin user on your server, add it to the sudo group with the option to run all commands without having to type a password, and will copy the authorized key from the root to your new user so that the SSH setup you made for the root user still works with this new one. Now update the fujin.toml file with the new user:

fujin.toml
[host]
domain_name = "SERVER_IP.sslip.io"
user = "fujin"
envfile = ".env.prod"

Deploy

Now that your project is ready, run the commands below to deploy for the first time:

fujin up

The first time, the process can take a few minutes. At the end of it, you should have a link to your deployed app.

FAQ

What about my database?

I’m currently using SQLite for my side projects, so this isn’t an issue for me at the moment. That’s why fujin does not currently assist with databases. However, you can still SSH into your server and manually install PostgreSQL or any other database or services you need.

I plan to add support for managing additional tools like Redis or databases by declaring containers via the fujin.toml file. These containers will be managed with podman, To follow the development of this feature, subscribe to this issue.