Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 67

Creating a Gunicorn Systemd Service

Recommended resources

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.

Systemd Unit File Location Hierarchy
This diagram shows the three main locations for systemd unit files. Files in `/etc/systemd/system` have the highest priority and are used for system-specific configurations created by administrators like you. This is why we are working in this directory.

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.

Gunicorn Systemd Service Unit File Configuration
This image shows a complete `gunicorn.service` file, similar to the one we are building. It clearly lays out the [Unit], [Service], and [Install] sections with their respective directives.

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 in systemctl status output.
  • Requires=gunicorn.socket: This creates a strong dependency. If gunicorn.socket fails or is stopped, systemd will also stop gunicorn.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-powerful root user. 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 where systemd is invoked from.
  • ExecStart=...: This is the exact command that systemd runs 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 that systemd created 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 tells systemd that 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

  1. Create a Gunicorn Configuration File:
    First, let's create a Python file to hold our Gunicorn settings. This is much cleaner than a long ExecStart line.

    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.py
    

    Paste the following content. It defines the same settings we had before, but in a Python file.

    bind = "unix:/run/gunicorn.sock"
    workers = 3
    

    Save and close the file.

  2. Modify the gunicorn.service file:
    Now, let's edit the service file to use our new configuration and add a crucial directive: Restart.

    sudo vim /etc/systemd/system/gunicorn.service
    

    Modify the [Service] section to look like this. Make sure to replace your_user with 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:application
    

    What did we change?

    • Restart=on-failure: This is a powerful feature. It tells systemd to 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 --bind and --workers arguments with a single --config flag pointing to our new gunicorn_config.py file. This is much more organized.
  3. Reload systemd and Restart Gunicorn:
    Whenever you change a unit file, you must tell systemd to 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
    
  4. Verify Everything:
    Finally, check the status to ensure your changes were applied and the service is running correctly.

    sudo systemctl status gunicorn.service
    

    You should see active (running), and the service should be running successfully. You can also re-run the curl test 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 .service file 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 User is a fundamental security practice.
  • The Restart=on-failure directive 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 your systemd unit file clean and your settings organized.
  • You must run sudo systemctl daemon-reload after 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.