deploy¶
The fujin deploy command deploys your application to the server.
Overview¶
This is the core deployment command. It builds your application locally, bundles all necessary files, uploads them to the server, and installs/configures everything.
Use fujin deploy for:
Deploying code changes
Updating configuration
Updating environment variables
Refreshing systemd units or Caddy configuration
How it works¶
Here’s a high-level overview of what happens when you run the deploy command:
Resolve Secrets: If you have a
secretsconfiguration, Fujin retrieves secrets defined in your environment file from the configured adapter (Bitwarden, 1Password, Doppler, etc.).Build the Application: Your application is built locally using the
build_commandspecified in your configuration.Create Deployment Bundle: Fujin creates a Python zipapp (.pyz file) containing:
Your distribution file (wheel or binary)
Optional
requirements.txt(for Python packages)Resolved
.envfile with secretsRendered systemd unit files (services, sockets, timers)
Systemd dropin configurations
Caddyfile (if available)
Installer script (
_installer/__main__.py)Installation metadata (
config.json)
Upload Bundle: The zipapp is uploaded to
{app_dir}/.install/.versions/{app_name}-{version}.pyzand verified using SHA256 checksum.Execute Installer: The remote Python interpreter runs the zipapp (
python3 installer.pyz install), which:Creates the app user if needed
Sets up the
.install/directory structureInstalls the application (creates virtualenv for Python packages, copies binary for binary mode)
Creates
.appenvshell environment setupInstalls systemd units and dropin configurations
Cleans up stale units and dropins
Enables and restarts services
Configures and reloads Caddy (when enabled)
Prune Old Bundles: Old zipapp bundles are removed from
.install/.versions/according toversions_to_keepconfiguration.Record Deployment: Deployment metadata (version, timestamp, git commit) is appended to the audit log.
Completion: A success message is displayed with deployment details.
Deployment Layout and Permissions¶
All applications are deployed to /opt/fujin/{app_name} with a secure permission model that separates deployment and runtime privileges.
Directory Structure¶
/opt/fujin/{app_name}/
├── .install/ # Deployment infrastructure
│ ├── .env # Environment variables file (640)
│ ├── .appenv # Application-specific environment setup
│ ├── .version # Current deployed version
│ ├── .venv/ # Virtual environment (Python from shared dir)
│ └── .versions/ # Stored deployment bundles
│ ├── app-1.2.3.pyz
│ └── app-1.2.2.pyz
├── db.sqlite3 # App runtime data (owned by app user)
└── uploads/ # App runtime data (owned by app user)
/opt/fujin/{app_name}/
├── .install/ # Deployment infrastructure
│ ├── .env # Environment variables file (640)
│ ├── .appenv # Application-specific environment setup
│ ├── .version # Current deployed version
│ ├── app_binary # Installed binary (755)
│ └── .versions/ # Stored deployment bundles
│ ├── app-1.2.3.pyz
│ └── app-1.2.2.pyz
├── data/ # App runtime data (owned by app user)
└── cache/ # App runtime data (owned by app user)
Permission Model¶
Fujin uses a multi-user security model with three components:
fujin group (
root:fujin): Members can deploy applicationsCreated during
fujin server bootstrapDeploy users are added to this group
Grants write access to
/opt/fujin/
Deploy user (e.g.,
tobi): Owns application filesMember of
fujingroupCan deploy and manage applications
Owns
.install/directory (deployment infrastructure)
App user (e.g.,
bookstore): Runs servicesNon-privileged user created per-application
Cannot modify application code or
.venvCan write to database files and logs within app directory
Used automatically for
fujin server exec --appenvandfujin appcommands
The .install/ subdirectory isolates deployment infrastructure from application runtime data. This means:
Deployment operations (like
chown) only affect.install/, not app dataApp runtime files (databases, caches, uploads) remain owned by the app user
No risk of permission conflicts between deployment and runtime operations
Note
Running Commands as App User
When you use fujin server exec --appenv or fujin app exec, commands automatically run as the app user (not the deploy user). This ensures commands can write to app-owned files like databases, logs, and uploads.
For example, Django’s createsuperuser command needs to write to db.sqlite3 (owned by bookstore:bookstore). Running it as the deploy user would fail with permission errors. Fujin handles this automatically:
# These commands run as the app user
fujin app shell # Opens shell as 'bookstore'
fujin server exec --appenv python # Runs Python as 'bookstore'
fujin app exec migrate # Runs Django migration as 'bookstore'
# Plain server commands still run as deploy user
fujin server exec ls -la # Runs as 'tobi' (deploy user)
Inside the shell: The .appenv file contains a wrapper function that automatically runs the app binary (e.g., bookstore) as the app user. This means when you’re in fujin app shell, you can simply type bookstore migrate and it will work correctly without manual sudo.
Example permissions:
/opt/fujin/ root:fujin drwxrwxr-x (775)
├── .python/ root:fujin drwxrwxr-x (775)
└── bookstore/ tobi:bookstore drwxrwxr-x (775)
├── .install/ tobi:bookstore drwxrwx--- (770)
│ ├── .env tobi:bookstore -rw-r----- (640)
│ └── .venv/ tobi:bookstore drwxr-xr-x (755)
└── db.sqlite3 bookstore:... -rw-r--r-- (664)
Security Benefits¶
This model provides defense-in-depth:
Process isolation: Services run as non-root app user
Code protection: App user cannot modify source code or
.venvDatabase access: App user can read/write database files
Home directory isolation: Systemd
ProtectHome=trueprevents access to/homeSystem protection: Systemd
ProtectSystem=strictmakes most of filesystem read-onlyMulti-user support: Any member of
fujingroup can deploy
If an application is compromised, the attacker:
✗ Cannot modify application code
✗ Cannot access other users’ home directories
✗ Cannot access other applications’ directories (no read/write permissions)
✓ Can only read/write within the app directory with group permissions
Migrating Existing Deployments¶
If you have applications deployed before the shared Python directory feature was added, you need to migrate them to use the new security model.
Symptoms of unmigrated deployments:
Services fail with exit code 203/EXEC
Python installed in
~/.local/share/uv/pythonSystemd units using
ProtectHome=read-only(old insecure setting)
Migration steps:
Update systemd service files in
.fujin/systemd/*.service:# Change from: ProtectHome=read-only ReadWritePaths={app_dir}/.venv # To: ProtectHome=true ReadWritePaths={app_dir}
Run bootstrap to create shared Python directory:
fujin server bootstrapThis creates
/opt/fujin/.pythonwith proper permissions.Redeploy your application:
fujin deployThe installer will automatically:
Install Python to
/opt/fujin/.pythonRecreate the venv pointing to the shared Python
Apply the updated systemd units with
ProtectHome=true
Verification:
After redeployment, verify the migration:
# Check Python location
ssh user@host "readlink /opt/fujin/myapp/.venv/bin/python"
# Should show: /opt/fujin/.python/cpython-3.x.x-.../bin/python3.x
# Verify systemd security
ssh user@host "systemd-analyze security myapp-web.service | grep ProtectHome"
# Should show: ✓ ProtectHome=true
# Check services are running
fujin app status
Notes:
Old Python installations in
~/.local/share/uv/pythonare not automatically removedThey can be manually deleted to free disk space
Multiple applications share the same Python installation in
/opt/fujin/.pythonIf you have multiple apps, redeploy each one to migrate them all