Hello! Welcome back.
In our last lesson, we successfully set up Gunicorn and created the initial systemd unit files to have it managed as a service. We verified that systemd's socket activation could start Gunicorn on demand. That's a huge step!
However, the unit files we created were basic. For a real-world, production environment—which is our goal—we need to make our service configuration more robust, secure, and maintainable.
Today's lesson focuses on exactly that: writing a custom systemd service unit file to manage the Gunicorn process. We will dissect the structure of a .service file, understand the purpose of key directives, and then customize our gunicorn.service file with best practices for a production deployment.
1. The Anatomy of a systemd Unit File
In the last lesson, you created /etc/systemd/system/gunicorn.service. This is a systemd unit file. While there are several types of unit files (.socket, .timer, etc.), the .service file is the most common, used to manage daemon processes like Gunicorn.
These files have a clear, structured format using sections (like [Unit]) and directives (like Description=...). To build a solid foundation, let's explore these components in more detail.
Creating systemd Service Files
The video "Creating systemd Service Files" from the DevDungeon channel provides an excellent and clear explanation of the fundamental structure of a .service file. It's not specific to Gunicorn, which makes it perfect for understanding the general principles.
Please watch the following segments: 00:55 - 01:52: This part covers the location of service files and the essential [Service] section with its ExecStart directive. 02:50 - 05:01: Focus on the common options discussed here: Restart, WorkingDirectory, User, and Group. These are critical for our Gunicorn service. 05:01 - 05:52: This segment explains the [Unit] and [Install] sections, introducing Description, After, and WantedBy. As you watch, note down the purpose of each of these three main sections.
To summarize and place this in your Fedora context, systemd looks for unit files in a specific order of priority.

2. A Deeper Look at Our gunicorn.service File
Let's apply that general knowledge to the gunicorn.service file you created. Open it again to follow along: sudo vim /etc/systemd/system/gunicorn.service.
Here is a breakdown of the file, explaining why each line is there.

The [Unit] Section
This section provides metadata about the service and defines its relationship with other systemd units.
Description=gunicorn daemon: A human-readable description that shows up insystemctl statusoutput.Requires=gunicorn.socket: This creates a strong dependency. Ifgunicorn.socketfails or is stopped,systemdwill also stopgunicorn.service. This is exactly what we want in our socket-activated setup.After=network.target: This defines the startup order. It ensures that our Gunicorn service only starts after the basic networking stack is up and running.
The [Service] Section
This is the core of the configuration, defining how the service is executed.
User=your_user&Group=your_user: This is a critical security measure. It ensures your Django application code is run by your non-privileged user account, not as the all-powerfulrootuser. If a vulnerability were ever found in your application, this would significantly limit the potential damage.WorkingDirectory=/var/www/myproject: This sets the current working directory before the command is executed. Django and other Python projects often rely on relative paths, and this directive ensures they resolve correctly, no matter wheresystemdis invoked from.ExecStart=...: This is the exact command thatsystemdruns to start the service. Let's look at it closely:/var/www/myproject/myprojectenv/bin/gunicorn: We use the absolute path to the Gunicorn executable inside our project's virtual environment. This guarantees we are using the correct version of Gunicorn with the correct Python dependencies.--workers 3: This tells Gunicorn to create 3 worker processes to handle requests. A common formula is(2 * CPU cores) + 1. This allows your app to handle multiple requests in parallel.--bind unix:/run/gunicorn.sock: This instructs Gunicorn to listen on the Unix socket file thatsystemdcreated for us, not on a TCP port.
The [Install] Section
This section is used when you enable or disable the service.
WantedBy=multi-user.target: This tellssystemdthat when you enable this service, it should be part of the "multi-user" target. This is the standard runlevel for a server, meaning your service will be started automatically at boot.
3. Hands-On: Customizing for Robustness
Now for the practical part. Let's make our service more robust and easier to manage, reflecting real-world best practices.
Set Up Django with Postgres, Nginx, and Gunicorn on Ubuntu
The article "Set Up Django with Postgres, Nginx, and Gunicorn on Ubuntu" from DigitalOcean, which we've used before, provides a great template. We'll also borrow an idea from another concise blog post to make our configuration even cleaner.
You don't need to read the whole article again. Just quickly review the gunicorn.service file example in Step 7 and skim the Advanced Performance Optimizations section to see how many parameters can be passed to Gunicorn.
Notice how the ExecStart line can get very long with all the performance-tuning parameters. A cleaner way is to use a separate configuration file for Gunicorn.
Task: Refactor and Enhance gunicorn.service
-
Create a Gunicorn Configuration File:
First, let's create a Python file to hold our Gunicorn settings. This is much cleaner than a longExecStartline.Create the file inside your project directory:
# Make sure you are in your project directory cd /var/www/myproject # Create and open the config file vim gunicorn_config.pyPaste the following content. It defines the same settings we had before, but in a Python file.
bind = "unix:/run/gunicorn.sock" workers = 3Save and close the file.
-
Modify the
gunicorn.servicefile:
Now, let's edit the service file to use our new configuration and add a crucial directive:Restart.sudo vim /etc/systemd/system/gunicorn.serviceModify the
[Service]section to look like this. Make sure to replaceyour_userwith your actual username.[Service] User=your_user Group=your_user WorkingDirectory=/var/www/myproject # Add this line! Restart=on-failure # Modify this line to use the config file ExecStart=/var/www/myproject/myprojectenv/bin/gunicorn \ --config /var/www/myproject/gunicorn_config.py \ myproject.wsgi:applicationWhat did we change?
Restart=on-failure: This is a powerful feature. It tellssystemdto automatically restart the Gunicorn service if it ever exits with an error code. This makes your application self-healing if it crashes for some reason.ExecStart: We replaced the--bindand--workersarguments with a single--configflag pointing to our newgunicorn_config.pyfile. This is much more organized.
-
Reload
systemdand Restart Gunicorn:
Whenever you change a unit file, you must tellsystemdto reload its configuration.# Tell systemd to re-read the modified file sudo systemctl daemon-reload # Restart the gunicorn service to apply the changes sudo systemctl restart gunicorn.service -
Verify Everything:
Finally, check the status to ensure your changes were applied and the service is running correctly.sudo systemctl status gunicorn.serviceYou should see
active (running), and the service should be running successfully. You can also re-run thecurltest from the last lesson to be absolutely sure:curl --unix-socket /run/gunicorn.sock localhost.
Conclusion
Excellent work! You've moved beyond a basic configuration and created a systemd service that is secure, robust, and maintainable—just like you would for a professional deployment.
Key Takeaways:
- A
systemd.servicefile is structured into[Unit],[Service], and[Install]sections, which define what the service is, how it runs, and when it starts. - Running your application as a non-privileged
Useris a fundamental security practice. - The
Restart=on-failuredirective adds robustness by allowing your service to recover automatically from crashes. - Using a separate configuration file (e.g.,
gunicorn_config.py) for your application server keeps yoursystemdunit file clean and your settings organized. - You must run
sudo systemctl daemon-reloadafter modifying a unit file for the changes to be recognized.
Preview of the Next Lesson:
We now have Nginx ready to serve static files and a robust Gunicorn service ready to run our Django application. The final piece of the puzzle is to connect them. In the next lesson, we will configure Nginx as a reverse proxy to forward web requests to Gunicorn's socket. We will also tackle the final SELinux permission required to allow Nginx to communicate with that socket.